summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-10-25 02:13:24 +0800
committerLin Jen-Shin <godfat@godfat.org>2016-10-25 02:13:24 +0800
commit600da9ee0bb823e4b14fd45d6ff0e5f0b61b9737 (patch)
treef91eeeee22da72eed5bdf5b87ca27bdc95b136a0
parent40ff7579e9ba025610dfada9703386b4dc657d6d (diff)
parentcb38290ababe43aca0c635fb87d3a38c4c5debcd (diff)
downloadgitlab-ce-19737-read-only-auditor.tar.gz
Merge remote-tracking branch 'upstream/master' into 19737-read-only-auditor19737-read-only-auditor
* upstream/master: (1277 commits) Grapify the labels API Fix typo in project settings that prevents users from enabling container registry. Fix old monitoring links to point to the new location Added path parameter to Commits API fixes build with cache:clear issue Merge branch 'security-fix-leaking-namespace-name' into 'security' Fix authored vote from notes Grapify builds API Add changelog item for groups 404 on relative url Add relative url support to routing contrainers Update project member controller to match recent master logic Add parentheses around return redirect_to method Trigger change even in select2 test helper to produce production-like behaviour Refactor js that disable form submit if no members selected Improve create project member test at project_members_controller_spec Move changelog item to 8.14 Refactor create member tests from group_members_controller_spec Refactor groups/projects members controller Gracefully handle adding of no users to projects and groups Revert "Change "Group#web_url" to return "/groups/twitter" rather than "/twitter"." ...
-rw-r--r--.csscomb.json2
-rw-r--r--.gitattributes2
-rw-r--r--.gitlab-ci.yml54
-rw-r--r--.gitlab/merge_request_templates/Documentation.md2
-rw-r--r--.rubocop.yml10
-rw-r--r--.rubocop_todo.yml170
-rw-r--r--.scss-lint.yml6
-rw-r--r--.vagrant_enabled0
-rw-r--r--CHANGELOG.md (renamed from CHANGELOG)605
-rw-r--r--CONTRIBUTING.md49
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile44
-rw-r--r--Gemfile.lock70
-rw-r--r--README.md4
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gifbin0 -> 3654 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gifbin0 -> 3040 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gifbin0 -> 663 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gifbin0 -> 369 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gifbin0 -> 278 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gifbin0 -> 1013 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gifbin0 -> 660 bytes
-rw-r--r--app/assets/javascripts/LabelManager.js115
-rw-r--r--app/assets/javascripts/activities.js12
-rw-r--r--app/assets/javascripts/api.js19
-rw-r--r--app/assets/javascripts/application.js7
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js46
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es640
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js25
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es621
-rw-r--r--app/assets/javascripts/blob/template_selector.js100
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es6102
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js19
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es63
-rw-r--r--app/assets/javascripts/boards/components/board.js.es68
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es66
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es69
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es658
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es63
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es62
-rw-r--r--app/assets/javascripts/boards/models/list.js.es611
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es616
-rw-r--r--app/assets/javascripts/build.js38
-rw-r--r--app/assets/javascripts/commit/image_file.js (renamed from app/assets/javascripts/commit/image-file.js)0
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es6 (renamed from app/assets/javascripts/compare_autocomplete.js)18
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js18
-rw-r--r--app/assets/javascripts/create_label.js.es69
-rw-r--r--app/assets/javascripts/cycle_analytics.js.es6 (renamed from app/assets/javascripts/cycle-analytics.js.es6)8
-rw-r--r--app/assets/javascripts/diff.js7
-rw-r--r--app/assets/javascripts/dispatcher.js.es6 (renamed from app/assets/javascripts/dispatcher.js)41
-rw-r--r--app/assets/javascripts/due_date_select.js107
-rw-r--r--app/assets/javascripts/due_date_select.js.es6161
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es638
-rw-r--r--app/assets/javascripts/gl_dropdown.js102
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es6167
-rw-r--r--app/assets/javascripts/groups.js13
-rw-r--r--app/assets/javascripts/groups_select.js5
-rw-r--r--app/assets/javascripts/issuable.js.es61
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es6 (renamed from app/assets/javascripts/issues-bulk-assignment.js)128
-rw-r--r--app/assets/javascripts/label_manager.js.es6106
-rw-r--r--app/assets/javascripts/labels_select.js136
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js5
-rw-r--r--app/assets/javascripts/member_expiration_date.js8
-rw-r--r--app/assets/javascripts/members.js.es637
-rw-r--r--app/assets/javascripts/merge_conflict_data_provider.js.es6341
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es683
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es693
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es612
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es614
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es615
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es630
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6437
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es689
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es612
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es618
-rw-r--r--app/assets/javascripts/merge_request.js9
-rw-r--r--app/assets/javascripts/merge_request_tabs.js93
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es6 (renamed from app/assets/javascripts/merge_request_widget.js)87
-rw-r--r--app/assets/javascripts/milestone_select.js27
-rw-r--r--app/assets/javascripts/network/branch_graph.js (renamed from app/assets/javascripts/network/branch-graph.js)0
-rw-r--r--app/assets/javascripts/pipeline.js.es615
-rw-r--r--app/assets/javascripts/pipelines.js.es640
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es6 (renamed from app/assets/javascripts/profile/gl_crop.js)123
-rw-r--r--app/assets/javascripts/profile/profile.js106
-rw-r--r--app/assets/javascripts/profile/profile.js.es6100
-rw-r--r--app/assets/javascripts/project_find_file.js9
-rw-r--r--app/assets/javascripts/project_members.js10
-rw-r--r--app/assets/javascripts/project_new.js34
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 (renamed from app/assets/javascripts/protected_branch_access_dropdown.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js.es6 (renamed from app/assets/javascripts/protected_branch_create.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 (renamed from app/assets/javascripts/protected_branch_dropdown.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 (renamed from app/assets/javascripts/protected_branch_edit.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 (renamed from app/assets/javascripts/protected_branch_edit_list.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js1
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es6 (renamed from app/assets/javascripts/search_autocomplete.js)158
-rw-r--r--app/assets/javascripts/single_file_diff.js13
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es628
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es612
-rw-r--r--app/assets/javascripts/todos.js.es6 (renamed from app/assets/javascripts/todos.js)143
-rw-r--r--app/assets/javascripts/user.js.es610
-rw-r--r--app/assets/javascripts/user_tabs.js188
-rw-r--r--app/assets/javascripts/user_tabs.js.es6157
-rw-r--r--app/assets/javascripts/username_validator.js.es6133
-rw-r--r--app/assets/javascripts/users_select.js38
-rw-r--r--app/assets/stylesheets/behaviors.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss8
-rw-r--r--app/assets/stylesheets/framework/buttons.scss26
-rw-r--r--app/assets/stylesheets/framework/callout.scss5
-rw-r--r--app/assets/stylesheets/framework/common.scss41
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss25
-rw-r--r--app/assets/stylesheets/framework/files.scss10
-rw-r--r--app/assets/stylesheets/framework/flash.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss15
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss17
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss12
-rw-r--r--app/assets/stylesheets/framework/lists.scss13
-rw-r--r--app/assets/stylesheets/framework/logo.scss85
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss19
-rw-r--r--app/assets/stylesheets/framework/mobile.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss1
-rw-r--r--app/assets/stylesheets/framework/panels.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss31
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss15
-rw-r--r--app/assets/stylesheets/framework/timeline.scss1
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss10
-rw-r--r--app/assets/stylesheets/framework/typography.scss42
-rw-r--r--app/assets/stylesheets/framework/variables.scss7
-rw-r--r--app/assets/stylesheets/highlight/dark.scss128
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss118
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss136
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss136
-rw-r--r--app/assets/stylesheets/highlight/white.scss2
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss2
-rw-r--r--app/assets/stylesheets/notify.scss10
-rw-r--r--app/assets/stylesheets/pages/admin.scss12
-rw-r--r--app/assets/stylesheets/pages/boards.scss36
-rw-r--r--app/assets/stylesheets/pages/builds.scss34
-rw-r--r--app/assets/stylesheets/pages/commit.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss1
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss38
-rw-r--r--app/assets/stylesheets/pages/editor.scss26
-rw-r--r--app/assets/stylesheets/pages/environments.scss43
-rw-r--r--app/assets/stylesheets/pages/events.scss10
-rw-r--r--app/assets/stylesheets/pages/groups.scss51
-rw-r--r--app/assets/stylesheets/pages/help.scss8
-rw-r--r--app/assets/stylesheets/pages/issuable.scss8
-rw-r--r--app/assets/stylesheets/pages/issues.scss1
-rw-r--r--app/assets/stylesheets/pages/labels.scss33
-rw-r--r--app/assets/stylesheets/pages/lint.scss1
-rw-r--r--app/assets/stylesheets/pages/login.scss233
-rw-r--r--app/assets/stylesheets/pages/members.scss98
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss49
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss71
-rw-r--r--app/assets/stylesheets/pages/milestone.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss12
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss258
-rw-r--r--app/assets/stylesheets/pages/profile.scss26
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss74
-rw-r--r--app/assets/stylesheets/pages/runners.scss1
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss19
-rw-r--r--app/assets/stylesheets/pages/tree.scss12
-rw-r--r--app/assets/stylesheets/pages/xterm.scss290
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/admin/services_controller.rb8
-rw-r--r--app/controllers/application_controller.rb14
-rw-r--r--app/controllers/ci/application_controller.rb7
-rw-r--r--app/controllers/ci/lints_controller.rb3
-rw-r--r--app/controllers/ci/projects_controller.rb2
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb15
-rw-r--r--app/controllers/concerns/issuable_actions.rb5
-rw-r--r--app/controllers/concerns/membership_actions.rb13
-rw-r--r--app/controllers/concerns/spammable_actions.rb2
-rw-r--r--app/controllers/dashboard/labels_controller.rb4
-rw-r--r--app/controllers/explore/projects_controller.rb3
-rw-r--r--app/controllers/groups/group_members_controller.rb11
-rw-r--r--app/controllers/groups/labels_controller.rb92
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb5
-rw-r--r--app/controllers/namespaces_controller.rb25
-rw-r--r--app/controllers/projects/board_lists_controller.rb65
-rw-r--r--app/controllers/projects/boards/issues_controller.rb42
-rw-r--r--app/controllers/projects/boards/lists_controller.rb23
-rw-r--r--app/controllers/projects/boards_controller.rb28
-rw-r--r--app/controllers/projects/builds_controller.rb4
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb25
-rw-r--r--app/controllers/projects/graphs_controller.rb6
-rw-r--r--app/controllers/projects/group_links_controller.rb42
-rw-r--r--app/controllers/projects/issues_controller.rb7
-rw-r--r--app/controllers/projects/labels_controller.rb46
-rw-r--r--app/controllers/projects/merge_requests_controller.rb180
-rw-r--r--app/controllers/projects/project_members_controller.rb35
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb46
-rw-r--r--app/controllers/snippets_controller.rb14
-rw-r--r--app/controllers/users_controller.rb6
-rw-r--r--app/finders/access_requests_finder.rb27
-rw-r--r--app/finders/issuable_finder.rb32
-rw-r--r--app/finders/labels_finder.rb92
-rw-r--r--app/finders/trending_projects_finder.rb11
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/application_helper.rb26
-rw-r--r--app/helpers/application_settings_helper.rb12
-rw-r--r--app/helpers/avatars_helper.rb5
-rw-r--r--app/helpers/award_emoji_helper.rb9
-rw-r--r--app/helpers/boards_helper.rb12
-rw-r--r--app/helpers/broadcast_messages_helper.rb6
-rw-r--r--app/helpers/builds_helper.rb8
-rw-r--r--app/helpers/button_helper.rb5
-rw-r--r--app/helpers/dropdowns_helper.rb3
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/helpers/gitlab_markdown_helper.rb44
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/issuables_helper.rb56
-rw-r--r--app/helpers/issues_helper.rb5
-rw-r--r--app/helpers/labels_helper.rb87
-rw-r--r--app/helpers/merge_requests_helper.rb27
-rw-r--r--app/helpers/milestones_helper.rb5
-rw-r--r--app/helpers/page_layout_helper.rb8
-rw-r--r--app/helpers/preferences_helper.rb16
-rw-r--r--app/helpers/projects_helper.rb33
-rw-r--r--app/helpers/search_helper.rb14
-rw-r--r--app/helpers/selects_helper.rb6
-rw-r--r--app/helpers/todos_helper.rb20
-rw-r--r--app/mailers/.gitkeep0
-rw-r--r--app/mailers/devise_mailer.rb8
-rw-r--r--app/mailers/emails/members.rb2
-rw-r--r--app/mailers/emails/pipelines.rb43
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/abuse_report.rb7
-rw-r--r--app/models/appearance.rb4
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/broadcast_message.rb3
-rw-r--r--app/models/ci/build.rb47
-rw-r--r--app/models/ci/pipeline.rb39
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/commit_range.rb2
-rw-r--r--app/models/commit_status.rb46
-rw-r--r--app/models/compare.rb21
-rw-r--r--app/models/concerns/access_requestable.rb5
-rw-r--r--app/models/concerns/cache_markdown_field.rb131
-rw-r--r--app/models/concerns/expirable.rb6
-rw-r--r--app/models/concerns/has_status.rb29
-rw-r--r--app/models/concerns/issuable.rb26
-rw-r--r--app/models/concerns/mentionable.rb27
-rw-r--r--app/models/concerns/protected_branch_access.rb5
-rw-r--r--app/models/concerns/sortable.rb10
-rw-r--r--app/models/cycle_analytics.rb14
-rw-r--r--app/models/deployment.rb38
-rw-r--r--app/models/email.rb6
-rw-r--r--app/models/environment.rb47
-rw-r--r--app/models/event.rb36
-rw-r--r--app/models/external_issue.rb5
-rw-r--r--app/models/global_label.rb4
-rw-r--r--app/models/global_milestone.rb5
-rw-r--r--app/models/group.rb37
-rw-r--r--app/models/group_label.rb11
-rw-r--r--app/models/issue.rb24
-rw-r--r--app/models/label.rb132
-rw-r--r--app/models/label_priority.rb8
-rw-r--r--app/models/list.rb11
-rw-r--r--app/models/member.rb84
-rw-r--r--app/models/members/group_member.rb16
-rw-r--r--app/models/members/project_member.rb50
-rw-r--r--app/models/merge_request.rb88
-rw-r--r--app/models/merge_request_diff.rb21
-rw-r--r--app/models/milestone.rb10
-rw-r--r--app/models/namespace.rb17
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/project.rb75
-rw-r--r--app/models/project_feature.rb24
-rw-r--r--app/models/project_group_link.rb2
-rw-r--r--app/models/project_label.rb34
-rw-r--r--app/models/project_services/builds_email_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb58
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jira_service.rb5
-rw-r--r--app/models/project_services/pipelines_email_service.rb96
-rw-r--r--app/models/project_services/slack_service/issue_message.rb2
-rw-r--r--app/models/project_services/slack_service/merge_message.rb2
-rw-r--r--app/models/project_services/slack_service/note_message.rb2
-rw-r--r--app/models/project_services/slack_service/wiki_page_message.rb2
-rw-r--r--app/models/project_team.rb14
-rw-r--r--app/models/release.rb4
-rw-r--r--app/models/repository.rb103
-rw-r--r--app/models/service.rb6
-rw-r--r--app/models/snippet.rb10
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/models/trending_project.rb35
-rw-r--r--app/models/user.rb28
-rw-r--r--app/policies/group_label_policy.rb5
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/policies/project_label_policy.rb5
-rw-r--r--app/policies/project_policy.rb35
-rw-r--r--app/services/base_service.rb7
-rw-r--r--app/services/boards/base_service.rb5
-rw-r--r--app/services/boards/create_service.rb17
-rw-r--r--app/services/boards/issues/create_service.rb23
-rw-r--r--app/services/boards/issues/list_service.rb13
-rw-r--r--app/services/boards/issues/move_service.rb8
-rw-r--r--app/services/boards/list_service.rb14
-rw-r--r--app/services/boards/lists/create_service.rb18
-rw-r--r--app/services/boards/lists/destroy_service.rb6
-rw-r--r--app/services/boards/lists/generate_service.rb19
-rw-r--r--app/services/boards/lists/list_service.rb9
-rw-r--r--app/services/boards/lists/move_service.rb5
-rw-r--r--app/services/ci/process_pipeline_service.rb2
-rw-r--r--app/services/ci/send_pipeline_notification_service.rb19
-rw-r--r--app/services/compare_service.rb7
-rw-r--r--app/services/create_deployment_service.rb49
-rw-r--r--app/services/event_create_service.rb4
-rw-r--r--app/services/files/base_service.rb11
-rw-r--r--app/services/files/multi_service.rb124
-rw-r--r--app/services/files/update_service.rb6
-rw-r--r--app/services/git_push_service.rb15
-rw-r--r--app/services/issuable_base_service.rb15
-rw-r--r--app/services/issues/move_service.rb8
-rw-r--r--app/services/labels/find_or_create_service.rb33
-rw-r--r--app/services/labels/transfer_service.rb78
-rw-r--r--app/services/members/authorized_destroy_service.rb2
-rw-r--r--app/services/members/destroy_service.rb39
-rw-r--r--app/services/members/request_access_service.rb25
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb4
-rw-r--r--app/services/merge_requests/assign_issues_service.rb35
-rw-r--r--app/services/merge_requests/base_service.rb36
-rw-r--r--app/services/merge_requests/build_service.rb23
-rw-r--r--app/services/merge_requests/merge_service.rb20
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb9
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/resolve_service.rb24
-rw-r--r--app/services/merge_requests/update_service.rb18
-rw-r--r--app/services/notes/create_service.rb6
-rw-r--r--app/services/notification_service.rb25
-rw-r--r--app/services/projects/autocomplete_service.rb2
-rw-r--r--app/services/projects/create_service.rb20
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/services/projects/import_service.rb5
-rw-r--r--app/services/projects/transfer_service.rb4
-rw-r--r--app/services/protected_branches/api_create_service.rb29
-rw-r--r--app/services/protected_branches/api_update_service.rb47
-rw-r--r--app/services/slash_commands/interpret_service.rb39
-rw-r--r--app/services/system_hooks_service.rb2
-rw-r--r--app/services/system_note_service.rb24
-rw-r--r--app/services/todo_service.rb6
-rw-r--r--app/validators/namespace_validator.rb6
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml2
-rw-r--r--app/views/admin/appearances/preview.html.haml17
-rw-r--r--app/views/admin/application_settings/_form.html.haml6
-rw-r--r--app/views/admin/background_jobs/_head.html.haml49
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml9
-rw-r--r--app/views/admin/broadcast_messages/preview.js.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml57
-rw-r--r--app/views/admin/dashboard/index.html.haml5
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml2
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml24
-rw-r--r--app/views/admin/runners/show.html.haml16
-rw-r--r--app/views/ci/lints/_create.html.haml12
-rw-r--r--app/views/dashboard/groups/_empty_state.html.haml7
-rw-r--r--app/views/dashboard/groups/index.html.haml13
-rw-r--r--app/views/dashboard/todos/_todo.html.haml1
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/confirmations/almost_there.haml4
-rw-r--r--app/views/devise/confirmations/new.html.haml12
-rw-r--r--app/views/devise/passwords/edit.html.haml20
-rw-r--r--app/views/devise/passwords/new.html.haml10
-rw-r--r--app/views/devise/sessions/_new_base.html.haml16
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml12
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml12
-rw-r--r--app/views/devise/sessions/new.html.haml23
-rw-r--r--app/views/devise/sessions/two_factor.html.haml17
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml15
-rw-r--r--app/views/devise/shared/_sign_in_link.html.haml1
-rw-r--r--app/views/devise/shared/_signin_box.html.haml46
-rw-r--r--app/views/devise/shared/_signup_box.html.haml37
-rw-r--r--app/views/devise/shared/_tab_single.html.haml3
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml10
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml5
-rw-r--r--app/views/devise/unlocks/new.html.haml10
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml8
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml35
-rw-r--r--app/views/groups/group_members/index.html.haml40
-rw-r--r--app/views/groups/group_members/update.js.haml4
-rw-r--r--app/views/groups/labels/destroy.js.haml2
-rw-r--r--app/views/groups/labels/edit.html.haml7
-rw-r--r--app/views/groups/labels/index.html.haml20
-rw-r--r--app/views/groups/labels/new.html.haml8
-rw-r--r--app/views/groups/milestones/new.html.haml4
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml4
-rw-r--r--app/views/layouts/_flash.html.haml6
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml9
-rw-r--r--app/views/layouts/_page.html.haml6
-rw-r--r--app/views/layouts/application.html.haml1
-rw-r--r--app/views/layouts/devise.html.haml63
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/nav/_group.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml177
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb31
-rw-r--r--app/views/notify/pipeline_success_email.html.haml154
-rw-r--r--app/views/notify/pipeline_success_email.text.erb24
-rw-r--r--app/views/profiles/accounts/show.html.haml4
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-rw-r--r--app/views/projects/_activity.html.haml19
-rw-r--r--app/views/projects/_customize_workflow.html.haml8
-rw-r--r--app/views/projects/_home_panel.html.haml7
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_wiki.html.haml19
-rw-r--r--app/views/projects/_zen.html.haml4
-rw-r--r--app/views/projects/blame/show.html.haml87
-rw-r--r--app/views/projects/blob/_editor.html.haml7
-rw-r--r--app/views/projects/blob/edit.html.haml43
-rw-r--r--app/views/projects/boards/components/_board.html.haml41
-rw-r--r--app/views/projects/boards/components/_card.html.haml6
-rw-r--r--app/views/projects/boards/index.html.haml16
-rw-r--r--app/views/projects/boards/show.html.haml5
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml23
-rw-r--r--app/views/projects/builds/_table.html.haml4
-rw-r--r--app/views/projects/builds/_user.html.haml7
-rw-r--r--app/views/projects/builds/index.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml95
-rw-r--r--app/views/projects/buttons/_download.html.haml4
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml3
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml66
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml10
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml82
-rw-r--r--app/views/projects/commit/_commit_box.html.haml8
-rw-r--r--app/views/projects/commit/_pipeline.html.haml81
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml2
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml20
-rw-r--r--app/views/projects/commit/_pipelines_list.haml3
-rw-r--r--app/views/projects/commit/builds.html.haml11
-rw-r--r--app/views/projects/commit/show.html.haml25
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-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.haml3
-rw-r--r--app/views/projects/compare/index.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml4
-rw-r--r--app/views/projects/deployments/_actions.haml35
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml14
-rw-r--r--app/views/projects/deployments/_rollback.haml6
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml5
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/diffs/_file_header.html.haml1
-rw-r--r--app/views/projects/edit.html.haml103
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/_environment.html.haml21
-rw-r--r--app/views/projects/environments/_external_url.html.haml3
-rw-r--r--app/views/projects/environments/_stop.html.haml5
-rw-r--r--app/views/projects/environments/edit.html.haml11
-rw-r--r--app/views/projects/environments/index.html.haml63
-rw-r--r--app/views/projects/environments/new.html.haml11
-rw-r--r--app/views/projects/environments/show.html.haml49
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml15
-rw-r--r--app/views/projects/graphs/_head.html.haml35
-rw-r--r--app/views/projects/group_links/index.html.haml6
-rw-r--r--app/views/projects/group_links/update.js.haml3
-rw-r--r--app/views/projects/issues/_head.html.haml57
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/edit.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml10
-rw-r--r--app/views/projects/labels/_label.html.haml50
-rw-r--r--app/views/projects/labels/destroy.js.haml2
-rw-r--r--app/views/projects/labels/edit.html.haml11
-rw-r--r--app/views/projects/labels/index.html.haml13
-rw-r--r--app/views/projects/labels/new.html.haml11
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml13
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml1
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml68
-rw-r--r--app/views/projects/merge_requests/_show.html.haml95
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml29
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml16
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml12
-rw-r--r--app/views/projects/merge_requests/conflicts/_inline_view.html.haml28
-rw-r--r--app/views/projects/merge_requests/conflicts/_parallel_view.html.haml27
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml31
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml13
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml15
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml10
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml14
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml20
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml1
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml3
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/edit.html.haml12
-rw-r--r--app/views/projects/milestones/new.html.haml11
-rw-r--r--app/views/projects/milestones/show.html.haml83
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml8
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml49
-rw-r--r--app/views/projects/pipelines/_info.html.haml4
-rw-r--r--app/views/projects/pipelines/index.html.haml11
-rw-r--r--app/views/projects/pipelines/show.html.haml13
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml6
-rw-r--r--app/views/projects/project_members/_group_members.html.haml2
-rw-r--r--app/views/projects/project_members/_groups.html.haml7
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml35
-rw-r--r--app/views/projects/project_members/_team.html.haml16
-rw-r--r--app/views/projects/project_members/index.html.haml40
-rw-r--r--app/views/projects/project_members/update.js.haml4
-rw-r--r--app/views/projects/protected_branches/index.html.haml2
-rw-r--r--app/views/projects/repositories/_feed.html.haml2
-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.haml20
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml14
-rw-r--r--app/views/projects/runners/index.html.haml12
-rw-r--r--app/views/projects/show.html.haml131
-rw-r--r--app/views/projects/snippets/_actions.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/projects/wikis/_nav.html.haml25
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_milestone.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml1
-rw-r--r--app/views/shared/_label.html.haml53
-rw-r--r--app/views/shared/_label_row.html.haml9
-rw-r--r--app/views/shared/_labels_row.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml24
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/_visibility_radios.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/icons/_icon_empty_groups.svg1
-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/icons/_illustration_no_commits.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml25
-rw-r--r--app/views/shared/issuable/_form.html.haml39
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml23
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml23
-rw-r--r--app/views/shared/issuable/_nav.html.haml20
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml21
-rw-r--r--app/views/shared/labels/_form.html.haml (renamed from app/views/projects/labels/_form.html.haml)6
-rw-r--r--app/views/shared/members/_group.html.haml29
-rw-r--r--app/views/shared/members/_member.html.haml108
-rw-r--r--app/views/shared/members/_requests.html.haml2
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml3
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/snippets/_blob.html.haml7
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/snippets/_actions.html.haml2
-rw-r--r--app/views/snippets/show.html.haml1
-rw-r--r--app/views/u2f/_authenticate.html.haml2
-rw-r--r--app/views/users/show.html.haml6
-rw-r--r--app/workers/admin_email_worker.rb3
-rw-r--r--app/workers/build_coverage_worker.rb9
-rw-r--r--app/workers/build_email_worker.rb1
-rw-r--r--app/workers/build_finished_worker.rb11
-rw-r--r--app/workers/build_hooks_worker.rb9
-rw-r--r--app/workers/build_success_worker.rb27
-rw-r--r--app/workers/clear_database_cache_worker.rb24
-rw-r--r--app/workers/concerns/build_queue.rb8
-rw-r--r--app/workers/concerns/cronjob_queue.rb9
-rw-r--r--app/workers/concerns/dedicated_sidekiq_queue.rb9
-rw-r--r--app/workers/concerns/pipeline_queue.rb8
-rw-r--r--app/workers/concerns/repository_check_queue.rb8
-rw-r--r--app/workers/delete_user_worker.rb1
-rw-r--r--app/workers/email_receiver_worker.rb3
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/expire_build_artifacts_worker.rb12
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb16
-rw-r--r--app/workers/git_garbage_collect_worker.rb3
-rw-r--r--app/workers/gitlab_shell_worker.rb3
-rw-r--r--app/workers/group_destroy_worker.rb3
-rw-r--r--app/workers/import_export_project_cleanup_worker.rb3
-rw-r--r--app/workers/irker_worker.rb1
-rw-r--r--app/workers/merge_worker.rb3
-rw-r--r--app/workers/new_note_worker.rb3
-rw-r--r--app/workers/pipeline_hooks_worker.rb9
-rw-r--r--app/workers/pipeline_metrics_worker.rb29
-rw-r--r--app/workers/pipeline_process_worker.rb9
-rw-r--r--app/workers/pipeline_success_worker.rb12
-rw-r--r--app/workers/pipeline_update_worker.rb9
-rw-r--r--app/workers/post_receive.rb3
-rw-r--r--app/workers/project_cache_worker.rb28
-rw-r--r--app/workers/project_destroy_worker.rb3
-rw-r--r--app/workers/project_export_worker.rb3
-rw-r--r--app/workers/project_service_worker.rb3
-rw-r--r--app/workers/project_web_hook_worker.rb3
-rw-r--r--app/workers/prune_old_events_worker.rb1
-rw-r--r--app/workers/remove_expired_group_links_worker.rb1
-rw-r--r--app/workers/remove_expired_members_worker.rb1
-rw-r--r--app/workers/repository_archive_cache_worker.rb3
-rw-r--r--app/workers/repository_check/batch_worker.rb21
-rw-r--r--app/workers/repository_check/clear_worker.rb3
-rw-r--r--app/workers/repository_check/single_repository_worker.rb3
-rw-r--r--app/workers/repository_fork_worker.rb3
-rw-r--r--app/workers/repository_import_worker.rb3
-rw-r--r--app/workers/requests_profiles_worker.rb3
-rw-r--r--app/workers/stuck_ci_builds_worker.rb1
-rw-r--r--app/workers/system_hook_worker.rb3
-rw-r--r--app/workers/trending_projects_worker.rb10
-rw-r--r--app/workers/update_merge_requests_worker.rb17
-rwxr-xr-xbin/background_jobs3
-rw-r--r--config/application.rb26
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--config/initializers/1_settings.rb5
-rw-r--r--config/initializers/7_redis.rb3
-rw-r--r--config/initializers/ar5_batching.rb41
-rw-r--r--config/initializers/ar_speed_up_migration_checking.rb18
-rw-r--r--config/initializers/attr_encrypted_no_db_connection.rb25
-rw-r--r--config/initializers/gitlab_shell_secret_token.rb2
-rw-r--r--config/initializers/metrics.rb1
-rw-r--r--config/initializers/postgresql_limit_fix.rb27
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/locales/en.yml1
-rw-r--r--config/mail_room.yml2
-rw-r--r--config/routes.rb891
-rw-r--r--config/routes/admin.rb102
-rw-r--r--config/routes/api.rb2
-rw-r--r--config/routes/ci.rb15
-rw-r--r--config/routes/dashboard.rb27
-rw-r--r--config/routes/development.rb13
-rw-r--r--config/routes/explore.rb16
-rw-r--r--config/routes/group.rb34
-rw-r--r--config/routes/help.rb4
-rw-r--r--config/routes/import.rb42
-rw-r--r--config/routes/profile.rb43
-rw-r--r--config/routes/project.rb472
-rw-r--r--config/routes/sherlock.rb12
-rw-r--r--config/routes/sidekiq.rb4
-rw-r--r--config/routes/snippets.rb9
-rw-r--r--config/routes/uploads.rb21
-rw-r--r--config/routes/user.rb44
-rw-r--r--config/sidekiq_queues.yml47
-rw-r--r--db/fixtures/development/06_teams.rb2
-rw-r--r--db/fixtures/development/14_pipelines.rb5
-rw-r--r--db/migrate/20160829114652_add_markdown_cache_columns.rb38
-rw-r--r--db/migrate/20160919144305_add_type_to_labels.rb14
-rw-r--r--db/migrate/20160919145149_add_group_id_to_labels.rb13
-rw-r--r--db/migrate/20161006104309_add_state_to_environment.rb15
-rw-r--r--db/migrate/20161007133303_precalculate_trending_projects.rb38
-rw-r--r--db/migrate/20161012180455_add_repository_access_level_to_project_feature.rb14
-rw-r--r--db/migrate/20161014173530_create_label_priorities.rb25
-rw-r--r--db/migrate/20161017095000_add_properties_to_deployment.rb9
-rw-r--r--db/migrate/20161017125927_add_unique_index_to_labels.rb32
-rw-r--r--db/migrate/20161018024215_migrate_labels_priority.rb36
-rw-r--r--db/migrate/20161018024550_remove_priority_from_labels.rb17
-rw-r--r--db/migrate/20161018124658_make_project_owners_masters.rb15
-rw-r--r--db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb109
-rw-r--r--db/migrate/20161019213545_generate_project_feature_for_projects.rb28
-rw-r--r--db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb63
-rw-r--r--db/schema.rb1186
-rw-r--r--doc/README.md14
-rw-r--r--doc/administration/container_registry.md96
-rw-r--r--doc/administration/environment_variables.md20
-rw-r--r--doc/administration/high_availability/gitlab.md6
-rw-r--r--doc/administration/housekeeping.md2
-rw-r--r--doc/administration/img/housekeeping_settings.pngbin19347 -> 27420 bytes
-rw-r--r--doc/administration/monitoring/performance/gitlab_configuration.md40
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md111
-rw-r--r--doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.pngbin0 -> 14368 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_dashboard_import.pngbin0 -> 18267 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_data_source_configuration.pngbin0 -> 26060 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_data_source_empty.pngbin0 -> 21821 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_save_icon.pngbin0 -> 9107 bytes
-rw-r--r--doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.pngbin0 -> 61357 bytes
-rw-r--r--doc/administration/monitoring/performance/img/request_profile_result.pngbin0 -> 9720 bytes
-rw-r--r--doc/administration/monitoring/performance/img/request_profiling_token.pngbin0 -> 30076 bytes
-rw-r--r--doc/administration/monitoring/performance/influxdb_configuration.md193
-rw-r--r--doc/administration/monitoring/performance/influxdb_schema.md97
-rw-r--r--doc/administration/monitoring/performance/introduction.md65
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md16
-rw-r--r--doc/administration/operations.md6
-rw-r--r--doc/administration/operations/cleaning_up_redis_sessions.md52
-rw-r--r--doc/administration/operations/moving_repositories.md180
-rw-r--r--doc/administration/operations/sidekiq_memory_killer.md40
-rw-r--r--doc/administration/operations/unicorn.md86
-rw-r--r--doc/administration/reply_by_email.md302
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md324
-rw-r--r--doc/administration/restart_gitlab.md2
-rw-r--r--doc/administration/troubleshooting/debug.md4
-rw-r--r--doc/api/README.md20
-rw-r--r--doc/api/award_emoji.md18
-rw-r--r--doc/api/boards.md251
-rw-r--r--doc/api/builds.md46
-rw-r--r--doc/api/ci/runners.md6
-rw-r--r--doc/api/commits.md97
-rw-r--r--doc/api/deployments.md12
-rw-r--r--doc/api/issues.md38
-rw-r--r--doc/api/keys.md2
-rw-r--r--doc/api/labels.md2
-rw-r--r--doc/api/merge_requests.md18
-rw-r--r--doc/api/notes.md6
-rw-r--r--doc/api/oauth2.md8
-rw-r--r--doc/api/pipelines.md10
-rw-r--r--doc/api/projects.md464
-rw-r--r--doc/api/settings.md12
-rw-r--r--doc/api/system_hooks.md7
-rw-r--r--doc/api/templates/gitignores.md579
-rw-r--r--doc/api/templates/gitlab_ci_ymls.md120
-rw-r--r--doc/api/templates/licenses.md (renamed from doc/api/licenses.md)8
-rw-r--r--doc/api/todos.md18
-rw-r--r--doc/api/users.md160
-rw-r--r--doc/api/version.md23
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/docker/using_docker_images.md8
-rw-r--r--doc/ci/environments.md13
-rw-r--r--doc/ci/examples/README.md2
-rw-r--r--doc/ci/examples/test-phoenix-application.md56
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/variables/README.md34
-rw-r--r--doc/ci/yaml/README.md201
-rw-r--r--doc/container_registry/README.md99
-rw-r--r--doc/container_registry/img/container_registry.pngbin222782 -> 0 bytes
-rw-r--r--doc/container_registry/img/project_feature.pngbin248750 -> 0 bytes
-rw-r--r--doc/container_registry/troubleshooting.md142
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/code_review.md11
-rw-r--r--doc/development/doc_styleguide.md33
-rw-r--r--doc/development/frontend.md236
-rw-r--r--doc/development/licensing.md2
-rw-r--r--doc/development/migration_style_guide.md8
-rw-r--r--doc/development/performance.md7
-rw-r--r--doc/development/sidekiq_style_guide.md38
-rw-r--r--doc/gitlab-basics/README.md6
-rw-r--r--doc/gitlab-basics/add-file.md28
-rw-r--r--doc/gitlab-basics/add-merge-request.md49
-rw-r--r--doc/gitlab-basics/basicsimages/add_new_merge_request.pngbin9003 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/add_sshkey.pngbin1394 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_info.pngbin7572 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_name.pngbin2137 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branches.pngbin3548 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/button-create-mr.pngbin5927 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/click-on-new-group.pngbin1957 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_changes.pngbin4941 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_message.pngbin5103 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commits.pngbin4112 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/compare_branches.pngbin1520 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_file.pngbin2451 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_group.pngbin3184 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/edit_file.pngbin2221 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_located.pngbin3078 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_name.pngbin2412 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_file.pngbin8426 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_group.pngbin5897 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/fork.pngbin896 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/group_info.pngbin15479 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/groups.pngbin4752 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/https.pngbin2822 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/image_file.pngbin2796 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issue_title.pngbin8311 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issues.pngbin4153 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/key.pngbin1177 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/merge_requests.pngbin4213 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_issue.pngbin2974 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_merge_request.pngbin3162 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_project.pngbin2234 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/newbranch.pngbin1244 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/paste_sshkey.pngbin7699 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/profile_settings.pngbin1101 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/project_info.pngbin21041 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group.pngbin6034 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group2.pngbin5040 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_branch.pngbin11207 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_project.pngbin16176 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/settings.pngbin4149 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/shh_keys.pngbin4782 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/submit_new_issue.pngbin8644 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/title_description_mr.pngbin11919 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/white_space.pngbin2192 -> 0 bytes
-rw-r--r--doc/gitlab-basics/command-line-commands.md26
-rw-r--r--doc/gitlab-basics/create-branch.md43
-rw-r--r--doc/gitlab-basics/create-group.md51
-rw-r--r--doc/gitlab-basics/create-issue.md33
-rw-r--r--doc/gitlab-basics/create-project.md27
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md38
-rw-r--r--doc/gitlab-basics/fork-project.md19
-rw-r--r--doc/gitlab-basics/img/create_new_group_info.pngbin0 -> 53103 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_group_sidebar.pngbin0 -> 5396 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin0 -> 10050 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_from_group.pngbin0 -> 6545 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin0 -> 49451 bytes
-rw-r--r--doc/gitlab-basics/img/fork_choose_namespace.pngbin0 -> 39253 bytes
-rw-r--r--doc/gitlab-basics/img/fork_new.pngbin0 -> 25540 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_new.pngbin0 -> 3596 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_page.pngbin0 -> 91432 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_select_branch.pngbin0 -> 50707 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_button.pngbin0 -> 3070 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_page.pngbin0 -> 53268 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings.pngbin0 -> 5975 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys.pngbin0 -> 42977 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.pngbin0 -> 37486 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.pngbin0 -> 18498 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_title.pngbin0 -> 2362 bytes
-rw-r--r--doc/gitlab-basics/img/project_clone_url.pngbin0 -> 40490 bytes
-rw-r--r--doc/gitlab-basics/img/project_navbar.pngbin0 -> 5745 bytes
-rw-r--r--doc/gitlab-basics/img/public_file_link.png (renamed from doc/gitlab-basics/basicsimages/public_file_link.png)bin3023 -> 3023 bytes
-rw-r--r--doc/gitlab-basics/img/select_group_dropdown.pngbin0 -> 8038 bytes
-rw-r--r--doc/gitlab-basics/start-using-git.md20
-rw-r--r--doc/incoming_email/README.md303
-rw-r--r--doc/incoming_email/postfix.md322
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/monitoring/health_check.md67
-rw-r--r--doc/monitoring/performance/gitlab_configuration.md41
-rw-r--r--doc/monitoring/performance/grafana_configuration.md112
-rw-r--r--doc/monitoring/performance/influxdb_configuration.md194
-rw-r--r--doc/monitoring/performance/influxdb_schema.md98
-rw-r--r--doc/monitoring/performance/introduction.md66
-rw-r--r--doc/operations/README.md6
-rw-r--r--doc/operations/cleaning_up_redis_sessions.md53
-rw-r--r--doc/operations/moving_repositories.md181
-rw-r--r--doc/operations/sidekiq_memory_killer.md41
-rw-r--r--doc/operations/unicorn.md87
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin33943 -> 30956 bytes
-rw-r--r--doc/raketasks/backup_hrz.pngbin8907 -> 31784 bytes
-rw-r--r--doc/raketasks/backup_restore.md63
-rw-r--r--doc/raketasks/user_management.md15
-rw-r--r--doc/university/README.md283
-rw-r--r--doc/university/bookclub/booklist.md113
-rw-r--r--doc/university/bookclub/index.md19
-rw-r--r--doc/university/glossary/README.md385
-rw-r--r--doc/update/8.0-to-8.1.md4
-rw-r--r--doc/update/8.1-to-8.2.md4
-rw-r--r--doc/update/8.10-to-8.11.md8
-rw-r--r--doc/update/8.11-to-8.12.md10
-rw-r--r--doc/update/8.12-to-8.13.md205
-rw-r--r--doc/update/8.2-to-8.3.md4
-rw-r--r--doc/update/8.3-to-8.4.md4
-rw-r--r--doc/update/8.4-to-8.5.md4
-rw-r--r--doc/update/8.5-to-8.6.md4
-rw-r--r--doc/update/8.6-to-8.7.md4
-rw-r--r--doc/update/8.7-to-8.8.md4
-rw-r--r--doc/update/8.8-to-8.9.md4
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--doc/user/admin_area/monitoring/health_check.md66
-rw-r--r--doc/user/admin_area/monitoring/img/health_check_token.png (renamed from doc/monitoring/img/health_check_token.png)bin6630 -> 6630 bytes
-rw-r--r--doc/user/markdown.md8
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/container_registry.md253
-rw-r--r--doc/user/project/cycle_analytics.md110
-rw-r--r--doc/user/project/git_attributes.md22
-rw-r--r--doc/user/project/img/container_registry_enable.pngbin0 -> 5526 bytes
-rw-r--r--doc/user/project/img/container_registry_panel.pngbin0 -> 96315 bytes
-rw-r--r--doc/user/project/img/container_registry_tab.pngbin0 -> 7284 bytes
-rw-r--r--doc/user/project/img/cycle_analytics_landing_page.pngbin58203 -> 66080 bytes
-rw-r--r--doc/user/project/img/mitmproxy-docker.png (renamed from doc/container_registry/img/mitmproxy-docker.png)bin407004 -> 407004 bytes
-rw-r--r--doc/user/project/issue_board.md7
-rw-r--r--doc/user/project/merge_requests/img/versions_compare.png (renamed from doc/user/project/merge_requests/img/versions-compare.png)bin68722 -> 68722 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_dropdown.png (renamed from doc/user/project/merge_requests/img/versions-dropdown.png)bin60587 -> 60587 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_system_note.pngbin0 -> 18731 bytes
-rw-r--r--doc/user/project/merge_requests/merge_when_build_succeeds.md20
-rw-r--r--doc/user/project/merge_requests/versions.md30
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md52
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_from_issue.pngbin0 -> 4728 bytes
-rw-r--r--doc/user/project/repository/web_editor.md4
-rw-r--r--doc/user/project/settings/import_export.md6
-rw-r--r--doc/user/project/slash_commands.md3
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/importing/migrating_from_svn.md108
-rw-r--r--docker/README.md4
-rw-r--r--features/dashboard/dashboard.feature13
-rw-r--r--features/explore/projects.feature1
-rw-r--r--features/groups.feature5
-rw-r--r--features/profile/ssh_keys.feature20
-rw-r--r--features/project/commits/commits.feature5
-rw-r--r--features/project/issues/issues.feature1
-rw-r--r--features/project/source/browse_files.feature17
-rw-r--r--features/steps/admin/groups.rb2
-rw-r--r--features/steps/admin/projects.rb2
-rw-r--r--features/steps/dashboard/dashboard.rb27
-rw-r--r--features/steps/dashboard/new_project.rb1
-rw-r--r--features/steps/group/members.rb14
-rw-r--r--features/steps/groups.rb12
-rw-r--r--features/steps/profile/ssh_keys.rb46
-rw-r--r--features/steps/project/active_tab.rb2
-rw-r--r--features/steps/project/commits/commits.rb34
-rw-r--r--features/steps/project/fork.rb1
-rw-r--r--features/steps/project/forked_merge_requests.rb14
-rw-r--r--features/steps/project/graph.rb4
-rw-r--r--features/steps/project/issues/issues.rb3
-rw-r--r--features/steps/project/issues/labels.rb2
-rw-r--r--features/steps/project/merge_requests.rb14
-rw-r--r--features/steps/project/source/browse_files.rb4
-rw-r--r--features/steps/project/team_management.rb17
-rw-r--r--features/steps/shared/note.rb4
-rw-r--r--features/steps/shared/project.rb4
-rw-r--r--features/support/capybara.rb2
-rw-r--r--features/support/db_cleaner.rb2
-rw-r--r--features/support/env.rb2
-rw-r--r--features/support/rerun.rb2
-rw-r--r--features/support/wait_for_ajax.rb11
-rw-r--r--lib/api/access_requests.rb73
-rw-r--r--lib/api/api.rb13
-rw-r--r--lib/api/award_emoji.rb64
-rw-r--r--lib/api/boards.rb132
-rw-r--r--lib/api/branches.rb48
-rw-r--r--lib/api/builds.rb162
-rw-r--r--lib/api/commit_statuses.rb62
-rw-r--r--lib/api/commits.rb163
-rw-r--r--lib/api/entities.rb31
-rw-r--r--lib/api/groups.rb3
-rw-r--r--lib/api/helpers.rb26
-rw-r--r--lib/api/keys.rb7
-rw-r--r--lib/api/labels.rb93
-rw-r--r--lib/api/license_templates.rb58
-rw-r--r--lib/api/members.rb113
-rw-r--r--lib/api/merge_requests.rb15
-rw-r--r--lib/api/milestones.rb3
-rw-r--r--lib/api/namespaces.rb22
-rw-r--r--lib/api/projects.rb68
-rw-r--r--lib/api/system_hooks.rb66
-rw-r--r--lib/api/templates.rb124
-rw-r--r--lib/api/todos.rb45
-rw-r--r--lib/api/users.rb20
-rw-r--r--lib/api/variables.rb89
-rw-r--r--lib/api/version.rb12
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb16
-rw-r--r--lib/banzai/filter/emoji_filter.rb53
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb29
-rw-r--r--lib/banzai/filter/external_link_filter.rb34
-rw-r--r--lib/banzai/filter/html_entity_filter.rb12
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb8
-rw-r--r--lib/banzai/filter/label_reference_filter.rb51
-rw-r--r--lib/banzai/filter/sanitization_filter.rb2
-rw-r--r--lib/banzai/filter/set_direction_filter.rb15
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb2
-rw-r--r--lib/banzai/filter/task_list_filter.rb20
-rw-r--r--lib/banzai/filter/user_reference_filter.rb14
-rw-r--r--lib/banzai/note_renderer.rb5
-rw-r--r--lib/banzai/object_renderer.rb53
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb1
-rw-r--r--lib/banzai/renderer.rb32
-rw-r--r--lib/ci/api/builds.rb6
-rw-r--r--lib/ci/api/helpers.rb29
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb32
-rw-r--r--lib/ci/version_info.rb52
-rw-r--r--lib/constraints/group_url_constrainer.rb7
-rw-r--r--lib/constraints/namespace_url_constrainer.rb24
-rw-r--r--lib/constraints/user_url_constrainer.rb7
-rw-r--r--lib/event_filter.rb30
-rw-r--r--lib/extracts_path.rb40
-rw-r--r--lib/gitlab/access.rb4
-rw-r--r--lib/gitlab/backend/shell.rb46
-rw-r--r--lib/gitlab/ci/config/node/environment.rb18
-rw-r--r--lib/gitlab/ci/trace_reader.rb49
-rw-r--r--lib/gitlab/conflict/file.rb62
-rw-r--r--lib/gitlab/conflict/file_collection.rb4
-rw-r--r--lib/gitlab/conflict/parser.rb15
-rw-r--r--lib/gitlab/conflict/resolution_error.rb6
-rw-r--r--lib/gitlab/diff/file.rb4
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb10
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb4
-rw-r--r--lib/gitlab/emoji.rb10
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb30
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb10
-rw-r--r--lib/gitlab/github_import/client.rb14
-rw-r--r--lib/gitlab/github_import/importer.rb3
-rw-r--r--lib/gitlab/github_import/label_formatter.rb10
-rw-r--r--lib/gitlab/github_import/project_creator.rb35
-rw-r--r--lib/gitlab/google_code_import/importer.rb24
-rw-r--r--lib/gitlab/identifier.rb58
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb13
-rw-r--r--lib/gitlab/import_export/command_line_util.rb9
-rw-r--r--lib/gitlab/import_export/file_importer.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml13
-rw-r--r--lib/gitlab/import_export/json_hash_builder.rb6
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb9
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb55
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb2
-rw-r--r--lib/gitlab/import_export/repo_saver.rb2
-rw-r--r--lib/gitlab/import_export/version_saver.rb4
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb2
-rw-r--r--lib/gitlab/issues_labels.rb4
-rw-r--r--lib/gitlab/lfs_token.rb9
-rw-r--r--lib/gitlab/project_search_results.rb6
-rw-r--r--lib/gitlab/redis.rb34
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/arguments_logger.rb2
-rw-r--r--lib/gitlab/workhorse.rb2
-rw-r--r--lib/tasks/.gitkeep0
-rw-r--r--lib/tasks/cache.rake43
-rw-r--r--lib/tasks/ce_to_ee_merge_check.rake4
-rw-r--r--lib/tasks/ci/.gitkeep0
-rw-r--r--lib/tasks/flog.rake25
-rw-r--r--lib/tasks/gitlab/backup.rake2
-rw-r--r--lib/tasks/gitlab/check.rake6
-rw-r--r--lib/tasks/gitlab/dev.rake107
-rw-r--r--lib/tasks/gitlab/shell.rake2
-rw-r--r--lib/tasks/gitlab/users.rake11
-rwxr-xr-xscripts/lint-doc.sh6
-rwxr-xr-xscripts/prepare_build.sh15
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb167
-rw-r--r--spec/controllers/namespaces_controller_spec.rb118
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb1
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb106
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb61
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb84
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb16
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb41
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb44
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb37
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb76
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb265
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb265
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb14
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb62
-rw-r--r--spec/controllers/sessions_controller_spec.rb38
-rw-r--r--spec/controllers/users_controller_spec.rb4
-rw-r--r--spec/factories/boards.rb5
-rw-r--r--spec/factories/group_members.rb1
-rw-r--r--spec/factories/label_priorities.rb7
-rw-r--r--spec/factories/labels.rb18
-rw-r--r--spec/factories/merge_requests.rb10
-rw-r--r--spec/factories/project_members.rb24
-rw-r--r--spec/factories/projects.rb32
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb25
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb81
-rw-r--r--spec/features/calendar_spec.rb4
-rw-r--r--spec/features/compare_spec.rb26
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb41
-rw-r--r--spec/features/dashboard_issues_spec.rb3
-rw-r--r--spec/features/environments_spec.rb128
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb40
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups_spec.rb78
-rw-r--r--spec/features/issues/award_emoji_spec.rb46
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb129
-rw-r--r--spec/features/issues/filter_issues_spec.rb43
-rw-r--r--spec/features/issues/form_spec.rb119
-rw-r--r--spec/features/issues/move_spec.rb2
-rw-r--r--spec/features/issues/reset_filters_spec.rb8
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb10
-rw-r--r--spec/features/issues_spec.rb5
-rw-r--r--spec/features/login_spec.rb2
-rw-r--r--spec/features/merge_requests/assign_issues_spec.rb51
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb137
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb16
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb3
-rw-r--r--spec/features/merge_requests/form_spec.rb273
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb19
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb59
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb53
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb61
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb12
-rw-r--r--spec/features/profiles/keys_spec.rb47
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb30
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb41
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb42
-rw-r--r--spec/features/projects/guest_navigation_menu_spec.rb28
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb53
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin1363770 -> 681774 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb56
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb100
-rw-r--r--spec/features/projects/members/group_links_spec.rb66
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb8
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-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/pipelines_spec.rb2
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb (renamed from spec/features/pipelines_settings_spec.rb)3
-rw-r--r--spec/features/projects_spec.rb6
-rw-r--r--spec/features/runners_spec.rb4
-rw-r--r--spec/features/security/project/private_access_spec.rb2
-rw-r--r--spec/features/signup_spec.rb8
-rw-r--r--spec/features/todos/todos_spec.rb8
-rw-r--r--spec/features/u2f_spec.rb14
-rw-r--r--spec/features/users_spec.rb53
-rw-r--r--spec/finders/access_requests_finder_spec.rb89
-rw-r--r--spec/finders/joined_groups_finder_spec.rb2
-rw-r--r--spec/finders/labels_finder_spec.rb69
-rw-r--r--spec/finders/projects_finder_spec.rb2
-rw-r--r--spec/finders/trending_projects_finder_spec.rb39
-rw-r--r--spec/fixtures/api/schemas/board.json11
-rw-r--r--spec/fixtures/api/schemas/boards.json4
-rw-r--r--spec/fixtures/api/schemas/conflicts.json137
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/fixtures/emails/commands_in_reply.eml2
-rw-r--r--spec/fixtures/emails/commands_only_reply.eml2
-rw-r--r--spec/helpers/broadcast_messages_helper_spec.rb4
-rw-r--r--spec/helpers/events_helper_spec.rb17
-rw-r--r--spec/helpers/issuables_helper_spec.rb105
-rw-r--r--spec/helpers/issues_helper_spec.rb36
-rw-r--r--spec/helpers/labels_helper_spec.rb27
-rw-r--r--spec/helpers/members_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb4
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/javascripts/activities_spec.js.es661
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es62
-rw-r--r--spec/javascripts/boards/issue_spec.js.es62
-rw-r--r--spec/javascripts/boards/list_spec.js.es62
-rw-r--r--spec/javascripts/boards/mock_data.js.es68
-rw-r--r--spec/javascripts/fixtures/event_filter.html.haml21
-rw-r--r--spec/javascripts/fixtures/gl_field_errors.html.haml15
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es6111
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js1
-rw-r--r--spec/javascripts/merge_request_widget_spec.js54
-rw-r--r--spec/javascripts/search_autocomplete_spec.js23
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js2
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb85
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb72
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb34
-rw-r--r--spec/lib/banzai/filter/html_entity_filter_spec.rb19
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb17
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb82
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/task_list_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb9
-rw-r--r--spec/lib/banzai/note_renderer_spec.rb3
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb66
-rw-r--r--spec/lib/banzai/pipeline/description_pipeline_spec.rb12
-rw-r--r--spec/lib/banzai/renderer_spec.rb74
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb48
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb10
-rw-r--r--spec/lib/constraints/namespace_url_constrainer_spec.rb35
-rw-r--r--spec/lib/constraints/user_url_constrainer_spec.rb10
-rw-r--r--spec/lib/event_filter_spec.rb49
-rw-r--r--spec/lib/extracts_path_spec.rb87
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb9
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/node/environment_spec.rb64
-rw-r--r--spec/lib/gitlab/ci/trace_reader_spec.rb40
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb11
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb8
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb24
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb26
-rw-r--r--spec/lib/gitlab/git_access_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb24
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb7
-rw-r--r--spec/lib/gitlab/identifier_spec.rb123
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml8
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb5
-rw-r--r--spec/lib/gitlab/import_export/project.json52
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb37
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb23
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb125
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml12
-rw-r--r--spec/lib/gitlab/redis_spec.rb34
-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/mailers/emails/builds_spec.rb1
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb1
-rw-r--r--spec/mailers/emails/profile_spec.rb1
-rw-r--r--spec/mailers/notify_spec.rb60
-rw-r--r--spec/models/abuse_report_spec.rb4
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/build_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb51
-rw-r--r--spec/models/commit_spec.rb8
-rw-r--r--spec/models/commit_status_spec.rb55
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb181
-rw-r--r--spec/models/concerns/expirable_spec.rb31
-rw-r--r--spec/models/concerns/has_status_spec.rb43
-rw-r--r--spec/models/concerns/mentionable_spec.rb22
-rw-r--r--spec/models/cycle_analytics/code_spec.rb88
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb2
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb2
-rw-r--r--spec/models/cycle_analytics/review_spec.rb4
-rw-r--r--spec/models/cycle_analytics/test_spec.rb6
-rw-r--r--spec/models/deploy_key_spec.rb3
-rw-r--r--spec/models/deployment_spec.rb55
-rw-r--r--spec/models/email_spec.rb5
-rw-r--r--spec/models/environment_spec.rb87
-rw-r--r--spec/models/event_spec.rb142
-rw-r--r--spec/models/external_issue_spec.rb15
-rw-r--r--spec/models/forked_project_link_spec.rb1
-rw-r--r--spec/models/group_label_spec.rb47
-rw-r--r--spec/models/group_spec.rb1
-rw-r--r--spec/models/issue/metrics_spec.rb8
-rw-r--r--spec/models/issue_spec.rb8
-rw-r--r--spec/models/key_spec.rb3
-rw-r--r--spec/models/label_link_spec.rb3
-rw-r--r--spec/models/label_priority_spec.rb20
-rw-r--r--spec/models/label_spec.rb120
-rw-r--r--spec/models/member_spec.rb245
-rw-r--r--spec/models/members/group_member_spec.rb27
-rw-r--r--spec/models/members/project_member_spec.rb61
-rw-r--r--spec/models/merge_request/metrics_spec.rb2
-rw-r--r--spec/models/merge_request_diff_spec.rb62
-rw-r--r--spec/models/merge_request_spec.rb180
-rw-r--r--spec/models/milestone_spec.rb4
-rw-r--r--spec/models/namespace_spec.rb1
-rw-r--r--spec/models/project_feature_spec.rb23
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_label_spec.rb120
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb18
-rw-r--r--spec/models/project_services/jira_service_spec.rb9
-rw-r--r--spec/models/project_services/pipeline_email_service_spec.rb182
-rw-r--r--spec/models/project_services/redmine_service_spec.rb8
-rw-r--r--spec/models/project_services/slack_service/issue_message_spec.rb6
-rw-r--r--spec/models/project_services/slack_service/merge_message_spec.rb6
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb10
-rw-r--r--spec/models/project_services/slack_service/push_message_spec.rb12
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb6
-rw-r--r--spec/models/project_spec.rb87
-rw-r--r--spec/models/project_team_spec.rb4
-rw-r--r--spec/models/repository_spec.rb101
-rw-r--r--spec/models/service_spec.rb19
-rw-r--r--spec/models/snippet_spec.rb7
-rw-r--r--spec/models/trending_project_spec.rb56
-rw-r--r--spec/models/user_spec.rb17
-rw-r--r--spec/policies/project_policy_spec.rb148
-rw-r--r--spec/requests/api/access_requests_spec.rb32
-rw-r--r--spec/requests/api/api_helpers_spec.rb39
-rw-r--r--spec/requests/api/boards_spec.rb201
-rw-r--r--spec/requests/api/branches_spec.rb190
-rw-r--r--spec/requests/api/builds_spec.rb27
-rw-r--r--spec/requests/api/commit_statuses_spec.rb4
-rw-r--r--spec/requests/api/commits_spec.rb296
-rw-r--r--spec/requests/api/fork_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb10
-rw-r--r--spec/requests/api/issues_spec.rb4
-rw-r--r--spec/requests/api/labels_spec.rb17
-rw-r--r--spec/requests/api/license_templates_spec.rb136
-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.rb8
-rw-r--r--spec/requests/api/notes_spec.rb17
-rw-r--r--spec/requests/api/project_hooks_spec.rb5
-rw-r--r--spec/requests/api/projects_spec.rb85
-rw-r--r--spec/requests/api/repositories_spec.rb8
-rw-r--r--spec/requests/api/settings_spec.rb36
-rw-r--r--spec/requests/api/system_hooks_spec.rb7
-rw-r--r--spec/requests/api/templates_spec.rb204
-rw-r--r--spec/requests/api/users_spec.rb108
-rw-r--r--spec/requests/api/version_spec.rb27
-rw-r--r--spec/requests/ci/api/builds_spec.rb43
-rw-r--r--spec/requests/git_http_spec.rb4
-rw-r--r--spec/routing/project_routing_spec.rb2
-rw-r--r--spec/routing/routing_spec.rb26
-rw-r--r--spec/services/boards/create_service_spec.rb22
-rw-r--r--spec/services/boards/issues/create_service_spec.rb33
-rw-r--r--spec/services/boards/issues/list_service_spec.rb41
-rw-r--r--spec/services/boards/issues/move_service_spec.rb36
-rw-r--r--spec/services/boards/list_service_spec.rb37
-rw-r--r--spec/services/boards/lists/create_service_spec.rb18
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb12
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb19
-rw-r--r--spec/services/boards/lists/list_service_spec.rb16
-rw-r--r--spec/services/boards/lists/move_service_spec.rb8
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb101
-rw-r--r--spec/services/ci/send_pipeline_notification_service_spec.rb48
-rw-r--r--spec/services/compare_service_spec.rb21
-rw-r--r--spec/services/create_deployment_service_spec.rb88
-rw-r--r--spec/services/event_create_service_spec.rb19
-rw-r--r--spec/services/files/update_service_spec.rb4
-rw-r--r--spec/services/git_push_service_spec.rb68
-rw-r--r--spec/services/issues/create_service_spec.rb21
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb51
-rw-r--r--spec/services/labels/transfer_service_spec.rb56
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb2
-rw-r--r--spec/services/members/destroy_service_spec.rb115
-rw-r--r--spec/services/members/request_access_service_spec.rb57
-rw-r--r--spec/services/merge_requests/assign_issues_service_spec.rb61
-rw-r--r--spec/services/merge_requests/build_service_spec.rb20
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb79
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb117
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb19
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb141
-rw-r--r--spec/services/merge_requests/update_service_spec.rb18
-rw-r--r--spec/services/notification_service_spec.rb71
-rw-r--r--spec/services/projects/destroy_service_spec.rb23
-rw-r--r--spec/services/projects/fork_service_spec.rb25
-rw-r--r--spec/services/projects/import_service_spec.rb10
-rw-r--r--spec/services/projects/transfer_service_spec.rb10
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb70
-rw-r--r--spec/services/system_note_service_spec.rb30
-rw-r--r--spec/services/todo_service_spec.rb22
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/import_export/export_file_helper.rb4
-rw-r--r--spec/support/issue_tracker_service_shared_example.rb15
-rw-r--r--spec/support/matchers/be_like_time.rb13
-rw-r--r--spec/support/matchers/have_issuable_counts.rb21
-rw-r--r--spec/support/mentionable_shared_examples.rb4
-rw-r--r--spec/support/notify_shared_examples.rb (renamed from spec/mailers/shared/notify.rb)10
-rw-r--r--spec/support/select2_helper.rb4
-rw-r--r--spec/support/test_env.rb13
-rw-r--r--spec/support/wait_for_ajax.rb4
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb1
-rw-r--r--spec/tasks/gitlab/users_rake_spec.rb38
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb2
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb97
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb4
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb2
-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.rb28
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb7
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb15
-rw-r--r--spec/views/projects/notes/_form.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/workers/build_coverage_worker_spec.rb23
-rw-r--r--spec/workers/build_finished_worker_spec.rb30
-rw-r--r--spec/workers/build_hooks_worker_spec.rb23
-rw-r--r--spec/workers/build_success_worker_spec.rb36
-rw-r--r--spec/workers/concerns/build_queue_spec.rb14
-rw-r--r--spec/workers/concerns/cronjob_queue_spec.rb18
-rw-r--r--spec/workers/concerns/dedicated_sidekiq_queue_spec.rb20
-rw-r--r--spec/workers/concerns/pipeline_queue_spec.rb14
-rw-r--r--spec/workers/concerns/repository_check_queue_spec.rb18
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb4
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb44
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb51
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb89
-rw-r--r--spec/workers/pipeline_hooks_worker_spec.rb23
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb46
-rw-r--r--spec/workers/pipeline_proccess_worker_spec.rb22
-rw-r--r--spec/workers/pipeline_success_worker_spec.rb24
-rw-r--r--spec/workers/pipeline_update_worker_spec.rb22
-rw-r--r--spec/workers/post_receive_spec.rb12
-rw-r--r--spec/workers/project_cache_worker_spec.rb38
-rw-r--r--spec/workers/trending_projects_worker_spec.rb11
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb38
-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/Go.gitignore3
-rw-r--r--vendor/gitignore/Node.gitignore3
-rw-r--r--vendor/gitignore/TeX.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore8
-rw-r--r--vendor/gitlab-ci-yml/.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml34
-rw-r--r--vendor/gitlab-ci-yml/Julia.gitlab-ci.yml54
1371 files changed, 30880 insertions, 12909 deletions
diff --git a/.csscomb.json b/.csscomb.json
index 741cc1488b5..aa6a17f7517 100644
--- a/.csscomb.json
+++ b/.csscomb.json
@@ -6,7 +6,7 @@
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
- "color-shorthand": true,
+ "color-shorthand": false,
"element-case": "lower",
"space-before-colon": "",
"space-after-colon": " ",
diff --git a/.gitattributes b/.gitattributes
index 17cbaa5eef5..ab791a4cd6c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,2 @@
-CHANGELOG merge=union
+CHANGELOG.md merge=union
*.js.es6 gitlab-language=javascript
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3f3873e57c1..76117a48730 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,9 +1,8 @@
-image: "ruby:2.3.1"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1"
cache:
key: "ruby-231"
paths:
- - vendor/apt
- vendor/ruby
variables:
@@ -23,7 +22,7 @@ before_script:
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack
- - '[ "$SETUP_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 add_limits_mysql'
stages:
- prepare
@@ -100,7 +99,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
+ - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
paths:
@@ -141,14 +140,13 @@ spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.1
.ruby-21: &ruby-21
- image: "ruby:2.1"
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
<<: *use-db
only:
- master
cache:
key: "ruby21"
paths:
- - vendor/apt
- vendor/ruby
.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
@@ -209,14 +207,16 @@ rubocop: *exec
rake haml_lint: *exec
rake scss_lint: *exec
rake brakeman: *exec
-rake flog:
- <<: *exec
- allow_failure: yes
-rake flay:
- <<: *exec
- allow_failure: yes
+rake flay: *exec
license_finder: *exec
rake downtime_check: *exec
+rake ce_to_ee_merge_check:
+ <<: *exec
+ only:
+ - branches
+ except:
+ - tags
+ allow_failure: yes
rake db:migrate:reset:
stage: test
@@ -262,6 +262,12 @@ lint-doc:
script:
- scripts/lint-doc.sh
+bundler:check:
+ stage: test
+ <<: *ruby-static-analysis
+ script:
+ - bundle check
+
bundler:audit:
stage: test
<<: *ruby-static-analysis
@@ -300,6 +306,17 @@ coverage:
- coverage/index.html
- coverage/assets/
+# Trigger docs build
+trigger_docs:
+ stage: post-test
+ before_script: []
+ cache: {}
+ artifacts: {}
+ script:
+ - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master https://gitlab.com/api/v3/projects/38069/trigger/builds"
+ only:
+ - master
+
# Notify slack in the end
notify:slack:
@@ -332,3 +349,16 @@ pages:
- public
only:
- master
+
+# Insurance in case a gem needed by one of our releases gets yanked from
+# rubygems.org in the future.
+cache gems:
+ only:
+ - tags
+ variables:
+ SETUP_DB: "false"
+ script:
+ - bundle package --all --all-platforms
+ artifacts:
+ paths:
+ - vendor/cache
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index d2a1eb56423..9b541aadad1 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -1,4 +1,4 @@
-See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html.
+See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html
## What does this MR do?
diff --git a/.rubocop.yml b/.rubocop.yml
index 5bd31ccf329..13df3f99613 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -5,7 +5,7 @@ require:
inherit_from: .rubocop_todo.yml
AllCops:
- TargetRubyVersion: 2.3
+ TargetRubyVersion: 2.1
# Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option.
@@ -453,6 +453,10 @@ Style/VariableName:
EnforcedStyle: snake_case
Enabled: true
+# Use the configured style when numbering variables.
+Style/VariableNumber:
+ Enabled: false
+
# Use when x then ... for one-line cases.
Style/WhenThen:
Enabled: true
@@ -639,6 +643,10 @@ Lint/RescueException:
Lint/ShadowedException:
Enabled: false
+# Checks for Object#to_s usage in string interpolation.
+Lint/StringConversionInInterpolation:
+ Enabled: true
+
# Do not use prefix `_` for a variable that is used.
Lint/UnderscorePrefixedVariableName:
Enabled: true
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 87520c67dd5..11b34fafa2a 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-09-14 15:44:53 -0400 using RuboCop version 0.42.0.
+# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.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: 158
+# Offense count: 160
Lint/AmbiguousRegexpLiteral:
Enabled: false
-# Offense count: 41
+# Offense count: 40
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Enabled: false
-# Offense count: 16
+# Offense count: 18
Lint/HandleExceptions:
Enabled: false
@@ -23,16 +23,21 @@ Lint/HandleExceptions:
Lint/Loop:
Enabled: false
-# Offense count: 16
+# Offense count: 19
Lint/ShadowingOuterLocalVariable:
Enabled: false
-# Offense count: 6
+# Offense count: 9
+# Cop supports --auto-correct.
+Lint/UnifiedInteger:
+ Enabled: false
+
+# Offense count: 13
# Cop supports --auto-correct.
-Lint/StringConversionInInterpolation:
+Lint/UnneededSplatExpansion:
Enabled: false
-# Offense count: 49
+# Offense count: 69
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
@@ -44,32 +49,81 @@ Lint/UnusedBlockArgument:
Lint/UnusedMethodArgument:
Enabled: false
-# Offense count: 9
-# Cop supports --auto-correct.
-Performance/PushSplat:
- Enabled: false
-
# Offense count: 2
# Cop supports --auto-correct.
Performance/RedundantBlockCall:
Enabled: false
-# Offense count: 4
+# Offense count: 5
# Cop supports --auto-correct.
Performance/RedundantMatch:
Enabled: false
-# Offense count: 27
+# Offense count: 26
# Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
Enabled: false
-# Offense count: 61
+# Offense count: 7
+RSpec/BeEql:
+ Enabled: false
+
+# Offense count: 20
+# Configuration parameters: CustomIncludeMethods.
+RSpec/EmptyExampleGroup:
+ Enabled: false
+
+# Offense count: 16
+RSpec/ExpectActual:
+ Enabled: false
+
+# Offense count: 34
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: implicit, each, example
+RSpec/HookArgument:
+ Enabled: false
+
+# Offense count: 168
+RSpec/LeadingSubject:
+ Enabled: false
+
+# Offense count: 162
+RSpec/LetSetup:
+ Enabled: false
+
+# Offense count: 10
+RSpec/MessageChain:
+ Enabled: false
+
+# Offense count: 714
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: allow, expect
+RSpec/MessageExpectation:
+ Enabled: false
+
+# Offense count: 2423
+RSpec/MultipleExpectations:
+ Max: 36
+
+# Offense count: 1504
+RSpec/NamedSubject:
+ Enabled: false
+
+# Offense count: 1335
+# Configuration parameters: MaxNesting.
+RSpec/NestedGroups:
+ Enabled: false
+
+# Offense count: 99
+RSpec/SubjectStub:
+ Enabled: false
+
+# Offense count: 64
Rails/OutputSafety:
Enabled: false
-# Offense count: 129
+# Offense count: 151
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: strict, flexible
Rails/TimeZone:
@@ -82,58 +136,63 @@ Rails/TimeZone:
Rails/Validation:
Enabled: false
-# Offense count: 273
+# Offense count: 2
+# Cop supports --auto-correct.
+Security/JSONLoad:
+ Enabled: false
+
+# Offense count: 284
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: with_first_parameter, with_fixed_indentation
Style/AlignParameters:
Enabled: false
-# Offense count: 30
+# Offense count: 28
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: always, conditionals
Style/AndOr:
Enabled: false
-# Offense count: 50
+# Offense count: 52
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 289
+# Offense count: 291
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: braces, no_braces, context_dependent
Style/BracesAroundHashParameters:
Enabled: false
-# Offense count: 5
+# Offense count: 6
Style/CaseEquality:
Enabled: false
-# Offense count: 19
+# Offense count: 26
# Cop supports --auto-correct.
Style/ColonMethodCall:
Enabled: false
-# Offense count: 3
+# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: Keywords.
# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
Style/CommentAnnotation:
Enabled: false
-# Offense count: 33
+# Offense count: 30
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Enabled: false
-# Offense count: 881
+# Offense count: 957
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
@@ -144,12 +203,12 @@ Style/DotPosition:
Style/DoubleNegation:
Enabled: false
-# Offense count: 4
+# Offense count: 6
# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
-# Offense count: 25
+# Offense count: 26
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
@@ -161,24 +220,24 @@ Style/EmptyElse:
Style/EmptyLiteral:
Enabled: false
-# Offense count: 135
+# Offense count: 140
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
Enabled: false
-# Offense count: 7
+# Offense count: 6
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Enabled: false
-# Offense count: 51
+# Offense count: 201
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 9
+# Offense count: 11
Style/IfInsideElse:
Enabled: false
@@ -188,21 +247,21 @@ Style/IfInsideElse:
Style/IfUnlessModifier:
Enabled: false
-# Offense count: 52
+# Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Style/IndentArray:
Enabled: false
-# Offense count: 97
+# Offense count: 95
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Style/IndentHash:
Enabled: false
-# Offense count: 12
+# Offense count: 29
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: line_count_dependent, lambda, literal
@@ -214,7 +273,7 @@ Style/Lambda:
Style/LineEndConcatenation:
Enabled: false
-# Offense count: 13
+# Offense count: 15
# Cop supports --auto-correct.
Style/MethodCallParentheses:
Enabled: false
@@ -223,7 +282,7 @@ Style/MethodCallParentheses:
Style/MethodMissing:
Enabled: false
-# Offense count: 85
+# Offense count: 95
# Cop supports --auto-correct.
Style/MutableConstant:
Enabled: false
@@ -240,14 +299,14 @@ Style/NestedParenthesizedCalls:
Style/Next:
Enabled: false
-# Offense count: 8
+# Offense count: 12
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
-# Offense count: 64
+# Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
@@ -259,7 +318,7 @@ Style/NumericPredicate:
Style/ParallelAssignment:
Enabled: false
-# Offense count: 264
+# Offense count: 294
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@@ -277,7 +336,7 @@ Style/PercentQLiterals:
Style/PerlBackrefs:
Enabled: false
-# Offense count: 35
+# Offense count: 38
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@@ -285,7 +344,7 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 27
+# Offense count: 26
# Cop supports --auto-correct.
Style/PreferredHashMethods:
Enabled: false
@@ -317,12 +376,12 @@ Style/RedundantException:
Style/RedundantFreeze:
Enabled: false
-# Offense count: 408
+# Offense count: 427
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 93
+# Offense count: 97
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
@@ -334,7 +393,12 @@ Style/RegexpLiteral:
Style/RescueModifier:
Enabled: false
-# Offense count: 5
+# Offense count: 114
+# Cop supports --auto-correct.
+Style/SafeNavigation:
+ Enabled: false
+
+# Offense count: 7
# Cop supports --auto-correct.
Style/SelfAssignment:
Enabled: false
@@ -351,7 +415,7 @@ Style/SingleLineBlockParams:
Style/SingleLineMethods:
Enabled: false
-# Offense count: 124
+# Offense count: 125
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
@@ -364,19 +428,19 @@ Style/SpaceBeforeBlockBraces:
Style/SpaceBeforeFirstArg:
Enabled: false
-# Offense count: 141
+# Offense count: 145
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 96
+# Offense count: 99
# Cop supports --auto-correct.
Style/SpaceInsideBrackets:
Enabled: false
-# Offense count: 62
+# Offense count: 65
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
@@ -386,21 +450,21 @@ Style/SpaceInsideParens:
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 40
+# Offense count: 41
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
-# Offense count: 30
+# Offense count: 31
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 32
+# Offense count: 33
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
@@ -414,7 +478,7 @@ Style/SymbolProc:
Style/TernaryParentheses:
Enabled: false
-# Offense count: 24
+# Offense count: 29
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 66f9975d4ce..5093702519b 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -61,7 +61,7 @@ linters:
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
- enabled: false
+ enabled: true
# Reports when you have an empty rule set.
EmptyRule:
@@ -79,7 +79,7 @@ linters:
# HEX colors should use three-character values where possible.
HexLength:
- enabled: true
+ enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
@@ -219,7 +219,7 @@ linters:
# Property values, @extend, @include, and @import directives, and variable
# declarations should always end with a semicolon.
TrailingSemicolon:
- enabled: false
+ enabled: true
# Reports lines containing trailing whitespace.
TrailingWhitespace:
diff --git a/.vagrant_enabled b/.vagrant_enabled
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/.vagrant_enabled
+++ /dev/null
diff --git a/CHANGELOG b/CHANGELOG.md
index 34614a5c719..508b9905d02 100644
--- a/CHANGELOG
+++ b/CHANGELOG.md
@@ -1,37 +1,239 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.13.0 (unreleased)
- - Use gitlab-shell v3.6.2 (GIT TRACE logging)
+## 8.14.0 (2016-11-22)
+ - Adds user project membership expired event to clarify why user was removed (Callum Dryden)
+ - Trim leading and trailing whitespace on project_path (Linus Thiel)
+ - Prevent award emoji via notes for issues/MRs authored by user (barthc)
+ - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
+ - Fix HipChat notifications rendering (airatshigapov, eisnerd)
+ - Add hover to trash icon in notes !7008 (blackst0ne)
+ - Simpler arguments passed to named_route on toggle_award_url helper method
+ - Fix: Backup restore doesn't clear cache
+ - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
+ - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
+ - Fix documents and comments on Build API `scope`
+ - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
+
+## 8.13.1 (unreleased)
+ - Fix error in generating labels
+ - Fix reply-by-email not working due to queue name mismatch
+ - Expire and build repository cache after project import
+ - Fix 404 for group pages when GitLab setup uses relative url
+ - Simpler arguments passed to named_route on toggle_award_url helper method
+ - Better handle when no users were selected for adding to group or project. (Linus Thiel)
+
+## 8.13.0 (2016-10-22)
+ - Removes extra line for empty issue description. (!7045)
+ - Fix save button on project pipeline settings page. (!6955)
+ - All Sidekiq workers now use their own queue
+ - Avoid race condition when asynchronously removing expired artifacts. (!6881)
+ - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
+ - Respond with 404 Not Found for non-existent tags (Linus Thiel)
+ - Truncate long labels with ellipsis in labels page
+ - Improve tabbing usability for sign in page (ClemMakesApps)
+ - Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint
+ - Adding members no longer silently fails when there is extra whitespace
+ - Update runner version only when updating contacted_at
+ - Add link from system note to compare with previous version
+ - Use gitlab-shell v3.6.6
+ - Ignore references to internal issues when using external issues tracker
+ - Ability to resolve merge request conflicts with editor !6374
+ - Add `/projects/visible` API endpoint (Ben Boeckel)
+ - Fix centering of custom header logos (Ashley Dumaine)
+ - Keep around commits only pipeline creation as pipeline data doesn't change over time
+ - Update duration at the end of pipeline
+ - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
+ - Add group level labels. (!6425)
+ - Fix Cycle analytics not showing correct data when filtering by date. !6906
+ - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
+ - Cancelled pipelines could be retried. !6927
+ - Updating verbiage on git basics to be more intuitive
+ - Fix project_feature record not generated on project creation
+ - Clarify documentation for Runners API (Gennady Trafimenkov)
+ - The instrumentation for Banzai::Renderer has been restored
+ - Change user & group landing page routing from /u/:username to /:username
+ - Fixed issue boards user link when in subdirectory
+ - Added documentation for .gitattributes files
+ - Move Pipeline Metrics to separate worker
- AbstractReferenceFilter caches project_refs on RequestStore when active
+ - Replaced the check sign to arrow in the show build view. !6501
+ - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
+ - ProjectCacheWorker updates caches at most once per 15 minutes per project
+ - Fix Error 500 when viewing old merge requests with bad diff data
+ - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
+ - Fix viewing merged MRs when the source project has been removed !6991
- Speed-up group milestones show page
+ - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
+ - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
+ - Fix discussion thread from emails for merge requests. !7010
+ - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
+ - Add tag shortcut from the Commit page. !6543
+ - Keep refs for each deployment
+ - Allow browsing branches that end with '.atom'
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
+ - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
- Add more tests for calendar contribution (ClemMakesApps)
+ - Update Gitlab Shell to fix some problems with moving projects between storages
+ - Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
+ - Simplify Mentionable concern instance methods
+ - API: Ability to retrieve version information (Robert Schilling)
- Fix permission for setting an issue's due date
+ - API: Multi-file commit !6096 (mahcsig)
+ - Unicode emoji are now converted to images
+ - Revert "Label list shows all issues (opened or closed) with that label"
+ - Expose expires_at field when sharing project on API
+ - Fix VueJS template tags being rendered in code comments
+ - Added copy file path button to merge request diff files
+ - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
+ - Add Issue Board API support (andrebsguedes)
+ - Allow the Koding integration to be configured through the API
+ - Add new issue button to each list on Issues Board
+ - Execute specific named route method from toggle_award_url helper method
+ - Added soft wrap button to repository file/blob editor
+ - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
+ - Show the time ago a merge request was deployed to an environment
+ - Add RTL support to markdown renderer (Ebrahim Byagowi)
+ - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
+ - Fix todos page mobile viewport layout (ClemMakesApps)
+ - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
+ - Remove redundant mixins (ClemMakesApps)
+ - Added 'Download' button to the Snippets page (Justin DiPierro)
+ - Add visibility level to project repository
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
+ - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
+ - Fix that manual jobs would no longer block jobs in the next stage. !6604
+ - Add configurable email subject suffix (Fu Xu)
+ - Use defined colour for a language when available !6748 (nilsding)
+ - Added tooltip to fork count on project show page. (Justin DiPierro)
+ - Use a ConnectionPool for Rails.cache on Sidekiq servers
+ - Replace `alias_method_chain` with `Module#prepend`
+ - Enable GitLab Import/Export for non-admin users.
+ - Preserve label filters when sorting !6136 (Joseph Frazier)
+ - MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed
+ - Take filters in account in issuable counters. !6496
+ - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
+ - Append issue template to existing description !6149 (Joseph Frazier)
+ - Trending projects now only show public projects and the list of projects is cached for a day
+ - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- Revoke button in Applications Settings underlines on hover.
+ - Use higher size on Gitlab::Redis connection pool on Sidekiq servers
+ - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
+ - Revert avoid touching file system on Build#artifacts?
+ - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
+ - Add disabled delete button to protected branches (ClemMakesApps)
+ - Add broadcast messages and alerts below sub-nav
+ - Better empty state for Groups view
+ - API: New /users/:id/events endpoint
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
+ - Replace bootstrap caret with fontawesome caret (ClemMakesApps)
+ - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
+ - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
+ - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
+ - Fix deploy status responsiveness error !6633
+ - Make searching for commits case insensitive
+ - 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)
+ - Reduce queries needed to find users using their SSH keys when pushing commits
+ - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
+ - Fix broken repository 500 errors in project list
+ - Fix the diff in the merge request view when converting a symlink to a regular file
+ - Fix Pipeline list commit column width should be adjusted
+ - Close todos when accepting merge requests via the API !6486 (tonygambone)
+ - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
+ - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
+ - Retouch environments list and deployments list
+ - Add multiple command support for all label related slash commands !6780 (barthc)
+ - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
+ - Add Nofollow for uppercased scheme in external urls !6820 (the-undefined)
+ - Allow empty merge requests !6384 (Artem Sidorenko)
+ - Grouped pipeline dropdown is a scrollable container
+ - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
+ - Fixes padding in all clipboard icons that have .btn class
+ - Fix a typo in doc/api/labels.md
+ - Fix double-escaping in activities tab (Alexandre Maia)
+ - API: all unknown routing will be handled with 404 Not Found
+ - Add docs for request profiling
+ - Delete dynamic environments
+ - Fix buggy iOS tooltip layering behavior.
+ - Make guests unable to view MRs on private projects
+ - Fix broken Project API docs (Takuya Noguchi)
+ - Migrate invalid project members (owner -> master)
+
+## 8.12.7
+
+ - Prevent running `GfmAutocomplete` setup for each diff note. !6569
+ - Fix long commit messages overflow viewport in file tree. !6573
+ - Use `gitlab-markup` gem instead of `github-markup` to fix `.rst` file rendering. !6659
+ - Prevent flash alert text from being obscured when container is fluid. !6694
+ - Fix due date being displayed as `NaN` in Safari. !6797
+ - Fix JS bug with select2 because of missing `data-field` attribute in select box. !6812
+ - Do not alter `force_remove_source_branch` options on MergeRequest unless specified. !6817
+ - Fix GFM autocomplete setup being called several times. !6840
+ - Handle case where deployment ref no longer exists. !6855
+
+## 8.12.6
+
+ - Update mailroom to 0.8.1 in Gemfile.lock !6814
+
+## 8.12.5
+
+ - Switch from request to env in ::API::Helpers. !6615
+ - Update the mail_room gem to 0.8.1 to fix a race condition with the mailbox watching thread. !6714
+ - Improve issue load time performance by avoiding ORDER BY in find_by call. !6724
+ - Add a new gitlab:users:clear_all_authentication_tokens task. !6745
+ - Don't send Private-Token (API authentication) headers to Sentry
+ - Share projects via the API only with groups the authenticated user can access
+
+## 8.12.4
+
+ - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
+ - Fix padding in build sidebar. !6506
+ - Changed compare dropdowns to dropdowns with isolated search input. !6550
+ - Fix race condition on LFS Token. !6592
+ - Fix type mismatch bug when closing Jira issue. !6619
+ - Fix lint-doc error. !6623
+ - Skip wiki creation when GitHub project has wiki enabled. !6665
+ - Fix issues importing services via Import/Export. !6667
+ - Restrict failed login attempts for users with 2FA enabled. !6668
+ - Fix failed project deletion when feature visibility set to private. !6688
+ - Prevent claiming associated model IDs via import.
+ - Set GitLab project exported file permissions to owner only
+ - Improve the way merge request versions are compared with each other
+
+## 8.12.3
+
+ - Update Gitlab Shell to support low IO priority for storage moves
+
+## 8.12.2
-v 8.12.2 (unreleased)
- - Added University content to doc/university
- 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
+ - Refactor remnants of CoffeeScript destructured opts and super !6261
+
+## 8.12.1
-v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
- Fix issue with search filter labels not displaying
- - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
-v 8.12.0
+## 8.12.0 (2016-09-22)
+
+ - Removes inconsistency regarding tagging immediatelly as merged once you create a new branch. !6408
- 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
@@ -46,7 +248,6 @@ v 8.12.0
- 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 resolve discussion buttons endpoint path
- 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.
@@ -67,6 +268,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page
+ - Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735
- Fix sorting of issues in API
- Fix download artifacts button links !6407
@@ -83,8 +285,10 @@ v 8.12.0
- 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)
+ - Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278
+ - Add pipeline email service !6019
- 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)
@@ -106,6 +310,7 @@ v 8.12.0
- 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
+ - Add due date to issue todos
- 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
@@ -125,6 +330,7 @@ v 8.12.0
- Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls
- Add BroadcastMessage API
+ - Merge request tabs are fixed when scrolling page
- 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)
@@ -190,6 +396,7 @@ v 8.12.0
- 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 branch protection API !6215
- 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
@@ -217,13 +424,28 @@ v 8.12.0
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
-v 8.11.7
+## 8.11.9
+
+ - Don't send Private-Token (API authentication) headers to Sentry
+ - Share projects via the API only with groups the authenticated user can access
+
+## 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
+
+## 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.
- Add the audit user for read only admin access. !6355 (brianp)
+ - Login/Register UX upgrade !6328
+
+## 8.11.6
-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
@@ -233,7 +455,8 @@ v 8.11.6
- Fix DB schema to match latest migration. !6256
- Exclude some pending or inactivated rows in Member scopes.
-v 8.11.5
+## 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
@@ -246,7 +469,8 @@ v 8.11.5
- Fix confidential issues being exposed as public using gitlab.com export
- Use oj gem for faster JSON processing
-v 8.11.4
+## 8.11.4
+
- Fix resolving conflicts on forks. !6082
- Fix diff commenting on merge requests created prior to 8.10. !6029
- Fix pipelines tab layout regression. !5952
@@ -263,7 +487,8 @@ v 8.11.4
- Remove gitorious. !5866
- Allow compare merge request versions
-v 8.11.3
+## 8.11.3
+
- Allow system info page to handle case where info is unavailable
- Label list shows all issues (opened or closed) with that label
- Don't show resolve conflicts link before MR status is updated
@@ -274,17 +499,20 @@ v 8.11.3
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Issues filters reset button
-v 8.11.2
+## 8.11.2
+
- Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
- Use gitlab-workhorse 0.7.11 !5983
- Does not halt the GitHub import process when an error occurs. !5763
- Fix file links on project page when default view is Files !5933
- Fixed enter key in search input not working !5888
-v 8.11.1
+## 8.11.1
+
- Pulled due to packaging error.
-v 8.11.0
+## 8.11.0 (2016-08-22)
+
- Use test coverage value from the latest successful pipeline in badge. !5862
- Add test coverage report badge. !5708
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
@@ -437,38 +665,58 @@ v 8.11.0
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
-v 8.10.10
+## 8.10.12
+
+ - Don't send Private-Token (API authentication) headers to Sentry
+ - Share projects via the API only with groups the authenticated user can access
+
+## 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
+
+## 8.10.10
+
- Allow the Rails cookie to be used for API authentication.
-v 8.10.9
+## 8.10.9
+
- Exclude some pending or inactivated rows in Member scopes
-v 8.10.8
+## 8.10.8
+
- Fix information disclosure in issue boards.
- Fix privilege escalation in project import.
-v 8.10.7
+## 8.10.7
+
- Upgrade Hamlit to 2.6.1. !5873
- Upgrade Doorkeeper to 4.2.0. !5881
-v 8.10.6
+## 8.10.6
+
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Restore "Largest repository" sort option on Admin > Projects page. !5797
- Fix privilege escalation via project export.
- Require administrator privileges to perform a project import.
-v 8.10.5
+## 8.10.5
+
- Add a data migration to fix some missing timestamps in the members table. !5670
- Revert the "Defend against 'Host' header injection" change in the source NGINX templates. !5706
- Cache project count for 5 minutes to reduce DB load. !5746 & !5754
-v 8.10.4
+## 8.10.4
+
- Don't close referenced upstream issues from a forked project.
- Fixes issue with dropdowns `enter` key not working correctly. !5544
- Fix Import/Export project import not working in HA mode. !5618
- Fix Import/Export error checking versions. !5638
-v 8.10.3
+## 8.10.3
+
- Fix Import/Export issue importing milestones and labels not associated properly. !5426
- Fix timing problems running imports on production. !5523
- Add a log message when a project is scheduled for destruction for debugging. !5540
@@ -479,7 +727,8 @@ v 8.10.3
- Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
- Fix label already exist error message in the right sidebar.
-v 8.10.2
+## 8.10.2
+
- User can now search branches by name. !5144
- Page is now properly rendered after committing the first file and creating the first branch. !5399
- Add branch or tag icon to ref in builds page. !5434
@@ -500,7 +749,8 @@ v 8.10.2
- Fix missing schema update for `20160722221922`. !5512
- Update `gitlab-shell` version to 3.2.1 in the 8.9->8.10 update guide. !5516
-v 8.10.1
+## 8.10.1
+
- Refactor repository storages documentation. !5428
- Gracefully handle case when keep-around references are corrupted or exist already. !5430
- Add detailed info on storage path mountpoints. !5437
@@ -509,7 +759,8 @@ v 8.10.1
- Ignore invalid trusted proxies in X-Forwarded-For header. !5454
- Add links to the real markdown.md file for all GFM examples. !5458
-v 8.10.0
+## 8.10.0 (2016-07-22)
+
- Fix profile activity heatmap to show correct day name (eanplatter)
- Speed up ExternalWikiHelper#get_project_wiki_path
- Expose {should,force}_remove_source_branch (Ben Boeckel)
@@ -673,20 +924,32 @@ v 8.10.0
- 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.10
+## 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
+
+## 8.9.10
+
- Allow the Rails cookie to be used for API authentication.
-v 8.9.9
+## 8.9.9
+
- Exclude some pending or inactivated rows in Member scopes
-v 8.9.8
+## 8.9.8
+
- Upgrade Doorkeeper to 4.2.0. !5881
-v 8.9.7
+## 8.9.7
+
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Require administrator privileges to perform a project import.
-v 8.9.6
+## 8.9.6
+
- Fix importing of events under notes for GitLab projects. !5154
- Fix log statements in import/export. !5129
- Fix commit avatar alignment in compare view. !5128
@@ -695,7 +958,8 @@ v 8.9.6
- Keeps issue number when importing from Gitlab.com
- Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska)
-v 8.9.5
+## 8.9.5
+
- Add more debug info to import/export and memory killer. !5108
- Fixed avatar alignment in new MR view. !5095
- Fix diff comments not showing up in activity feed. !5069
@@ -710,7 +974,8 @@ v 8.9.5
- Update RedCloth to 4.3.2 for CVE-2012-6684. !4929 (Takuya Noguchi)
- Improve the request / withdraw access button. !4860
-v 8.9.4
+## 8.9.4
+
- Fix privilege escalation issue with OAuth external users.
- Ensure references to private repos aren't shown to logged-out users.
- Fixed search field blur not removing focus. !4704
@@ -724,7 +989,8 @@ v 8.9.4
- Expiry date on pinned nav cookie. !5009
- Updated breakpoint for sidebar pinning. !5019
-v 8.9.3
+## 8.9.3
+
- Fix encrypted data backwards compatibility after upgrading attr_encrypted gem. !4963
- Fix rendering of commit notes. !4953
- Resolve "Pin should show up at 1280px min". !4947
@@ -741,12 +1007,14 @@ v 8.9.3
- Use update_columns to bypass all the dirty code on active_record. !4985
- Fix restore Rake task warning message output !4980
-v 8.9.2
+## 8.9.2
+
- Fix visibility of snippets when searching.
- Fix an information disclosure when requesting access to a group containing private projects.
- Update omniauth-saml to 1.6.0 !4951
-v 8.9.1
+## 8.9.1
+
- Refactor labels documentation. !3347
- Eager load award emoji on notes. !4628
- Fix some CI wording in documentation. !4660
@@ -790,7 +1058,8 @@ v 8.9.1
- Add SMTP as default delivery method to match gitlab-org/omnibus-gitlab!826. !4915
- Remove duplicate 'New Page' button on edit wiki page
-v 8.9.0
+## 8.9.0 (2016-06-22)
+
- Fix group visibility form layout in application settings
- Fix builds API response not including commit data
- Fix error when CI job variables key specified but not defined
@@ -945,21 +1214,26 @@ v 8.9.0
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
-v 8.8.9
+## 8.8.9
+
- Upgrade Doorkeeper to 4.2.0. !5881
-v 8.8.8
+## 8.8.8
+
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
-v 8.8.7
+## 8.8.7
+
- Fix privilege escalation issue with OAuth external users.
- Ensure references to private repos aren't shown to logged-out users.
-v 8.8.6
+## 8.8.6
+
- Fix visibility of snippets when searching.
- Update omniauth-saml to 1.6.0 !4951
-v 8.8.5
+## 8.8.5
+
- Import GitHub repositories respecting the API rate limit !4166
- Fix todos page throwing errors when you have a project pending deletion !4300
- Disable Webhooks before proceeding with the GitHub import !4470
@@ -972,12 +1246,14 @@ v 8.8.5
- Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
- Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
-v 8.8.4
+## 8.8.4
+
- Fix LDAP-based login for users with 2FA enabled. !4493
- Added descriptions to notification settings dropdown
- Due date can be removed from milestones
-v 8.8.3
+## 8.8.3
+
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
- Fixed JS error when trying to remove discussion form. !4303
- Fixed issue with button color when no CI enabled. !4287
@@ -996,7 +1272,8 @@ v 8.8.3
- Fix missing number on generated ordered list element. !4437
- Prevent disclosure of notes on confidential issues in search results.
-v 8.8.2
+## 8.8.2
+
- Added remove due date button. !4209
- Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources. !4242
- Fix Error 500 in CI charts by gracefully handling commits with no durations. !4245
@@ -1007,13 +1284,15 @@ v 8.8.2
- When creating a .gitignore file a dropdown with templates will be provided. !4075
- Fix concurrent request when updating build log in browser. !4183
-v 8.8.1
+## 8.8.1
+
- Add documentation for the "Health Check" feature
- Allow anonymous users to access a public project's pipelines !4233
- Fix MySQL compatibility in zero downtime migrations helpers
- Fix the CI login to Container Registry (the gitlab-ci-token user)
-v 8.8.0
+## 8.8.0 (2016-05-22)
+
- Implement GFM references for milestones (Alejandro Rodríguez)
- Snippets tab under user profile. !4001 (Long Nguyen)
- Fix error when using link to uploads in global snippets
@@ -1089,34 +1368,40 @@ v 8.8.0
- When creating a .gitignore file a dropdown with templates will be provided
- Shows the issue/MR list search/filter form and corrects the mobile styling for guest users. #17562
-v 8.7.9
+## 8.7.9
+
- Fix privilege escalation issue with OAuth external users.
- Ensure references to private repos aren't shown to logged-out users.
-v 8.7.8
+## 8.7.8
+
- Fix visibility of snippets when searching.
- Update omniauth-saml to 1.6.0 !4951
-v 8.7.7
+## 8.7.7
+
- Fix import by `Any Git URL` broken if the URL contains a space
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
-v 8.7.6
+## 8.7.6
+
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
- Fix import from GitLab.com to a private instance failure. !4181
- Fix external imports not finding the import data. !4106
- Fix notification delay when changing status of an issue
- Bump Workhorse to 0.7.5 so it can serve raw diffs
-v 8.7.5
+## 8.7.5
+
- Fix relative links in wiki pages. !4050
- Fix always showing build notification message when switching between merge requests !4086
- Fix an issue when filtering merge requests with more than one label. !3886
- Fix short note for the default scope on build page (Takuya Noguchi)
-v 8.7.4
+## 8.7.4
+
- Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss)
- Fix setting trusted proxies !3970
- Fix BitBucket importer bug when throwing exceptions !3941
@@ -1125,20 +1410,23 @@ v 8.7.4
- Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100
- Use a case-insensitive comparison in sanitizing URI schemes
-v 8.7.3
+## 8.7.3
+
- Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented
- Merge request widget displays TeamCity build state and code coverage correctly again.
- Fix the line code when importing PR review comments from GitHub. !4010
- Wikis are now initialized on legacy projects when checking repositories
- Remove animate.css in favor of a smaller subset of animations. !3937 (Connor Shea)
-v 8.7.2
+## 8.7.2
+
- The "New Branch" button is now loaded asynchronously
- Fix error 500 when trying to create a wiki page
- Updated spacing between notification label and button
- Label titles in filters are now escaped properly
-v 8.7.1
+## 8.7.1
+
- Throttle the update of `project.last_activity_at` to 1 minute. !3848
- Fix .gitlab-ci.yml parsing issue when hidde job is a template without script definition. !3849
- Fix license detection to detect all license files, not only known licenses. !3878
@@ -1148,7 +1436,8 @@ v 8.7.1
- Update width of search box to fix Safari bug. !3900 (Jedidiah)
- Use the `can?` helper instead of `current_user.can?`
-v 8.7.0
+## 8.7.0 (2016-04-22)
+
- Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented
- Fix vulnerability that made it possible to gain access to private labels and milestones
- The number of InfluxDB points stored per UDP packet can now be configured
@@ -1264,12 +1553,14 @@ v 8.7.0
- Add RAW build trace output and button on build page
- Add incremental build trace update into CI API
-v 8.6.9
+## 8.6.9
+
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
-v 8.6.8
+## 8.6.8
+
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
- Prevent privilege escalation via project webhook API
@@ -1282,12 +1573,14 @@ v 8.6.8
- Prevent information disclosure via project labels
- Prevent information disclosure via new merge request page
-v 8.6.7
+## 8.6.7
+
- Fix persistent XSS vulnerability in `commit_person_link` helper
- Fix persistent XSS vulnerability in Label and Milestone dropdowns
- Fix vulnerability that made it possible to enumerate private projects belonging to group
-v 8.6.6
+## 8.6.6
+
- Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- Fix revoking of authorized OAuth applications (Connor Shea). !3690
@@ -1295,7 +1588,8 @@ v 8.6.6
- Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile
-v 8.6.5
+## 8.6.5
+
- Fix importing from GitHub Enterprise. !3529
- Perform the language detection after updating merge requests in `GitPushService`, leading to faster visual feedback for the end-user. !3533
- Check permissions when user attempts to import members from another project. !3535
@@ -1304,11 +1598,13 @@ v 8.6.5
- Unblock user when active_directory is disabled and it can be found !3550
- Fix a 2FA authentication spoofing vulnerability.
-v 8.6.4
+## 8.6.4
+
- Don't attempt to fetch any tags from a forked repo (Stan Hu)
- Redesign the Labels page
-v 8.6.3
+## 8.6.3
+
- Mentions on confidential issues doesn't create todos for non-members. !3374
- Destroy related todos when an Issue/MR is deleted. !3376
- Fix error 500 when target is nil on todo list. !3376
@@ -1321,7 +1617,8 @@ v 8.6.3
- Fix issue with dropdowns not selecting values. !3478
- Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280
-v 8.6.2
+## 8.6.2
+
- Fix dropdown alignment. !3298
- Fix issuable sidebar overlaps on tablet. !3299
- Make dropdowns pixel perfect. !3337
@@ -1343,7 +1640,8 @@ v 8.6.2
- Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402
- Fixed issue with notification settings not saving. !3452
-v 8.6.1
+## 8.6.1
+
- Add option to reload the schema before restoring a database backup. !2807
- Display navigation controls on mobile. !3214
- Fixed bug where participants would not work correctly on merge requests. !3329
@@ -1358,7 +1656,8 @@ v 8.6.1
- Fixes issue with assign milestone not loading milestone list. !3346
- Fix an issue causing the Dashboard/Milestones page to be blank. !3348
-v 8.6.0
+## 8.6.0 (2016-03-22)
+
- Add ability to move issue to another project
- Prevent tokens in the import URL to be showed by the UI
- Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
@@ -1423,11 +1722,13 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
-v 8.5.13
+## 8.5.13
+
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
-v 8.5.12
+## 8.5.12
+
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
- Prevent privilege escalation via project webhook API
@@ -1438,41 +1739,51 @@ v 8.5.12
- Prevent information disclosure via project labels
- Prevent information disclosure via new merge request page
-v 8.5.11
+## 8.5.11
+
- Fix persistent XSS vulnerability in `commit_person_link` helper
-v 8.5.10
+## 8.5.10
+
- Fix a 2FA authentication spoofing vulnerability.
-v 8.5.9
+## 8.5.9
+
- Don't attempt to fetch any tags from a forked repo (Stan Hu).
-v 8.5.8
+## 8.5.8
+
- Bump Git version requirement to 2.7.4
-v 8.5.7
+## 8.5.7
+
- Bump Git version requirement to 2.7.3
-v 8.5.6
+## 8.5.6
+
- Obtain a lease before querying LDAP
-v 8.5.5
+## 8.5.5
+
- Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
-v 8.5.4
+## 8.5.4
+
- Do not cache requests for badges (including builds badge)
-v 8.5.3
+## 8.5.3
+
- Flush repository caches before renaming projects
- Sort starred projects on dashboard based on last activity by default
- Show commit message in JIRA mention comment
- Makes issue page and merge request page usable on mobile browsers.
- Improved UI for profile settings
-v 8.5.2
+## 8.5.2
+
- Fix sidebar overlapping content when screen width was below 1200px
- Don't repeat labels listed on Labels tab
- Bring the "branded appearance" feature from EE to CE
@@ -1489,7 +1800,8 @@ v 8.5.2
- Don't show "Welcome to GitLab" when the search didn't return any projects
- Add Todos documentation
-v 8.5.1
+## 8.5.1
+
- Fix group projects styles
- Show Crowd login tab when sign in is disabled and Crowd is enabled (Peter Hudec)
- Fix a set of small UI glitches in project, profile, and wiki pages
@@ -1509,7 +1821,8 @@ v 8.5.1
- Add build coverage in project's builds page (Steffen Köhler)
- Changed # to ! for merge requests in activity view
-v 8.5.0
+## 8.5.0 (2016-02-22)
+
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
- Cache various Repository methods to improve performance
- Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu)
@@ -1588,11 +1901,13 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
-v 8.4.11
+## 8.4.11
+
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
-v 8.4.10
+## 8.4.10
+
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
- Prevent privilege escalation via project webhook API
@@ -1603,28 +1918,35 @@ v 8.4.10
- Prevent information disclosure via project labels
- Prevent information disclosure via new merge request page
-v 8.4.9
+## 8.4.9
+
- Fix persistent XSS vulnerability in `commit_person_link` helper
-v 8.4.8
+## 8.4.8
+
- Fix a 2FA authentication spoofing vulnerability.
-v 8.4.7
+## 8.4.7
+
- Don't attempt to fetch any tags from a forked repo (Stan Hu).
-v 8.4.6
+## 8.4.6
+
- Bump Git version requirement to 2.7.4
-v 8.4.5
+## 8.4.5
+
- No CE-specific changes
-v 8.4.4
+## 8.4.4
+
- Update omniauth-saml gem to 1.4.2
- Prevent long-running backup tasks from timing out the database connection
- Add a Project setting to allow guests to view build logs (defaults to true)
- Sort project milestones by due date including issue editor (Oliver Rogers / Orih)
-v 8.4.3
+## 8.4.3
+
- Increase lfs_objects size column to 8-byte integer to allow files larger
than 2.1GB
- Correctly highlight MR diff when MR has merge conflicts
@@ -1635,7 +1957,8 @@ v 8.4.3
performance monitoring
- Allow autosize textareas to also be manually resized
-v 8.4.2
+## 8.4.2
+
- Bump required gitlab-workhorse version to bring in a fix for missing
artifacts in the build artifacts browser
- Get rid of those ugly borders on the file tree view
@@ -1648,14 +1971,16 @@ v 8.4.2
- Fix method undefined when using external commit status in builds
- Fix highlighting in blame view.
-v 8.4.1
+## 8.4.1
+
- Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3),
and Nokogiri (1.6.7.2)
- Fix redirect loop during import
- Fix diff highlighting for all syntax themes
- Delete project and associations in a background worker
-v 8.4.0
+## 8.4.0 (2016-01-22)
+
- Allow LDAP users to change their email if it was not set by the LDAP server
- Ensure Gravatar host looks like an actual host
- Consider re-assign as a mention from a notification point of view
@@ -1728,11 +2053,13 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
-v 8.3.10
+## 8.3.10
+
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
-v 8.3.9
+## 8.3.9
+
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
- Prevent privilege escalation via project webhook API
@@ -1741,22 +2068,28 @@ v 8.3.9
- Prevent information disclosure via project labels
- Prevent information disclosure via new merge request page
-v 8.3.8
+## 8.3.8
+
- Fix persistent XSS vulnerability in `commit_person_link` helper
-v 8.3.7
+## 8.3.7
+
- Fix a 2FA authentication spoofing vulnerability.
-v 8.3.6
+## 8.3.6
+
- Don't attempt to fetch any tags from a forked repo (Stan Hu).
-v 8.3.5
+## 8.3.5
+
- Bump Git version requirement to 2.7.4
-v 8.3.4
+## 8.3.4
+
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
-v 8.3.3
+## 8.3.3
+
- Preserve CE behavior with JIRA integration by only calling API if URL is set
- Fix duplicated branch creation/deletion events when using Web UI (Stan Hu)
- Add configurable LDAP server query timeout
@@ -1772,17 +2105,20 @@ v 8.3.3
- Fix: maintain milestone filter between Open and Closed tabs (Greg Smethells)
- Fix missing artifacts and build traces for build created before 8.3
-v 8.3.2
+## 8.3.2
+
- Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
- Add support for Google reCAPTCHA in user registration
-v 8.3.1
+## 8.3.1
+
- Fix Error 500 when global milestones have slashes (Stan Hu)
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
- Fix LDAP identity and user retrieval when special characters are used
- Move Sidekiq-cron configuration to gitlab.yml
-v 8.3.0
+## 8.3.0 (2015-12-22)
+
- Bump rack-attack to 4.3.1 for security fix (Stan Hu)
- API support for starred projects for authorized user (Zeger-Jan van de Weg)
- Add open_issues_count to project API (Stan Hu)
@@ -1850,11 +2186,13 @@ v 8.3.0
- Expose Git's version in the admin area
- Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
-v 8.2.6
+## 8.2.6
+
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
-v 8.2.5
+## 8.2.5
+
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
- Prevent privilege escalation via project webhook API
@@ -1862,10 +2200,12 @@ v 8.2.5
- Prevent information disclosure via project labels
- Prevent information disclosure via new merge request page
-v 8.2.4
+## 8.2.4
+
- Bump Git version requirement to 2.7.4
-v 8.2.3
+## 8.2.3
+
- Fix application settings cache not expiring after changes (Stan Hu)
- Fix Error 500s when creating global milestones with Unicode characters (Stan Hu)
- Update documentation for "Guest" permissions
@@ -1874,7 +2214,8 @@ v 8.2.3
- Webhook payload has an added, modified and removed properties for each commit
- Fix 500 error when creating a merge request that removes a submodule
-v 8.2.2
+## 8.2.2
+
- Fix 404 in redirection after removing a project (Stan Hu)
- Ensure cached application settings are refreshed at startup (Stan Hu)
- Fix Error 500 when viewing user's personal projects from admin page (Stan Hu)
@@ -1884,11 +2225,13 @@ v 8.2.2
- Make current user the first user in assignee dropdown in issues detail page (Stan Hu)
- Fix: duplicate email notifications on issue comments
-v 8.2.1
+## 8.2.1
+
- Forcefully update builds that didn't want to update with state machine
- Fix: saving GitLabCiService as Admin Template
-v 8.2.0
+## 8.2.0 (2015-11-22)
+
- Improved performance of finding projects and groups in various places
- Improved performance of rendering user profile pages and Atom feeds
- Expose build artifacts path as config option
@@ -1948,19 +2291,22 @@ v 8.2.0
- Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez)
- Add Award Emoji to issue and merge request pages
-v 8.1.4
+## 8.1.4
+
- Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
- Prevent redirect loop when home_page_url is set to the root URL
- Fix incoming email config defaults
- Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
-v 8.1.3
+## 8.1.3
+
- Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu)
- Spread out runner contacted_at updates
- Use issue editor as cross reference comment author when issue is edited with a new mention
- Add Facebook authentication
-v 8.1.2
+## 8.1.2
+
- Fix cloning Wiki repositories via HTTP (Stan Hu)
- Add migration to remove satellites directory
- Fix specific runners visibility
@@ -1970,10 +2316,12 @@ v 8.1.2
- Fix CI badge
- Allow developer to manage builds
-v 8.1.1
+## 8.1.1
+
- Removed, see 8.1.2
-v 8.1.0
+## 8.1.0 (2015-10-22)
+
- Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu)
- Fix duplicate repositories in GitHub import page (Stan Hu)
- Redirect to a default path if HTTP_REFERER is not set (Stan Hu)
@@ -2058,11 +2406,13 @@ v 8.1.0
- Fix padding of outdated discussion item.
- Animate the logo on hover
-v 8.0.5
+## 8.0.5
+
- Correct lookup-by-email for LDAP logins
- Fix loading spinner sometimes not being hidden on Merge Request tab switches
-v 8.0.4
+## 8.0.4
+
- Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu)
- Fix referrals for :back and relative URL installs
- Fix anchors to comments in diffs
@@ -2071,13 +2421,15 @@ v 8.0.4
- Fix search in Files
- Add full project namespace to payload of system webhooks (Ricardo Band)
-v 8.0.3
+## 8.0.3
+
- Fix URL shown in Slack notifications
- Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
- Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
- Add work_in_progress key to MR webhooks (Ben Boeckel)
-v 8.0.2
+## 8.0.2
+
- Fix default avatar not rendering in network graph (Stan Hu)
- Skip check_initd_configured_correctly on omnibus installs
- Prevent double-prefixing of help page paths
@@ -2091,10 +2443,12 @@ v 8.0.2
- Add option to use StartTLS with Reply by email IMAP server.
- Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie)
-v 8.0.1
+## 8.0.1
+
- Improve CI migration procedure and documentation
-v 8.0.0
+## 8.0.0 (2015-09-22)
+
- Fix Markdown links not showing up in dashboard activity feed (Stan Hu)
- Remove milestones from merge requests when milestones are deleted (Stan Hu)
- Fix HTML link that was improperly escaped in new user e-mail (Stan Hu)
@@ -2159,5 +2513,6 @@ 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 through 0.8.0
- - See changelogs/archive.md
+## 7.14.3 through 0.8.0
+
+- See [changelogs/archive.md](changelogs/archive.md)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d5e15bfce14..b4635e50c28 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -226,8 +226,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting merge requests`. Please include screenshots or
wireframes if the feature will also change the UI.
-Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at
-[github.com][github-mr-tracker].
+Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
If you are new to GitLab development (or web development in general), see the
[I want to contribute!](#i-want-to-contribute) section to get you started with
@@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows:
1. Fork the project into your personal space on GitLab.com
-1. Create a feature branch, branch away from `master`.
+1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
-1. Add your changes to the [CHANGELOG](CHANGELOG)
-1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide]
+1. Add your changes to the [CHANGELOG.md](CHANGELOG.md):
+ 1. If you are fixing a ~regression issue, you can add your entry to the next
+ patch release (e.g. `8.12.5` if current version is `8.12.4`)
+ 1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if
+ current version is `8.12.4`
+ 1. Please add your entry at a random place among the entries of the targeted
+ release
+1. If you are writing documentation, make sure to follow the
+ [documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by
[squashing them][git-squash]
1. Push the commit(s) to your fork
@@ -258,7 +264,7 @@ request is as follows:
1. The MR description should give a motive for your change and the method you
used to achieve it, see the [merge request description format]
(#merge-request-description-format)
-1. If the MR changes the UI it should include before and after screenshots
+1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R`
1. Link any relevant [issues][ce-tracker] in the merge request description and
@@ -270,7 +276,9 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md).
-1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
+1. When writing commit messages please follow
+ [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+ [guidelines](http://chris.beams.io/posts/git-commit/).
1. If your merge request adds one or more migrations, make sure to execute all
migrations on a fresh database before the MR is reviewed. If the review leads
to large changes in the MR, do this again once the review is complete.
@@ -305,23 +313,6 @@ Please ensure that your merge request meets the contribution acceptance criteria
When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account.
-### Merge request description format
-
-Please submit merge requests using the following template in the merge request
-description area. Copy-paste it to retain the markdown format.
-
-```
-## What does this MR do?
-
-## Are there points in the code the reviewer needs to double check?
-
-## Why was this MR needed?
-
-## What are the relevant issue numbers?
-
-## Screenshots (if relevant)
-```
-
### Contribution acceptance criteria
1. The change is as small as possible
@@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format.
aforementioned failing test
1. Your MR initially contains a single commit (please use `git rebase -i` to
squash commits)
-1. Your changes can merge without problems (if not please merge `master`, never
- rebase commits pushed to the remote server)
+1. Your changes can merge without problems (if not please rebase if you're the
+ only one working on your feature branch, otherwise, merge `master`)
1. Does not break any existing functionality
1. Fixes one specific issue or implements one specific feature (do not combine
things, send separate merge requests if needed)
@@ -352,7 +343,10 @@ description area. Copy-paste it to retain the markdown format.
entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant.
-1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
+1. If the merge request adds any new libraries (gems, JavaScript libraries,
+ etc.), they should conform to our [Licensing guidelines][license-finder-doc].
+ See the instructions in that document for help if your MR fails the
+ "license-finder" test with a "Dependencies that need approval" error.
## Changes for Stable Releases
@@ -468,7 +462,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
-[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index b72762837ea..4f2c1d15f6d 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.6.2
+3.6.6
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 100435be135..7ada0d303f3 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.8.2
+0.8.5
diff --git a/Gemfile b/Gemfile
index f80d745fff8..46245ab62d1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,7 +17,7 @@ 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'
@@ -29,7 +29,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
-gem 'omniauth-saml', '~> 1.6.0'
+gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
@@ -51,7 +51,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.6.6'
+gem 'gitlab_git', '~> 10.6.8'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -99,17 +99,18 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
-gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
-gem 'github-markup', '~> 1.4'
-gem 'redcarpet', '~> 3.3.3'
-gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~>3.6'
-gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~> 0.5.0'
-gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
-gem 'rouge', '~> 2.0'
+gem 'html-pipeline', '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
+gem 'gitlab-markup', '~> 1.5.0'
+gem 'redcarpet', '~> 3.3.3'
+gem 'RedCloth', '~> 4.3.2'
+gem 'rdoc', '~>3.6'
+gem 'org-ruby', '~> 0.9.12'
+gem 'creole', '~> 0.5.0'
+gem 'wikicloth', '0.8.1'
+gem 'asciidoctor', '~> 1.5.2'
+gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -130,7 +131,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0'
# Issue tags
-gem 'acts-as-taggable-on', '~> 3.4'
+gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 4.2'
@@ -224,14 +225,14 @@ gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0'
-gem 'request_store', '~> 1.3.0'
+gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
-gem 'sentry-raven', '~> 1.1.0'
+gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0'
@@ -295,12 +296,11 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.42.0', require: false
+ gem 'rubocop', '~> 0.43.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
@@ -308,6 +308,8 @@ group :development, :test do
gem 'license_finder', '~> 2.1.0', require: false
gem 'knapsack', '~> 1.11.0'
+
+ gem 'activerecord_sane_schema_dumper', '0.2'
end
group :test do
@@ -324,7 +326,7 @@ gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
-gem 'mail_room', '~> 0.8'
+gem 'mail_room', '~> 0.8.1'
gem 'email_reply_parser', '~> 0.5.8'
@@ -341,7 +343,7 @@ gem 'oauth2', '~> 1.2.0'
gem 'paranoia', '~> 2.0'
# Health check
-gem 'health_check', '~> 2.1.0'
+gem 'health_check', '~> 2.2.0'
# System information
gem 'vmstat', '~> 2.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 911ffcd7d9c..442184b9228 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -38,14 +38,16 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3)
railties (>= 4.0, < 5.1)
+ activerecord_sane_schema_dumper (0.2)
+ rails (>= 4, < 5)
activesupport (4.2.7.1)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
- acts-as-taggable-on (3.5.0)
- activerecord (>= 3.2, < 5)
+ acts-as-taggable-on (4.0.0)
+ activerecord (>= 4.0)
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
@@ -157,11 +159,15 @@ GEM
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
+ deckar01-task_list (1.0.5)
+ activesupport (~> 4.0)
+ html-pipeline
+ rack (~> 1.0)
default_value_for (3.0.2)
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)
@@ -209,9 +215,6 @@ 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
@@ -279,7 +282,8 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.6.6)
+ gitlab-markup (1.5.0)
+ gitlab_git (10.6.8)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -333,7 +337,7 @@ GEM
thor
tilt
hashie (3.4.4)
- health_check (2.1.0)
+ health_check (2.2.1)
rails (>= 4.0)
hipchat (1.5.2)
httparty
@@ -398,7 +402,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.8.0)
+ mail_room (0.8.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
@@ -469,9 +473,9 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
- omniauth-saml (1.6.0)
+ omniauth-saml (1.7.0)
omniauth (~> 1.3)
- ruby-saml (~> 1.3)
+ ruby-saml (~> 1.4)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
omniauth-twitter (1.2.1)
@@ -486,7 +490,7 @@ GEM
orm_adapter (0.5.0)
paranoia (2.1.4)
activerecord (~> 4.0)
- parser (2.3.1.2)
+ parser (2.3.1.4)
ast (~> 2.2)
pg (0.18.4)
pkg-config (1.1.7)
@@ -587,7 +591,7 @@ 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)
@@ -619,7 +623,7 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rubocop (0.42.0)
+ rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
@@ -631,7 +635,7 @@ GEM
crack (~> 0.4)
ruby-prof (0.16.2)
ruby-progressbar (1.8.1)
- ruby-saml (1.3.0)
+ ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.8.2)
sexp_processor (~> 4.1)
@@ -664,8 +668,8 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
- sentry-raven (1.1.0)
- faraday (>= 0.7.6)
+ sentry-raven (2.0.2)
+ faraday (>= 0.7.6, < 0.10.x)
settingslogic (2.0.9)
sexp_processor (4.7.0)
sham_rack (1.3.6)
@@ -728,8 +732,6 @@ GEM
ffi
sysexits (1.2.0)
systemu (2.6.5)
- task_list (1.0.2)
- html-pipeline
teaspoon (1.1.5)
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
@@ -746,6 +748,9 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
+ truncato (0.7.8)
+ htmlentities (~> 4.3.1)
+ nokogiri (~> 1.6.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2)
@@ -803,7 +808,8 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
- acts-as-taggable-on (~> 3.4)
+ activerecord_sane_schema_dumper (= 0.2)
+ acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
@@ -834,8 +840,9 @@ DEPENDENCIES
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
+ deckar01-task_list (= 1.0.5)
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)
@@ -845,7 +852,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)
@@ -859,9 +865,9 @@ DEPENDENCIES
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
github-linguist (~> 4.7.0)
- github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.6.6)
+ gitlab-markup (~> 1.5.0)
+ gitlab_git (~> 10.6.8)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
@@ -870,7 +876,7 @@ DEPENDENCIES
grape-entity (~> 0.4.2)
haml_lint (~> 0.18.2)
hamlit (~> 2.6.1)
- health_check (~> 2.1.0)
+ health_check (~> 2.2.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
@@ -887,7 +893,7 @@ DEPENDENCIES
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
loofah (~> 2.0.3)
- mail_room (~> 0.8)
+ mail_room (~> 0.8.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
@@ -909,7 +915,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0)
- omniauth-saml (~> 1.6.0)
+ omniauth-saml (~> 1.7.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
@@ -932,14 +938,14 @@ DEPENDENCIES
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-rails (~> 4.0.0)
- request_store (~> 1.3.0)
+ request_store (~> 1.3)
rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rubocop (~> 0.42.0)
+ rubocop (~> 0.43.0)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
@@ -949,7 +955,7 @@ DEPENDENCIES
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- sentry-raven (~> 1.1.0)
+ sentry-raven (~> 2.0.0)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
@@ -967,12 +973,12 @@ DEPENDENCIES
sprockets-es6 (~> 0.9.2)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
- task_list (~> 1.0.2)
teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
timecop (~> 0.8.0)
+ truncato (~> 0.7.8)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
@@ -988,4 +994,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.13.1
+ 1.13.2
diff --git a/README.md b/README.md
index 8236f986b56..a6b30aff5a0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# GitLab
-[![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)
+[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![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)
diff --git a/VERSION b/VERSION
index dff4cd02d5f..919f462addc 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.13.0-pre
+8.14.0-pre
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
new file mode 100644
index 00000000000..3f4ef31947b
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
new file mode 100644
index 00000000000..387628f831c
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif
new file mode 100644
index 00000000000..5f8f8ca143c
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif
new file mode 100644
index 00000000000..27a55b1d61f
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif
new file mode 100644
index 00000000000..8fe3281d2f6
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif
new file mode 100644
index 00000000000..4260e312929
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif
new file mode 100644
index 00000000000..6de166ce0a2
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif
Binary files differ
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js
deleted file mode 100644
index d4a4c7abaa1..00000000000
--- a/app/assets/javascripts/LabelManager.js
+++ /dev/null
@@ -1,115 +0,0 @@
-(function() {
- this.LabelManager = (function() {
- LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
-
- function LabelManager(opts) {
- // Defaults
- var ref, ref1, ref2;
- if (opts == null) {
- opts = {};
- }
- this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels');
- this.prioritizedLabels.sortable({
- items: 'li',
- placeholder: 'list-placeholder',
- axis: 'y',
- update: this.onPrioritySortUpdate.bind(this)
- });
- this.bindEvents();
- }
-
- LabelManager.prototype.bindEvents = function() {
- return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
- };
-
- LabelManager.prototype.onTogglePriorityClick = function(e) {
- var $btn, $label, $tooltip, _this, action;
- e.preventDefault();
- _this = e.data;
- $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);
- };
-
- LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) {
- var $from, $target, _this, url, xhr;
- if (persistState == null) {
- persistState = true;
- }
- _this = this;
- 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;
- }
- if ($from.find('li').length === 1) {
- $from.find('.empty-message').removeClass('hidden');
- }
- if (!$target.find('li').length) {
- $target.find('.empty-message').addClass('hidden');
- }
- $label.detach().appendTo($target);
- // Return if we are not persisting state
- if (!persistState) {
- return;
- }
- if (action === 'remove') {
- xhr = $.ajax({
- url: url,
- type: 'DELETE'
- });
- // Restore empty message
- if (!$from.find('li').length) {
- $from.find('.empty-message').removeClass('hidden');
- }
- } else {
- xhr = this.savePrioritySort($label, action);
- }
- return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
- };
-
- LabelManager.prototype.onPrioritySortUpdate = function() {
- var xhr;
- xhr = this.savePrioritySort();
- return xhr.fail(function() {
- return new Flash(this.errorMessage, 'alert');
- });
- };
-
- LabelManager.prototype.savePrioritySort = function() {
- return $.post({
- url: this.prioritizedLabels.data('url'),
- data: {
- label_ids: this.getSortedLabelsIds()
- }
- });
- };
-
- LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) {
- var action;
- action = originalAction === 'remove' ? 'add' : 'remove';
- this.toggleLabelPriority($label, action, false);
- return new Flash(this.errorMessage, 'alert');
- };
-
- LabelManager.prototype.getSortedLabelsIds = function() {
- var sortedIds;
- sortedIds = [];
- this.prioritizedLabels.find('li').each(function() {
- return sortedIds.push($(this).data('id'));
- });
- return sortedIds;
- };
-
- return LabelManager;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index d5e11e22be5..f4f8cf04184 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -21,16 +21,14 @@
};
Activities.prototype.toggleFilter = function(sender) {
- var event_filters, filter;
+ var filter = sender.attr("id").split("_")[0];
+
$('.event-filter .active').removeClass("active");
- event_filters = $.cookie("event_filter");
- filter = sender.attr("id").split("_")[0];
- $.cookie("event_filter", (event_filters !== filter ? filter : ""), {
+ $.cookie("event_filter", filter, {
path: gon.relative_url_root || '/'
});
- if (event_filters !== filter) {
- return sender.closest('li').toggleClass("active");
- }
+
+ sender.closest('li').toggleClass("active");
};
return Activities;
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1cd2302111e..56ec1489f89 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,12 +5,11 @@
namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
- labelsPath: "/api/:version/projects/:id/labels",
- licensePath: "/api/:version/licenses/:key",
- gitignorePath: "/api/:version/gitignores/:key",
- gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
+ labelsPath: "/:namespace_path/:project_path/labels",
+ licensePath: "/api/:version/templates/licenses/:key",
+ gitignorePath: "/api/:version/templates/gitignores/:key",
+ gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
-
group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath)
.replace(':id', group_id);
@@ -23,12 +22,13 @@
},
// Return groups list. Filtered by query
// Only active groups retrieved
- groups: function(query, skip_ldap, callback) {
+ groups: function(query, skip_ldap, skip_groups, callback) {
var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
url: url,
data: {
search: query,
+ skip_groups: skip_groups,
per_page: 20
},
dataType: "json"
@@ -65,13 +65,14 @@
return callback(projects);
});
},
- newLabel: function(project_id, data, callback) {
+ newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
- .replace(':id', project_id);
+ .replace(':namespace_path', namespace_path)
+ .replace(':project_path', project_path);
return $.ajax({
url: url,
type: "POST",
- data: data,
+ data: {'label': data},
dataType: "json"
}).done(function(label) {
return callback(label);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index c029bf3b5ca..17cbfd0e66f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -83,14 +83,15 @@
};
// Disable button if text field is empty
- window.disableButtonIfEmptyField = function(field_selector, button_selector) {
+ window.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
+ event_name = event_name || 'input';
var closest_submit, field;
field = $(field_selector);
closest_submit = field.closest('form').find(button_selector);
if (rstrip(field.val()) === "") {
closest_submit.disable();
}
- return field.on('input', function() {
+ return field.on(event_name, function() {
if (rstrip($(this).val()) === "") {
return closest_submit.disable();
} else {
@@ -247,7 +248,7 @@
$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();
}
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
deleted file mode 100644
index 68758574967..00000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js
+++ /dev/null
@@ -1,46 +0,0 @@
-
-/*= require blob/template_selector */
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.BlobCiYamlSelector = (function(superClass) {
- extend(BlobCiYamlSelector, superClass);
-
- function BlobCiYamlSelector() {
- return BlobCiYamlSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobCiYamlSelector.prototype.requestFile = function(query) {
- return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
- };
-
- return BlobCiYamlSelector;
-
- })(TemplateSelector);
-
- this.BlobCiYamlSelectors = (function() {
- function BlobCiYamlSelectors(opts) {
- var ref;
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor;
- this.$dropdowns.each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new BlobCiYamlSelector({
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown,
- editor: _this.editor
- });
- };
- })(this));
- }
-
- return BlobCiYamlSelectors;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
new file mode 100644
index 00000000000..d6ea4f84f57
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -0,0 +1,40 @@
+/*= require blob/template_selector */
+((global) => {
+
+ class BlobCiYamlSelector extends gl.TemplateSelector {
+ requestFile(query) {
+ return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
+ }
+
+ requestFileSuccess(file) {
+ return super.requestFileSuccess(file);
+ }
+ }
+
+ global.BlobCiYamlSelector = BlobCiYamlSelector;
+
+ class BlobCiYamlSelectors {
+ constructor({ editor, $dropdowns } = {}) {
+ this.editor = editor;
+ this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ const editor = this.editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobCiYamlSelector({
+ editor,
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown
+ });
+ });
+ }
+ }
+
+ global.BlobCiYamlSelectors = BlobCiYamlSelectors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 54a09e919f8..cd746b05cf6 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -18,6 +18,6 @@
return BlobGitignoreSelector;
- })(TemplateSelector);
+ })(gl.TemplateSelector);
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 9a8ef08f4e5..2701df3e6de 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -23,6 +23,6 @@
return BlobLicenseSelector;
- })(TemplateSelector);
+ })(gl.TemplateSelector);
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js
deleted file mode 100644
index 39237705e8d..00000000000
--- a/app/assets/javascripts/blob/blob_license_selectors.js
+++ /dev/null
@@ -1,25 +0,0 @@
-(function() {
- this.BlobLicenseSelectors = (function() {
- function BlobLicenseSelectors(opts) {
- var ref;
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor;
- this.$dropdowns.each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new BlobLicenseSelector({
- pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-license-selector-wrap'),
- dropdown: $dropdown,
- editor: _this.editor
- });
- };
- })(this));
- }
-
- return BlobLicenseSelectors;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
new file mode 100644
index 00000000000..153ed457559
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -0,0 +1,21 @@
+((global) => {
+ class BlobLicenseSelectors {
+ constructor({ $dropdowns, editor }) {
+ this.$dropdowns = $('.js-license-selector');
+ this.editor = editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobLicenseSelector({
+ editor,
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+ }
+
+ global.BlobLicenseSelectors = BlobLicenseSelectors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
deleted file mode 100644
index 95352164d76..00000000000
--- a/app/assets/javascripts/blob/template_selector.js
+++ /dev/null
@@ -1,100 +0,0 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.TemplateSelector = (function() {
- function TemplateSelector(opts) {
- var ref;
- if (opts == null) {
- opts = {};
- }
- this.onClick = bind(this.onClick, this);
- this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
- this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
- this.buildDropdown();
- this.bindEvents();
- this.onFilenameUpdate();
-
- this.autosizeUpdateEvent = document.createEvent('Event');
- this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
- }
-
- TemplateSelector.prototype.buildDropdown = function() {
- return this.dropdown.glDropdown({
- data: this.data,
- filterable: true,
- selectable: true,
- toggleLabel: this.toggleLabel,
- search: {
- fields: ['name']
- },
- clicked: this.onClick,
- text: function(item) {
- return item.name;
- }
- });
- };
-
- TemplateSelector.prototype.bindEvents = function() {
- return this.$input.on('keyup blur', (function(_this) {
- return function(e) {
- return _this.onFilenameUpdate();
- };
- })(this));
- };
-
- TemplateSelector.prototype.toggleLabel = function(item) {
- return item.name;
- };
-
- TemplateSelector.prototype.onFilenameUpdate = function() {
- var filenameMatches;
- if (!this.$input.length) {
- return;
- }
- filenameMatches = this.pattern.test(this.$input.val().trim());
- if (!filenameMatches) {
- this.wrapper.addClass('hidden');
- return;
- }
- return this.wrapper.removeClass('hidden');
- };
-
- TemplateSelector.prototype.onClick = function(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
- };
-
- TemplateSelector.prototype.requestFile = function(item) {
- // This `requestFile` method is an abstract method that should
- // 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() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
- };
-
- TemplateSelector.prototype.stopLoadingSpinner = function() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
- };
-
- return TemplateSelector;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
new file mode 100644
index 00000000000..4e309e480b0
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -0,0 +1,102 @@
+((global) => {
+ class TemplateSelector {
+ constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
+ this.onClick = this.onClick.bind(this);
+ this.dropdown = dropdown;
+ this.data = data;
+ this.pattern = pattern;
+ this.wrapper = wrapper;
+ this.editor = editor;
+ this.fileEndpoint = fileEndpoint;
+ this.$input = $input || $('#file_name');
+ this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
+ this.buildDropdown();
+ this.bindEvents();
+ this.onFilenameUpdate();
+
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+ }
+
+ buildDropdown() {
+ return this.dropdown.glDropdown({
+ data: this.data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: this.toggleLabel,
+ search: {
+ fields: ['name']
+ },
+ clicked: this.onClick,
+ text: function(item) {
+ return item.name;
+ }
+ });
+ }
+
+ bindEvents() {
+ return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
+ }
+
+ toggleLabel(item) {
+ return item.name;
+ }
+
+ onFilenameUpdate() {
+ var filenameMatches;
+ if (!this.$input.length) {
+ return;
+ }
+ filenameMatches = this.pattern.test(this.$input.val().trim());
+ if (!filenameMatches) {
+ this.wrapper.addClass('hidden');
+ return;
+ }
+ return this.wrapper.removeClass('hidden');
+ }
+
+ onClick(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
+
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
+
+ // To be implemented on the extending class
+ // e.g.
+ // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+ requestFileSuccess(file, { skipFocus, append } = {}) {
+ const oldValue = this.editor.getValue();
+ let newValue = file.content;
+
+ if (append && oldValue.length && oldValue !== newValue) {
+ newValue = oldValue + '\n\n' + newValue;
+ }
+
+ this.editor.setValue(newValue, 1);
+ if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
+ }
+
+ startLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ stopLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
+ }
+ }
+
+ global.TemplateSelector = TemplateSelector;
+ })(window.gl || ( window.gl = {}));
+
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index b846bab0424..8db4f6a3b28 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -22,13 +22,14 @@
// submitted textarea
})(this));
this.initModePanesAndLinks();
- new BlobLicenseSelectors({
+ this.initSoftWrap();
+ new gl.BlobLicenseSelectors({
editor: this.editor
});
new BlobGitignoreSelectors({
editor: this.editor
});
- new BlobCiYamlSelectors({
+ new gl.BlobCiYamlSelectors({
editor: this.editor
});
}
@@ -50,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) {
@@ -57,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/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 91c12570e09..d4f8f4b9420 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -28,12 +28,13 @@ $(() => {
state: Store.state,
loading: true,
endpoint: $boardApp.dataset.endpoint,
+ boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase
},
init: Store.create.bind(Store),
created () {
- gl.boardService = new BoardService(this.endpoint);
+ gl.boardService = new BoardService(this.endpoint, this.boardId);
},
ready () {
Store.disabled = this.disabled;
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 7e86f001f44..cacb36a897f 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -21,7 +21,8 @@
},
data () {
return {
- filters: Store.state.filters
+ filters: Store.state.filters,
+ showIssueForm: false
};
},
watch: {
@@ -33,6 +34,11 @@
deep: true
}
},
+ methods: {
+ showNewIssueForm() {
+ this.showIssueForm = !this.showIssueForm;
+ }
+ },
ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 63d72d857d9..ff90f2d6d75 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -8,10 +8,8 @@
data () {
return {
predefinedLabels: [
- new ListLabel({ title: 'Development', color: '#5CB85C' }),
- new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
- new ListLabel({ title: 'Production', color: '#FF5F00' }),
- new ListLabel({ title: 'Ready', color: '#FF0000' })
+ new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Doing', color: '#5CB85C' })
]
}
},
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 474805c1437..7022a29e818 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,5 @@
//= require ./board_card
+//= require ./board_new_issue
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({
components: {
- 'board-card': gl.issueBoards.BoardCard
+ 'board-card': gl.issueBoards.BoardCard,
+ 'board-new-issue': gl.issueBoards.BoardNewIssue
},
props: {
disabled: Boolean,
list: Object,
issues: Array,
loading: Boolean,
- issueLinkBase: String
+ issueLinkBase: String,
+ showIssueForm: Boolean
},
data () {
return {
@@ -73,7 +76,7 @@
group: 'issues',
sort: false,
disabled: this.disabled,
- filter: '.board-list-count',
+ filter: '.board-list-count, .is-disabled',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
new file mode 100644
index 00000000000..a4fad422eca
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -0,0 +1,58 @@
+(() => {
+ window.gl = window.gl || {};
+
+ gl.issueBoards.BoardNewIssue = Vue.extend({
+ props: {
+ list: Object,
+ showIssueForm: Boolean
+ },
+ data() {
+ return {
+ title: '',
+ error: false
+ };
+ },
+ watch: {
+ showIssueForm () {
+ this.$els.input.focus();
+ }
+ },
+ methods: {
+ submit(e) {
+ e.preventDefault();
+ if (this.title.trim() === '') return;
+
+ this.error = false;
+
+ const labels = this.list.label ? [this.list.label] : [];
+ const issue = new ListIssue({
+ title: this.title,
+ labels
+ });
+
+ this.list.newIssue(issue)
+ .then((data) => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$els.submitButton).enable();
+ })
+ .catch(() => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$els.submitButton).enable();
+
+ // Remove the issue
+ this.list.removeIssue(issue);
+
+ // Show error message
+ this.error = true;
+ this.showIssueForm = true;
+ });
+
+ this.cancel();
+ },
+ cancel() {
+ this.showIssueForm = false;
+ this.title = '';
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 1a4d8157970..6ccd83e2d84 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () {
const $this = $(this);
-
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index 44addb3ea98..f629d45c587 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
- filter: '.has-tooltip',
+ filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 91fd620fdb3..5d0a561cdba 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -87,6 +87,17 @@ class List {
});
}
+ newIssue (issue) {
+ this.addIssue(issue);
+ this.issuesSize++;
+
+ return gl.boardService.newIssue(this.id, issue)
+ .then((resp) => {
+ const data = resp.json();
+ issue.id = data.iid;
+ });
+ }
+
createIssues (data) {
data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj));
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index 9b80fb2e99f..b9c91cbf31e 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,15 +1,15 @@
class BoardService {
- constructor (root) {
+ constructor (root, boardId) {
Vue.http.options.root = root;
- this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
+ this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
- url: `${root}/lists/generate.json`
+ url: `${root}/${boardId}/lists/generate.json`
}
});
- this.issue = Vue.resource(`${root}/issues{/id}`, {});
- this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
+ this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
+ this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
@@ -58,4 +58,10 @@ class BoardService {
to_list_id
});
}
+
+ newIssue (id, issue) {
+ return this.issues.save({ id }, {
+ issue
+ });
+ }
};
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 78d21c0552a..97462a5959c 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -15,18 +15,17 @@
this.hideSidebar = bind(this.hideSidebar, this);
this.toggleSidebar = bind(this.toggleSidebar, this);
this.updateDropdown = bind(this.updateDropdown, this);
+ this.$document = $(document);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
- $('.js-build-sidebar').niceScroll();
+ this.initSidebar();
this.populateJobs(this.build_stage);
this.updateStageDropdownText(this.build_stage);
- this.hideSidebar();
- $(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);
+ this.$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) {
@@ -62,6 +61,21 @@
}
}
+ Build.prototype.initSidebar = function() {
+ this.$sidebar = $('.js-build-sidebar');
+ this.sidebarTranslationLimits = {
+ min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
+ }
+ this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
+ this.$sidebar.css({
+ top: this.sidebarTranslationLimits.max
+ });
+ this.$sidebar.niceScroll();
+ this.hideSidebar();
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+ this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
+ };
+
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
@@ -129,15 +143,23 @@
Build.prototype.toggleSidebar = function() {
if (this.shouldHideSidebar()) {
- return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed');
+ return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed');
}
};
+ Build.prototype.translateSidebar = function(e) {
+ var newPosition = this.sidebarTranslationLimits.max - document.body.scrollTop;
+ if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
+ this.$sidebar.css({
+ top: newPosition
+ });
+ };
+
Build.prototype.hideSidebar = function() {
if (this.shouldHideSidebar()) {
- return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
} else {
- return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
}
};
@@ -146,7 +168,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
- return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' '));
+ return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
}
};
diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image_file.js
index e893491b19b..e893491b19b 100644
--- a/app/assets/javascripts/commit/image-file.js
+++ b/app/assets/javascripts/commit/image_file.js
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js.es6
index 4e3a28cd163..9a2082d97e0 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -9,7 +9,10 @@
var $dropdown, selected;
$dropdown = $(this);
selected = $dropdown.data('selected');
- return $dropdown.glDropdown({
+ const $dropdownContainer = $dropdown.closest('.dropdown');
+ const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
+ const $filterInput = $('input[type="search"]', $dropdownContainer);
+ $dropdown.glDropdown({
data: function(term, callback) {
return $.ajax({
url: $dropdown.data('refs-url'),
@@ -23,8 +26,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) {
@@ -41,6 +45,14 @@
return $el.text().trim();
}
});
+ $filterInput.on('keyup', (e) => {
+ const keyCode = e.keyCode || e.which;
+ if (keyCode !== 13) return;
+ const text = $filterInput.val();
+ $fieldInput.val(text);
+ $('.dropdown-toggle-text', $dropdown).text(text);
+ $dropdownContainer.removeClass('open');
+ });
});
};
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 3e20db7e308..e23bda2fa4e 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -26,15 +26,15 @@
};
showTooltip = function(target, title) {
- return $(target).tooltip({
- container: 'body',
- html: 'true',
- placement: 'auto bottom',
- title: title,
- trigger: 'manual'
- }).tooltip('show').one('mouseleave', function() {
- return $(this).tooltip('hide');
- });
+ var $target = $(target);
+ var originalTitle = $target.data('original-title');
+
+ $target
+ .attr('title', 'Copied!')
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
};
$(function() {
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 46d1c3f00c1..c5f8c29242d 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,8 +1,9 @@
(function (w) {
class CreateLabelDropdown {
- constructor ($el, projectId) {
+ constructor ($el, namespacePath, projectPath) {
this.$el = $el;
- this.projectId = projectId;
+ this.namespacePath = namespacePath;
+ this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el);
@@ -91,8 +92,8 @@
e.preventDefault();
e.stopPropagation();
- Api.newLabel(this.projectId, {
- name: this.$newLabelField.val(),
+ Api.newLabel(this.namespacePath, this.projectPath, {
+ title: this.$newLabelField.val(),
color: this.$newColorField.val()
}, (label) => {
this.$newLabelCreateButton.enable();
diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6
index cd9886ba58d..20791bab942 100644
--- a/app/assets/javascripts/cycle-analytics.js.es6
+++ b/app/assets/javascripts/cycle_analytics.js.es6
@@ -1,3 +1,5 @@
+//= require vue
+
((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
@@ -34,7 +36,11 @@
method: 'GET',
dataType: 'json',
contentType: 'application/json',
- data: { start_date: options.startDate }
+ data: {
+ cycle_analytics: {
+ start_date: options.startDate
+ }
+ }
}).done((data) => {
this.decorateData(data);
this.initDropdown();
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index c8634b78f2b..8086c10ad6b 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -7,6 +7,9 @@
function Diff() {
$('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton();
+ if (this.diffViewType() === 'parallel') {
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+ }
$(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) {
return function(event) {
@@ -52,6 +55,10 @@
})(this));
}
+ Diff.prototype.diffViewType = function() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) {
return [0, 0];
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js.es6
index ddf11ecf34c..afc0d6f8c62 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -8,6 +8,7 @@
Dispatcher = (function() {
function Dispatcher() {
this.initSearch();
+ this.initFieldErrors();
this.initPageScripts();
}
@@ -20,13 +21,17 @@
path = page.split(':');
shortcut_handler = null;
switch (page) {
+ case 'sessions:new':
+ new UsernameValidator();
+ break;
case 'projects:boards:show':
+ case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
- new IssuableBulkActions();
+ new gl.IssuableBulkActions();
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
@@ -40,12 +45,12 @@
new Milestone();
break;
case 'dashboard:todos:index':
- new Todos();
+ new gl.Todos();
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
new ZenMode();
- new DueDateSelect();
+ new gl.DueDateSelectors();
new GLForm($('.milestone-form'));
break;
case 'groups:milestones:new':
@@ -59,7 +64,9 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
- new IssuableTemplateSelectors();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
@@ -67,7 +74,9 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
- new IssuableTemplateSelectors();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new gl.IssuableTemplateSelectors();
break;
case 'projects:tags:new':
new ZenMode();
@@ -92,9 +101,6 @@
new ZenMode();
new MergedButtons();
break;
- case "projects:merge_requests:conflicts":
- window.mcui = new MergeConflictResolver()
- break;
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
@@ -122,6 +128,9 @@
new TreeView();
}
break;
+ case 'projects:pipelines:show':
+ new gl.Pipelines();
+ break;
case 'groups:activity':
new Activities();
break;
@@ -132,12 +141,12 @@
break;
case 'groups:group_members:index':
new gl.MemberExpirationDate();
- new GroupMembers();
+ new gl.Members();
new UsersSelect();
break;
case 'projects:project_members:index':
new gl.MemberExpirationDate();
- new ProjectMembers();
+ new gl.Members();
new UsersSelect();
break;
case 'groups:new':
@@ -159,13 +168,15 @@
shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true);
break;
+ case 'groups:labels:new':
+ case 'groups:labels:edit':
case 'projects:labels:new':
case 'projects:labels:edit':
new Labels();
break;
case 'projects:labels:index':
if ($('.prioritized-labels').length) {
- new LabelManager();
+ new gl.LabelManager();
}
break;
case 'projects:network:show':
@@ -279,10 +290,16 @@
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
- return new SearchAutocomplete();
+ return new gl.SearchAutocomplete();
}
};
+ Dispatcher.prototype.initFieldErrors = function() {
+ $('.show-gl-field-errors').each((i, form) => {
+ new gl.GlFieldErrors(form);
+ });
+ };
+
return Dispatcher;
})();
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
deleted file mode 100644
index bf68b7e3a9b..00000000000
--- a/app/assets/javascripts/due_date_select.js
+++ /dev/null
@@ -1,107 +0,0 @@
-(function() {
- this.DueDateSelect = (function() {
- function DueDateSelect() {
- var $datePicker, $dueDate, $loading;
- // Milestone edit/new form
- $datePicker = $('.datepicker');
- if ($datePicker.length) {
- $dueDate = $('#milestone_due_date');
- $datePicker.datepicker({
- dateFormat: 'yy-mm-dd',
- onSelect: function(dateText, inst) {
- return $dueDate.val(dateText);
- }
- }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
- }
- $('.js-clear-due-date').on('click', function(e) {
- 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;
- $dropdown = $(dropdown);
- $dropdownParent = $dropdown.closest('.dropdown');
- $datePicker = $dropdownParent.find('.js-due-date-calendar');
- $block = $dropdown.closest('.block');
- $selectbox = $dropdown.closest('.selectbox');
- $value = $block.find('.value');
- $valueContent = $block.find('.value-content');
- $sidebarValue = $('.js-due-date-sidebar-value', $block);
- fieldName = $dropdown.data('field-name');
- abilityName = $dropdown.data('ability-name');
- issueUpdateURL = $dropdown.data('issue-update');
- $dropdown.glDropdown({
- hidden: function() {
- $selectbox.hide();
- return $value.css('display', '');
- }
- });
- 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'), ','));
- mediumDate = $.datepicker.formatDate('M d, yy', date);
- } else {
- mediumDate = 'No due date';
- }
- data = {};
- data[abilityName] = {};
- data[abilityName].due_date = value;
- return $.ajax({
- type: 'PUT',
- url: issueUpdateURL,
- data: data,
- dataType: 'json',
- beforeSend: function() {
- var cssClass;
- $loading.fadeIn();
- if (isDropdown) {
- $dropdown.trigger('loading.gl.dropdown');
- $selectbox.hide();
- }
- $value.css('display', '');
- cssClass = Date.parse(mediumDate) ? 'bold' : 'no-value';
- $valueContent.html("<span class='" + cssClass + "'>" + mediumDate + "</span>");
- $sidebarValue.html(mediumDate);
- if (value !== '') {
- return $('.js-remove-due-date-holder').removeClass('hidden');
- } else {
- return $('.js-remove-due-date-holder').addClass('hidden');
- }
- }
- }).done(function(data) {
- if (isDropdown) {
- $dropdown.trigger('loaded.gl.dropdown');
- $dropdown.dropdown('toggle');
- }
- return $loading.fadeOut();
- });
- };
- $block.on('click', '.js-remove-due-date', function(e) {
- e.preventDefault();
- $("input[name='" + fieldName + "']").val('');
- return addDueDate(false);
- });
- return $datePicker.datepicker({
- dateFormat: 'yy-mm-dd',
- defaultDate: $("input[name='" + fieldName + "']").val(),
- altField: "input[name='" + fieldName + "']",
- onSelect: function() {
- return addDueDate(true);
- }
- });
- });
- $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', function(e) {
- return e.stopImmediatePropagation();
- });
- }
-
- return DueDateSelect;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
new file mode 100644
index 00000000000..41925fcc8e3
--- /dev/null
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -0,0 +1,161 @@
+(function(global) {
+ class DueDateSelect {
+ constructor({ $dropdown, $loading } = {}) {
+ const $dropdownParent = $dropdown.closest('.dropdown');
+ const $block = $dropdown.closest('.block');
+ this.$loading = $loading;
+ this.$dropdown = $dropdown;
+ this.$dropdownParent = $dropdownParent;
+ this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
+ this.$block = $block;
+ this.$selectbox = $dropdown.closest('.selectbox');
+ this.$value = $block.find('.value');
+ this.$valueContent = $block.find('.value-content');
+ this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
+ this.fieldName = $dropdown.data('field-name'),
+ this.abilityName = $dropdown.data('ability-name'),
+ this.issueUpdateURL = $dropdown.data('issue-update')
+
+ this.rawSelectedDate = null;
+ this.displayedDate = null;
+ this.datePayload = null;
+
+ this.initGlDropdown();
+ this.initRemoveDueDate();
+ this.initDatePicker();
+ this.initStopPropagation();
+ }
+
+ initGlDropdown() {
+ this.$dropdown.glDropdown({
+ hidden: () => {
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+ }
+ });
+ }
+
+ initDatePicker() {
+ this.$datePicker.datepicker({
+ dateFormat: 'yy-mm-dd',
+ defaultDate: $("input[name='" + this.fieldName + "']").val(),
+ altField: "input[name='" + this.fieldName + "']",
+ onSelect: () => {
+ return this.saveDueDate(true);
+ }
+ });
+ }
+
+ initRemoveDueDate() {
+ this.$block.on('click', '.js-remove-due-date', (e) => {
+ e.preventDefault();
+ $("input[name='" + this.fieldName + "']").val('');
+ return this.saveDueDate(false);
+ });
+ }
+
+ initStopPropagation() {
+ $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
+ return e.stopImmediatePropagation();
+ });
+ }
+
+ saveDueDate(isDropdown) {
+ this.parseSelectedDate();
+ this.prepSelectedDate();
+ this.submitSelectedDate(isDropdown);
+ }
+
+ parseSelectedDate() {
+ this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val();
+ if (this.rawSelectedDate.length) {
+ let dateObj = new Date(this.rawSelectedDate);
+ this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
+ } else {
+ this.displayedDate = 'No due date';
+ }
+ }
+
+ prepSelectedDate() {
+ const datePayload = {};
+ datePayload[this.abilityName] = {};
+ datePayload[this.abilityName].due_date = this.rawSelectedDate;
+ this.datePayload = datePayload;
+ }
+
+ submitSelectedDate(isDropdown) {
+ return $.ajax({
+ type: 'PUT',
+ url: this.issueUpdateURL,
+ data: this.datePayload,
+ dataType: 'json',
+ beforeSend: () => {
+ const selectedDateValue = this.datePayload[this.abilityName].due_date;
+ const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+
+ this.$loading.fadeIn();
+
+ if (isDropdown) {
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
+ }
+
+ this.$value.css('display', '');
+ this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
+ this.$sidebarValue.html(this.displayedDate);
+
+ return selectedDateValue.length ?
+ $('.js-remove-due-date-holder').removeClass('hidden') :
+ $('.js-remove-due-date-holder').addClass('hidden');
+
+ }
+ }).done((data) => {
+ if (isDropdown) {
+ this.$dropdown.trigger('loaded.gl.dropdown');
+ this.$dropdown.dropdown('toggle');
+ }
+ return this.$loading.fadeOut();
+ });
+ }
+ }
+
+ class DueDateSelectors {
+ constructor() {
+ this.initMilestoneDueDate();
+ this.initIssuableSelect();
+ }
+
+ initMilestoneDueDate() {
+ const $datePicker = $('.datepicker');
+
+ if ($datePicker.length) {
+ const $dueDate = $('#milestone_due_date');
+ $datePicker.datepicker({
+ dateFormat: 'yy-mm-dd',
+ onSelect: (dateText, inst) => {
+ $dueDate.val(dateText);
+ }
+ }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
+ }
+ $('.js-clear-due-date').on('click', (e) => {
+ e.preventDefault();
+ $.datepicker._clearDate($datePicker);
+ });
+ }
+
+ initIssuableSelect() {
+ const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+
+ $('.js-due-date-select').each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ new DueDateSelect({
+ $dropdown,
+ $loading
+ });
+ });
+ }
+ }
+
+ global.DueDateSelectors = DueDateSelectors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index d0786bf0053..845313b6b38 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -52,37 +52,27 @@
}
}
},
- setup: function(input) {
+ setup: _.debounce(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) {
- this.dataLoading = true;
- setTimeout((function(_this) {
- return function() {
- var fetch;
- fetch = _this.fetchData(_this.dataSource);
- return fetch.done(function(data) {
- _this.dataLoading = false;
- 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) {
- return this.loadData(this.cachedData);
- }
+
+ if (this.dataSource && !this.dataLoading && !this.cachedData) {
+ this.dataLoading = true;
+ return this.fetchData(this.dataSource)
+ .done((data) => {
+ this.dataLoading = false;
+ this.loadData(data);
+ });
+ };
+
+ if (this.cachedData != null) {
+ return this.loadData(this.cachedData);
}
- },
+ }, 1000),
setupAtWho: function() {
// Emoji
this.input.atwho({
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 1b6db641200..53762f2965c 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -25,7 +25,7 @@
return function(e) {
e.preventDefault();
e.stopPropagation();
- return _this.input.val('').trigger('keyup').focus();
+ return _this.input.val('').trigger('input').focus();
};
})(this));
// Key events
@@ -37,28 +37,16 @@
e.preventDefault()
}
})
- .on('keyup', function(e) {
- var keyCode;
- keyCode = e.which;
- if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) {
- return;
- }
+ .on('input', function() {
if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.addClass(HAS_VALUE_CLASS);
} else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS);
}
- 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() {
- var blurField = this.shouldBlur(keyCode);
- if (blurField && this.filterInputBlur) {
- this.input.blur();
- }
return this.options.query(this.input.val(), function(data) {
return this.options.callback(data);
}.bind(this));
@@ -255,7 +243,7 @@
_this.fullData = data;
_this.parseData(_this.fullData);
if (_this.options.filterable && _this.filter && _this.filter.input) {
- return _this.filter.input.trigger('keyup');
+ return _this.filter.input.trigger('input');
}
};
// Remote data
@@ -443,6 +431,7 @@
var contentHtml;
this.resetRows();
this.addArrowKeyEvent();
+
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
}
@@ -460,9 +449,21 @@
if (this.options.filterable) {
this.filterInput.focus();
}
+
+ if (this.options.showMenuAbove) {
+ this.positionMenuAbove();
+ }
+
return this.dropdown.trigger('shown.gl.dropdown');
};
+ GitLabDropdown.prototype.positionMenuAbove = function() {
+ var $button = $(this.el);
+ var $menu = this.dropdown.find('.dropdown-menu');
+
+ $menu.css('top', ($button.height() + $menu.height()) * -1);
+ };
+
GitLabDropdown.prototype.hidden = function(e) {
var $input;
this.resetRows();
@@ -474,7 +475,7 @@
// 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");
+ $input.trigger("input");
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
@@ -487,14 +488,27 @@
// Render the full menu
GitLabDropdown.prototype.renderMenu = function(html) {
- var menu_html;
- menu_html = "";
if (this.options.renderMenu) {
- menu_html = this.options.renderMenu(html);
+ return this.options.renderMenu(html);
} else {
- menu_html = $('<ul />').append(html);
+ var ul = document.createElement('ul');
+
+ for (var i = 0; i < html.length; i++) {
+ var el = html[i];
+
+ if (el instanceof jQuery) {
+ el = el.get(0);
+ }
+
+ if (typeof el === 'string') {
+ ul.innerHTML += el;
+ } else {
+ ul.appendChild(el);
+ }
+ }
+
+ return ul;
}
- return menu_html;
};
// Append the menu into the dropdown
@@ -508,7 +522,7 @@
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
- var cssClass, field, fieldName, groupAttrs, html, selected, text, url, value;
+ var field, fieldName, html, selected, text, url, value;
if (group == null) {
group = false;
}
@@ -516,18 +530,16 @@
// 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>";
+ html = document.createElement('li');
+ if (data === 'divider' || data === 'separator') {
+ html.className = data;
+ return html;
}
// Header
if (data.header != null) {
- return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
+ html.className = 'dropdown-header';
+ html.innerHTML = data.header;
+ return html;
}
if (this.options.renderRow) {
// Call the render function
@@ -554,24 +566,25 @@
} else {
text = data.text != null ? data.text : '';
}
- cssClass = "";
- if (selected) {
- cssClass = "is-active";
- }
if (this.highlight) {
text = this.highlightTextMatches(text, this.filterInput.val());
}
+ // Create the list item & the link
+ var link = document.createElement('a');
+
+ link.href = url;
+ link.innerHTML = text;
+
+ if (selected) {
+ link.className = 'is-active';
+ }
+
if (group) {
- groupAttrs = 'data-group=' + group + ' data-index=' + index;
- } else {
- groupAttrs = '';
+ link.dataset.group = group;
+ link.dataset.index = index;
}
- html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
- url: url,
- groupAttrs: groupAttrs,
- cssClass: cssClass,
- text: text
- });
+
+ html.appendChild(link);
}
return html;
};
@@ -725,6 +738,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
+ e.preventDefault();
_this.selectRowAtIndex();
}
};
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
new file mode 100644
index 00000000000..8657e7b4abf
--- /dev/null
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -0,0 +1,167 @@
+((global) => {
+ /*
+ * This class overrides the browser's validation error bubbles, displaying custom
+ * error messages for invalid fields instead. To begin validating any form, add the
+ * class `show-gl-field-errors` to the form element, and ensure error messages are
+ * declared in each inputs' title attribute.
+ *
+ * Example:
+ *
+ * <form class='show-gl-field-errors'>
+ * <input type='text' name='username' title='Username is required.'/>
+ *</form>
+ *
+ * */
+
+ const errorMessageClass = 'gl-field-error';
+ const inputErrorClass = 'gl-field-error-outline';
+
+ class GlFieldError {
+ constructor({ input, formErrors }) {
+ this.inputElement = $(input);
+ this.inputDomElement = this.inputElement.get(0);
+ this.form = formErrors;
+ this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
+ this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${ this.errorMessage }</p>`);
+
+ this.state = {
+ valid: false,
+ empty: true
+ };
+
+ this.initFieldValidation();
+ }
+
+ initFieldValidation() {
+ // hidden when injected into DOM
+ this.inputElement.after(this.fieldErrorElement);
+ this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
+ this.scopedSiblings = this.safelySelectSiblings();
+ }
+
+ safelySelectSiblings() {
+ // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled with input validity
+ const ignoreSelector = '.validation-ignore';
+ const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreSelector})`);
+ const parentContainer = this.inputElement.parent('.form-group');
+
+ // Only select siblings when they're scoped within a form-group with one input
+ const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
+
+ return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
+ }
+
+ renderValidity() {
+ this.renderClear();
+
+ if (this.state.valid) {
+ return this.renderValid();
+ }
+
+ if (this.state.empty) {
+ return this.renderEmpty();
+ }
+
+ if (!this.state.valid) {
+ return this.renderInvalid();
+ }
+
+ }
+
+ handleInvalidSubmit(event) {
+ event.preventDefault();
+ const currentValue = this.accessCurrentValue();
+ this.state.valid = false;
+ this.state.empty = currentValue === '';
+
+ this.renderValidity();
+ this.form.focusOnFirstInvalid.apply(this.form);
+ // For UX, wait til after first invalid submission to check each keyup
+ this.inputElement.off('keyup.field_validator')
+ .on('keyup.field_validator', this.updateValidity.bind(this));
+
+ }
+
+ /* Get or set current input value */
+ accessCurrentValue(newVal) {
+ return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
+ }
+
+ getInputValidity() {
+ return this.inputDomElement.validity.valid;
+ }
+
+ updateValidity() {
+ const inputVal = this.accessCurrentValue();
+ this.state.empty = !inputVal.length;
+ this.state.valid = this.getInputValidity();
+ this.renderValidity();
+ }
+
+ renderValid() {
+ return this.renderClear();
+ }
+
+ renderEmpty() {
+ return this.renderInvalid();
+ }
+
+ renderInvalid() {
+ this.inputElement.addClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ return this.fieldErrorElement.show();
+ }
+
+ renderClear() {
+ const inputVal = this.accessCurrentValue();
+ if (!inputVal.split(' ').length) {
+ const trimmedInput = inputVal.trim();
+ this.accessCurrentValue(trimmedInput);
+ }
+ this.inputElement.removeClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ this.fieldErrorElement.hide();
+ }
+ }
+
+ const customValidationFlag = 'no-gl-field-errors';
+
+ class GlFieldErrors {
+ constructor(form) {
+ this.form = $(form);
+ this.state = {
+ inputs: [],
+ valid: false
+ };
+ this.initValidators();
+ }
+
+ initValidators () {
+ // select all non-hidden inputs in form
+ this.state.inputs = this.form.find(':input:not([type=hidden])').toArray()
+ .filter((input) => !input.classList.contains(customValidationFlag))
+ .map((input) => new GlFieldError({ input, formErrors: this }));
+
+ this.form.on('submit', this.catchInvalidFormSubmit);
+ }
+
+ /* Neccessary to prevent intercept and override invalid form submit
+ * because Safari & iOS quietly allow form submission when form is invalid
+ * and prevents disabling of invalid submit button by application.js */
+
+ catchInvalidFormSubmit (event) {
+ if (!event.currentTarget.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ focusOnFirstInvalid () {
+ const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
+ firstInvalid.inputElement.focus();
+ }
+ }
+
+ global.GlFieldErrors = GlFieldErrors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js
deleted file mode 100644
index 4382dd6860f..00000000000
--- a/app/assets/javascripts/groups.js
+++ /dev/null
@@ -1,13 +0,0 @@
-(function() {
- this.GroupMembers = (function() {
- function GroupMembers() {
- $('li.group_member').bind('ajax:success', function() {
- return $(this).fadeOut();
- });
- }
-
- return GroupMembers;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 7c2eebcdd44..5f06186504b 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -5,14 +5,15 @@
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
return function(i, select) {
- var skip_ldap;
+ var skip_ldap, skip_groups;
skip_ldap = $(select).hasClass('skip_ldap');
+ skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({
placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
- return Api.groups(query.term, skip_ldap, function(groups) {
+ return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
var data;
data = {
results: groups
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 73e2664e9c0..57f7e4ef230 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -51,7 +51,6 @@
}).remove();
// Submit the form to get new data
Issuable.filterResults($('.filter-form'));
- return $('.js-label-select').trigger('update.label');
});
},
filterResults: (function(_this) {
diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 62a7fc9a06c..0808f538f01 100644
--- a/app/assets/javascripts/issues-bulk-assignment.js
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,13 +1,10 @@
-(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('.issuable-list > li');
- // Save instance
+((global) => {
+
+ class IssuableBulkActions {
+ constructor({ container, form, issues } = {}) {
+ this.container = container || $('.content'),
+ this.form = form || this.getElement('.bulk-update');
+ this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
@@ -15,53 +12,46 @@
Issuable.initChecks();
}
- IssuableBulkActions.prototype.getElement = function(selector) {
+ getElement(selector) {
return this.container.find(selector);
- };
+ }
- IssuableBulkActions.prototype.bindEvents = function() {
+ bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
- };
+ }
- IssuableBulkActions.prototype.onFormSubmit = function(e) {
+ onFormSubmit(e) {
e.preventDefault();
return this.submit();
- };
+ }
- IssuableBulkActions.prototype.submit = function() {
- var _this, xhr;
- _this = this;
- xhr = $.ajax({
+ submit() {
+ const _this = this;
+ const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
- xhr.done(function(response, status, xhr) {
- return location.reload();
- });
- xhr.fail(function() {
- return new Flash("Issue update failed");
- });
+ xhr.done(() => window.location.reload());
+ xhr.fail(() => new Flash("Issue update failed"));
return xhr.always(this.onFormSubmitAlways.bind(this));
- };
+ }
- IssuableBulkActions.prototype.onFormSubmitAlways = function() {
+ onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable();
- };
+ }
- IssuableBulkActions.prototype.getSelectedIssues = function() {
+ getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
- };
+ }
- IssuableBulkActions.prototype.getLabelsFromSelection = function() {
- var labels;
- labels = [];
+ getLabelsFromSelection() {
+ const labels = [];
this.getSelectedIssues().map(function() {
- var _labels;
- _labels = $(this).data('labels');
- if (_labels) {
- return _labels.map(function(labelId) {
+ const labelsData = $(this).data('labels');
+ if (labelsData) {
+ return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
@@ -69,7 +59,7 @@
}
});
return labels;
- };
+ }
/**
@@ -77,25 +67,21 @@
* @return {Array} Label IDs
*/
- IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() {
- var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result;
- result = [];
- labelsToKeep = [];
- ref = this.getElement('.labels-filter .is-indeterminate');
- for (i = 0, len = ref.length; i < len; i++) {
- el = ref[i];
- labelsToKeep.push($(el).data('labelId'));
- }
- 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
+ getUnmarkedIndeterminedLabels() {
+ const result = [];
+ const labelsToKeep = [];
+
+ this.getElement('.labels-filter .is-indeterminate')
+ .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+
+ this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
- }
+ });
+
return result;
- };
+ }
/**
@@ -103,9 +89,8 @@
* Returns key/value pairs from form data
*/
- IssuableBulkActions.prototype.getFormDataAsObject = function() {
- var formData;
- formData = {
+ getFormDataAsObject() {
+ const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
@@ -125,19 +110,18 @@
});
}
return formData;
- };
+ }
- IssuableBulkActions.prototype.getLabelsToApply = function() {
- var $labels, labelIds;
- labelIds = [];
- $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
+ getLabelsToApply() {
+ const labelIds = [];
+ const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels.each(function(k, label) {
if (label) {
return labelIds.push(parseInt($(label).val()));
}
});
return labelIds;
- };
+ }
/**
@@ -145,11 +129,10 @@
* @return {Array} Array of labels IDs
*/
- IssuableBulkActions.prototype.getLabelsToRemove = function() {
- var indeterminatedLabels, labelsToApply, result;
- result = [];
- indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
- labelsToApply = this.getLabelsToApply();
+ getLabelsToRemove() {
+ const result = [];
+ const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
+ const 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
@@ -158,10 +141,9 @@
}
});
return result;
- };
-
- return IssuableBulkActions;
+ }
+ }
- })();
+ global.IssuableBulkActions = IssuableBulkActions;
-}).call(this);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
new file mode 100644
index 00000000000..bc68e53504f
--- /dev/null
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -0,0 +1,106 @@
+((global) => {
+
+ class LabelManager {
+ constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
+ this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
+ this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
+ this.otherLabels = otherLabels || $('.js-other-labels');
+ this.errorMessage = 'Unable to update label prioritization at this time';
+ this.prioritizedLabels.sortable({
+ items: 'li',
+ placeholder: 'list-placeholder',
+ axis: 'y',
+ update: this.onPrioritySortUpdate.bind(this)
+ });
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
+ }
+
+ onTogglePriorityClick(e) {
+ e.preventDefault();
+ const _this = e.data;
+ const $btn = $(e.currentTarget);
+ const $label = $(`#${$btn.data('domId')}`);
+ const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+ const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
+ $tooltip.tooltip('destroy');
+ return _this.toggleLabelPriority($label, action);
+ }
+
+ toggleLabelPriority($label, action, persistState) {
+ if (persistState == null) {
+ persistState = true;
+ }
+ let xhr;
+ const _this = this;
+ const url = $label.find('.js-toggle-priority').data('url');
+ let $target = this.prioritizedLabels;
+ let $from = this.otherLabels;
+ if (action === 'remove') {
+ $target = this.otherLabels;
+ $from = this.prioritizedLabels;
+ }
+ if ($from.find('li').length === 1) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ if (!$target.find('li').length) {
+ $target.find('.empty-message').addClass('hidden');
+ }
+ $label.detach().appendTo($target);
+ // Return if we are not persisting state
+ if (!persistState) {
+ return;
+ }
+ if (action === 'remove') {
+ xhr = $.ajax({
+ url,
+ type: 'DELETE'
+ });
+ // Restore empty message
+ if (!$from.find('li').length) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ } else {
+ xhr = this.savePrioritySort($label, action);
+ }
+ return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
+ }
+
+ onPrioritySortUpdate() {
+ const xhr = this.savePrioritySort();
+ return xhr.fail(function() {
+ return new Flash(this.errorMessage, 'alert');
+ });
+ }
+
+ savePrioritySort() {
+ return $.post({
+ url: this.prioritizedLabels.data('url'),
+ data: {
+ label_ids: this.getSortedLabelsIds()
+ }
+ });
+ }
+
+ rollbackLabelPosition($label, originalAction) {
+ const action = originalAction === 'remove' ? 'add' : 'remove';
+ this.toggleLabelPriority($label, action, false);
+ return new Flash(this.errorMessage, 'alert');
+ }
+
+ getSortedLabelsIds() {
+ const sortedIds = [];
+ this.prioritizedLabels.find('li').each(function() {
+ sortedIds.push($(this).data('id'));
+ });
+ return sortedIds;
+ }
+ }
+
+ gl.LabelManager = LabelManager;
+
+})(window.gl || (window.gl = {}));
+
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index ce79e2e348a..b4f6e70f694 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,9 +4,11 @@
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, initialSelected;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
$dropdown = $(dropdown);
- projectId = $dropdown.data('project-id');
+ $toggleText = $dropdown.find('.dropdown-toggle-text');
+ namespacePath = $dropdown.data('namespace-path');
+ projectPath = $dropdown.data('project-path');
labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
@@ -15,6 +17,7 @@
}
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
+ showMenuAbove = $dropdown.data('showMenuAbove');
defaultLabel = $dropdown.data('default-label');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
@@ -24,6 +27,9 @@
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
+ 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 () {
@@ -40,12 +46,12 @@
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
}
saveLabelData = function() {
var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
+ selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
return this.value;
}).get();
@@ -75,7 +81,8 @@
if (data.labels.length) {
template = labelHTMLTemplate(data);
labelCount = data.labels.length;
- } else {
+ }
+ else {
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
@@ -92,7 +99,8 @@
}
labelTooltipTitle = labelTitles.join(', ');
- } else {
+ }
+ else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
@@ -114,6 +122,7 @@
});
};
return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: labelUrl
@@ -133,23 +142,29 @@
};
}).value();
if ($dropdown.hasClass('js-extra-options')) {
+ var extraData = [];
if (showNo) {
- data.unshift({
+ extraData.unshift({
id: 0,
title: 'No Label'
});
}
if (showAny) {
- data.unshift({
+ extraData.unshift({
isAny: true,
title: 'Any Label'
});
}
- if (data.length > 2) {
- data.splice(2, 0, 'divider');
+ if (extraData.length) {
+ extraData.push('divider');
+ data = extraData.concat(data);
}
}
- return callback(data);
+
+ callback(data);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
});
},
renderRow: function(label, instance) {
@@ -157,7 +172,7 @@
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
- removesAll = label.id === 0 || (label.id == null);
+ removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = instance.indeterminateIds;
active = instance.activeIds;
@@ -194,14 +209,16 @@
return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
}).join(',');
color = "linear-gradient(" + color + ")";
- } else {
+ }
+ else {
if (label.color != null) {
color = label.color[0];
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- } else {
+ }
+ else {
colorEl = '';
}
// We need to identify which items are actually labels
@@ -219,30 +236,46 @@
},
selectable: true,
filterable: true,
+ selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) {
- var selected_labels;
- selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active');
- if (selected && (selected.title != null)) {
- if (selected_labels.length > 1) {
- return selected.title + " +" + (selected_labels.length - 1) + " more";
- } else {
- return selected.title;
- }
- } else if (!selected && selected_labels.length !== 0) {
- if (selected_labels.length > 1) {
- return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more";
- } else if (selected_labels.length === 1) {
- return $(selected_labels).text();
- }
- } else {
+ var isSelected = el !== null ? el.hasClass('is-active') : false;
+ var title = selected.title;
+ var selectedLabels = this.selected;
+
+ if (selected.id === 0) {
+ this.selected = [];
+ return 'No Label';
+ }
+ else if (isSelected) {
+ this.selected.push(title);
+ }
+ else {
+ var index = this.selected.indexOf(title);
+ this.selected.splice(index, 1);
+ }
+
+ if (selectedLabels.length === 1) {
+ return selectedLabels;
+ }
+ else if (selectedLabels.length) {
+ return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ }
+ else {
return defaultLabel;
}
},
fieldName: $dropdown.data('field-name'),
id: function(label) {
+ if (label.id <= 0) return label.title;
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return label.id;
+ }
+
if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
return label.title;
- } else {
+ }
+ else {
return label.id;
}
},
@@ -254,16 +287,23 @@
$selectbox.hide();
// display:block overrides the hide-collapse rule
$value.removeAttr('style');
- if (page === 'projects:boards:show') {
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
+
+ if ($('html').hasClass('issue-boards-page')) {
return;
}
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
Issuable.filterResults($dropdown.closest('form'));
- } else if ($dropdown.hasClass('js-filter-submit')) {
+ }
+ else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
- } else {
+ }
+ else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
}
@@ -280,18 +320,28 @@
clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
- if ($dropdown.hasClass('js-filter-bulk-update')) {
+
+ if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
+ $dropdown.parent()
+ .find('.dropdown-clear-active')
+ .removeClass('is-active')
+ }
+
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
return;
}
+
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
- if (page === 'projects:boards:show') {
+ if ($('html').hasClass('issue-boards-page')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
- } else if ($el.hasClass('is-active')) {
+ }
+ else if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
- } else {
+ }
+ else {
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title;
@@ -302,17 +352,21 @@
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
return;
- } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ }
+ else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
}
- } else if ($dropdown.hasClass('js-filter-submit')) {
+ }
+ else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
- } else {
+ }
+ else {
if ($dropdown.hasClass('js-multiselect')) {
- } else {
+ }
+ else {
return saveLabelData();
}
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9299d0eabd2..b170e26eebf 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -38,6 +38,11 @@
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
+ gl.utils.parseUrl = function (url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ return parser;
+ };
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 1935af491f7..e1532fd9ec4 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -14,14 +14,18 @@
inputs.datepicker({
dateFormat: 'yy-mm-dd',
minDate: 1,
- onSelect: toggleClearInput
+ onSelect: function () {
+ $(this).trigger('change');
+ toggleClearInput.call(this);
+ }
});
inputs.next('.js-clear-input').on('click', function(event) {
event.preventDefault();
var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
- input.datepicker('setDate', null);
+ input.datepicker('setDate', null)
+ .trigger('change');
toggleClearInput.call(input);
});
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
new file mode 100644
index 00000000000..2bdd0f7a637
--- /dev/null
+++ b/app/assets/javascripts/members.js.es6
@@ -0,0 +1,37 @@
+((w) => {
+ w.gl = w.gl || {};
+
+ class Members {
+ constructor() {
+ this.addListeners();
+ }
+
+ addListeners() {
+ $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
+ $('.js-member-update-control').off('change').on('change', this.formSubmit);
+ $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
+ disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
+ }
+
+ removeRow(e) {
+ const $target = $(e.target);
+
+ if ($target.hasClass('btn-remove')) {
+ $target.closest('.member')
+ .fadeOut(function () {
+ $(this).remove();
+ });
+ }
+ }
+
+ formSubmit() {
+ $(this).closest('form').trigger("submit.rails").end().disable();
+ }
+
+ formSuccess() {
+ $(this).find('.js-member-update-control').enable();
+ }
+ }
+
+ gl.Members = Members;
+})(window);
diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6
deleted file mode 100644
index cd92df8ddc5..00000000000
--- a/app/assets/javascripts/merge_conflict_data_provider.js.es6
+++ /dev/null
@@ -1,341 +0,0 @@
-const HEAD_HEADER_TEXT = 'HEAD//our changes';
-const ORIGIN_HEADER_TEXT = 'origin//their changes';
-const HEAD_BUTTON_TITLE = 'Use ours';
-const ORIGIN_BUTTON_TITLE = 'Use theirs';
-
-
-class MergeConflictDataProvider {
-
- getInitialData() {
- const diffViewType = $.cookie('diff_view');
-
- return {
- isLoading : true,
- hasError : false,
- isParallel : diffViewType === 'parallel',
- diffViewType : diffViewType,
- isSubmitting : false,
- conflictsData : {},
- resolutionData : {}
- }
- }
-
-
- decorateData(vueInstance, data) {
- this.vueInstance = vueInstance;
-
- if (data.type === 'error') {
- vueInstance.hasError = true;
- data.errorMessage = data.message;
- }
- else {
- data.shortCommitSha = data.commit_sha.slice(0, 7);
- data.commitMessage = data.commit_message;
-
- this.setParallelLines(data);
- this.setInlineLines(data);
- this.updateResolutionsData(data);
- }
-
- vueInstance.conflictsData = data;
- vueInstance.isSubmitting = false;
-
- const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
- vueInstance.conflictsData.conflictsText = conflictsText;
- }
-
-
- updateResolutionsData(data) {
- const vi = this.vueInstance;
-
- data.files.forEach( (file) => {
- file.sections.forEach( (section) => {
- if (section.conflict) {
- vi.$set(`resolutionData['${section.id}']`, false);
- }
- });
- });
- }
-
-
- setParallelLines(data) {
- data.files.forEach( (file) => {
- file.filePath = this.getFilePath(file);
- file.iconClass = `fa-${file.blob_icon}`;
- file.blobPath = file.blob_path;
- file.parallelLines = [];
- const linesObj = { left: [], right: [] };
-
- file.sections.forEach( (section) => {
- const { conflict, lines, id } = section;
-
- if (conflict) {
- linesObj.left.push(this.getOriginHeaderLine(id));
- linesObj.right.push(this.getHeadHeaderLine(id));
- }
-
- lines.forEach( (line) => {
- const { type } = line;
-
- if (conflict) {
- if (type === 'old') {
- linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
- }
- else if (type === 'new') {
- linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
- }
- }
- else {
- const lineType = type || 'context';
-
- linesObj.left.push (this.getLineForParallelView(line, id, lineType));
- linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
- }
- });
-
- this.checkLineLengths(linesObj);
- });
-
- for (let i = 0, len = linesObj.left.length; i < len; i++) {
- file.parallelLines.push([
- linesObj.right[i],
- linesObj.left[i]
- ]);
- }
-
- });
- }
-
-
- checkLineLengths(linesObj) {
- let { left, right } = linesObj;
-
- if (left.length !== right.length) {
- if (left.length > right.length) {
- const diff = left.length - right.length;
- for (let i = 0; i < diff; i++) {
- right.push({ lineType: 'emptyLine', richText: '' });
- }
- }
- else {
- const diff = right.length - left.length;
- for (let i = 0; i < diff; i++) {
- left.push({ lineType: 'emptyLine', richText: '' });
- }
- }
- }
- }
-
-
- setInlineLines(data) {
- data.files.forEach( (file) => {
- file.iconClass = `fa-${file.blob_icon}`;
- file.blobPath = file.blob_path;
- file.filePath = this.getFilePath(file);
- file.inlineLines = []
-
- file.sections.forEach( (section) => {
- let currentLineType = 'new';
- const { conflict, lines, id } = section;
-
- if (conflict) {
- file.inlineLines.push(this.getHeadHeaderLine(id));
- }
-
- lines.forEach( (line) => {
- const { type } = line;
-
- if ((type === 'new' || type === 'old') && currentLineType !== type) {
- currentLineType = type;
- file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
- }
-
- this.decorateLineForInlineView(line, id, conflict);
- file.inlineLines.push(line);
- })
-
- if (conflict) {
- file.inlineLines.push(this.getOriginHeaderLine(id));
- }
- });
- });
- }
-
-
- handleSelected(sectionId, selection) {
- const vi = this.vueInstance;
-
- vi.resolutionData[sectionId] = selection;
- vi.conflictsData.files.forEach( (file) => {
- file.inlineLines.forEach( (line) => {
- if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
- this.markLine(line, selection);
- }
- });
-
- file.parallelLines.forEach( (lines) => {
- const left = lines[0];
- const right = lines[1];
- const hasSameId = right.id === sectionId || left.id === sectionId;
- const isLeftMatch = left.hasConflict || left.isHeader;
- const isRightMatch = right.hasConflict || right.isHeader;
-
- if (hasSameId && (isLeftMatch || isRightMatch)) {
- this.markLine(left, selection);
- this.markLine(right, selection);
- }
- })
- });
- }
-
-
- updateViewType(newType) {
- const vi = this.vueInstance;
-
- if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
- return;
- }
-
- vi.diffView = newType;
- vi.isParallel = newType === 'parallel';
- $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
- $('.content-wrapper .container-fluid').toggleClass('container-limited');
- }
-
-
- markLine(line, selection) {
- if (selection === 'head' && line.isHead) {
- line.isSelected = true;
- line.isUnselected = false;
- }
- else if (selection === 'origin' && line.isOrigin) {
- line.isSelected = true;
- line.isUnselected = false;
- }
- else {
- line.isSelected = false;
- line.isUnselected = true;
- }
- }
-
-
- getConflictsCount() {
- return Object.keys(this.vueInstance.resolutionData).length;
- }
-
-
- getResolvedCount() {
- let count = 0;
- const data = this.vueInstance.resolutionData;
-
- for (const id in data) {
- const resolution = data[id];
- if (resolution) {
- count++;
- }
- }
-
- return count;
- }
-
-
- isReadyToCommit() {
- const { conflictsData, isSubmitting } = this.vueInstance
- const allResolved = this.getConflictsCount() === this.getResolvedCount();
- const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
-
- return !isSubmitting && hasCommitMessage && allResolved;
- }
-
-
- getCommitButtonText() {
- const initial = 'Commit conflict resolution';
- const inProgress = 'Committing...';
- const vue = this.vueInstance;
-
- return vue ? vue.isSubmitting ? inProgress : initial : initial;
- }
-
-
- decorateLineForInlineView(line, id, conflict) {
- const { type } = line;
- line.id = id;
- line.hasConflict = conflict;
- line.isHead = type === 'new';
- line.isOrigin = type === 'old';
- line.hasMatch = type === 'match';
- line.richText = line.rich_text;
- line.isSelected = false;
- line.isUnselected = false;
- }
-
- getLineForParallelView(line, id, lineType, isHead) {
- const { old_line, new_line, rich_text } = line;
- const hasConflict = lineType === 'conflict';
-
- return {
- id,
- lineType,
- hasConflict,
- isHead : hasConflict && isHead,
- isOrigin : hasConflict && !isHead,
- hasMatch : lineType === 'match',
- lineNumber : isHead ? new_line : old_line,
- section : isHead ? 'head' : 'origin',
- richText : rich_text,
- isSelected : false,
- isUnselected : false
- }
- }
-
-
- getHeadHeaderLine(id) {
- return {
- id : id,
- richText : HEAD_HEADER_TEXT,
- buttonTitle : HEAD_BUTTON_TITLE,
- type : 'new',
- section : 'head',
- isHeader : true,
- isHead : true,
- isSelected : false,
- isUnselected: false
- }
- }
-
-
- getOriginHeaderLine(id) {
- return {
- id : id,
- richText : ORIGIN_HEADER_TEXT,
- buttonTitle : ORIGIN_BUTTON_TITLE,
- type : 'old',
- section : 'origin',
- isHeader : true,
- isOrigin : true,
- isSelected : false,
- isUnselected: false
- }
- }
-
-
- handleFailedRequest(vueInstance, data) {
- vueInstance.hasError = true;
- vueInstance.conflictsData.errorMessage = 'Something went wrong!';
- }
-
-
- getCommitData() {
- return {
- commit_message: this.vueInstance.conflictsData.commitMessage,
- sections: this.vueInstance.resolutionData
- }
- }
-
-
- getFilePath(file) {
- const { old_path, new_path } = file;
- return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
- }
-
-}
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
deleted file mode 100644
index b56fd5aa658..00000000000
--- a/app/assets/javascripts/merge_conflict_resolver.js.es6
+++ /dev/null
@@ -1,83 +0,0 @@
-//= require vue
-
-class MergeConflictResolver {
-
- constructor() {
- this.dataProvider = new MergeConflictDataProvider()
- this.initVue()
- }
-
-
- initVue() {
- const that = this;
- this.vue = new Vue({
- el : '#conflicts',
- name : 'MergeConflictResolver',
- data : this.dataProvider.getInitialData(),
- created : this.fetchData(),
- computed : this.setComputedProperties(),
- methods : {
- handleSelected(sectionId, selection) {
- that.dataProvider.handleSelected(sectionId, selection);
- },
- handleViewTypeChange(newType) {
- that.dataProvider.updateViewType(newType);
- },
- commit() {
- that.commit();
- }
- }
- })
- }
-
-
- setComputedProperties() {
- const dp = this.dataProvider;
-
- return {
- conflictsCount() { return dp.getConflictsCount() },
- resolvedCount() { return dp.getResolvedCount() },
- readyToCommit() { return dp.isReadyToCommit() },
- commitButtonText() { return dp.getCommitButtonText() }
- }
- }
-
-
- fetchData() {
- const dp = this.dataProvider;
-
- $.get($('#conflicts').data('conflictsPath'))
- .done((data) => {
- dp.decorateData(this.vue, data);
- })
- .error((data) => {
- dp.handleFailedRequest(this.vue, data);
- })
- .always(() => {
- this.vue.isLoading = false;
-
- this.vue.$nextTick(() => {
- $('#conflicts .js-syntax-highlight').syntaxHighlight();
- });
-
- if (this.vue.diffViewType === 'parallel') {
- $('.content-wrapper .container-fluid').removeClass('container-limited');
- }
- })
- }
-
-
- commit() {
- this.vue.isSubmitting = true;
-
- $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
- .done((data) => {
- window.location.href = data.redirect_to;
- })
- .error(() => {
- this.vue.isSubmitting = false;
- new Flash('Something went wrong!');
- });
- }
-
-}
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
new file mode 100644
index 00000000000..5012bdfe997
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -0,0 +1,93 @@
+((global) => {
+
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ global.mergeConflicts.diffFileEditor = Vue.extend({
+ props: {
+ file: Object,
+ onCancelDiscardConfirmation: Function,
+ onAcceptDiscardConfirmation: Function
+ },
+ data() {
+ return {
+ saved: false,
+ loading: false,
+ fileLoaded: false,
+ originalContent: '',
+ }
+ },
+ computed: {
+ classObject() {
+ return {
+ 'saved': this.saved,
+ 'is-loading': this.loading
+ };
+ }
+ },
+ watch: {
+ ['file.showEditor'](val) {
+ this.resetEditorContent();
+
+ if (!val || this.fileLoaded || this.loading) {
+ return;
+ }
+
+ this.loadEditor();
+ }
+ },
+ ready() {
+ if (this.file.loadEditor) {
+ this.loadEditor();
+ }
+ },
+ methods: {
+ loadEditor() {
+ this.loading = true;
+
+ $.get(this.file.content_path)
+ .done((file) => {
+ let content = this.$el.querySelector('pre');
+ let fileContent = document.createTextNode(file.content);
+
+ content.textContent = fileContent.textContent;
+
+ this.originalContent = file.content;
+ this.fileLoaded = true;
+ this.editor = ace.edit(content);
+ this.editor.$blockScrolling = Infinity; // Turn off annoying warning
+ this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`);
+ this.editor.on('change', () => {
+ this.saveDiffResolution();
+ });
+ this.saveDiffResolution();
+ })
+ .fail(() => {
+ new Flash('Failed to load the file, please try again.');
+ })
+ .always(() => {
+ this.loading = false;
+ });
+ },
+ saveDiffResolution() {
+ this.saved = true;
+
+ // This probably be better placed in the data provider
+ this.file.content = this.editor.getValue();
+ this.file.resolveEditChanged = this.file.content !== this.originalContent;
+ this.file.promptDiscardConfirmation = false;
+ },
+ resetEditorContent() {
+ if (this.fileLoaded) {
+ this.editor.setValue(this.originalContent, -1);
+ }
+ },
+ cancelDiscardConfirmation(file) {
+ this.onCancelDiscardConfirmation(file);
+ },
+ acceptDiscardConfirmation(file) {
+ this.onAcceptDiscardConfirmation(file);
+ }
+ }
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
new file mode 100644
index 00000000000..b4be1c8988d
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -0,0 +1,12 @@
+((global) => {
+
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ global.mergeConflicts.inlineConflictLines = Vue.extend({
+ props: {
+ file: Object
+ },
+ mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
new file mode 100644
index 00000000000..8b0a8ab2073
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
@@ -0,0 +1,14 @@
+((global) => {
+
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ global.mergeConflicts.parallelConflictLine = Vue.extend({
+ props: {
+ file: Object,
+ line: Object
+ },
+ mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
+ template: '#parallel-conflict-line'
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
new file mode 100644
index 00000000000..eb4cc6a9dac
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -0,0 +1,15 @@
+((global) => {
+
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ global.mergeConflicts.parallelConflictLines = Vue.extend({
+ props: {
+ file: Object
+ },
+ mixins: [global.mergeConflicts.utils],
+ components: {
+ 'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
+ }
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
new file mode 100644
index 00000000000..da2fb8b1323
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -0,0 +1,30 @@
+((global) => {
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ class mergeConflictsService {
+ constructor(options) {
+ this.conflictsPath = options.conflictsPath;
+ this.resolveConflictsPath = options.resolveConflictsPath;
+ }
+
+ fetchConflictsData() {
+ return $.ajax({
+ dataType: 'json',
+ url: this.conflictsPath
+ });
+ }
+
+ submitResolveConflicts(data) {
+ return $.ajax({
+ url: this.resolveConflictsPath,
+ data: JSON.stringify(data),
+ contentType: 'application/json',
+ dataType: 'json',
+ method: 'POST'
+ });
+ }
+ };
+
+ global.mergeConflicts.mergeConflictsService = mergeConflictsService;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
new file mode 100644
index 00000000000..5c5c65f29d4
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -0,0 +1,437 @@
+((global) => {
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ const diffViewType = $.cookie('diff_view');
+ const HEAD_HEADER_TEXT = 'HEAD//our changes';
+ const ORIGIN_HEADER_TEXT = 'origin//their changes';
+ const HEAD_BUTTON_TITLE = 'Use ours';
+ const ORIGIN_BUTTON_TITLE = 'Use theirs';
+ const INTERACTIVE_RESOLVE_MODE = 'interactive';
+ const EDIT_RESOLVE_MODE = 'edit';
+ const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
+ const VIEW_TYPES = {
+ INLINE: 'inline',
+ PARALLEL: 'parallel'
+ };
+ const CONFLICT_TYPES = {
+ TEXT: 'text',
+ TEXT_EDITOR: 'text-editor'
+ };
+
+ global.mergeConflicts.mergeConflictsStore = {
+ state: {
+ isLoading: true,
+ hasError: false,
+ isSubmitting: false,
+ isParallel: diffViewType === VIEW_TYPES.PARALLEL,
+ diffViewType: diffViewType,
+ conflictsData: {}
+ },
+
+ setConflictsData(data) {
+ this.decorateFiles(data.files);
+
+ this.state.conflictsData = {
+ files: data.files,
+ commitMessage: data.commit_message,
+ sourceBranch: data.source_branch,
+ targetBranch: data.target_branch,
+ commitMessage: data.commit_message,
+ shortCommitSha: data.commit_sha.slice(0, 7),
+ };
+ },
+
+ decorateFiles(files) {
+ files.forEach((file) => {
+ file.content = '';
+ file.resolutionData = {};
+ file.promptDiscardConfirmation = false;
+ file.resolveMode = DEFAULT_RESOLVE_MODE;
+ file.filePath = this.getFilePath(file);
+ file.iconClass = `fa-${file.blob_icon}`;
+ file.blobPath = file.blob_path;
+
+ if (file.type === CONFLICT_TYPES.TEXT) {
+ file.showEditor = false;
+ file.loadEditor = false;
+
+ this.setInlineLine(file);
+ this.setParallelLine(file);
+ } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
+ file.showEditor = true;
+ file.loadEditor = true;
+ }
+ });
+ },
+
+ setInlineLine(file) {
+ file.inlineLines = [];
+
+ file.sections.forEach((section) => {
+ let currentLineType = 'new';
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ file.inlineLines.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach((line) => {
+ const { type } = line;
+
+ if ((type === 'new' || type === 'old') && currentLineType !== type) {
+ currentLineType = type;
+ file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
+ }
+
+ this.decorateLineForInlineView(line, id, conflict);
+ file.inlineLines.push(line);
+ })
+
+ if (conflict) {
+ file.inlineLines.push(this.getOriginHeaderLine(id));
+ }
+ });
+ },
+
+ setParallelLine(file) {
+ file.parallelLines = [];
+ const linesObj = { left: [], right: [] };
+
+ file.sections.forEach((section) => {
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ linesObj.left.push(this.getOriginHeaderLine(id));
+ linesObj.right.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach((line) => {
+ const { type } = line;
+
+ if (conflict) {
+ if (type === 'old') {
+ linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
+ } else if (type === 'new') {
+ linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
+ }
+ } else {
+ const lineType = type || 'context';
+
+ linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+ linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
+ }
+ });
+
+ this.checkLineLengths(linesObj);
+ });
+
+ for (let i = 0, len = linesObj.left.length; i < len; i++) {
+ file.parallelLines.push([
+ linesObj.right[i],
+ linesObj.left[i]
+ ]);
+ }
+ },
+
+ setLoadingState(state) {
+ this.state.isLoading = state;
+ },
+
+ setErrorState(state) {
+ this.state.hasError = state;
+ },
+
+ setFailedRequest(message) {
+ this.state.hasError = true;
+ this.state.conflictsData.errorMessage = message;
+ },
+
+ getConflictsCount() {
+ if (!this.state.conflictsData.files.length) {
+ return 0;
+ }
+
+ const files = this.state.conflictsData.files;
+ let count = 0;
+
+ files.forEach((file) => {
+ if (file.type === CONFLICT_TYPES.TEXT) {
+ file.sections.forEach((section) => {
+ if (section.conflict) {
+ count++;
+ }
+ });
+ } else {
+ count++;
+ }
+ });
+
+ return count;
+ },
+
+ getConflictsCountText() {
+ const count = this.getConflictsCount();
+ const text = count ? 'conflicts' : 'conflict';
+
+ return `${count} ${text}`;
+ },
+
+ setViewType(viewType) {
+ this.state.diffView = viewType;
+ this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
+
+ $.cookie('diff_view', viewType, {
+ path: gon.relative_url_root || '/'
+ });
+ },
+
+ getHeadHeaderLine(id) {
+ return {
+ id: id,
+ richText: HEAD_HEADER_TEXT,
+ buttonTitle: HEAD_BUTTON_TITLE,
+ type: 'new',
+ section: 'head',
+ isHeader: true,
+ isHead: true,
+ isSelected: false,
+ isUnselected: false
+ };
+ },
+
+ decorateLineForInlineView(line, id, conflict) {
+ const { type } = line;
+ line.id = id;
+ line.hasConflict = conflict;
+ line.isHead = type === 'new';
+ line.isOrigin = type === 'old';
+ line.hasMatch = type === 'match';
+ line.richText = line.rich_text;
+ line.isSelected = false;
+ line.isUnselected = false;
+ },
+
+ getLineForParallelView(line, id, lineType, isHead) {
+ const { old_line, new_line, rich_text } = line;
+ const hasConflict = lineType === 'conflict';
+
+ return {
+ id,
+ lineType,
+ hasConflict,
+ isHead: hasConflict && isHead,
+ isOrigin: hasConflict && !isHead,
+ hasMatch: lineType === 'match',
+ lineNumber: isHead ? new_line : old_line,
+ section: isHead ? 'head' : 'origin',
+ richText: rich_text,
+ isSelected: false,
+ isUnselected: false
+ };
+ },
+
+ getOriginHeaderLine(id) {
+ return {
+ id: id,
+ richText: ORIGIN_HEADER_TEXT,
+ buttonTitle: ORIGIN_BUTTON_TITLE,
+ type: 'old',
+ section: 'origin',
+ isHeader: true,
+ isOrigin: true,
+ isSelected: false,
+ isUnselected: false
+ };
+ },
+
+ getFilePath(file) {
+ const { old_path, new_path } = file;
+ return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
+ },
+
+ checkLineLengths(linesObj) {
+ let { left, right } = linesObj;
+
+ if (left.length !== right.length) {
+ if (left.length > right.length) {
+ const diff = left.length - right.length;
+ for (let i = 0; i < diff; i++) {
+ right.push({ lineType: 'emptyLine', richText: '' });
+ }
+ } else {
+ const diff = right.length - left.length;
+ for (let i = 0; i < diff; i++) {
+ left.push({ lineType: 'emptyLine', richText: '' });
+ }
+ }
+ }
+ },
+
+ setPromptConfirmationState(file, state) {
+ file.promptDiscardConfirmation = state;
+ },
+
+ setFileResolveMode(file, mode) {
+ if (mode === INTERACTIVE_RESOLVE_MODE) {
+ file.showEditor = false;
+ } else if (mode === EDIT_RESOLVE_MODE) {
+ // Restore Interactive mode when switching to Edit mode
+ file.showEditor = true;
+ file.loadEditor = true;
+ file.resolutionData = {};
+
+ this.restoreFileLinesState(file);
+ }
+
+ file.resolveMode = mode;
+ },
+
+ restoreFileLinesState(file) {
+ file.inlineLines.forEach((line) => {
+ if (line.hasConflict || line.isHeader) {
+ line.isSelected = false;
+ line.isUnselected = false;
+ }
+ });
+
+ file.parallelLines.forEach((lines) => {
+ const left = lines[0];
+ const right = lines[1];
+ const isLeftMatch = left.hasConflict || left.isHeader;
+ const isRightMatch = right.hasConflict || right.isHeader;
+
+ if (isLeftMatch || isRightMatch) {
+ left.isSelected = false;
+ left.isUnselected = false;
+ right.isSelected = false;
+ right.isUnselected = false;
+ }
+ });
+ },
+
+ isReadyToCommit() {
+ const files = this.state.conflictsData.files;
+ const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
+ let unresolved = 0;
+
+ for (let i = 0, l = files.length; i < l; i++) {
+ let file = files[i];
+
+ if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
+ let numberConflicts = 0;
+ let resolvedConflicts = Object.keys(file.resolutionData).length
+
+ // We only check for conflicts type 'text'
+ // since conflicts `text_editor` can´t be resolved in interactive mode
+ if (file.type === CONFLICT_TYPES.TEXT) {
+ for (let j = 0, k = file.sections.length; j < k; j++) {
+ if (file.sections[j].conflict) {
+ numberConflicts++;
+ }
+ }
+
+ if (resolvedConflicts !== numberConflicts) {
+ unresolved++;
+ }
+ }
+ } else if (file.resolveMode === EDIT_RESOLVE_MODE) {
+
+ // Unlikely to happen since switching to Edit mode saves content automatically.
+ // Checking anyway in case the save strategy changes in the future
+ if (!file.content) {
+ unresolved++;
+ continue;
+ }
+ }
+ }
+
+ return !this.state.isSubmitting && hasCommitMessage && !unresolved;
+ },
+
+ getCommitButtonText() {
+ const initial = 'Commit conflict resolution';
+ const inProgress = 'Committing...';
+
+ return this.state ? this.state.isSubmitting ? inProgress : initial : initial;
+ },
+
+ getCommitData() {
+ let commitData = {};
+
+ commitData = {
+ commit_message: this.state.conflictsData.commitMessage,
+ files: []
+ };
+
+ this.state.conflictsData.files.forEach((file) => {
+ let addFile;
+
+ addFile = {
+ old_path: file.old_path,
+ new_path: file.new_path
+ };
+
+ if (file.type === CONFLICT_TYPES.TEXT) {
+
+ // Submit only one data for type of editing
+ if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
+ addFile.sections = file.resolutionData;
+ } else if (file.resolveMode === EDIT_RESOLVE_MODE) {
+ addFile.content = file.content;
+ }
+ } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
+ addFile.content = file.content;
+ }
+
+ commitData.files.push(addFile);
+ });
+
+ return commitData;
+ },
+
+ handleSelected(file, sectionId, selection) {
+ Vue.set(file.resolutionData, sectionId, selection);
+
+ file.inlineLines.forEach((line) => {
+ if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
+ this.markLine(line, selection);
+ }
+ });
+
+ file.parallelLines.forEach((lines) => {
+ const left = lines[0];
+ const right = lines[1];
+ const hasSameId = right.id === sectionId || left.id === sectionId;
+ const isLeftMatch = left.hasConflict || left.isHeader;
+ const isRightMatch = right.hasConflict || right.isHeader;
+
+ if (hasSameId && (isLeftMatch || isRightMatch)) {
+ this.markLine(left, selection);
+ this.markLine(right, selection);
+ }
+ });
+ },
+
+ markLine(line, selection) {
+ if (selection === 'head' && line.isHead) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ } else if (selection === 'origin' && line.isOrigin) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ } else {
+ line.isSelected = false;
+ line.isUnselected = true;
+ }
+ },
+
+ setSubmitState(state) {
+ this.state.isSubmitting = state;
+ },
+
+ fileTextTypePresent() {
+ return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
+ }
+ };
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
new file mode 100644
index 00000000000..7fd3749b3e2
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -0,0 +1,89 @@
+//= require vue
+//= require ./merge_conflict_store
+//= require ./merge_conflict_service
+//= require ./mixins/line_conflict_utils
+//= require ./mixins/line_conflict_actions
+//= require ./components/diff_file_editor
+//= require ./components/inline_conflict_lines
+//= require ./components/parallel_conflict_line
+//= require ./components/parallel_conflict_lines
+
+$(() => {
+ const INTERACTIVE_RESOLVE_MODE = 'interactive';
+ const conflictsEl = document.querySelector('#conflicts');
+ const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
+ const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
+ conflictsPath: conflictsEl.dataset.conflictsPath,
+ resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
+ });
+
+ gl.MergeConflictsResolverApp = new Vue({
+ el: '#conflicts',
+ data: mergeConflictsStore.state,
+ components: {
+ 'diff-file-editor': gl.mergeConflicts.diffFileEditor,
+ 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
+ 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
+ },
+ computed: {
+ conflictsCountText() { return mergeConflictsStore.getConflictsCountText() },
+ readyToCommit() { return mergeConflictsStore.isReadyToCommit() },
+ commitButtonText() { return mergeConflictsStore.getCommitButtonText() },
+ showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() }
+ },
+ created() {
+ mergeConflictsService
+ .fetchConflictsData()
+ .done((data) => {
+ if (data.type === 'error') {
+ mergeConflictsStore.setFailedRequest(data.message);
+ } else {
+ mergeConflictsStore.setConflictsData(data);
+ }
+ })
+ .error(() => {
+ mergeConflictsStore.setFailedRequest();
+ })
+ .always(() => {
+ mergeConflictsStore.setLoadingState(false);
+
+ this.$nextTick(() => {
+ $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
+ });
+ });
+ },
+ methods: {
+ handleViewTypeChange(viewType) {
+ mergeConflictsStore.setViewType(viewType);
+ },
+ onClickResolveModeButton(file, mode) {
+ if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
+ mergeConflictsStore.setPromptConfirmationState(file, true);
+ return;
+ }
+
+ mergeConflictsStore.setFileResolveMode(file, mode);
+ },
+ acceptDiscardConfirmation(file) {
+ mergeConflictsStore.setPromptConfirmationState(file, false);
+ mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
+ },
+ cancelDiscardConfirmation(file) {
+ mergeConflictsStore.setPromptConfirmationState(file, false);
+ },
+ commit() {
+ mergeConflictsStore.setSubmitState(true);
+
+ mergeConflictsService
+ .submitResolveConflicts(mergeConflictsStore.getCommitData())
+ .done((data) => {
+ window.location.href = data.redirect_to;
+ })
+ .error(() => {
+ mergeConflictsStore.setSubmitState(false);
+ new Flash('Failed to save merge conflicts resolutions. Please try again!');
+ });
+ }
+ }
+ })
+});
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
new file mode 100644
index 00000000000..114a2c5b305
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -0,0 +1,12 @@
+((global) => {
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ global.mergeConflicts.actions = {
+ methods: {
+ handleSelected(file, sectionId, selection) {
+ gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
+ }
+ }
+ };
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
new file mode 100644
index 00000000000..b846a90ab2a
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -0,0 +1,18 @@
+((global) => {
+ global.mergeConflicts = global.mergeConflicts || {};
+
+ global.mergeConflicts.utils = {
+ methods: {
+ lineCssClass(line) {
+ return {
+ 'head': line.isHead,
+ 'origin': line.isOrigin,
+ 'match': line.hasMatch,
+ 'selected': line.isSelected,
+ 'unselected': line.isUnselected
+ };
+ }
+ }
+ };
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 05644b3d03c..02ff5a382e2 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -36,13 +36,10 @@
};
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');
+ if (window.mrTabs) {
+ window.mrTabs.unbindEvents();
}
+ window.mrTabs = new MergeRequestTabs(this.opts);
};
MergeRequest.prototype.showAllCommits = function() {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 18bbfa7a459..9f28738e06b 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -56,9 +56,14 @@
MergeRequestTabs.prototype.commitsLoaded = false;
+ MergeRequestTabs.prototype.fixedLayoutPref = null;
+
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
+
+ this.buildsLoaded = this.opts.buildsLoaded || false;
+
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
@@ -66,11 +71,17 @@
this._location = location;
this.bindEvents();
this.activateTab(this.opts.action);
+ this.initAffix();
}
MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
- return $(document).on('click', '.js-show-tab', this.showTab);
+ $(document).on('click', '.js-show-tab', this.showTab);
+ };
+
+ MergeRequestTabs.prototype.unbindEvents = function() {
+ $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
+ $(document).off('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.showTab = function(event) {
@@ -85,11 +96,15 @@
if (action === 'commits') {
this.loadCommits($target.attr('href'));
this.expandView();
- } else if (action === 'diffs') {
+ this.resetViewContainer();
+ } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight
@@ -97,11 +112,14 @@
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
+ this.resetViewContainer();
} else if (action === 'pipelines') {
this.loadPipelines($target.attr('href'));
this.expandView();
+ this.resetViewContainer();
} else {
this.expandView();
+ this.resetViewContainer();
}
if (this.opts.setUrl) {
this.setCurrentAction(action);
@@ -126,7 +144,7 @@
if (action === 'show') {
action = 'notes';
}
- return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
+ $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
};
// Replaces the current Merge Request-specific action in the URL with a new one
@@ -156,8 +174,9 @@
action = 'notes';
}
this.currentAction = action;
- // Remove a trailing '/commits' or '/diffs'
- new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
+ // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
+ new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+
// Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
@@ -196,8 +215,13 @@
if (this.diffsLoaded) {
return;
}
+
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ var url = gl.utils.parseUrl(source);
+
return this._get({
- url: (source + ".json") + this._location.search,
+ url: (url.pathname + ".json") + this._location.search,
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
@@ -209,7 +233,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
- if (_this.diffViewType() === 'parallel') {
+ if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
@@ -308,11 +332,25 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
- // Returns diff view type
+ };
+
+ MergeRequestTabs.prototype.isDiffAction = function(action) {
+ return action === 'diffs' || action === 'new/diffs'
};
MergeRequestTabs.prototype.expandViewContainer = function() {
- return $('.container-fluid').removeClass('container-limited');
+ var $wrapper = $('.content-wrapper .container-fluid');
+ if (this.fixedLayoutPref === null) {
+ this.fixedLayoutPref = $wrapper.hasClass('container-limited');
+ }
+ $wrapper.removeClass('container-limited');
+ };
+
+ MergeRequestTabs.prototype.resetViewContainer = function() {
+ if (this.fixedLayoutPref !== null) {
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', this.fixedLayoutPref);
+ }
};
MergeRequestTabs.prototype.shrinkView = function() {
@@ -343,6 +381,43 @@
// Only when sidebar is collapsed
};
+ MergeRequestTabs.prototype.initAffix = function () {
+ var $tabs = $('.js-tabs-affix');
+
+ // Screen space on small screens is usually very sparse
+ // So we dont affix the tabs on these
+ if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
+
+ var $diffTabs = $('#diff-notes-app'),
+ $fixedNav = $('.navbar-fixed-top'),
+ $layoutNav = $('.layout-nav');
+
+ $tabs.off('affix.bs.affix affix-top.bs.affix')
+ .affix({
+ offset: {
+ top: function () {
+ var tabsTop = $diffTabs.offset().top - $tabs.height();
+ tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height());
+
+ return tabsTop;
+ }
+ }
+ }).on('affix.bs.affix', function () {
+ $diffTabs.css({
+ marginTop: $tabs.height()
+ });
+ }).on('affix-top.bs.affix', function () {
+ $diffTabs.css({
+ marginTop: ''
+ });
+ });
+
+ // Fix bug when reloading the page already scrolling
+ if ($tabs.hasClass('affix')) {
+ $tabs.trigger('affix.bs.affix');
+ }
+ };
+
return MergeRequestTabs;
})();
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js.es6
index 7bbcdf59838..3ff6851d59b 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,7 +1,32 @@
-(function() {
+ ((global) => {
var 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; };
- this.MergeRequestWidget = (function() {
+ const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
+ <div class="ci_widget ci-success">
+ <%= ci_success_icon %>
+ <span>
+ Deployed to
+ <a href="<%- url %>" target="_blank" class="environment">
+ <%- name %>
+ </a>
+ <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
+ <%- deployed_at %>
+ </span>
+ <a class="js-environment-link" href="<%- external_url %>" target="_blank">
+ <i class="fa fa-external-link"></i>
+ View on <%- external_url_formatted %>
+ </a>
+ </span>
+ <span class="stop-env-container js-stop-env-link">
+ <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?">
+ <i class="fa fa-stop-circle-o"/>
+ Stop environment
+ </a>
+ </span>
+ </div>
+ </div>`;
+
+ global.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
@@ -10,17 +35,23 @@
// ci_status_url - String, URL to use to check CI status
//
this.opts = opts;
+ this.$widgetBody = $('.mr-widget-body');
$('#modal_merge_info').modal({
show: false
});
this.firstCICheck = true;
this.readyForCICheck = false;
+ this.readyForCIEnvironmentCheck = false;
this.cancel = false;
clearInterval(this.fetchBuildStatusInterval);
+ clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners();
this.addEventListeners();
this.getCIStatus(false);
+ this.getCIEnvironmentsStatus();
+ this.retrieveSuccessIcon();
this.pollCIStatus();
+ this.pollCIEnvironmentsStatus();
notifyPermissions();
}
@@ -41,6 +72,7 @@
page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) {
clearInterval(_this.fetchBuildStatusInterval);
+ clearInterval(_this.fetchBuildEnvironmentStatusInterval);
_this.cancelPolling();
return _this.clearEventListeners();
}
@@ -48,6 +80,12 @@
})(this));
};
+ MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
+ const $ciSuccessIcon = $('.js-success-icon');
+ this.$ciSuccessIcon = $ciSuccessIcon.html();
+ $ciSuccessIcon.remove();
+ }
+
MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
if (deleteSourceBranch == null) {
deleteSourceBranch = false;
@@ -62,7 +100,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
- return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
+ return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
@@ -118,6 +156,7 @@
if (data.status === '') {
return;
}
+ if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
@@ -150,6 +189,46 @@
})(this));
};
+ MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
+ this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
+ if (!this.readyForCIEnvironmentCheck) return;
+ this.getCIEnvironmentsStatus();
+ this.readyForCIEnvironmentCheck = false;
+ }, 300000);
+ };
+
+ MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
+ $.getJSON(this.opts.ci_environments_status_url, (environments) => {
+ if (this.cancel) return;
+ this.readyForCIEnvironmentCheck = true;
+ if (environments && environments.length) this.renderEnvironments(environments);
+ });
+ };
+
+ MergeRequestWidget.prototype.renderEnvironments = function(environments) {
+ for (let i = 0; i < environments.length; i++) {
+ const environment = environments[i];
+ if ($(`.mr-state-widget #${ environment.id }`).length) return;
+ const $template = $(DEPLOYMENT_TEMPLATE);
+ if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
+
+ if (!environment.stop_url) {
+ $('.js-stop-env-link', $template).remove();
+ }
+
+ if (environment.deployed_at && environment.deployed_at_formatted) {
+ environment.deployed_at = $.timeago(environment.deployed_at) + '.';
+ } else {
+ $('.js-environment-timeago', $template).remove();
+ environment.name += '.';
+ }
+ environment.ci_success_icon = this.$ciSuccessIcon;
+ const templateString = _.unescape($template[0].outerHTML);
+ const template = _.template(templateString)(environment)
+ this.$widgetBody.before(template);
+ }
+ };
+
MergeRequestWidget.prototype.showCIStatus = function(state) {
var allowed_states;
if (state == null) {
@@ -190,4 +269,4 @@
})();
-}).call(this);
+ })(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index c8031174dd2..cee42633c79 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -7,7 +7,7 @@
this.currentProject = JSON.parse(currentProject);
}
$('.js-milestone-select').each(function(i, dropdown) {
- var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId;
+ var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
@@ -15,6 +15,7 @@
selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
+ showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
@@ -31,12 +32,12 @@
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
}
return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: milestonesUrl
}).done(function(data) {
- var extraOptions;
- extraOptions = [];
+ var extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
@@ -58,10 +59,14 @@
title: 'Upcoming'
});
}
- if (extraOptions.length > 2) {
+ if (extraOptions.length) {
extraOptions.push('divider');
}
- return callback(extraOptions.concat(data));
+
+ callback(extraOptions.concat(data));
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
});
},
filterable: true,
@@ -69,19 +74,20 @@
fields: ['title']
},
selectable: true,
- toggleLabel: function(selected) {
- if (selected && 'id' in selected) {
+ toggleLabel: function(selected, el, e) {
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
return defaultLabel;
}
},
+ defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'),
text: function(milestone) {
return _.escape(milestone.title);
},
id: function(milestone) {
- if (!useId) {
+ if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
return milestone.id;
@@ -100,10 +106,11 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
- if ($dropdown.hasClass('js-filter-bulk-update')) {
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ e.preventDefault();
return;
}
- if (page === 'projects:boards:show') {
+ if ($('html').hasClass('issue-boards-page')) {
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch_graph.js
index 91132af273a..91132af273a 100644
--- a/app/assets/javascripts/network/branch-graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
deleted file mode 100644
index bf33eb10100..00000000000
--- a/app/assets/javascripts/pipeline.js.es6
+++ /dev/null
@@ -1,15 +0,0 @@
-(function() {
- function toggleGraph() {
- const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
- const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
- const $btnText = $(this).find('.toggle-btn-text');
-
- $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
-
- const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
-
- graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
- }
-
- $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
-})();
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
new file mode 100644
index 00000000000..a7624de6089
--- /dev/null
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -0,0 +1,40 @@
+((global) => {
+
+ class Pipelines {
+ constructor() {
+ $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
+ this.addMarginToBuildColumns();
+ }
+
+ toggleGraph() {
+ const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
+ const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
+ const $btnText = $(this).find('.toggle-btn-text');
+ const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+
+ $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+
+
+ graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
+ }
+
+ addMarginToBuildColumns() {
+ const $secondChildBuildNode = $('.build:nth-child(2)');
+ if ($secondChildBuildNode.length) {
+ const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
+ const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
+ const $previousColumn = $multiBuildColumn.prev('.stage-column');
+ $multiBuildColumn.addClass('left-margin');
+ $firstChildBuildNode.addClass('left-connector');
+ $previousColumn.each(function() {
+ $this = $(this);
+ if ($('.build', $this).length === 1) $this.addClass('no-margin');
+ });
+ }
+ $('.pipeline-graph').removeClass('hidden');
+ }
+ }
+
+ global.Pipelines = Pipelines;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js.es6
index 30cd6f6e470..a1b0126e857 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,47 +1,45 @@
-(function() {
- var GitLabCrop,
- bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+((global) => {
- GitLabCrop = (function() {
- var FILENAMEREGEX;
+ // Matches everything but the file name
+ const FILENAMEREGEX = /^.*[\\\/]/;
- // Matches everything but the file name
- FILENAMEREGEX = /^.*[\\\/]/;
+ class GitLabCrop {
+ constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
+ exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
- function GitLabCrop(input, opts) {
- var ref, ref1, ref2, ref3, ref4;
- if (opts == null) {
- opts = {};
- }
- this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this);
- this.onModalHide = bind(this.onModalHide, this);
- this.onModalShow = bind(this.onModalShow, this);
- this.onPickImageClick = bind(this.onPickImageClick, this);
+ this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
+ this.onModalHide = this.onModalHide.bind(this);
+ this.onModalShow = this.onModalShow.bind(this);
+ this.onPickImageClick = this.onPickImageClick.bind(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;
+ this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
+ this.exportWidth = exportWidth;
+ this.exportHeight = exportHeight;
+ this.cropBoxWidth = cropBoxWidth;
+ this.cropBoxHeight = cropBoxHeight;
+ this.form = this.fileInput.parents('form');
+ this.filename = filename;
+ this.previewImage = previewImage;
+ this.modalCrop = modalCrop;
+ this.pickImageEl = pickImageEl;
+ this.uploadImageBtn = uploadImageBtn;
+ this.modalCropImg = modalCropImg;
+ this.filename = this.getElement(filename);
+ this.previewImage = this.getElement(previewImage);
+ this.pickImageEl = this.getElement(pickImageEl);
+ this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
+ this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
+ this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents();
}
- GitLabCrop.prototype.getElement = function(selector) {
+ getElement(selector) {
return $(selector, this.form);
- };
+ }
- GitLabCrop.prototype.bindEvents = function() {
+ bindEvents() {
var _this;
_this = this;
this.fileInput.on('change', function(e) {
@@ -57,13 +55,13 @@
return _this.onActionBtnClick(btn);
});
return this.croppedImageBlob = null;
- };
+ }
- GitLabCrop.prototype.onPickImageClick = function() {
+ onPickImageClick() {
return this.fileInput.trigger('click');
- };
+ }
- GitLabCrop.prototype.onModalShow = function() {
+ onModalShow() {
var _this;
_this = this;
return this.modalCropImg.cropper({
@@ -95,44 +93,44 @@
});
}
});
- };
+ }
- GitLabCrop.prototype.onModalHide = function() {
+ onModalHide() {
return this.modalCropImg.attr('src', '').cropper('destroy');
- };
+ }
- GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image
- e.preventDefault(); // Destroy cropper instance
+ onUploadImageBtnClick(e) {
+ e.preventDefault();
this.setBlob();
this.setPreview();
this.modalCrop.modal('hide');
return this.fileInput.val('');
- };
+ }
- GitLabCrop.prototype.onActionBtnClick = function(btn) {
+ onActionBtnClick(btn) {
var data, result;
data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) {
return result = this.modalCropImg.cropper(data.method, data.option);
}
- };
+ }
- GitLabCrop.prototype.onFileInputChange = function(e, input) {
+ onFileInputChange(e, input) {
return this.readFile(input);
- };
+ }
- GitLabCrop.prototype.readFile = function(input) {
+ readFile(input) {
var _this, reader;
_this = this;
reader = new FileReader;
- reader.onload = function() {
+ reader.onload = () => {
_this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show');
};
return reader.readAsDataURL(input.files[0]);
- };
+ }
- GitLabCrop.prototype.dataURLtoBlob = function(dataURL) {
+ dataURLtoBlob(dataURL) {
var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
@@ -143,35 +141,32 @@
return new Blob([new Uint8Array(array)], {
type: 'image/png'
});
- };
+ }
- GitLabCrop.prototype.setPreview = function() {
+ setPreview() {
var filename;
this.previewImage.attr('src', this.dataURL);
filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename);
- };
+ }
- GitLabCrop.prototype.setBlob = function() {
+ setBlob() {
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
width: 200,
height: 200
}).toDataURL('image/png');
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
- };
+ }
- GitLabCrop.prototype.getBlob = function() {
+ getBlob() {
return this.croppedImageBlob;
- };
-
- return GitLabCrop;
-
- })();
+ }
+ }
$.fn.glCrop = function(opts) {
return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts));
});
- };
+ }
-}).call(this);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
deleted file mode 100644
index 60f9fba5777..00000000000
--- a/app/assets/javascripts/profile/profile.js
+++ /dev/null
@@ -1,106 +0,0 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Profile = (function() {
- function Profile(opts) {
- var cropOpts, ref;
- if (opts == null) {
- opts = {};
- }
- this.onSubmitForm = bind(this.onSubmitForm, this);
- 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();
- $(this).find('.update-success').hide();
- return $(this).find('.update-failed').hide();
- });
- $('.update-username').on('ajax:complete', function() {
- $('.loading-username').hide();
- $(this).find('.btn-save').enable();
- return $(this).find('.loading-gif').hide();
- });
- $('.update-notifications').on('ajax:success', function(e, data) {
- if (data.saved) {
- return new Flash("Notification settings saved", "notice");
- } else {
- return new Flash("Failed to save new settings", "alert");
- }
- });
- this.bindEvents();
- cropOpts = {
- filename: '.js-avatar-filename',
- previewImage: '.avatar-image .avatar',
- modalCrop: '.modal-profile-crop',
- pickImageEl: '.js-choose-user-avatar-button',
- uploadImageBtn: '.js-upload-user-avatar',
- modalCropImg: '.modal-profile-crop-image'
- };
- this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
- }
-
- Profile.prototype.bindEvents = function() {
- return this.form.on('submit', this.onSubmitForm);
- };
-
- Profile.prototype.onSubmitForm = function(e) {
- e.preventDefault();
- return this.saveForm();
- };
-
- Profile.prototype.saveForm = function() {
- var avatarBlob, formData, self;
- self = this;
- formData = new FormData(this.form[0]);
- avatarBlob = this.avatarGlCrop.getBlob();
- if (avatarBlob != null) {
- formData.append('user[avatar]', avatarBlob, 'avatar.png');
- }
- return $.ajax({
- url: this.form.attr('action'),
- type: this.form.attr('method'),
- data: formData,
- dataType: "json",
- processData: false,
- contentType: false,
- success: function(response) {
- return new Flash(response.message, 'notice');
- },
- error: function(jqXHR) {
- return new Flash(jqXHR.responseJSON.message, 'alert');
- },
- complete: function() {
- window.scrollTo(0, 0);
- // Enable submit button after requests ends
- return self.form.find(':input[disabled]').enable();
- }
- });
- };
-
- return Profile;
-
- })();
-
- $(function() {
- $(document).on('focusout.ssh_key', '#key_key', function() {
- var $title, comment;
- $title = $('#key_title');
- comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
- 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();
- }
- });
-
-}).call(this);
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
new file mode 100644
index 00000000000..b2307be73ad
--- /dev/null
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -0,0 +1,100 @@
+((global) => {
+
+ class Profile {
+ constructor({ form } = {}) {
+ this.onSubmitForm = this.onSubmitForm.bind(this);
+ this.form = form || $('.edit-user');
+ this.bindEvents();
+ this.initAvatarGlCrop();
+ }
+
+ initAvatarGlCrop() {
+ const cropOpts = {
+ filename: '.js-avatar-filename',
+ previewImage: '.avatar-image .avatar',
+ modalCrop: '.modal-profile-crop',
+ pickImageEl: '.js-choose-user-avatar-button',
+ uploadImageBtn: '.js-upload-user-avatar',
+ modalCropImg: '.modal-profile-crop-image'
+ };
+ this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
+ }
+
+ bindEvents() {
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+ $('#user_notification_email').on('change', this.submitForm);
+ $('.update-username').on('ajax:before', this.beforeUpdateUsername);
+ $('.update-username').on('ajax:complete', this.afterUpdateUsername);
+ $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
+ this.form.on('submit', this.onSubmitForm);
+ }
+
+ submitForm() {
+ return $(this).parents('form').submit();
+ }
+
+ onSubmitForm(e) {
+ e.preventDefault();
+ return this.saveForm();
+ }
+
+ beforeUpdateUsername() {
+ $('.loading-username').show();
+ $(this).find('.update-success').hide();
+ return $(this).find('.update-failed').hide();
+ }
+
+ afterUpdateUsername() {
+ $('.loading-username').hide();
+ $(this).find('.btn-save').enable();
+ return $(this).find('.loading-gif').hide();
+ }
+
+ onUpdateNotifs(e, data) {
+ return data.saved ?
+ new Flash("Notification settings saved", "notice") :
+ new Flash("Failed to save new settings", "alert");
+ }
+
+ saveForm() {
+ const self = this;
+ const formData = new FormData(this.form[0]);
+ const avatarBlob = this.avatarGlCrop.getBlob();
+
+ if (avatarBlob != null) {
+ formData.append('user[avatar]', avatarBlob, 'avatar.png');
+ }
+
+ return $.ajax({
+ url: this.form.attr('action'),
+ type: this.form.attr('method'),
+ data: formData,
+ dataType: "json",
+ processData: false,
+ contentType: false,
+ success: response => new Flash(response.message, 'notice'),
+ error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
+ complete: () => {
+ window.scrollTo(0, 0);
+ // Enable submit button after requests ends
+ return self.form.find(':input[disabled]').enable();
+ }
+ });
+ }
+ }
+
+ $(function() {
+ $(document).on('focusout.ssh_key', '#key_key', function() {
+ const $title = $('#key_title');
+ const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+ if (comment && comment.length > 1 && $title.val() === '') {
+ return $title.val(comment[1]).change();
+ }
+ // Extract the SSH Key title from its comment
+ });
+ if (global.utils.getPagePath() === 'profiles') {
+ return new Profile();
+ }
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 8e38ccf7e44..b8347367717 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -7,6 +7,7 @@
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);
@@ -154,6 +155,14 @@
return location.href = this.options.treeUrl;
};
+ ProjectFindFile.prototype.goToBlob = function() {
+ var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
+
+ if ($link.length) {
+ $link.get(0).click();
+ }
+ };
+
return ProjectFindFile;
})();
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
deleted file mode 100644
index 78f7b48bc7d..00000000000
--- a/app/assets/javascripts/project_members.js
+++ /dev/null
@@ -1,10 +0,0 @@
-(function() {
- this.ProjectMembers = (function() {
- function ProjectMembers() {
- $('li.project_member').bind('ajax:success', function() {
- return $(this).fadeOut();
- });
- }
- return ProjectMembers;
- })();
-}).call(this);
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index a787b11f2a9..478e82aa14d 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -5,6 +5,7 @@
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
this.$selects = $('.features select');
+ this.$repoSelects = this.$selects.filter('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
@@ -14,6 +15,7 @@
})(this));
this.toggleSettings();
this.toggleSettingsOnclick();
+ this.toggleRepoVisibility();
}
ProjectNew.prototype.toggleSettings = function() {
@@ -41,6 +43,38 @@
}
};
+ ProjectNew.prototype.toggleRepoVisibility = function () {
+ var $repoAccessLevel = $('.js-repo-access-level select');
+
+ this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
+ .nextAll()
+ .hide();
+
+ $repoAccessLevel.off('change')
+ .on('change', function () {
+ var selectedVal = parseInt($repoAccessLevel.val());
+
+ this.$repoSelects.each(function () {
+ var $this = $(this),
+ repoSelectVal = parseInt($this.val());
+
+ $this.find('option').show();
+
+ if (selectedVal < repoSelectVal) {
+ $this.val(selectedVal);
+ }
+
+ $this.find("option[value='" + selectedVal + "']").nextAll().hide();
+ });
+
+ if (selectedVal) {
+ this.$repoSelects.removeClass('disabled');
+ } else {
+ this.$repoSelects.addClass('disabled');
+ }
+ }.bind(this));
+ };
+
return ProjectNew;
})();
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 20b147500cf..4239ed2f889 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -23,7 +23,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
- return Api.groups(term, false, groupsCallback);
+ return Api.groups(term, false, false, groupsCallback);
};
} else {
projectsCallback = finalCallback;
@@ -72,7 +72,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
- return Api.groups(query.term, false, groupsCallback);
+ return Api.groups(query.term, false, false, groupsCallback);
};
} else {
projectsCallback = finalCallback;
diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 7aeb5f92514..7aeb5f92514 100644
--- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index 46beca469b9..46beca469b9 100644
--- a/app/assets/javascripts/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index 983322cbecc..983322cbecc 100644
--- a/app/assets/javascripts/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index 15a6dca2875..15a6dca2875 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index 9ff0fd12c76..9ff0fd12c76 100644
--- a/app/assets/javascripts/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
new file mode 100644
index 00000000000..15b3affd469
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -0,0 +1 @@
+/*= require_tree . */
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index d34346f862b..8074a94f33e 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -10,7 +10,7 @@
filterable: true,
fieldName: 'group_id',
data: function(term, callback) {
- return Api.groups(term, null, function(data) {
+ return Api.groups(term, false, false, function(data) {
data.unshift({
name: 'Any'
});
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js.es6
index 678d836f56f..b4c6226dc68 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,30 +1,21 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.SearchAutocomplete = (function() {
- var KEYCODE;
-
- KEYCODE = {
- ESCAPE: 27,
- BACKSPACE: 8,
- ENTER: 13,
- UP: 38,
- DOWN: 40
- };
-
- function SearchAutocomplete(opts) {
- var ref, ref1, ref2, ref3, ref4;
- if (opts == null) {
- opts = {};
- }
- this.onSearchInputBlur = bind(this.onSearchInputBlur, this);
- this.onClearInputClick = bind(this.onClearInputClick, this);
- this.onSearchInputFocus = bind(this.onSearchInputFocus, this);
- this.onSearchInputClick = bind(this.onSearchInputClick, this);
- 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
+((global) => {
+
+ const KEYCODE = {
+ ESCAPE: 27,
+ BACKSPACE: 8,
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
+ };
+
+ class SearchAutocomplete {
+ constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
+ this.bindEventContext();
+ this.wrap = wrap || $('.search');
+ this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
+ this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
+ this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
+ this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
@@ -46,19 +37,27 @@
}
// Finds an element inside wrapper element
- SearchAutocomplete.prototype.getElement = function(selector) {
+ bindEventContext() {
+ this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
+ this.onClearInputClick = this.onClearInputClick.bind(this);
+ this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
+ this.onSearchInputClick = this.onSearchInputClick.bind(this);
+ this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
+ this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+ }
+ getElement(selector) {
return this.wrap.find(selector);
- };
+ }
- SearchAutocomplete.prototype.saveOriginalState = function() {
+ saveOriginalState() {
return this.originalState = this.serializeState();
- };
+ }
- SearchAutocomplete.prototype.saveTextLength = function() {
+ saveTextLength() {
return this.lastTextLength = this.searchInput.val().length;
- };
+ }
- SearchAutocomplete.prototype.createAutocomplete = function() {
+ createAutocomplete() {
return this.searchInput.glDropdown({
filterInputBlur: false,
filterable: true,
@@ -73,9 +72,9 @@
selectable: true,
clicked: this.onClick.bind(this)
});
- };
+ }
- SearchAutocomplete.prototype.getData = function(term, callback) {
+ getData(term, callback) {
var _this, contents, jqXHR;
_this = this;
if (!term) {
@@ -138,9 +137,9 @@
}).always(function() {
return _this.loadingSuggestions = false;
});
- };
+ }
- SearchAutocomplete.prototype.getCategoryContents = function() {
+ getCategoryContents() {
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
userId = gon.current_user_id;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
@@ -173,9 +172,9 @@
items.splice(0, 1);
}
return items;
- };
+ }
- SearchAutocomplete.prototype.serializeState = function() {
+ serializeState() {
return {
// Search Criteria
search_project_id: this.projectInputEl.val(),
@@ -186,9 +185,9 @@
// Location badge
_location: this.locationBadgeEl.text()
};
- };
+ }
- SearchAutocomplete.prototype.bindEvents = function() {
+ bindEvents() {
this.searchInput.on('keydown', this.onSearchInputKeyDown);
this.searchInput.on('keyup', this.onSearchInputKeyUp);
this.searchInput.on('click', this.onSearchInputClick);
@@ -200,9 +199,9 @@
return _this.searchInput.focus();
};
})(this));
- };
+ }
- SearchAutocomplete.prototype.enableAutocomplete = function() {
+ enableAutocomplete() {
var _this;
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
@@ -216,12 +215,12 @@
}
};
- SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
// Saves last length of the entered text
+ onSearchInputKeyDown() {
return this.saveTextLength();
- };
+ }
- SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
+ onSearchInputKeyUp(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
// when trying to remove the location badge
@@ -259,54 +258,53 @@
}
}
this.wrap.toggleClass('has-value', !!e.target.value);
- };
+ }
// Avoid falsy value to be returned
- SearchAutocomplete.prototype.onSearchInputClick = function(e) {
- // Prevents closing the dropdown menu
+ onSearchInputClick(e) {
return e.stopImmediatePropagation();
- };
+ }
- SearchAutocomplete.prototype.onSearchInputFocus = function() {
+ onSearchInputFocus() {
this.isFocused = true;
this.wrap.addClass('search-active');
if (this.getValue() === '') {
return this.getData();
}
- };
+ }
- SearchAutocomplete.prototype.getValue = function() {
+ getValue() {
return this.searchInput.val();
- };
+ }
- SearchAutocomplete.prototype.onClearInputClick = function(e) {
+ onClearInputClick(e) {
e.preventDefault();
return this.searchInput.val('').focus();
- };
+ }
- SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
+ onSearchInputBlur(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
- };
+ }
- SearchAutocomplete.prototype.addLocationBadge = function(item) {
+ addLocationBadge(item) {
var badgeText, category, value;
category = item.category != null ? item.category + ": " : '';
value = item.value != null ? item.value : '';
badgeText = "" + category + value;
this.locationBadgeEl.text(badgeText).show();
return this.wrap.addClass('has-location-badge');
- };
+ }
- SearchAutocomplete.prototype.hasLocationBadge = function() {
+ hasLocationBadge() {
return this.wrap.is('.has-location-badge');
};
- SearchAutocomplete.prototype.restoreOriginalState = function() {
+ restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i++) {
@@ -320,13 +318,13 @@
value: this.originalState._location
});
}
- };
+ }
- SearchAutocomplete.prototype.badgePresent = function() {
+ badgePresent() {
return this.locationBadgeEl.length;
- };
+ }
- SearchAutocomplete.prototype.resetSearchState = function() {
+ resetSearchState() {
var i, input, inputs, len, results;
inputs = Object.keys(this.originalState);
results = [];
@@ -339,30 +337,30 @@
results.push(this.getElement("#" + input).val(''));
}
return results;
- };
+ }
- SearchAutocomplete.prototype.removeLocationBadge = function() {
+ removeLocationBadge() {
this.locationBadgeEl.hide();
this.resetSearchState();
this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete();
- };
+ }
- SearchAutocomplete.prototype.disableAutocomplete = function() {
+ disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
- };
+ }
- SearchAutocomplete.prototype.restoreMenu = function() {
+ restoreMenu() {
var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html);
};
- SearchAutocomplete.prototype.onClick = function(item, $el, e) {
+ onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) {
e.preventDefault();
if (!this.badgePresent) {
@@ -385,9 +383,9 @@
}
};
- return SearchAutocomplete;
+ }
- })();
+ global.SearchAutocomplete = SearchAutocomplete;
$(function() {
var $projectOptionsDataEl = $('.js-search-project-options');
@@ -408,16 +406,16 @@
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'),
@@ -426,4 +424,4 @@
}
});
-}).call(this);
+})(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/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index c32ddf80219..bd4e3c3d00d 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,7 +1,7 @@
/*= require ../blob/template_selector */
((global) => {
- class IssuableTemplateSelector extends TemplateSelector {
+ class IssuableTemplateSelector extends gl.TemplateSelector {
constructor(...args) {
super(...args);
this.projectPath = this.dropdown.data('project-path');
@@ -16,7 +16,13 @@
if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => {
- if (this.currentTemplate) this.setInputValueToTemplateContent();
+ this.setInputValueToTemplateContent();
+ });
+
+ $('.no-template', this.dropdown.parent()).on('click', () => {
+ this.currentTemplate = '';
+ this.setInputValueToTemplateContent();
+ $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
});
}
@@ -26,26 +32,28 @@
this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner();
- this.setInputValueToTemplateContent();
+ this.setInputValueToTemplateContent(true);
});
return;
}
- setInputValueToTemplateContent() {
+ setInputValueToTemplateContent(append) {
// `this.requestFileSuccess` sets the value of the description input field
- // to the content of the template selected.
+ // to the content of the template selected. If `append` is true, the
+ // template content will be appended to the previous value of the field,
+ // separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
- // skip focusing the description input by setting `true` as the 2nd
- // argument to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, true);
+ // skip focusing the description input by setting `true` as the
+ // `skipFocus` option to `requestFileSuccess`.
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate);
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
}
return;
}
}
global.IssuableTemplateSelector = IssuableTemplateSelector;
-})(window);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index bd8cdde033e..4e8247b89e1 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,12 +1,12 @@
((global) => {
class IssuableTemplateSelectors {
- constructor(opts = {}) {
- this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
- this.editor = opts.editor || this.initEditor();
+ constructor({ $dropdowns, editor } = {}) {
+ this.$dropdowns = $dropdowns || $('.js-issuable-selector');
+ this.editor = editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => {
- let $dropdown = $(dropdown);
- new IssuableTemplateSelector({
+ const $dropdown = $(dropdown);
+ new gl.IssuableTemplateSelector({
pattern: /(\.md)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
@@ -26,4 +26,4 @@
}
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
-})(window);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js.es6
index 93421649ac7..055228c5df8 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,34 +1,29 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Todos = (function() {
- function Todos(opts) {
- var ref;
- if (opts == null) {
- opts = {};
- }
- this.allDoneClicked = bind(this.allDoneClicked, this);
- this.doneClicked = bind(this.doneClicked, this);
- this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
+((global) => {
+
+ class Todos {
+ constructor({ el } = {}) {
+ this.allDoneClicked = this.allDoneClicked.bind(this);
+ this.doneClicked = this.doneClicked.bind(this);
+ this.el = el || $('.js-todos-options');
this.perPage = this.el.data('perPage');
this.clearListeners();
this.initBtnListeners();
this.initFilters();
}
- Todos.prototype.clearListeners = function() {
+ clearListeners() {
$('.done-todo').off('click');
$('.js-todos-mark-all').off('click');
return $('.todo').off('click');
- };
+ }
- Todos.prototype.initBtnListeners = function() {
+ initBtnListeners() {
$('.done-todo').on('click', this.doneClicked);
$('.js-todos-mark-all').on('click', this.allDoneClicked);
return $('.todo').on('click', this.goToTodoUrl);
- };
+ }
- Todos.prototype.initFilters = function() {
+ initFilters() {
new UsersSelect();
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
@@ -38,125 +33,117 @@
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
- };
+ }
- Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) {
+ initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({
+ fieldName,
selectable: true,
filterable: searchFields ? true : false,
- fieldName: fieldName,
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: function() {
return $dropdown.closest('form.filter-form').submit();
}
})
- };
+ }
- Todos.prototype.doneClicked = function(e) {
- var $this;
+ doneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
- $this = $(e.currentTarget);
- $this.disable();
+ const $target = $(e.currentTarget);
+ $target.disable();
return $.ajax({
type: 'POST',
- url: $this.attr('href'),
+ url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete'
},
- success: (function(_this) {
- return function(data) {
- _this.redirectIfNeeded(data.count);
- _this.clearDone($this.closest('li'));
- return _this.updateBadges(data);
- };
- })(this)
+ success: (data) => {
+ this.redirectIfNeeded(data.count);
+ this.clearDone($target.closest('li'));
+ return this.updateBadges(data);
+ }
});
- };
+ }
- Todos.prototype.allDoneClicked = function(e) {
- var $this;
+ allDoneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
- $this = $(e.currentTarget);
- $this.disable();
+ $target = $(e.currentTarget);
+ $target.disable();
return $.ajax({
type: 'POST',
- url: $this.attr('href'),
+ url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete'
},
- success: (function(_this) {
- return function(data) {
- $this.remove();
- $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
- return _this.updateBadges(data);
- };
- })(this)
+ success: (data) => {
+ $target.remove();
+ $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
+ return this.updateBadges(data);
+ }
});
- };
+ }
- Todos.prototype.clearDone = function($row) {
- var $ul;
- $ul = $row.closest('ul');
+ clearDone($row) {
+ const $ul = $row.closest('ul');
$row.remove();
if (!$ul.find('li').length) {
return $ul.parents('.panel').remove();
}
- };
+ }
- Todos.prototype.updateBadges = function(data) {
+ updateBadges(data) {
$('.todos-pending .badge, .todos-pending-count').text(data.count);
return $('.todos-done .badge').text(data.done_count);
- };
+ }
- Todos.prototype.getTotalPages = function() {
+ getTotalPages() {
return this.el.data('totalPages');
- };
+ }
- Todos.prototype.getCurrentPage = function() {
+ getCurrentPage() {
return this.el.data('currentPage');
- };
+ }
- Todos.prototype.getTodosPerPage = function() {
+ getTodosPerPage() {
return this.el.data('perPage');
- };
+ }
+
+ redirectIfNeeded(total) {
+ const currPages = this.getTotalPages();
+ const currPage = this.getCurrentPage();
- Todos.prototype.redirectIfNeeded = function(total) {
- var currPage, currPages, newPages, pageParams, url;
- currPages = this.getTotalPages();
- currPage = this.getCurrentPage();
// Refresh if no remaining Todos
if (!total) {
- location.reload();
+ window.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
+
+ const newPages = Math.ceil(total / this.getTodosPerPage());
+ let url = location.href;
+
if (newPages !== currPages) {
// Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) {
- pageParams = {
+ const pageParams = {
page: currPages - 1
};
url = gl.utils.mergeUrlParams(pageParams, url);
}
return Turbolinks.visit(url);
}
- };
+ }
- Todos.prototype.goToTodoUrl = function(e) {
- var todoLink;
- todoLink = $(this).data('url');
+ goToTodoUrl(e) {
+ const todoLink = $(this).data('url');
if (!todoLink) {
return;
}
@@ -167,10 +154,8 @@
} else {
return Turbolinks.visit(todoLink);
}
- };
-
- return Todos;
-
- })();
+ }
+ }
-}).call(this);
+ global.Todos = Todos;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 6889d3a7491..0f97924d94e 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,7 +1,7 @@
-(global => {
+((global) => {
global.User = class {
- constructor(opts) {
- this.opts = opts;
+ constructor({ action }) {
+ this.action = action;
this.placeProfileAvatarsToTop();
this.initTabs();
this.hideProjectLimitMessage();
@@ -14,9 +14,9 @@
}
initTabs() {
- return new UserTabs({
+ return new global.UserTabs({
parentEl: '.user-profile',
- action: this.opts.action
+ action: this.action
});
}
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
deleted file mode 100644
index 8a657780eb6..00000000000
--- a/app/assets/javascripts/user_tabs.js
+++ /dev/null
@@ -1,188 +0,0 @@
-// 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); }; };
-
- this.UserTabs = (function() {
- function UserTabs(opts) {
- 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;
- }
- this.activateTab(this.action);
- }
-
- 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);
- };
-
- UserTabs.prototype.tabShown = function(event) {
- var $target, action, source;
- $target = $(event.target);
- action = $target.data('action');
- source = $target.attr('href');
- this.setTab(source, action);
- return this.setCurrentAction(action);
- };
-
- UserTabs.prototype.activateTab = function(action) {
- return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show');
- };
-
- UserTabs.prototype.setTab = function(source, action) {
- if (this.loaded[action] === true) {
- return;
- }
- if (action === 'activity') {
- this.loadActivities(source);
- }
- if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') {
- return this.loadTab(source, action);
- }
- };
-
- UserTabs.prototype.loadTab = function(source, action) {
- return $.ajax({
- beforeSend: (function(_this) {
- return function() {
- return _this.toggleLoading(true);
- };
- })(this),
- complete: (function(_this) {
- return function() {
- return _this.toggleLoading(false);
- };
- })(this),
- dataType: 'json',
- type: 'GET',
- url: source + ".json",
- success: (function(_this) {
- return function(data) {
- var tabSelector;
- tabSelector = 'div#' + action;
- _this.parentEl.find(tabSelector).html(data.html);
- _this.loaded[action] = true;
- // Fix tooltips
- return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
- };
- })(this)
- });
- };
-
- UserTabs.prototype.loadActivities = function(source) {
- var $calendarWrap;
- if (this.loaded['activity'] === true) {
- return;
- }
- $calendarWrap = this.parentEl.find('.user-calendar');
- $calendarWrap.load($calendarWrap.data('href'));
- new Activities();
- return this.loaded['activity'] = true;
- };
-
- UserTabs.prototype.toggleLoading = function(status) {
- return this.parentEl.find('.loading-status .loading').toggle(status);
- };
-
- 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,
- url: new_state
- }, document.title, new_state);
- return new_state;
- };
-
- return UserTabs;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
new file mode 100644
index 00000000000..dfdfa1e7f75
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -0,0 +1,157 @@
+/*
+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>
+*/
+((global) => {
+ class UserTabs {
+ constructor ({ defaultAction, action, parentEl }) {
+ this.loaded = {};
+ this.defaultAction = defaultAction || 'activity';
+ this.action = action || this.defaultAction;
+ this.$parentEl = $(parentEl) || $(document);
+ this._location = window.location;
+ this.$parentEl.find('.nav-links a')
+ .each((i, navLink) => {
+ this.loaded[$(navLink).attr('data-action')] = false;
+ });
+ this.actions = Object.keys(this.loaded);
+ this.bindEvents();
+
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+
+ this.activateTab(this.action);
+ }
+
+ bindEvents() {
+ return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
+ }
+
+ tabShown(event) {
+ const $target = $(event.target);
+ const action = $target.data('action');
+ const source = $target.attr('href');
+ this.setTab(source, action);
+ return this.setCurrentAction(source, action);
+ }
+
+ activateTab(action) {
+ return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
+ .tab('show');
+ }
+
+ setTab(source, action) {
+ if (this.loaded[action]) {
+ return;
+ }
+ if (action === 'activity') {
+ this.loadActivities(source);
+ }
+
+ const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
+ if (loadableActions.indexOf(action) > -1) {
+ return this.loadTab(source, action);
+ }
+ }
+
+ loadTab(source, action) {
+ return $.ajax({
+ beforeSend: () => this.toggleLoading(true),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ url: `${source}.json`,
+ success: (data) => {
+ const tabSelector = `div#${action}`;
+ this.$parentEl.find(tabSelector).html(data.html);
+ this.loaded[action] = true;
+ return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ }
+ });
+ }
+
+ loadActivities(source) {
+ if (this.loaded['activity']) {
+ return;
+ }
+ const $calendarWrap = this.$parentEl.find('.user-calendar');
+ $calendarWrap.load($calendarWrap.data('href'));
+ new Activities();
+ return this.loaded['activity'] = true;
+ }
+
+ toggleLoading(status) {
+ return this.$parentEl.find('.loading-status .loading')
+ .toggle(status);
+ }
+
+ setCurrentAction(source, action) {
+ let new_state = source
+ new_state = new_state.replace(/\/+$/, '');
+ new_state += this._location.search + this._location.hash;
+ history.replaceState({
+ turbolinks: true,
+ url: new_state
+ }, document.title, new_state);
+ return new_state;
+ }
+ }
+ global.UserTabs = UserTabs;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6
new file mode 100644
index 00000000000..bf4b2e320cd
--- /dev/null
+++ b/app/assets/javascripts/username_validator.js.es6
@@ -0,0 +1,133 @@
+((global) => {
+ const debounceTimeoutDuration = 1000;
+ const invalidInputClass = 'gl-field-error-outline';
+ const successInputClass = 'gl-field-success-outline';
+ const unavailableMessageSelector = '.username .validation-error';
+ const successMessageSelector = '.username .validation-success';
+ const pendingMessageSelector = '.username .validation-pending';
+ const invalidMessageSelector = '.username .gl-field-error';
+
+ class UsernameValidator {
+ constructor() {
+ this.inputElement = $('#new_user_username');
+ this.inputDomElement = this.inputElement.get(0);
+ this.state = {
+ available: false,
+ valid: false,
+ pending: false,
+ empty: true
+ };
+
+ const debounceTimeout = _.debounce((username) => {
+ this.validateUsername(username);
+ }, debounceTimeoutDuration);
+
+ this.inputElement.on('keyup.username_check', () => {
+ const username = this.inputElement.val();
+
+ this.state.valid = this.inputDomElement.validity.valid;
+ this.state.empty = !username.length;
+
+ if (this.state.valid) {
+ return debounceTimeout(username);
+ }
+
+ this.renderState();
+ });
+
+ // Override generic field validation
+ this.inputElement.on('invalid', this.interceptInvalid.bind(this));
+ }
+
+ renderState() {
+ // Clear all state
+ this.clearFieldValidationState();
+
+ if (this.state.valid && this.state.available) {
+ return this.setSuccessState();
+ }
+
+ if (this.state.empty) {
+ return this.clearFieldValidationState();
+ }
+
+ if (this.state.pending) {
+ return this.setPendingState();
+ }
+
+ if (!this.state.available) {
+ return this.setUnavailableState();
+ }
+
+ if (!this.state.valid) {
+ return this.setInvalidState();
+ }
+ }
+
+ interceptInvalid(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ validateUsername(username) {
+ if (this.state.valid) {
+ this.state.pending = true;
+ this.state.available = false;
+ this.renderState();
+ return $.ajax({
+ type: 'GET',
+ url: `/users/${username}/exists`,
+ dataType: 'json',
+ success: (res) => this.setAvailabilityState(res.exists)
+ });
+ }
+ }
+
+ setAvailabilityState(usernameTaken) {
+ if (usernameTaken) {
+ this.state.valid = false;
+ this.state.available = false;
+ } else {
+ this.state.available = true;
+ }
+ this.state.pending = false;
+ this.renderState();
+ }
+
+ clearFieldValidationState() {
+ this.inputElement.siblings('p').hide();
+
+ this.inputElement.removeClass(invalidInputClass)
+ .removeClass(successInputClass);
+ }
+
+ setUnavailableState() {
+ const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector);
+ this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
+ $usernameUnavailableMessage.show();
+ }
+
+ setSuccessState() {
+ const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector);
+ this.inputElement.addClass(successInputClass).removeClass(invalidInputClass);
+ $usernameSuccessMessage.show();
+ }
+
+ setPendingState() {
+ const $usernamePendingMessage = $(pendingMessageSelector);
+ if (this.state.pending) {
+ $usernamePendingMessage.show();
+ } else {
+ $usernamePendingMessage.hide();
+ }
+ }
+
+ setInvalidState() {
+ const $inputErrorMessage = $(invalidMessageSelector);
+ this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
+ $inputErrorMessage.show();
+ }
+ }
+
+ global.UsernameValidator = UsernameValidator;
+})(window);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 9c277998db4..3020b7cc239 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -14,11 +14,12 @@
$('.js-user-search').each((function(_this) {
return function(i, dropdown) {
var options = {};
- var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser;
+ var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id');
options.showCurrentUser = $dropdown.data('current-user');
showNullUser = $dropdown.data('null-user');
+ showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id');
@@ -70,9 +71,10 @@
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
- collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
- assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
+ collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
+ assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
data: function(term, callback) {
var isAuthorFilter;
isAuthorFilter = $('.js-author-search');
@@ -116,8 +118,11 @@
if (showDivider) {
users.splice(showDivider, 0, "divider");
}
- // Send the data back
- return callback(users);
+
+ callback(users);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
});
},
filterable: true,
@@ -127,8 +132,8 @@
},
selectable: true,
fieldName: $dropdown.data('field-name'),
- toggleLabel: function(selected) {
- if (selected && 'id' in selected) {
+ toggleLabel: function(selected, el) {
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
if (selected.text) {
return selected.text;
} else {
@@ -138,6 +143,7 @@
return defaultLabel;
}
},
+ defaultLabel: defaultLabel,
inputId: 'issue_assignee_id',
hidden: function(e) {
$selectbox.hide();
@@ -149,10 +155,12 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
- if ($dropdown.hasClass('js-filter-bulk-update')) {
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ e.preventDefault();
+ selectedId = user.id;
return;
}
- if (page === 'projects:boards:show') {
+ if ($('html').hasClass('issue-boards-page')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
@@ -167,6 +175,9 @@
return assignTo(selected);
}
},
+ id: function (user) {
+ return user.id;
+ },
renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : "";
@@ -250,10 +261,11 @@
}
}
if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
+ var trimmed = query.term.trim();
emailUser = {
name: "Invite \"" + query.term + "\"",
- username: query.term,
- id: query.term
+ username: trimmed,
+ id: trimmed
};
data.results.unshift(emailUser);
}
@@ -313,6 +325,10 @@
};
UsersSelect.prototype.user = function(user_id, callback) {
+ if(!/^\d+$/.test(user_id)) {
+ return false;
+ }
+
var url;
url = this.buildUrl(this.userPath);
url = url.replace(':id', user_id);
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index 897bc49e7df..e3ca7f6373a 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -5,6 +5,7 @@
display: none;
&.hide { display: block; }
}
+
&.open .content {
display: block;
&.hide { display: none; }
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index c79b22d4d21..98e301d3799 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -4,7 +4,7 @@
width: 40px;
height: 40px;
padding: 0;
- @include border-radius($avatar_radius);
+ border-radius: $avatar_radius;
border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline {
@@ -17,7 +17,7 @@
}
&.avatar-tile {
- @include border-radius(0);
+ border-radius: 0;
border: none;
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 2432ddb72f4..df2e2ea8d2c 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -19,10 +19,9 @@
&.diff-collapsed {
padding: 5px;
- cursor: pointer;
- &:hover {
- background-color: $row-hover;
+ .click-to-expand {
+ cursor: pointer;
}
}
}
@@ -135,7 +134,7 @@
}
.identicon {
- @include border-radius(50%);
+ border-radius: 50%;
}
}
@@ -205,6 +204,7 @@
}
}
}
+
&.user-cover-block {
padding: 24px 0 0;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ce489f7c3de..e6656c2d69a 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,5 +1,5 @@
@mixin btn-default {
- @include border-radius(3px);
+ border-radius: 3px;
font-size: $gl-font-size;
font-weight: 500;
padding: $gl-vert-padding $gl-btn-padding;
@@ -8,7 +8,7 @@
&:active {
outline: none;
background-color: $btn-active-gray;
- @include box-shadow($gl-btn-active-background);
+ box-shadow: $gl-btn-active-background;
}
}
@@ -25,7 +25,7 @@
&:focus {
background-color: $hover-background;
color: $hover-text;
- border-color: $hover-border;;
+ border-color: $hover-border;
}
}
@@ -43,7 +43,7 @@
&:active,
&.active {
- @include box-shadow ($gl-btn-active-background);
+ box-shadow: $gl-btn-active-background;
background-color: $dark;
border-color: $border-dark;
@@ -152,7 +152,8 @@
@include btn-blue-medium;
}
- &.btn-info {
+ &.btn-info,
+ &.btn-register {
@include btn-blue;
}
@@ -194,10 +195,17 @@
pointer-events: none !important;
}
- .caret {
+ .fa-caret-down,
+ .fa-caret-up {
margin-left: 5px;
}
+ &.dropdown-toggle {
+ .fa-caret-down {
+ margin-left: 3px;
+ }
+ }
+
svg {
height: 15px;
width: 15px;
@@ -233,6 +241,7 @@
width: 100%;
margin: 0;
margin-bottom: 15px;
+
&.btn {
padding: 6px 0;
}
@@ -272,7 +281,7 @@
}
.active {
- @include box-shadow($gl-btn-active-background);
+ box-shadow: $gl-btn-active-background;
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
@@ -314,6 +323,7 @@
.btn-build {
margin-left: 10px;
+
i {
color: $gl-icon-color;
}
@@ -321,6 +331,7 @@
.clone-dropdown-btn a {
color: $dropdown-link-color;
+
&:hover {
text-decoration: none;
}
@@ -330,6 +341,7 @@
background-color: $background-color !important;
border: 1px solid lightgrey;
cursor: default;
+
&:active {
-moz-box-shadow: inset 0 0 0 white;
-webkit-box-shadow: inset 0 0 0 white;
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index da7bab74a32..f3b6ad88ad6 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -13,10 +13,12 @@
color: $text-color;
background: $background-color;
}
+
.bs-callout h4 {
margin-top: 0;
margin-bottom: 5px;
}
+
.bs-callout p:last-child {
margin-bottom: 0;
}
@@ -27,16 +29,19 @@
border-color: #eed3d7;
color: #b94a48;
}
+
.bs-callout-warning {
background-color: #faf8f0;
border-color: #faebcc;
color: #8a6d3b;
}
+
.bs-callout-info {
background-color: #f4f8fa;
border-color: #bce8f1;
color: #34789a;
}
+
.bs-callout-success {
background-color: #dff0d8;
border-color: #5ca64d;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 5957dce89bc..800e2dba018 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,31 +1,31 @@
/** COLORS **/
.cgray { color: $gl-gray; }
-.clgray { color: #bbb }
+.clgray { color: #bbb; }
.cred { color: $gl-text-red; }
.cgreen { color: $gl-text-green; }
-.cdark { color: #444 }
+.cdark { color: #444; }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
-.prepend-top-10 { margin-top: 10px }
+.prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top: 20px }
-.prepend-left-5 { margin-left: 5px }
-.prepend-left-10 { margin-left: 10px }
+.prepend-top-20 { margin-top: 20px; }
+.prepend-left-5 { margin-left: 5px; }
+.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
-.prepend-left-20 { margin-left: 20px }
-.append-right-5 { margin-right: 5px }
-.append-right-10 { margin-right: 10px }
+.prepend-left-20 { margin-left: 20px; }
+.append-right-5 { margin-right: 5px; }
+.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
-.append-right-20 { margin-right: 20px }
-.append-bottom-0 { margin-bottom: 0 }
-.append-bottom-10 { margin-bottom: 10px }
-.append-bottom-15 { margin-bottom: 15px }
-.append-bottom-20 { margin-bottom: 20px }
+.append-right-20 { margin-right: 20px; }
+.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-10 { margin-bottom: 10px; }
+.append-bottom-15 { margin-bottom: 15px; }
+.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
-.inline { display: inline-block }
-.center { text-align: center }
+.inline { display: inline-block; }
+.center { text-align: center; }
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: #999; }
@@ -97,6 +97,7 @@ span.update-author {
color: #999;
font-weight: normal;
font-style: italic;
+
strong {
font-weight: bold;
font-style: normal;
@@ -128,7 +129,7 @@ p.time {
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
- img { max-width: 100% }
+ img { max-width: 100%; }
.note-title {
li {
border-bottom: none !important;
@@ -172,6 +173,7 @@ li.note {
@extend .col-md-6;
text-align: left;
margin-top: 40px;
+
pre {
background: white;
border: none;
@@ -197,6 +199,7 @@ li.note {
background: #c67;
color: #fff;
font-weight: bold;
+
a {
color: #fff;
text-decoration: underline;
@@ -227,6 +230,7 @@ li.note {
&.milestone-closed {
background: $gray-light;
}
+
.progress {
margin-bottom: 0;
margin-top: 4px;
@@ -286,6 +290,7 @@ table {
.footer-links {
margin-bottom: 20px;
+
a {
margin-right: 15px;
}
@@ -367,3 +372,5 @@ table {
margin-right: -$gl-padding;
border-top: 1px solid $border-color;
}
+
+.hide-bottom-border { border-bottom: none !important; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index b0ba112476b..a839371a6f2 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1,20 +1,3 @@
-.caret {
- display: inline-block;
- width: 0;
- height: 0;
- margin-left: 2px;
- vertical-align: middle;
- border-top: $caret-width-base dashed;
- border-right: $caret-width-base solid transparent;
- border-left: $caret-width-base solid transparent;
-}
-
-.btn-group {
- .caret {
- margin-left: 0;
- }
-}
-
.dropdown {
position: relative;
@@ -29,6 +12,7 @@
.dropdown-menu,
.dropdown-menu-nav {
display: block;
+
@media (max-width: $screen-xs-max) {
width: 100%;
}
@@ -65,6 +49,7 @@
margin-top: -6px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
+
&.fa-spinner {
font-size: 16px;
margin-top: -8px;
@@ -604,3 +589,9 @@
display: block;
color: $gl-placeholder-color;
}
+
+.dropdown-toggle-text {
+ &.is-default {
+ color: $gl-placeholder-color;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 76a3c083697..f49d7b92a00 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -57,6 +57,7 @@
margin-top: -3px;
}
}
+
.file-content {
background: #fff;
@@ -96,22 +97,27 @@
border: none;
margin: 0;
}
+
tr {
border-bottom: 1px solid #eee;
}
+
td {
&:first-child {
border-left: none;
}
+
&:last-child {
border-right: none;
}
}
+
td.blame-commit {
padding: 0 10px;
min-width: 400px;
background: $gray-light;
}
+
td.line-numbers {
float: none;
border-left: 1px solid #ddd;
@@ -121,6 +127,7 @@
margin-right: 0;
}
}
+
td.lines {
padding: 0;
}
@@ -137,8 +144,10 @@
border-left: 1px solid $border-color;
margin-bottom: 0;
background: white;
+
li {
color: #888;
+
p {
margin: 0;
color: #333;
@@ -158,7 +167,6 @@
*/
&.code {
padding: 0;
- -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 7ae309ba103..a55dcf4a699 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -3,6 +3,8 @@
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
+ position: relative;
+ z-index: 1;
.flash-notice {
@extend .alert;
@@ -19,7 +21,8 @@
.flash-notice, .flash-alert {
border-radius: $border-radius-default;
- .container-fluid.container-limited.flash-text {
+ .container-fluid,
+ .container-fluid.container-limited {
background: transparent;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 37ff7e22ed1..761c07384f4 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -9,7 +9,7 @@ input {
input[type='text'].danger {
background: #f2dede!important;
border-color: #d66;
- text-shadow: 0 1px 1px #fff
+ text-shadow: 0 1px 1px #fff;
}
.datetime-controls {
@@ -74,17 +74,17 @@ label {
.form-control {
@include box-shadow(none);
- border-radius: 3px;
+ border-radius: 2px;
padding: $gl-vert-padding $gl-input-padding;
}
.select-wrapper {
position: relative;
- .caret {
+ .fa-caret-down {
position: absolute;
right: 10px;
- top: $gl-padding;
+ top: 10px;
color: $gray-darkest;
pointer-events: none;
}
@@ -117,9 +117,11 @@ label {
display: table-cell;
width: 200px !important;
}
+
.input-group-addon {
background-color: #f7f8fa;
}
+
.input-group-addon:not(:first-child):not(:last-child) {
border-left: 0;
border-right: 0;
@@ -129,3 +131,8 @@ label {
.help-block {
margin-bottom: 0;
}
+
+.gl-field-error {
+ color: $red-normal;
+}
+
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 3673b81f183..fe834f4e2f6 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -62,7 +62,7 @@
}
i {
- color: $white-light
+ color: $white-light;
}
path,
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index d4a030f7f7a..3a4fdd0da22 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -57,6 +57,10 @@ header {
&:hover, &:focus, &:active {
background-color: $background-color;
}
+
+ .fa-caret-down {
+ font-size: 15px;
+ }
}
.navbar-toggle {
@@ -112,11 +116,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;
}
@@ -126,8 +134,12 @@ header {
}
@media (max-width: $screen-xs-max) {
- right: 25px;
+ right: 20px;
left: auto;
+
+ #logo {
+ left: auto;
+ }
}
}
@@ -156,6 +168,7 @@ header {
a {
color: $gl-text-color;
+
&:hover {
text-decoration: underline;
}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 8bfc0d583c5..ba3930e03bd 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -16,7 +16,7 @@
margin-top: 5px;
}
- @include border-radius(3px);
+ border-radius: 3px;
display: block;
float: left;
margin-right: 10px;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 8bb047db2dd..7baa4296abf 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -27,3 +27,15 @@ body {
.container-limited {
max-width: $fixed-layout-width;
}
+
+
+/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch,
+which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
+effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children
+of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */
+
+.navbar,
+.page-gutter,
+.page-with-sidebar {
+ -webkit-overflow-scrolling: auto;
+}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index efc348214c2..4b2627c1b87 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -60,6 +60,7 @@
padding-top: 1px;
margin: 0;
color: $gray-dark;
+
img {
position: relative;
top: 3px;
@@ -128,6 +129,10 @@ ul.content-list {
color: $gl-dark-link-color;
}
+ .member-group-link {
+ color: $blue-normal;
+ }
+
.description {
p {
@include str-truncated;
@@ -168,6 +173,14 @@ ul.content-list {
}
}
+ .member-controls {
+ float: none;
+
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ }
+ }
+
// When dragging a list item
&.ui-sortable-helper {
border-bottom: none;
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
index 3ee3fb4cee5..a90e45bb5f4 100644
--- a/app/assets/stylesheets/framework/logo.scss
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -1,15 +1,3 @@
-@mixin unique-keyframes {
- $animation-name: unique-id();
- @include webkit-prefix(animation-name, $animation-name);
-
- @-webkit-keyframes #{$animation-name} {
- @content;
- }
- @keyframes #{$animation-name} {
- @content;
- }
-}
-
@mixin tanuki-logo-colors($path-color) {
fill: $path-color;
transition: all 0.8s;
@@ -20,28 +8,6 @@
}
}
-@mixin tanuki-second-highlight-animations($tanuki-color) {
- @include unique-keyframes {
- 10%, 80% {
- fill: #{$tanuki-color}
- }
- 20%, 90% {
- fill: lighten($tanuki-color, 25%);
- }
- }
-}
-
-@mixin tanuki-forth-highlight-animations($tanuki-color) {
- @include unique-keyframes {
- 30%, 60% {
- fill: #{$tanuki-color};
- }
- 40%, 70% {
- fill: lighten($tanuki-color, 25%);
- }
- }
-}
-
.tanuki-logo {
.tanuki-left-ear,
@@ -67,10 +33,11 @@
}
.tanuki-left-cheek {
- @include unique-keyframes {
+ @include include-keyframes(animate-tanuki-left-cheek) {
0%, 10%, 100% {
fill: lighten($tanuki-yellow, 25%);
}
+
90% {
fill: $tanuki-yellow;
}
@@ -78,18 +45,35 @@
}
.tanuki-left-eye {
- @include tanuki-second-highlight-animations($tanuki-orange);
+ @include include-keyframes(animate-tanuki-left-eye) {
+ 10%, 80% {
+ fill: $tanuki-orange;
+ }
+
+ 20%, 90% {
+ fill: lighten($tanuki-orange, 25%);
+ }
+ }
}
.tanuki-left-ear {
- @include tanuki-second-highlight-animations($tanuki-red);
+ @include include-keyframes(animate-tanuki-left-ear) {
+ 10%, 80% {
+ fill: $tanuki-red;
+ }
+
+ 20%, 90% {
+ fill: lighten($tanuki-red, 25%);
+ }
+ }
}
.tanuki-nose {
- @include unique-keyframes {
+ @include include-keyframes(animate-tanuki-nose) {
20%, 70% {
fill: $tanuki-red;
}
+
30%, 80% {
fill: lighten($tanuki-red, 25%);
}
@@ -97,22 +81,39 @@
}
.tanuki-right-eye {
- @include tanuki-forth-highlight-animations($tanuki-orange);
+ @include include-keyframes(animate-tanuki-right-eye) {
+ 30%, 60% {
+ fill: $tanuki-orange;
+ }
+
+ 40%, 70% {
+ fill: lighten($tanuki-orange, 25%);
+ }
+ }
}
.tanuki-right-ear {
- @include tanuki-forth-highlight-animations($tanuki-red);
+ @include include-keyframes(animate-tanuki-right-ear) {
+ 30%, 60% {
+ fill: $tanuki-red;
+ }
+
+ 40%, 70% {
+ fill: lighten($tanuki-red, 25%);
+ }
+ }
}
.tanuki-right-cheek {
- @include unique-keyframes {
+ @include include-keyframes(animate-tanuki-right-cheek) {
40% {
fill: $tanuki-yellow;
}
+
60% {
fill: lighten($tanuki-yellow, 25%);
}
}
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index edea4ad00eb..6d28d98b283 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -86,7 +86,7 @@
}
.markdown-area {
- @include border-radius(0);
+ border-radius: 0;
background: #fff;
border: 1px solid #ddd;
min-height: 140px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1ec08cdef23..f84ca36d10f 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -1,15 +1,4 @@
/**
- * Generic mixins
- */
-@mixin box-shadow($shadow) {
- box-shadow: $shadow;
-}
-
-@mixin border-radius($radius) {
- border-radius: $radius;
-}
-
-/**
* Prefilled mixins
* Mixins with fixed values
*/
@@ -45,6 +34,7 @@
&.active {
background: $gray-light;
+
a {
font-weight: 600;
}
@@ -95,3 +85,10 @@
@content;
}
}
+
+@mixin include-keyframes($animation-name) {
+ @include webkit-prefix(animation-name, $animation-name);
+ @include keyframes($animation-name) {
+ @content;
+ }
+}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 76b93b23b95..9fe390eb09d 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -133,5 +133,5 @@
font-size: 20px;
color: #777;
z-index: 100;
- @include box-shadow(0 1px 2px #ddd);
+ box-shadow: 0 1px 2px #ddd;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea43f4afc37..899db045b74 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -210,6 +210,7 @@
@media (max-width: $screen-xs-max) {
padding-bottom: 0;
width: 100%;
+
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
margin: 0 0 10px;
display: block;
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index c6f30e144fd..5ba0486177f 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -13,6 +13,11 @@
.dropdown-menu-toggle {
line-height: 20px;
}
+
+ .badge {
+ margin-top: -2px;
+ margin-left: 5px;
+ }
}
.panel-body {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index c75dacf95d9..e0708c65695 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -21,7 +21,14 @@
padding-right: 10px;
b {
- @extend .caret;
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: $caret-width-base dashed;
+ border-right: $caret-width-base solid transparent;
+ border-left: $caret-width-base solid transparent;
color: $gray-darkest;
}
}
@@ -39,8 +46,8 @@
}
.select2-drop {
- @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
- @include border-radius ($border-radius-default);
+ box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
+ border-radius: $border-radius-default;
border: none;
min-width: 175px;
}
@@ -65,7 +72,7 @@
.select2-container-active {
.select2-choice, .select2-choices {
- @include box-shadow(none);
+ box-shadow: none;
}
}
@@ -75,18 +82,18 @@
outline: 0;
background-image: none;
background-color: $white-dark;
- @include box-shadow($gl-btn-active-gradient);
+ box-shadow: $gl-btn-active-gradient;
}
}
.select2-container-multi {
.select2-choices {
- @include border-radius($border-radius-default);
+ border-radius: $border-radius-default;
border-color: $input-border;
background: none;
.select2-search-field input {
- padding: $gl-padding / 2;
+ padding: 5px $gl-padding / 2;
font-size: 13px;
height: auto;
font-family: inherit;
@@ -94,7 +101,7 @@
}
.select2-search-choice {
- margin: 8px 0 0 8px;
+ margin: 5px 0 0 8px;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
@@ -116,7 +123,7 @@
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
- @include box-shadow($gl-btn-active-gradient);
+ box-shadow: $gl-btn-active-gradient;
}
}
@@ -130,6 +137,7 @@
.select2-results {
max-height: 350px;
+
.select2-highlighted {
background: $gl-primary;
}
@@ -150,7 +158,7 @@
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
- @include border-radius($border-radius-default);
+ border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
@@ -205,9 +213,11 @@
.group-image {
float: left;
}
+
.group-name {
font-weight: bold;
}
+
.group-path {
color: #999;
}
@@ -232,6 +242,7 @@
color: #aaa;
font-weight: normal;
}
+
.namespace-path {
margin-left: 10px;
font-weight: bolder;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3b7de4b57bb..1d8e64a0e4b 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -4,7 +4,7 @@
&.page-sidebar-pinned {
.sidebar-wrapper {
- @include box-shadow(none);
+ box-shadow: none;
}
}
@@ -17,7 +17,7 @@
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
- @include box-shadow(2px 0 16px 0 $black-transparent);
+ box-shadow: 2px 0 16px 0 $black-transparent;
}
}
@@ -100,7 +100,7 @@
.count {
float: right;
padding: 0 8px;
- @include border-radius(6px);
+ border-radius: 6px;
}
}
@@ -142,6 +142,7 @@
transition-duration: .3s;
position: absolute;
top: 0;
+ cursor: pointer;
&:hover,
&:focus {
@@ -184,6 +185,10 @@ header.header-sidebar-pinned {
@media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width;
+
+ .merge-request-tabs-holder.affix {
+ right: $sidebar_collapsed_width;
+ }
}
.sidebar-collapsed-icon {
@@ -206,6 +211,10 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
+
+ .merge-request-tabs-holder.affix {
+ right: $gutter_width;
+ }
}
&.with-overlay {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 0b0bd80c326..eb63a9f214b 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -48,6 +48,7 @@
&:before {
background: none;
}
+
.timeline-entry .timeline-entry-inner {
.timeline-icon {
display: none;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index e3154657c54..f4106641269 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -48,31 +48,40 @@
.clearfix {
@include clearfix();
}
+
.center-block {
@include center-block();
}
+
.pull-right {
float: right !important;
}
+
.pull-left {
float: left !important;
}
+
.hide {
display: none;
}
+
.show {
display: block !important;
}
+
.invisible {
visibility: hidden;
}
+
.text-hide {
@include text-hide();
}
+
.hidden {
display: none !important;
visibility: hidden !important;
}
+
.affix {
position: fixed;
}
@@ -146,6 +155,7 @@
padding: 6px 15px;
font-size: 13px;
font-weight: normal;
+
a {
color: #777;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9f2d53d5206..55de9053be5 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -45,40 +45,38 @@
}
h1 {
- font-size: 2em;
+ font-size: 1.75em;
font-weight: 600;
- margin: 1em 0 10px;
+ margin: 16px 0 10px;
padding: 0 0 0.3em;
- border-bottom: 1px solid $btn-default-border;
+ border-bottom: 1px solid $white-dark;
color: $gl-gray-dark;
}
h2 {
- font-size: 1.6em;
+ font-size: 1.5em;
font-weight: 600;
- margin: 1em 0 10px;
- padding-bottom: 0.3em;
- border-bottom: 1px solid $btn-default-border;
+ margin: 16px 0 10px;
color: $gl-gray-dark;
}
h3 {
- margin: 1em 0 10px;
- font-size: 1.4em;
+ margin: 16px 0 10px;
+ font-size: 1.3em;
}
h4 {
- margin: 1em 0 10px;
- font-size: 1.25em;
+ margin: 16px 0 10px;
+ font-size: 1.2em;
}
h5 {
- margin: 1em 0 10px;
+ margin: 16px 0 10px;
font-size: 1em;
}
h6 {
- margin: 1em 0 10px;
+ margin: 16px 0 10px;
font-size: 0.95em;
}
@@ -87,7 +85,12 @@
font-size: inherit;
padding: 8px 21px;
margin: 12px 0;
- border-left: 3px solid #e7e9ed;
+ border-left: 3px solid $white-dark;
+ }
+
+ blockquote:dir(rtl) {
+ border-left: 0;
+ border-right: 3px solid $white-dark;
}
blockquote p {
@@ -106,17 +109,22 @@
@extend .table-bordered;
margin: 12px 0;
color: #5c5d5e;
+
th {
background: #f8fafc;
}
}
+ table:dir(rtl) th {
+ text-align: right;
+ }
+
pre {
margin: 12px 0;
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
- @include border-radius(2px);
+ border-radius: 2px;
}
p > code {
@@ -128,6 +136,10 @@
margin: 3px 0 3px 28px !important;
}
+ ul:dir(rtl), ol:dir(rtl) {
+ margin: 3px 28px 3px 0 !important;
+ }
+
li {
line-height: 1.6em;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 14ec310de2d..eafe84570a8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -16,9 +16,12 @@ $white-light: #fff;
$white-normal: #ededed;
$white-dark: #ececec;
+$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
+$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
$gray-dark: #ededed;
+$gray-darker: #eee;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
@@ -33,6 +36,8 @@ $blue-medium-light: #3498cb;
$blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4;
+$blue-light-transparent: rgba(44, 159, 216, 0.05);
+
$orange-light: #fc8a51;
$orange-normal: #e75e40;
$orange-dark: #ce5237;
@@ -52,6 +57,7 @@ $border-gray-light: #dcdcdc;
$border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf;
+$border-green-extra-light: #9adb84;
$border-green-light: #2faa60;
$border-green-normal: #2ca05b;
$border-green-dark: #279654;
@@ -91,6 +97,7 @@ $table-text-gray: #8f8f8f;
$gl-font-size: 15px;
$gl-title-color: #333;
$gl-text-color: #5c5c5c;
+$gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 16ffbe57a99..a3acee299e3 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -55,68 +55,68 @@
color: #000 !important;
}
- .hll { background-color: #373b41 }
- .c { color: #969896 } /* Comment */
- .err { color: #c66 } /* Error */
- .k { color: #b294bb } /* Keyword */
- .l { color: #de935f } /* Literal */
- .n { color: #c5c8c6 } /* Name */
- .o { color: #8abeb7 } /* Operator */
- .p { color: #c5c8c6 } /* Punctuation */
- .cm { color: #969896 } /* Comment.Multiline */
- .cp { color: #969896 } /* Comment.Preproc */
- .c1 { color: #969896 } /* Comment.Single */
- .cs { color: #969896 } /* Comment.Special */
- .gd { color: #c66 } /* Generic.Deleted */
- .ge { font-style: italic } /* Generic.Emph */
- .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
- .gi { color: #b5bd68 } /* Generic.Inserted */
- .gp { color: #969896; font-weight: bold } /* Generic.Prompt */
- .gs { font-weight: bold } /* Generic.Strong */
- .gu { color: #8abeb7; font-weight: bold } /* Generic.Subheading */
- .kc { color: #b294bb } /* Keyword.Constant */
- .kd { color: #b294bb } /* Keyword.Declaration */
- .kn { color: #8abeb7 } /* Keyword.Namespace */
- .kp { color: #b294bb } /* Keyword.Pseudo */
- .kr { color: #b294bb } /* Keyword.Reserved */
- .kt { color: #f0c674 } /* Keyword.Type */
- .ld { color: #b5bd68 } /* Literal.Date */
- .m { color: #de935f } /* Literal.Number */
- .s { color: #b5bd68 } /* Literal.String */
- .na { color: #81a2be } /* Name.Attribute */
- .nb { color: #c5c8c6 } /* Name.Builtin */
- .nc { color: #f0c674 } /* Name.Class */
- .no { color: #c66 } /* Name.Constant */
- .nd { color: #8abeb7 } /* Name.Decorator */
- .ni { color: #c5c8c6 } /* Name.Entity */
- .ne { color: #c66 } /* Name.Exception */
- .nf { color: #81a2be } /* Name.Function */
- .nl { color: #c5c8c6 } /* Name.Label */
- .nn { color: #f0c674 } /* Name.Namespace */
- .nx { color: #81a2be } /* Name.Other */
- .py { color: #c5c8c6 } /* Name.Property */
- .nt { color: #8abeb7 } /* Name.Tag */
- .nv { color: #c66 } /* Name.Variable */
- .ow { color: #8abeb7 } /* Operator.Word */
- .w { color: #c5c8c6 } /* Text.Whitespace */
- .mf { color: #de935f } /* Literal.Number.Float */
- .mh { color: #de935f } /* Literal.Number.Hex */
- .mi { color: #de935f } /* Literal.Number.Integer */
- .mo { color: #de935f } /* Literal.Number.Oct */
- .sb { color: #b5bd68 } /* Literal.String.Backtick */
- .sc { color: #c5c8c6 } /* Literal.String.Char */
- .sd { color: #969896 } /* Literal.String.Doc */
- .s2 { color: #b5bd68 } /* Literal.String.Double */
- .se { color: #de935f } /* Literal.String.Escape */
- .sh { color: #b5bd68 } /* Literal.String.Heredoc */
- .si { color: #de935f } /* Literal.String.Interpol */
- .sx { color: #b5bd68 } /* Literal.String.Other */
- .sr { color: #b5bd68 } /* Literal.String.Regex */
- .s1 { color: #b5bd68 } /* Literal.String.Single */
- .ss { color: #b5bd68 } /* Literal.String.Symbol */
- .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
- .vc { color: #c66 } /* Name.Variable.Class */
- .vg { color: #c66 } /* Name.Variable.Global */
- .vi { color: #c66 } /* Name.Variable.Instance */
- .il { color: #de935f } /* Literal.Number.Integer.Long */
+ .hll { background-color: #373b41; }
+ .c { color: #969896; } /* Comment */
+ .err { color: #c66; } /* Error */
+ .k { color: #b294bb; } /* Keyword */
+ .l { color: #de935f; } /* Literal */
+ .n { color: #c5c8c6; } /* Name */
+ .o { color: #8abeb7; } /* Operator */
+ .p { color: #c5c8c6; } /* Punctuation */
+ .cm { color: #969896; } /* Comment.Multiline */
+ .cp { color: #969896; } /* Comment.Preproc */
+ .c1 { color: #969896; } /* Comment.Single */
+ .cs { color: #969896; } /* Comment.Special */
+ .gd { color: #c66; } /* Generic.Deleted */
+ .ge { font-style: italic; } /* Generic.Emph */
+ .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */
+ .gi { color: #b5bd68; } /* Generic.Inserted */
+ .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */
+ .gs { font-weight: bold; } /* Generic.Strong */
+ .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */
+ .kc { color: #b294bb; } /* Keyword.Constant */
+ .kd { color: #b294bb; } /* Keyword.Declaration */
+ .kn { color: #8abeb7; } /* Keyword.Namespace */
+ .kp { color: #b294bb; } /* Keyword.Pseudo */
+ .kr { color: #b294bb; } /* Keyword.Reserved */
+ .kt { color: #f0c674; } /* Keyword.Type */
+ .ld { color: #b5bd68; } /* Literal.Date */
+ .m { color: #de935f; } /* Literal.Number */
+ .s { color: #b5bd68; } /* Literal.String */
+ .na { color: #81a2be; } /* Name.Attribute */
+ .nb { color: #c5c8c6; } /* Name.Builtin */
+ .nc { color: #f0c674; } /* Name.Class */
+ .no { color: #c66; } /* Name.Constant */
+ .nd { color: #8abeb7; } /* Name.Decorator */
+ .ni { color: #c5c8c6; } /* Name.Entity */
+ .ne { color: #c66; } /* Name.Exception */
+ .nf { color: #81a2be; } /* Name.Function */
+ .nl { color: #c5c8c6; } /* Name.Label */
+ .nn { color: #f0c674; } /* Name.Namespace */
+ .nx { color: #81a2be; } /* Name.Other */
+ .py { color: #c5c8c6; } /* Name.Property */
+ .nt { color: #8abeb7; } /* Name.Tag */
+ .nv { color: #c66; } /* Name.Variable */
+ .ow { color: #8abeb7; } /* Operator.Word */
+ .w { color: #c5c8c6; } /* Text.Whitespace */
+ .mf { color: #de935f; } /* Literal.Number.Float */
+ .mh { color: #de935f; } /* Literal.Number.Hex */
+ .mi { color: #de935f; } /* Literal.Number.Integer */
+ .mo { color: #de935f; } /* Literal.Number.Oct */
+ .sb { color: #b5bd68; } /* Literal.String.Backtick */
+ .sc { color: #c5c8c6; } /* Literal.String.Char */
+ .sd { color: #969896; } /* Literal.String.Doc */
+ .s2 { color: #b5bd68; } /* Literal.String.Double */
+ .se { color: #de935f; } /* Literal.String.Escape */
+ .sh { color: #b5bd68; } /* Literal.String.Heredoc */
+ .si { color: #de935f; } /* Literal.String.Interpol */
+ .sx { color: #b5bd68; } /* Literal.String.Other */
+ .sr { color: #b5bd68; } /* Literal.String.Regex */
+ .s1 { color: #b5bd68; } /* Literal.String.Single */
+ .ss { color: #b5bd68; } /* Literal.String.Symbol */
+ .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */
+ .vc { color: #c66; } /* Name.Variable.Class */
+ .vg { color: #c66; } /* Name.Variable.Global */
+ .vi { color: #c66; } /* Name.Variable.Instance */
+ .il { color: #de935f; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 7de920e074b..e9228c94db9 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -55,65 +55,65 @@
color: #000 !important;
}
- .hll { background-color: #49483e }
- .c { color: #75715e } /* Comment */
- .err { color: #960050; background-color: #1e0010 } /* Error */
- .k { color: #66d9ef } /* Keyword */
- .l { color: #ae81ff } /* Literal */
- .n { color: #f8f8f2 } /* Name */
- .o { color: #f92672 } /* Operator */
- .p { color: #f8f8f2 } /* Punctuation */
- .cm { color: #75715e } /* Comment.Multiline */
- .cp { color: #75715e } /* Comment.Preproc */
- .c1 { color: #75715e } /* Comment.Single */
- .cs { color: #75715e } /* Comment.Special */
- .ge { font-style: italic } /* Generic.Emph */
- .gs { font-weight: bold } /* Generic.Strong */
- .kc { color: #66d9ef } /* Keyword.Constant */
- .kd { color: #66d9ef } /* Keyword.Declaration */
- .kn { color: #f92672 } /* Keyword.Namespace */
- .kp { color: #66d9ef } /* Keyword.Pseudo */
- .kr { color: #66d9ef } /* Keyword.Reserved */
- .kt { color: #66d9ef } /* Keyword.Type */
- .ld { color: #e6db74 } /* Literal.Date */
- .m { color: #ae81ff } /* Literal.Number */
- .s { color: #e6db74 } /* Literal.String */
- .na { color: #a6e22e } /* Name.Attribute */
- .nb { color: #f8f8f2 } /* Name.Builtin */
- .nc { color: #a6e22e } /* Name.Class */
- .no { color: #66d9ef } /* Name.Constant */
- .nd { color: #a6e22e } /* Name.Decorator */
- .ni { color: #f8f8f2 } /* Name.Entity */
- .ne { color: #a6e22e } /* Name.Exception */
- .nf { color: #a6e22e } /* Name.Function */
- .nl { color: #f8f8f2 } /* Name.Label */
- .nn { color: #f8f8f2 } /* Name.Namespace */
- .nx { color: #a6e22e } /* Name.Other */
- .py { color: #f8f8f2 } /* Name.Property */
- .nt { color: #f92672 } /* Name.Tag */
- .nv { color: #f8f8f2 } /* Name.Variable */
- .ow { color: #f92672 } /* Operator.Word */
- .w { color: #f8f8f2 } /* Text.Whitespace */
- .mf { color: #ae81ff } /* Literal.Number.Float */
- .mh { color: #ae81ff } /* Literal.Number.Hex */
- .mi { color: #ae81ff } /* Literal.Number.Integer */
- .mo { color: #ae81ff } /* Literal.Number.Oct */
- .sb { color: #e6db74 } /* Literal.String.Backtick */
- .sc { color: #e6db74 } /* Literal.String.Char */
- .sd { color: #e6db74 } /* Literal.String.Doc */
- .s2 { color: #e6db74 } /* Literal.String.Double */
- .se { color: #ae81ff } /* Literal.String.Escape */
- .sh { color: #e6db74 } /* Literal.String.Heredoc */
- .si { color: #e6db74 } /* Literal.String.Interpol */
- .sx { color: #e6db74 } /* Literal.String.Other */
- .sr { color: #e6db74 } /* Literal.String.Regex */
- .s1 { color: #e6db74 } /* Literal.String.Single */
- .ss { color: #e6db74 } /* Literal.String.Symbol */
- .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
- .vc { color: #f8f8f2 } /* Name.Variable.Class */
- .vg { color: #f8f8f2 } /* Name.Variable.Global */
- .vi { color: #f8f8f2 } /* Name.Variable.Instance */
- .il { color: #ae81ff } /* Literal.Number.Integer.Long */
+ .hll { background-color: #49483e; }
+ .c { color: #75715e; } /* Comment */
+ .err { color: #960050; background-color: #1e0010; } /* Error */
+ .k { color: #66d9ef; } /* Keyword */
+ .l { color: #ae81ff; } /* Literal */
+ .n { color: #f8f8f2; } /* Name */
+ .o { color: #f92672; } /* Operator */
+ .p { color: #f8f8f2; } /* Punctuation */
+ .cm { color: #75715e; } /* Comment.Multiline */
+ .cp { color: #75715e; } /* Comment.Preproc */
+ .c1 { color: #75715e; } /* Comment.Single */
+ .cs { color: #75715e; } /* Comment.Special */
+ .ge { font-style: italic; } /* Generic.Emph */
+ .gs { font-weight: bold; } /* Generic.Strong */
+ .kc { color: #66d9ef; } /* Keyword.Constant */
+ .kd { color: #66d9ef; } /* Keyword.Declaration */
+ .kn { color: #f92672; } /* Keyword.Namespace */
+ .kp { color: #66d9ef; } /* Keyword.Pseudo */
+ .kr { color: #66d9ef; } /* Keyword.Reserved */
+ .kt { color: #66d9ef; } /* Keyword.Type */
+ .ld { color: #e6db74; } /* Literal.Date */
+ .m { color: #ae81ff; } /* Literal.Number */
+ .s { color: #e6db74; } /* Literal.String */
+ .na { color: #a6e22e; } /* Name.Attribute */
+ .nb { color: #f8f8f2; } /* Name.Builtin */
+ .nc { color: #a6e22e; } /* Name.Class */
+ .no { color: #66d9ef; } /* Name.Constant */
+ .nd { color: #a6e22e; } /* Name.Decorator */
+ .ni { color: #f8f8f2; } /* Name.Entity */
+ .ne { color: #a6e22e; } /* Name.Exception */
+ .nf { color: #a6e22e; } /* Name.Function */
+ .nl { color: #f8f8f2; } /* Name.Label */
+ .nn { color: #f8f8f2; } /* Name.Namespace */
+ .nx { color: #a6e22e; } /* Name.Other */
+ .py { color: #f8f8f2; } /* Name.Property */
+ .nt { color: #f92672; } /* Name.Tag */
+ .nv { color: #f8f8f2; } /* Name.Variable */
+ .ow { color: #f92672; } /* Operator.Word */
+ .w { color: #f8f8f2; } /* Text.Whitespace */
+ .mf { color: #ae81ff; } /* Literal.Number.Float */
+ .mh { color: #ae81ff; } /* Literal.Number.Hex */
+ .mi { color: #ae81ff; } /* Literal.Number.Integer */
+ .mo { color: #ae81ff; } /* Literal.Number.Oct */
+ .sb { color: #e6db74; } /* Literal.String.Backtick */
+ .sc { color: #e6db74; } /* Literal.String.Char */
+ .sd { color: #e6db74; } /* Literal.String.Doc */
+ .s2 { color: #e6db74; } /* Literal.String.Double */
+ .se { color: #ae81ff; } /* Literal.String.Escape */
+ .sh { color: #e6db74; } /* Literal.String.Heredoc */
+ .si { color: #e6db74; } /* Literal.String.Interpol */
+ .sx { color: #e6db74; } /* Literal.String.Other */
+ .sr { color: #e6db74; } /* Literal.String.Regex */
+ .s1 { color: #e6db74; } /* Literal.String.Single */
+ .ss { color: #e6db74; } /* Literal.String.Symbol */
+ .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */
+ .vc { color: #f8f8f2; } /* Name.Variable.Class */
+ .vg { color: #f8f8f2; } /* Name.Variable.Global */
+ .vi { color: #f8f8f2; } /* Name.Variable.Instance */
+ .il { color: #ae81ff; } /* Literal.Number.Integer.Long */
.gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */
.gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */
.gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index b11499c71ee..c3c7773b9e2 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -72,72 +72,72 @@
green #859900 operators, other keywords
*/
- .c { color: #586e75 } /* Comment */
- .err { color: #93a1a1 } /* Error */
- .g { color: #93a1a1 } /* Generic */
- .k { color: #859900 } /* Keyword */
- .l { color: #93a1a1 } /* Literal */
- .n { color: #93a1a1 } /* Name */
- .o { color: #859900 } /* Operator */
- .x { color: #cb4b16 } /* Other */
- .p { color: #93a1a1 } /* Punctuation */
- .cm { color: #586e75 } /* Comment.Multiline */
- .cp { color: #859900 } /* Comment.Preproc */
- .c1 { color: #586e75 } /* Comment.Single */
- .cs { color: #859900 } /* Comment.Special */
- .gd { color: #2aa198 } /* Generic.Deleted */
- .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */
- .gr { color: #dc322f } /* Generic.Error */
- .gh { color: #cb4b16 } /* Generic.Heading */
- .gi { color: #859900 } /* Generic.Inserted */
- .go { color: #93a1a1 } /* Generic.Output */
- .gp { color: #93a1a1 } /* Generic.Prompt */
- .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */
- .gu { color: #cb4b16 } /* Generic.Subheading */
- .gt { color: #93a1a1 } /* Generic.Traceback */
- .kc { color: #cb4b16 } /* Keyword.Constant */
- .kd { color: #268bd2 } /* Keyword.Declaration */
- .kn { color: #859900 } /* Keyword.Namespace */
- .kp { color: #859900 } /* Keyword.Pseudo */
- .kr { color: #268bd2 } /* Keyword.Reserved */
- .kt { color: #dc322f } /* Keyword.Type */
- .ld { color: #93a1a1 } /* Literal.Date */
- .m { color: #2aa198 } /* Literal.Number */
- .s { color: #2aa198 } /* Literal.String */
- .na { color: #93a1a1 } /* Name.Attribute */
- .nb { color: #b58900 } /* Name.Builtin */
- .nc { color: #268bd2 } /* Name.Class */
- .no { color: #cb4b16 } /* Name.Constant */
- .nd { color: #268bd2 } /* Name.Decorator */
- .ni { color: #cb4b16 } /* Name.Entity */
- .ne { color: #cb4b16 } /* Name.Exception */
- .nf { color: #268bd2 } /* Name.Function */
- .nl { color: #93a1a1 } /* Name.Label */
- .nn { color: #93a1a1 } /* Name.Namespace */
- .nx { color: #93a1a1 } /* Name.Other */
- .py { color: #93a1a1 } /* Name.Property */
- .nt { color: #268bd2 } /* Name.Tag */
- .nv { color: #268bd2 } /* Name.Variable */
- .ow { color: #859900 } /* Operator.Word */
- .w { color: #93a1a1 } /* Text.Whitespace */
- .mf { color: #2aa198 } /* Literal.Number.Float */
- .mh { color: #2aa198 } /* Literal.Number.Hex */
- .mi { color: #2aa198 } /* Literal.Number.Integer */
- .mo { color: #2aa198 } /* Literal.Number.Oct */
- .sb { color: #586e75 } /* Literal.String.Backtick */
- .sc { color: #2aa198 } /* Literal.String.Char */
- .sd { color: #93a1a1 } /* Literal.String.Doc */
- .s2 { color: #2aa198 } /* Literal.String.Double */
- .se { color: #cb4b16 } /* Literal.String.Escape */
- .sh { color: #93a1a1 } /* Literal.String.Heredoc */
- .si { color: #2aa198 } /* Literal.String.Interpol */
- .sx { color: #2aa198 } /* Literal.String.Other */
- .sr { color: #dc322f } /* Literal.String.Regex */
- .s1 { color: #2aa198 } /* Literal.String.Single */
- .ss { color: #2aa198 } /* Literal.String.Symbol */
- .bp { color: #268bd2 } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2 } /* Name.Variable.Class */
- .vg { color: #268bd2 } /* Name.Variable.Global */
- .vi { color: #268bd2 } /* Name.Variable.Instance */
- .il { color: #2aa198 } /* Literal.Number.Integer.Long */
+ .c { color: #586e75; } /* Comment */
+ .err { color: #93a1a1; } /* Error */
+ .g { color: #93a1a1; } /* Generic */
+ .k { color: #859900; } /* Keyword */
+ .l { color: #93a1a1; } /* Literal */
+ .n { color: #93a1a1; } /* Name */
+ .o { color: #859900; } /* Operator */
+ .x { color: #cb4b16; } /* Other */
+ .p { color: #93a1a1; } /* Punctuation */
+ .cm { color: #586e75; } /* Comment.Multiline */
+ .cp { color: #859900; } /* Comment.Preproc */
+ .c1 { color: #586e75; } /* Comment.Single */
+ .cs { color: #859900; } /* Comment.Special */
+ .gd { color: #2aa198; } /* Generic.Deleted */
+ .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */
+ .gr { color: #dc322f; } /* Generic.Error */
+ .gh { color: #cb4b16; } /* Generic.Heading */
+ .gi { color: #859900; } /* Generic.Inserted */
+ .go { color: #93a1a1; } /* Generic.Output */
+ .gp { color: #93a1a1; } /* Generic.Prompt */
+ .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */
+ .gu { color: #cb4b16; } /* Generic.Subheading */
+ .gt { color: #93a1a1; } /* Generic.Traceback */
+ .kc { color: #cb4b16; } /* Keyword.Constant */
+ .kd { color: #268bd2; } /* Keyword.Declaration */
+ .kn { color: #859900; } /* Keyword.Namespace */
+ .kp { color: #859900; } /* Keyword.Pseudo */
+ .kr { color: #268bd2; } /* Keyword.Reserved */
+ .kt { color: #dc322f; } /* Keyword.Type */
+ .ld { color: #93a1a1; } /* Literal.Date */
+ .m { color: #2aa198; } /* Literal.Number */
+ .s { color: #2aa198; } /* Literal.String */
+ .na { color: #93a1a1; } /* Name.Attribute */
+ .nb { color: #b58900; } /* Name.Builtin */
+ .nc { color: #268bd2; } /* Name.Class */
+ .no { color: #cb4b16; } /* Name.Constant */
+ .nd { color: #268bd2; } /* Name.Decorator */
+ .ni { color: #cb4b16; } /* Name.Entity */
+ .ne { color: #cb4b16; } /* Name.Exception */
+ .nf { color: #268bd2; } /* Name.Function */
+ .nl { color: #93a1a1; } /* Name.Label */
+ .nn { color: #93a1a1; } /* Name.Namespace */
+ .nx { color: #93a1a1; } /* Name.Other */
+ .py { color: #93a1a1; } /* Name.Property */
+ .nt { color: #268bd2; } /* Name.Tag */
+ .nv { color: #268bd2; } /* Name.Variable */
+ .ow { color: #859900; } /* Operator.Word */
+ .w { color: #93a1a1; } /* Text.Whitespace */
+ .mf { color: #2aa198; } /* Literal.Number.Float */
+ .mh { color: #2aa198; } /* Literal.Number.Hex */
+ .mi { color: #2aa198; } /* Literal.Number.Integer */
+ .mo { color: #2aa198; } /* Literal.Number.Oct */
+ .sb { color: #586e75; } /* Literal.String.Backtick */
+ .sc { color: #2aa198; } /* Literal.String.Char */
+ .sd { color: #93a1a1; } /* Literal.String.Doc */
+ .s2 { color: #2aa198; } /* Literal.String.Double */
+ .se { color: #cb4b16; } /* Literal.String.Escape */
+ .sh { color: #93a1a1; } /* Literal.String.Heredoc */
+ .si { color: #2aa198; } /* Literal.String.Interpol */
+ .sx { color: #2aa198; } /* Literal.String.Other */
+ .sr { color: #dc322f; } /* Literal.String.Regex */
+ .s1 { color: #2aa198; } /* Literal.String.Single */
+ .ss { color: #2aa198; } /* Literal.String.Symbol */
+ .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
+ .vc { color: #268bd2; } /* Name.Variable.Class */
+ .vg { color: #268bd2; } /* Name.Variable.Global */
+ .vi { color: #268bd2; } /* Name.Variable.Instance */
+ .il { color: #2aa198; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 657bb5e3cd9..5956a28cafe 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -78,72 +78,72 @@
green #859900 operators, other keywords
*/
- .c { color: #93a1a1 } /* Comment */
- .err { color: #586e75 } /* Error */
- .g { color: #586e75 } /* Generic */
- .k { color: #859900 } /* Keyword */
- .l { color: #586e75 } /* Literal */
- .n { color: #586e75 } /* Name */
- .o { color: #859900 } /* Operator */
- .x { color: #cb4b16 } /* Other */
- .p { color: #586e75 } /* Punctuation */
- .cm { color: #93a1a1 } /* Comment.Multiline */
- .cp { color: #859900 } /* Comment.Preproc */
- .c1 { color: #93a1a1 } /* Comment.Single */
- .cs { color: #859900 } /* Comment.Special */
- .gd { color: #2aa198 } /* Generic.Deleted */
- .ge { color: #586e75; font-style: italic } /* Generic.Emph */
- .gr { color: #dc322f } /* Generic.Error */
- .gh { color: #cb4b16 } /* Generic.Heading */
- .gi { color: #859900 } /* Generic.Inserted */
- .go { color: #586e75 } /* Generic.Output */
- .gp { color: #586e75 } /* Generic.Prompt */
- .gs { color: #586e75; font-weight: bold } /* Generic.Strong */
- .gu { color: #cb4b16 } /* Generic.Subheading */
- .gt { color: #586e75 } /* Generic.Traceback */
- .kc { color: #cb4b16 } /* Keyword.Constant */
- .kd { color: #268bd2 } /* Keyword.Declaration */
- .kn { color: #859900 } /* Keyword.Namespace */
- .kp { color: #859900 } /* Keyword.Pseudo */
- .kr { color: #268bd2 } /* Keyword.Reserved */
- .kt { color: #dc322f } /* Keyword.Type */
- .ld { color: #586e75 } /* Literal.Date */
- .m { color: #2aa198 } /* Literal.Number */
- .s { color: #2aa198 } /* Literal.String */
- .na { color: #586e75 } /* Name.Attribute */
- .nb { color: #b58900 } /* Name.Builtin */
- .nc { color: #268bd2 } /* Name.Class */
- .no { color: #cb4b16 } /* Name.Constant */
- .nd { color: #268bd2 } /* Name.Decorator */
- .ni { color: #cb4b16 } /* Name.Entity */
- .ne { color: #cb4b16 } /* Name.Exception */
- .nf { color: #268bd2 } /* Name.Function */
- .nl { color: #586e75 } /* Name.Label */
- .nn { color: #586e75 } /* Name.Namespace */
- .nx { color: #586e75 } /* Name.Other */
- .py { color: #586e75 } /* Name.Property */
- .nt { color: #268bd2 } /* Name.Tag */
- .nv { color: #268bd2 } /* Name.Variable */
- .ow { color: #859900 } /* Operator.Word */
- .w { color: #586e75 } /* Text.Whitespace */
- .mf { color: #2aa198 } /* Literal.Number.Float */
- .mh { color: #2aa198 } /* Literal.Number.Hex */
- .mi { color: #2aa198 } /* Literal.Number.Integer */
- .mo { color: #2aa198 } /* Literal.Number.Oct */
- .sb { color: #93a1a1 } /* Literal.String.Backtick */
- .sc { color: #2aa198 } /* Literal.String.Char */
- .sd { color: #586e75 } /* Literal.String.Doc */
- .s2 { color: #2aa198 } /* Literal.String.Double */
- .se { color: #cb4b16 } /* Literal.String.Escape */
- .sh { color: #586e75 } /* Literal.String.Heredoc */
- .si { color: #2aa198 } /* Literal.String.Interpol */
- .sx { color: #2aa198 } /* Literal.String.Other */
- .sr { color: #dc322f } /* Literal.String.Regex */
- .s1 { color: #2aa198 } /* Literal.String.Single */
- .ss { color: #2aa198 } /* Literal.String.Symbol */
- .bp { color: #268bd2 } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2 } /* Name.Variable.Class */
- .vg { color: #268bd2 } /* Name.Variable.Global */
- .vi { color: #268bd2 } /* Name.Variable.Instance */
- .il { color: #2aa198 } /* Literal.Number.Integer.Long */
+ .c { color: #93a1a1; } /* Comment */
+ .err { color: #586e75; } /* Error */
+ .g { color: #586e75; } /* Generic */
+ .k { color: #859900; } /* Keyword */
+ .l { color: #586e75; } /* Literal */
+ .n { color: #586e75; } /* Name */
+ .o { color: #859900; } /* Operator */
+ .x { color: #cb4b16; } /* Other */
+ .p { color: #586e75; } /* Punctuation */
+ .cm { color: #93a1a1; } /* Comment.Multiline */
+ .cp { color: #859900; } /* Comment.Preproc */
+ .c1 { color: #93a1a1; } /* Comment.Single */
+ .cs { color: #859900; } /* Comment.Special */
+ .gd { color: #2aa198; } /* Generic.Deleted */
+ .ge { color: #586e75; font-style: italic; } /* Generic.Emph */
+ .gr { color: #dc322f; } /* Generic.Error */
+ .gh { color: #cb4b16; } /* Generic.Heading */
+ .gi { color: #859900; } /* Generic.Inserted */
+ .go { color: #586e75; } /* Generic.Output */
+ .gp { color: #586e75; } /* Generic.Prompt */
+ .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */
+ .gu { color: #cb4b16; } /* Generic.Subheading */
+ .gt { color: #586e75; } /* Generic.Traceback */
+ .kc { color: #cb4b16; } /* Keyword.Constant */
+ .kd { color: #268bd2; } /* Keyword.Declaration */
+ .kn { color: #859900; } /* Keyword.Namespace */
+ .kp { color: #859900; } /* Keyword.Pseudo */
+ .kr { color: #268bd2; } /* Keyword.Reserved */
+ .kt { color: #dc322f; } /* Keyword.Type */
+ .ld { color: #586e75; } /* Literal.Date */
+ .m { color: #2aa198; } /* Literal.Number */
+ .s { color: #2aa198; } /* Literal.String */
+ .na { color: #586e75; } /* Name.Attribute */
+ .nb { color: #b58900; } /* Name.Builtin */
+ .nc { color: #268bd2; } /* Name.Class */
+ .no { color: #cb4b16; } /* Name.Constant */
+ .nd { color: #268bd2; } /* Name.Decorator */
+ .ni { color: #cb4b16; } /* Name.Entity */
+ .ne { color: #cb4b16; } /* Name.Exception */
+ .nf { color: #268bd2; } /* Name.Function */
+ .nl { color: #586e75; } /* Name.Label */
+ .nn { color: #586e75; } /* Name.Namespace */
+ .nx { color: #586e75; } /* Name.Other */
+ .py { color: #586e75; } /* Name.Property */
+ .nt { color: #268bd2; } /* Name.Tag */
+ .nv { color: #268bd2; } /* Name.Variable */
+ .ow { color: #859900; } /* Operator.Word */
+ .w { color: #586e75; } /* Text.Whitespace */
+ .mf { color: #2aa198; } /* Literal.Number.Float */
+ .mh { color: #2aa198; } /* Literal.Number.Hex */
+ .mi { color: #2aa198; } /* Literal.Number.Integer */
+ .mo { color: #2aa198; } /* Literal.Number.Oct */
+ .sb { color: #93a1a1; } /* Literal.String.Backtick */
+ .sc { color: #2aa198; } /* Literal.String.Char */
+ .sd { color: #586e75; } /* Literal.String.Doc */
+ .s2 { color: #2aa198; } /* Literal.String.Double */
+ .se { color: #cb4b16; } /* Literal.String.Escape */
+ .sh { color: #586e75; } /* Literal.String.Heredoc */
+ .si { color: #2aa198; } /* Literal.String.Interpol */
+ .sx { color: #2aa198; } /* Literal.String.Other */
+ .sr { color: #dc322f; } /* Literal.String.Regex */
+ .s1 { color: #2aa198; } /* Literal.String.Single */
+ .ss { color: #2aa198; } /* Literal.String.Symbol */
+ .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
+ .vc { color: #268bd2; } /* Name.Variable.Class */
+ .vg { color: #268bd2; } /* Name.Variable.Global */
+ .vi { color: #268bd2; } /* Name.Variable.Instance */
+ .il { color: #2aa198; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 36a80a916b2..6f31a5235c0 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -86,7 +86,7 @@
background-color: #fafe3d !important;
}
- .hll { background-color: #f8f8f8 }
+ .hll { background-color: #f8f8f8; }
.c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 5bfe9bcb443..8d1a6020ca4 100644
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -78,7 +78,7 @@ span.highlight_word {
background-color: #fafe3d !important;
}
-.hll { background-color: #f8f8f8 }
+.hll { background-color: #f8f8f8; }
.c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index fc12964872d..ced8c4a9907 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -2,22 +2,28 @@ img {
max-width: 100%;
height: auto;
}
+
p.details {
font-style: italic;
- color: #777
+ color: #777;
}
+
.footer > p {
font-size: small;
- color: #777
+ color: #777;
}
+
pre.commit-message {
white-space: pre-wrap;
}
+
.file-stats > a {
text-decoration: none;
+
> .new-file {
color: #090;
}
+
> .deleted-file {
color: #b00;
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 8f71381f5c4..140d589024b 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -22,7 +22,7 @@
.admin-filter form {
.select2-container {
- width: 100%
+ width: 100%;
}
.controls {
@@ -31,7 +31,7 @@
.form-actions {
padding-left: 130px;
- background: #fff
+ background: #fff;
}
.visibility-levels {
@@ -106,26 +106,33 @@
.table {
table-layout: fixed;
}
+
.subheading {
padding-bottom: $gl-padding;
}
+
.message {
word-wrap: break-word;
}
+
.btn {
white-space: normal;
padding: $gl-btn-padding;
}
+
th {
width: 15%;
+
&.wide {
width: 55%;
}
}
+
@media (max-width: $screen-sm-max) {
th {
width: 100%;
}
+
td {
width: 100%;
float: left;
@@ -137,6 +144,7 @@
margin-left: $btn-side-margin;
margin-top: 3px;
}
+
span {
font-size: 19px;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 9c84dceed05..d8fabbdcebe 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -1,4 +1,3 @@
-lex
[v-cloak] {
display: none;
}
@@ -132,7 +131,7 @@ lex
}
.board-blank-state {
- height: 100%;
+ height: calc(100% - 49px);
padding: $gl-padding;
background-color: #fff;
}
@@ -162,6 +161,10 @@ lex
list-style: none;
overflow-y: scroll;
overflow-x: hidden;
+
+ &.is-smaller {
+ height: calc(100% - 185px);
+ }
}
.board-list-loading {
@@ -197,6 +200,7 @@ lex
a {
color: inherit;
+ word-wrap: break-word;
}
}
@@ -232,3 +236,31 @@ lex
margin-right: 5px;
}
}
+
+.board-new-issue-form {
+ margin: 5px;
+}
+
+.board-issue-count-holder {
+ margin-top: -3px;
+
+ .btn {
+ line-height: 12px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
+
+.board-issue-count {
+ padding-right: 10px;
+ padding-left: 10px;
+ line-height: 21px;
+ border-radius: $border-radius-base;
+ border: 1px solid $border-color;
+
+ &.has-btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-width: 1px 0 1px 1px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index a5a260d4c8f..d6a55fbd464 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -107,10 +107,14 @@
.block {
width: 100%;
- }
- .block-first {
- padding: 5px 16px 11px;
+ &.coverage {
+ padding: 0 16px 11px;
+ }
+
+ .btn-group-justified {
+ margin-top: 5px;
+ }
}
.js-build-variable {
@@ -133,6 +137,7 @@
.retry-link {
color: $gl-link-color;
+
&:hover {
text-decoration: underline;
}
@@ -190,7 +195,7 @@
.build-job {
position: relative;
- .fa {
+ .fa-arrow-right {
position: absolute;
left: 15px;
top: 20px;
@@ -200,20 +205,33 @@
&.active {
font-weight: bold;
- .fa {
+ .fa-arrow-right {
display: block;
}
}
+ &.retried {
+ background-color: $gray-lightest;
+ }
+
&:hover {
background-color: $row-hover;
}
+
+ .fa-refresh {
+ font-size: 13px;
+ margin-left: 3px;
+ }
}
}
}
.build-detail-row {
margin-bottom: 5px;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
}
.build-light-text {
@@ -226,3 +244,9 @@
right: 0;
margin-top: -17px;
}
+
+@media (min-width: $screen-md-min) {
+ .sub-nav.build {
+ width: calc(100% + #{$gutter_width});
+ }
+}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 53ec0002afe..264e7e01a34 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -51,6 +51,7 @@
margin-left: 4px;
}
}
+
.commit-committer-link,
.commit-author-link {
color: $gl-gray;
@@ -108,21 +109,25 @@
line-height: 20px;
}
}
+
.new-file {
a {
color: $gl-text-green;
}
}
+
.renamed-file {
a {
color: $gl-text-orange;
}
}
+
.deleted-file {
a {
color: $gl-text-red;
}
}
+
.edit-file {
a {
color: $gl-text-color;
@@ -158,6 +163,7 @@
position: absolute;
z-index: 1;
}
+
> textarea {
background-color: rgba(0, 0, 0, 0.0);
font-family: inherit;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index dc57a837155..2b5621e20d6 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -161,6 +161,7 @@
.branch-commit {
color: $gl-gray;
+
.commit-id, .commit-row-message {
color: $gl-gray;
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 778471a34d7..d732008de3d 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -50,7 +50,7 @@
.bordered-box {
border: 1px solid $border-color;
- @include border-radius($border-radius-default);
+ border-radius: $border-radius-default;
}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 42928ee279c..76225ed8d06 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -5,6 +5,7 @@
background: $background-color;
border-top-left-radius: 0;
}
+
border-top-left-radius: 0;
}
}
@@ -17,6 +18,7 @@
float: left;
@extend .col-md-2;
}
+
.btn {
margin-left: 5px;
float: left;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 4d9c73c6840..2357671c2ae 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -20,9 +20,11 @@
.detail-page-description {
.title {
- margin: 0;
- font-size: 23px;
+ margin: 0 0 16px;
+ font-size: 2em;
color: $gl-gray-dark;
+ padding: 0 0 0.3em;
+ border-bottom: 1px solid $white-dark;
}
.description {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index b8ef76cc74e..fe6421f8b3f 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -33,13 +33,25 @@
font-size: smaller;
}
}
+
+ .file-title {
+ cursor: pointer;
+
+ &:hover {
+ background-color: $dark-background-color;
+ }
+
+ .diff-toggle-caret {
+ padding-right: 6px;
+ }
+ }
+
.diff-content {
overflow: auto;
overflow-y: hidden;
background: #fff;
color: #333;
border-radius: 0 0 3px 3px;
- -webkit-overflow-scrolling: auto;
.unfold {
cursor: pointer;
@@ -123,15 +135,18 @@
max-width: 50px;
width: 35px;
@include user-select(none);
+
a {
float: left;
width: 35px;
font-weight: normal;
+
&:hover {
text-decoration: underline;
}
}
}
+
.line_content {
display: block;
margin: 0;
@@ -151,10 +166,12 @@
white-space: pre-wrap;
}
}
+
.image {
background: #ddd;
text-align: center;
padding: 30px;
+
.wrap {
display: inline-block;
}
@@ -163,6 +180,7 @@
display: inline-block;
background-color: #fff;
line-height: 0;
+
img {
border: 1px solid #fff;
background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%),
@@ -171,6 +189,7 @@
background-position: 0 0, 5px 5px;
max-width: 100%;
}
+
&.deleted {
border: 1px solid $deleted;
}
@@ -179,6 +198,7 @@
border: 1px solid $added;
}
}
+
.image-info {
font-size: 12px;
margin: 5px 0 0;
@@ -193,6 +213,7 @@
margin: auto;
position: relative;
}
+
.swipe-wrap {
overflow: hidden;
border-left: 1px solid #999;
@@ -201,10 +222,12 @@
top: 13px;
right: 7px;
}
+
.frame {
top: 0;
right: 0;
position: absolute;
+
&.deleted {
margin: 0;
display: block;
@@ -212,6 +235,7 @@
right: 7px;
}
}
+
.swipe-bar {
display: block;
height: 100%;
@@ -219,14 +243,17 @@
z-index: 100;
position: absolute;
cursor: pointer;
+
&:hover {
.top-handle {
background-position: -15px 3px;
}
+
.bottom-handle {
background-position: -15px -11px;
}
}
+
.top-handle {
display: block;
height: 14px;
@@ -235,6 +262,7 @@
top: 0;
background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
}
+
.bottom-handle {
display: block;
height: 14px;
@@ -252,12 +280,14 @@
margin: auto;
position: relative;
}
+
.frame.added, .frame.deleted {
position: absolute;
display: block;
top: 0;
left: 0;
}
+
.controls {
display: block;
height: 14px;
@@ -311,6 +341,7 @@
}
//.view.onion-skin
}
+
.view-modes {
padding: 10px;
text-align: center;
@@ -328,19 +359,24 @@
border-left: 1px solid #c1c1c1;
padding: 0 12px 0 16px;
cursor: pointer;
+
&:first-child {
border-left: none;
}
+
&:hover {
text-decoration: underline;
}
+
&.active {
&:hover {
text-decoration: none;
}
+
cursor: default;
color: #333;
}
+
&.disabled {
display: none;
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 1aa4e06d975..029dabd2138 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -1,7 +1,7 @@
.file-editor {
#editor {
border: none;
- @include border-radius(0);
+ border-radius: 0;
height: 500px;
margin: 0;
padding: 0;
@@ -15,6 +15,7 @@
.cancel-btn {
color: #b94a48;
+
&:hover {
color: #b94a48;
}
@@ -59,6 +60,7 @@
}
.encoding-selector,
+ .soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
@@ -67,6 +69,28 @@
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/environments.scss b/app/assets/stylesheets/pages/environments.scss
index d01c60ee6ab..12ee0a5dc3d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,4 +1,15 @@
+.environments-container,
+.deployments-container {
+ width: 100%;
+ overflow: auto;
+}
+
.environments {
+ .deployment-column {
+ .avatar {
+ float: none;
+ }
+ }
.commit-title {
margin: 0;
@@ -9,6 +20,7 @@
width: 12px;
}
+ .external-url,
.dropdown-new {
color: $table-text-gray;
}
@@ -21,16 +33,43 @@
}
}
+ .build-link,
.branch-name {
color: $gl-dark-link-color;
}
+
+ .stop-env-link {
+ color: $table-text-gray;
+
+ .stop-env-icon {
+ font-size: 14px;
+ }
+ }
+
+ .deployment {
+ .build-column {
+
+ .build-link {
+ color: $gl-dark-link-color;
+ }
+
+ .avatar {
+ float: none;
+ }
+ }
+ }
}
-.table.builds.environments {
- min-width: 500px;
+.table.ci-table.environments {
.icon-container {
width: 20px;
text-align: center;
}
+
+ .branch-commit {
+ .commit-id {
+ margin-right: 0;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 1d00da1266c..5d9a76dac05 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -78,6 +78,7 @@
margin-bottom: 0;
}
}
+
.event-note-icon {
color: #777;
float: left;
@@ -86,21 +87,23 @@
margin-right: 5px;
}
}
+
.event_icon {
position: relative;
float: right;
border: 1px solid #eee;
padding: 5px;
- @include border-radius(5px);
+ border-radius: 5px;
background: $gray-light;
margin-left: 10px;
top: -6px;
+
img {
width: 20px;
}
}
- &:last-child { border: none }
+ &:last-child { border: none; }
.event_commits {
li {
@@ -109,6 +112,7 @@
padding: 3px;
padding-left: 0;
border: none;
+
.commit-row-title {
font-size: $gl-font-size;
}
@@ -117,6 +121,7 @@
&.commits-stat {
display: block;
padding: 0 3px 0 0;
+
&:hover {
background: none;
}
@@ -158,6 +163,7 @@
overflow: visible;
max-width: 100%;
}
+
.avatar {
display: none;
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 732dc645c66..ee2a398f031 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -1,17 +1,3 @@
-.member-search-form {
- float: left;
-
- input[type='search'] {
- width: 225px;
- vertical-align: bottom;
-
- @media (max-width: $screen-xs-max) {
- width: 100px;
- vertical-align: bottom;
- }
- }
-}
-
.milestone-row {
@include str-truncated(90%);
}
@@ -48,6 +34,7 @@
.group-right-buttons {
position: absolute;
right: 16px;
+
.btn {
@include btn-gray;
padding: 3px 10px;
@@ -57,7 +44,6 @@
}
.groups-header {
-
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
@@ -68,3 +54,38 @@
}
}
}
+
+.groups-empty-state {
+ padding: 50px 100px;
+ overflow: hidden;
+
+ @media (max-width: $screen-md-min) {
+ padding: 50px 0;
+ }
+
+ svg {
+ float: right;
+
+ @media (max-width: $screen-md-min) {
+ float: none;
+ display: block;
+ width: 250px;
+ position: relative;
+ left: 50%;
+ margin-left: -125px;
+ }
+ }
+
+ .text-content {
+ float: left;
+ width: 460px;
+ margin-top: 120px;
+
+ @media (max-width: $screen-md-min) {
+ float: none;
+ margin-top: 60px;
+ width: auto;
+ text-align: center;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 00ab42bec5c..a48b4c65db8 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -23,28 +23,28 @@
color: #555;
tbody:first-child tr:first-child {
- padding-top: 0
+ padding-top: 0;
}
th {
padding-top: 15px;
line-height: 1.5;
color: #333;
- text-align: left
+ text-align: left;
}
td {
padding-top: 3px;
padding-bottom: 3px;
vertical-align: top;
- line-height: 20px
+ line-height: 20px;
}
.shortcut {
padding-right: 10px;
color: #999;
text-align: right;
- white-space: nowrap
+ white-space: nowrap;
}
.key {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 41079b6eeb5..230b927a17d 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -27,6 +27,7 @@
margin-right: 5px;
margin-bottom: 5px;
display: inline-block;
+
.color-label {
padding: 6px 10px;
}
@@ -128,7 +129,7 @@
}
.selectbox {
- display: none
+ display: none;
}
.btn-clipboard {
@@ -199,7 +200,7 @@
display: none;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
- display: block
+ display: block;
}
width: $sidebar_collapsed_width;
@@ -276,7 +277,7 @@
}
&.btn-primary {
- @extend .btn-primary
+ @extend .btn-primary;
}
}
@@ -400,6 +401,7 @@
.js-issuable-selector {
width: 100%;
}
+
@media (max-width: $screen-sm-max) {
margin-bottom: $gl-padding;
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3ac34cbc829..623da67a239 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -37,6 +37,7 @@ ul.related-merge-requests > li {
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
+
.merge-request-id {
flex-shrink: 0;
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 38c7cd98e41..397f89f501a 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -1,7 +1,8 @@
.suggest-colors {
margin-top: 5px;
+
a {
- @include border-radius(4px);
+ border-radius: 4px;
width: 30px;
height: 30px;
display: inline-block;
@@ -17,7 +18,7 @@
overflow: hidden;
a {
- @include border-radius(0);
+ border-radius: 0;
width: (100% / 7);
margin-right: 0;
margin-bottom: -5px;
@@ -59,6 +60,27 @@
width: 200px;
margin-bottom: 0;
}
+
+ .label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ max-width: 100%;
+ }
+ }
+
+ .label-type {
+ display: block;
+ margin-bottom: 10px;
+ margin-left: 50px;
+
+ @media (min-width: $screen-sm-min) {
+ display: inline-block;
+ width: 100px;
+ margin-left: 10px;
+ margin-bottom: 0;
+ vertical-align: middle;
+ }
}
.label-description {
@@ -201,6 +223,13 @@
}
.label-subscribe-button {
+ .label-subscribe-button-icon {
+ &[disabled] {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ }
+
.label-subscribe-button-loading {
display: none;
}
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
index 6926448519e..8290519dc25 100644
--- a/app/assets/stylesheets/pages/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
@@ -3,6 +3,7 @@
font-size: 19px;
color: red;
}
+
.correct-syntax {
font-size: 19px;
color: #47a447;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 403171d4532..8dac6ab999e 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -17,6 +17,7 @@
line-height: 1.5;
p {
+ font-size: 18px;
color: #888;
}
@@ -36,10 +37,14 @@
}
}
- .login-box {
- background: #fafafa;
- border-radius: 10px;
- box-shadow: 0 0 2px #ccc;
+ p {
+ font-size: 13px;
+ }
+
+ .login-box, .omniauth-container {
+ box-shadow: 0 0 0 1px $border-color;
+ border-bottom-right-radius: 2px;
+ border-bottom-left-radius: 2px;
padding: 15px;
.login-heading h3 {
@@ -48,6 +53,7 @@
margin: 0 0 10px;
}
+
.login-footer {
margin-top: 10px;
@@ -58,42 +64,158 @@
a.forgot {
float: right;
- padding-top: 6px
+ padding-top: 6px;
}
.nav .active a {
background: transparent;
}
+
+ // Styles the glowing border of focused input for username async validation
+ .login-body {
+ font-size: 13px;
+
+
+ input + p {
+ margin-top: 5px;
+ }
+
+ .gl-field-success-outline {
+ border: 1px solid $green-normal;
+
+ &:focus {
+ box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal;
+ border: 0 none;
+ }
+ }
+
+ .gl-field-error-outline {
+ border: 1px solid $red-normal;
+
+ &:focus {
+ box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6);
+ border: 0 none;
+ }
+ }
+
+ .username .validation-success,
+ .gl-field-success-message {
+ color: $green-normal;
+ }
+
+ .username .validation-error,
+ .gl-field-error-message {
+ color: $red-normal;
+ }
+
+ .gl-field-hint {
+ color: $gl-text-color;
+ }
+
+ }
}
- .form-control {
- font-size: 14px;
- padding: 10px 8px;
- width: 100%;
- height: auto;
+ .omniauth-container {
+ p {
+ margin: 0;
+ }
+ }
+
+ .new-session-tabs {
+ display: -webkit-flex;
+ display: flex;
+ box-shadow: 0 0 0 1px $border-color;
+ border-top-right-radius: $border-radius-default;
+ border-top-left-radius: $border-radius-default;
+
+ li {
+ flex: 1;
+ text-align: center;
+
+ &:first-of-type {
+ border-top-left-radius: $border-radius-default;
+ }
+
+ &:last-of-type {
+ border-left: 1px solid $border-color;
+ border-top-right-radius: $border-radius-default;
+ }
+
+ &:not(.active) {
+ background-color: $gray-light;
+ border-left: 1px solid $border-color;
+ }
+
+ a {
+ width: 100%;
+ font-size: 18px;
+
+ &:hover {
+ border: 1px solid transparent;
+ }
+ }
+
+ &.active {
+ border-bottom: 1px solid $border-color;
+
+ a {
+ border: none;
+ border-bottom: 2px solid $link-underline-blue;
+ color: $black;
- &.top {
- @include border-radius(5px 5px 0 0);
- margin-bottom: 0;
+ &:hover {
+ border-bottom: 2px solid $link-underline-blue;
+ }
+ }
+ }
}
+ }
- &.bottom {
- @include border-radius(0 0 5px 5px);
- border-top: 0;
- margin-bottom: 20px;
+ // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
+ // These styles prevent this from breaking the layout, and only applied when providers are configured.
+
+ .new-session-tabs.custom-provider-tabs {
+ flex-wrap: wrap;
+
+ li {
+ min-width: 85px;
+ flex-basis: auto;
+
+ // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
+ // We are making somewhat of an assumption about the configuration here: that users do not have more than
+ // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
+ // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
+ // above one of the bottom row elements. If you know a better way, please implement it!
+ &:nth-child(n+5) {
+ border-top: 1px solid $border-color;
+ }
}
- &.middle {
- border-top: 0;
- margin-bottom: 0;
- @include border-radius(0);
+ a {
+ font-size: 16px;
}
+ }
+
+ .form-control {
&:active, &:focus {
background-color: #fff;
}
}
+ label {
+ font-weight: normal;
+ }
+
+ .submit-container {
+ margin-top: 16px;
+ }
+
+ input[type="submit"] {
+ @extend .btn-block;
+ margin-bottom: 0;
+ }
+
.devise-errors {
h2 {
margin-top: 0;
@@ -101,20 +223,13 @@
color: #a00;
}
}
-
- .remember-me {
- margin-top: -10px;
-
- label {
- font-weight: normal;
- }
- }
}
@media (max-width: $screen-xs-max) {
.login-page {
.col-sm-5.pull-right {
float: none !important;
+ margin-bottom: 45px;
}
}
}
@@ -127,3 +242,63 @@
height: 32px;
}
}
+
+.devise-layout-html {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+// Fixes footer container to bottom of viewport
+.devise-layout-html body {
+ // offset height of fixed header + 1 to avoid scroll
+ height: calc(100% - 51px);
+ margin: 0;
+ padding: 0;
+
+ .page-wrap {
+ min-height: 100%;
+ position: relative;
+ }
+
+ .footer-container, hr.footer-fixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 40px;
+ background: $white-light;
+ }
+
+ .navless-container {
+ padding: 65px 15px; // height of footer + bottom padding of email confirmation link
+
+ @media (max-width: $screen-xs-max) {
+ padding: 0 15px 65px;
+ }
+ }
+}
+
+// For sign in pane only, to improve tab order, the following removes the submit button from
+// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
+
+.login-box {
+ .new_user {
+ position: relative;
+ padding-bottom: 35px;
+
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ .forgot-password {
+ float: none !important;
+ margin-top: 5px;
+ }
+ }
+ }
+
+ .move-submit-down {
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+ }
+}
+
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
new file mode 100644
index 00000000000..756efa9c7fa
--- /dev/null
+++ b/app/assets/stylesheets/pages/members.scss
@@ -0,0 +1,98 @@
+.project-members-title {
+ padding-bottom: 10px;
+ border-bottom: 1px solid $border-color;
+}
+
+.member {
+ .list-item-name {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ width: 50%;
+ }
+
+ strong {
+ font-weight: 600;
+ }
+ }
+
+ .controls {
+ @media (min-width: $screen-sm-min) {
+ display: -webkit-flex;
+ display: flex;
+ width: 400px;
+ max-width: 50%;
+ }
+ }
+
+ .form-horizontal {
+ margin-top: 5px;
+
+ @media (min-width: $screen-sm-min) {
+ display: -webkit-flex;
+ display: flex;
+ width: 100%;
+ margin-top: 3px;
+ }
+ }
+
+ .btn-remove {
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: auto;
+ }
+ }
+}
+
+.member-form-control {
+ @media (max-width: $screen-xs-max) {
+ padding: 5px 0;
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ width: 50%;
+ }
+}
+
+.member-access-text {
+ margin-left: auto;
+ line-height: 43px;
+}
+
+.member.existing-title {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+}
+
+.member-search-form {
+ position: relative;
+
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ }
+
+ .form-control {
+ width: 100%;
+ padding-right: 35px;
+
+ @media (min-width: $screen-sm-min) {
+ width: 350px;
+ }
+ }
+}
+
+.member-search-btn {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 35px;
+ padding-left: 10px;
+ padding-right: 10px;
+ color: $gray-darkest;
+ background: transparent;
+ border: 0;
+ outline: 0;
+}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 5ec660799e3..eed2b0ab7cc 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -131,6 +131,7 @@ $colors: (
}
}
}
+
&.head {
background-color: map-get($colors, #{$color}_header_head_neutral);
border-color: map-get($colors, #{$color}_header_head_neutral);
@@ -174,6 +175,7 @@ $colors: (
background-color: map-get($colors, #{$color}_line_not_chosen);
}
}
+
&.head {
background-color: map-get($colors, #{$color}_line_head_neutral);
@@ -235,4 +237,51 @@ $colors: (
.btn-success .fa-spinner {
color: #fff;
}
+
+ .editor-wrap {
+ &.is-loading {
+ .editor {
+ display: none;
+ }
+
+ .loading {
+ display: block;
+ }
+ }
+
+ &.saved {
+ .editor {
+ border-top: solid 2px $border-green-extra-light;
+ }
+ }
+
+ .editor {
+ pre {
+ height: 350px;
+ border: none;
+ border-radius: 0;
+ margin-bottom: 0;
+ }
+ }
+
+ .loading {
+ display: none;
+ }
+ }
+
+ .discard-changes-alert {
+ background-color: $background-color;
+ text-align: right;
+ padding: $gl-padding-top $gl-padding;
+ color: $gl-text-color;
+
+ .discard-actions {
+ display: inline-block;
+ margin-left: 10px;
+ }
+ }
+
+ .resolve-conflicts-form {
+ padding-top: $gl-padding;
+ }
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 926247e5e87..70afa568554 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -6,10 +6,11 @@
background: $background-color;
color: $gl-gray;
border: 1px solid $border-color;
- @include border-radius(2px);
+ border-radius: 2px;
form {
margin-bottom: 0;
+
.clearfix {
margin-bottom: 0;
}
@@ -46,6 +47,7 @@
&.right {
float: right;
+
a {
color: $gl-gray;
}
@@ -70,7 +72,8 @@
&.ci-success {
color: $gl-success;
- a.environment {
+ a.environment,
+ a.pipeline {
color: inherit;
}
}
@@ -120,6 +123,10 @@
color: #5c5d5e;
}
+ .js-deployment-link {
+ display: inline-block;
+ }
+
.mr-widget-body {
h4 {
font-weight: 600;
@@ -176,6 +183,15 @@
.ci-coverage {
float: right;
}
+
+ .stop-env-container {
+ color: $gl-text-color;
+ float: right;
+
+ a {
+ color: $gl-text-color;
+ }
+ }
}
.mr_source_commit,
@@ -187,6 +203,7 @@
padding-top: 2px;
padding-bottom: 2px;
list-style: none;
+
&:hover {
background: none;
}
@@ -203,6 +220,19 @@
word-break: break-all;
}
+.commits-empty {
+ text-align: center;
+
+ h4 {
+ padding-top: 20px;
+ padding-bottom: 10px;
+ }
+
+ svg {
+ width: 230px;
+ }
+}
+
.mr-list {
.merge-request {
padding: 10px 15px;
@@ -263,12 +293,6 @@
line-height: 31px;
}
-.builds {
- .table-holder {
- overflow-x: auto;
- }
-}
-
.panel-new-merge-request {
.panel-heading {
padding: 5px 10px;
@@ -349,10 +373,14 @@
.issuable-form-select-holder {
display: inline-block;
width: 250px;
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
}
.table-holder {
- .builds {
+ .ci-table {
th {
background-color: $white-light;
@@ -384,8 +412,12 @@
padding: 16px;
}
+ .content-block {
+ border-top: 1px solid $border-color;
+ padding: $gl-padding-top $gl-padding;
+ }
+
.comments-disabled-notif {
- padding: 10px 16px;
.btn {
margin-left: 5px;
}
@@ -396,10 +428,6 @@
margin: 0 7px;
}
- .comments-disabled-notif {
- border-top: 1px solid $border-color;
- }
-
.dropdown-title {
color: $gl-text-color;
}
@@ -410,9 +438,18 @@
}
}
-.merge-request-details {
+.merge-request-tabs-holder {
+ background-color: #fff;
+
+ &.affix {
+ top: 100px;
+ left: 0;
+ z-index: 9;
+ transition: right .15s;
+ }
- .title {
- margin-bottom: 20px;
+ &:not(.affix) .container-fluid {
+ padding-left: 0;
+ padding-right: 0;
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 6b865730487..dd6d1783667 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -33,6 +33,7 @@
// Issue title
span a {
color: $gl-text-color;
+ word-wrap: break-word;
}
}
}
@@ -58,6 +59,7 @@
color: $gl-placeholder-color;
margin-right: 5px;
}
+
.avatar {
float: none;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index bd875b9823f..17f28959414 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -11,6 +11,7 @@
filter: alpha(opacity=100);
}
}
+
.diff-file,
.discussion {
.new-note {
@@ -194,6 +195,7 @@
min-height: 140px;
max-height: 500px;
}
+
.note-form-actions {
background: transparent;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 54124a3d658..fffcdc812a7 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -147,9 +147,18 @@ ul.notes {
// Diff code in discussion view
.discussion-body .diff-file {
+ .file-title {
+ cursor: default;
+
+ &:hover {
+ background-color: $gray-light;
+ }
+ }
+
.diff-header > span {
margin-right: 10px;
}
+
.line_content {
white-space: pre-wrap;
}
@@ -334,7 +343,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
- @include border-radius(40px);
+ border-radius: 40px;
background: #fff;
padding: 4px;
font-size: 16px;
@@ -345,6 +354,7 @@ ul.notes {
width: 32px;
// "hide" it by default
display: none;
+
&:hover {
background: $gl-info;
color: #fff;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index b035bfc9f3c..5b8dc7f8c40 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -20,8 +20,20 @@
margin: 4px;
}
- .table.builds {
+ .table.ci-table {
min-width: 1200px;
+
+ .pipeline-id {
+ color: $black;
+ }
+
+ .branch-commit {
+ width: 30%;
+
+ .branch-name {
+ max-width: 195px;
+ }
+ }
}
}
@@ -39,13 +51,20 @@
overflow: auto;
}
-.table.builds {
+.table.ci-table {
min-width: 900px;
&.pipeline {
min-width: 650px;
}
+ &.builds-page {
+
+ tr {
+ height: 71px;
+ }
+ }
+
tr {
th {
padding: 16px 8px;
@@ -61,6 +80,10 @@
border-top-width: 1px;
}
+ .build.retried {
+ background-color: $gray-lightest;
+ }
+
.commit-link {
.ci-status {
@@ -76,6 +99,15 @@
}
}
+ .avatar {
+ margin-left: 0;
+ float: none;
+ }
+
+ .api {
+ color: $code-color;
+ }
+
.branch-commit {
.branch-name {
@@ -97,12 +129,11 @@
.fa {
font-size: 12px;
- color: $table-text-gray;
+ color: $gl-text-color;
}
.commit-id {
color: $gl-link-color;
- margin-right: 8px;
}
.commit-title {
@@ -113,10 +144,6 @@
text-overflow: ellipsis;
}
- .avatar {
- margin-left: 0;
- }
-
.label {
margin-right: 4px;
}
@@ -132,17 +159,11 @@
.icon-container {
display: inline-block;
- text-align: right;
- width: 15px;
-
- .fa {
- position: relative;
- right: 3px;
- }
+ width: 10px;
- svg {
- position: relative;
- right: 1px;
+ &.commit-icon {
+ width: 15px;
+ text-align: center;
}
}
@@ -171,7 +192,7 @@
&::after {
content: '';
width: 8px;
- position: absolute;;
+ position: absolute;
right: -7px;
bottom: 8px;
border-bottom: 2px solid $border-color;
@@ -224,9 +245,12 @@
.fa {
color: $table-text-gray;
- margin-right: 6px;
font-size: 14px;
}
+
+ svg, .fa {
+ margin-right: 0;
+ }
}
.btn-remove {
@@ -267,18 +291,8 @@
.toggle-pipeline-btn {
background-color: $gray-dark;
- .caret {
- border-top: none;
- border-bottom: 4px solid;
- }
-
&.graph-collapsed {
background-color: $white-light;
-
- .caret {
- border-bottom: none;
- border-top: 4px solid;
- }
}
}
@@ -305,16 +319,41 @@
.stage-column {
display: inline-block;
vertical-align: top;
- margin-right: 65px;
+
+ &:not(:last-child) {
+ margin-right: 44px;
+ }
+
+ &.left-margin {
+ &:not(:first-child) {
+ margin-left: 44px;
+
+ .left-connector {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -48px;
+ border-top: 2px solid $border-color;
+ width: 48px;
+ height: 1px;
+ }
+ }
+ }
+ }
+
+ &.no-margin {
+ margin: 0;
+ }
li {
list-style: none;
}
.stage-name {
- margin-bottom: 15px;
+ margin: 0 0 15px 10px;
font-weight: bold;
- width: 150px;
+ width: 176px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -323,17 +362,20 @@
.build {
border: 1px solid $border-color;
position: relative;
- padding: 6px 10px;
+ padding: 7px 10px 8px;
border-radius: 30px;
- width: 150px;
+ width: 186px;
margin-bottom: 10px;
+ &:hover {
+ background-color: $gray-lighter;
+ }
+
&.playable {
- background-color: $gray-light;
svg {
- height: 12px;
- width: 12px;
+ height: 13px;
+ width: 20px;
position: relative;
top: 1px;
@@ -344,10 +386,29 @@
}
.build-content {
- width: 130px;
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+ width: 164px;
+
+ .ci-status-icon {
+ svg {
+ height: 20px;
+ width: 20px;
+ }
+ }
+
+ .tooltip {
+ white-space: nowrap;
+
+ .tooltip-inner {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
.ci-status-text {
- width: 110px;
+ width: 135px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -358,42 +419,57 @@
}
a {
- color: $layout-link-gray;
+ color: $gl-text-color-light;
text-decoration: none;
-
- &:hover {
- .ci-status-text {
- text-decoration: underline;
- }
- }
}
.dropdown-menu-toggle {
+ background-color: transparent;
border: none;
width: auto;
padding: 0;
- color: $layout-link-gray;
+ color: $gl-text-color-light;
+ flex-grow: 1;
.ci-status-text {
- width: 80px;
+ max-width: 112px;
+ width: auto;
}
}
.grouped-pipeline-dropdown {
padding: 8px 0;
- width: 200px;
+ width: 186px;
left: auto;
- right: -214px;
+ right: -197px;
top: -9px;
- a:hover {
- .ci-status-text {
- text-decoration: none;
+ ul {
+ max-height: 245px;
+ overflow: auto;
+ }
+
+ a {
+ color: $gl-text-color;
+ padding: 7px 8px 8px;
+
+ &:hover {
+ background-color: $blue-light-transparent;
+ border-radius: 3px;
+
+ .ci-status-text {
+ text-decoration: none;
+ }
}
}
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+
.ci-status-text {
- width: 145px;
+ width: 112px;
}
.arrow {
@@ -426,9 +502,10 @@
}
.badge {
- background-color: $gray-dark;
- color: $layout-link-gray;
+ background-color: $gray-darker;
+ color: $gl-text-color-light;
font-weight: normal;
+ margin-left: $btn-xs-side-margin;
}
}
@@ -442,10 +519,10 @@
&::after {
content: '';
position: absolute;
- top: 50%;
- right: -69px;
+ top: 48%;
+ right: -48px;
border-top: 2px solid $border-color;
- width: 69px;
+ width: 48px;
height: 1px;
}
}
@@ -454,25 +531,25 @@
&:not(:first-child) {
&::after, &::before {
content: '';
- top: -47px;
+ top: -49px;
position: absolute;
border-bottom: 2px solid $border-color;
- width: 20px;
- height: 65px;
+ width: 25px;
+ height: 69px;
}
// Right connecting curves
&::after {
- right: -20px;
+ right: -25px;
border-right: 2px solid $border-color;
- border-radius: 0 0 15px;
+ border-radius: 0 0 20px;
}
// Left connecting curves
&::before {
- left: -20px;
+ left: -25px;
border-left: 2px solid $border-color;
- border-radius: 0 0 0 15px;
+ border-radius: 0 0 0 20px;
}
}
@@ -480,8 +557,9 @@
&:nth-child(2) {
&::after, &::before {
height: 29px;
- top: -10px;
+ top: -9px;
}
+
.curve {
display: block;
}
@@ -538,20 +616,20 @@
width: 21px;
height: 25px;
position: absolute;
- top: -29px;
+ top: -32px;
border-top: 2px solid $border-color;
}
&::after {
- left: -39px;
+ left: -44px;
border-right: 2px solid $border-color;
- border-radius: 0 15px;
+ border-radius: 0 20px;
}
&::before {
- right: -39px;
+ right: -44px;
border-left: 2px solid $border-color;
- border-radius: 15px 0 0;
+ border-radius: 20px 0 0;
}
}
}
@@ -567,19 +645,35 @@
}
}
-.pipelines.tab-pane {
+.tab-pane {
- .content-list.pipelines {
- overflow: auto;
- }
+ &.pipelines {
- .stage {
- max-width: 100px;
- width: 100px;
+ .ci-table {
+ min-width: 900px;
+ }
+
+ .content-list.pipelines {
+ overflow: auto;
+ }
+
+ .stage {
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .pipeline-actions {
+ min-width: initial;
+ }
}
- .pipeline-actions {
- min-width: initial;
+ &.builds {
+
+ .ci-table {
+ tr {
+ height: 71px;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 0fcdaf94a21..ed80d2beec2 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -94,7 +94,7 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
max-width: 600px;
- margin: 15px auto 0;
+ margin: 10px auto;
padding: 0 16px;
}
@@ -213,29 +213,22 @@
}
.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;
- }
+ margin: 0 auto 10px;
}
}
+
@media (max-width: $screen-xs-max) {
+
.cover-block {
padding-top: 20px;
}
@@ -250,6 +243,7 @@
.btn {
-webkit-flex-grow: 1;
flex-grow: 1;
+
&:first-child {
margin-left: 0;
}
@@ -258,10 +252,6 @@
}
}
-.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/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index e5859fe7384..f8da0983b77 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -4,7 +4,7 @@
text-align: center;
.preview {
- @include border-radius(4px);
+ border-radius: 4px;
height: 80px;
margin-bottom: 10px;
@@ -47,7 +47,7 @@
width: 160px;
img {
- @include border-radius(4px);
+ border-radius: 4px;
max-width: 100%;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 8c8c403244e..fe7cf3c87e3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -13,49 +13,71 @@
.new_project,
.edit-project {
+
fieldset {
- &.features .control-label {
- font-weight: normal;
+
+ &.features {
+
+ .label-light {
+ margin-bottom: 0;
+ }
+
+ .help-block {
+ margin-top: 0;
+ }
}
+
.form-group {
margin-bottom: 5px;
}
+
&> .form-group {
padding-left: 0;
}
}
+
.help-block {
margin-bottom: 10px;
}
+
.project-path {
padding-right: 0;
+
.form-control {
border-radius: $border-radius-base;
}
}
+
.input-group > div {
+
&:last-child {
padding-right: 0;
}
}
+
@media (max-width: $screen-xs-max) {
.input-group > div {
+
margin-bottom: 14px;
+
&:last-child {
margin-bottom: 0;
}
}
+
fieldset > .form-group:first-child {
padding-right: 0;
}
}
.input-group-addon {
+
&.static-namespace {
height: 35px;
border-radius: 3px;
border: 1px solid #e5e5e5;
}
+
&+ .select2 a {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
@@ -146,7 +168,8 @@
}
.project-repo-btn-group,
- .notification-dropdown {
+ .notification-dropdown,
+ .project-dropdown {
margin-left: 10px;
}
@@ -200,6 +223,7 @@
pointer-events: none;
}
}
+
.count {
@include btn-gray;
display: inline-block;
@@ -353,35 +377,40 @@ a.deploy-project-label {
justify-content: flex-start;
.fork-thumbnail {
- @include border-radius($border-radius-base);
+ border-radius: $border-radius-base;
background-color: $white-light;
border: 1px solid $border-white-light;
height: 202px;
margin: $gl-padding;
text-align: center;
width: 169px;
+
&:hover, &.forked {
background-color: $row-hover;
border-color: $row-hover-border;
}
+
.no-avatar {
width: 100px;
height: 100px;
background-color: $gray-light;
border: 1px solid $gray-dark;
margin: 0 auto;
- @include border-radius(50%);
+ border-radius: 50%;
+
i {
font-size: 100px;
color: $gray-dark;
}
}
+
a {
display: block;
width: 100%;
height: 100%;
padding-top: $gl-padding;
color: $gl-gray;
+
.caption {
min-height: 30px;
padding: $gl-padding 0;
@@ -389,7 +418,7 @@ a.deploy-project-label {
}
img {
- @include border-radius(50%);
+ border-radius: 50%;
max-width: 100px;
}
}
@@ -495,7 +524,7 @@ pre.light-well {
}
.light-well {
- @include border-radius (2px);
+ border-radius: 2px;
color: #5b6169;
font-size: 13px;
@@ -643,6 +672,7 @@ pre.light-well {
.clone-options {
display: table-cell;
+
a.btn {
width: 100%;
}
@@ -775,8 +805,36 @@ pre.light-well {
.form-control {
min-width: 100px;
}
+
.select2-choice {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
-} \ No newline at end of file
+}
+
+.project-home-empty {
+ border-top: 0;
+
+ .container-fluid {
+ background: none;
+ }
+
+ p {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 650px;
+ }
+}
+
+.project-feature-nested {
+ @media (min-width: $screen-sm-min) {
+ padding-left: 45px;
+ }
+}
+
+.project-repo-select {
+ &.disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index eec22c5dc96..7b3878c91df 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -6,6 +6,7 @@
&.runner-state-shared {
background: #32b186;
}
+
&.runner-state-specific {
background: #3498db;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 0ee7ceecae5..f1d53c7b8bc 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -4,7 +4,7 @@
margin-right: 10px;
border: 1px solid #eee;
white-space: nowrap;
- @include border-radius(4px);
+ border-radius: 4px;
&:hover {
text-decoration: none;
@@ -65,6 +65,7 @@
.ci-status-icon-success {
color: $gl-success;
}
+
.ci-status-icon-failed {
color: $gl-danger;
}
@@ -77,6 +78,7 @@
.ci-status-icon-running {
color: $blue-normal;
}
+
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found,
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 68a5d1ae06c..ea76fe18876 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -51,6 +51,7 @@
-webkit-flex-direction: column;
flex-direction: column;
margin-left: 10px;
+ min-width: 55px;
}
.todo-item {
@@ -120,6 +121,14 @@
}
}
+@media (max-width: $screen-sm-max) {
+ .todos-filters {
+ .dropdown-menu-toggle {
+ width: 135px;
+ }
+ }
+}
+
@media (max-width: $screen-xs-max) {
.todo {
.avatar {
@@ -141,4 +150,14 @@
padding-left: 10px;
}
}
+
+ .todos-filters {
+ .row-content-block {
+ padding-bottom: 50px;
+ }
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 7b6577c513e..6ea7a2b5498 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -5,6 +5,7 @@
.file-finder {
width: 50%;
+
.file-finder-input {
width: 95%;
display: inline-block;
@@ -27,7 +28,12 @@
}
.last-commit {
- @include str-truncated(60%);
+ @include str-truncated(506px);
+
+ @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
+ @include str-truncated(450px);
+ }
+
}
.commit-history-link-spacer {
@@ -163,4 +169,8 @@
margin-top: 11px;
position: relative;
z-index: 2;
+
+ .download-button {
+ margin-left: $btn-side-margin;
+ }
}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index c9846103762..3fa7fa3d7e3 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -23,15 +23,19 @@
.term-bold {
font-weight: bold;
}
+
.term-italic {
font-style: italic;
}
+
.term-conceal {
visibility: hidden;
}
+
.term-underline {
text-decoration: underline;
}
+
.term-cross {
text-decoration: line-through;
}
@@ -39,48 +43,63 @@
.term-fg-black {
color: $black;
}
+
.term-fg-red {
color: $red;
}
+
.term-fg-green {
color: $green;
}
+
.term-fg-yellow {
color: $yellow;
}
+
.term-fg-blue {
color: $blue;
}
+
.term-fg-magenta {
color: $magenta;
}
+
.term-fg-cyan {
color: $cyan;
}
+
.term-fg-white {
color: $white;
}
+
.term-fg-l-black {
color: $l-black;
}
+
.term-fg-l-red {
color: $l-red;
}
+
.term-fg-l-green {
color: $l-green;
}
+
.term-fg-l-yellow {
color: $l-yellow;
}
+
.term-fg-l-blue {
color: $l-blue;
}
+
.term-fg-l-magenta {
color: $l-magenta;
}
+
.term-fg-l-cyan {
color: $l-cyan;
}
+
.term-fg-l-white {
color: $l-white;
}
@@ -88,818 +107,1087 @@
.term-bg-black {
background-color: $black;
}
+
.term-bg-red {
background-color: $red;
}
+
.term-bg-green {
background-color: $green;
}
+
.term-bg-yellow {
background-color: $yellow;
}
+
.term-bg-blue {
background-color: $blue;
}
+
.term-bg-magenta {
background-color: $magenta;
}
+
.term-bg-cyan {
background-color: $cyan;
}
+
.term-bg-white {
background-color: $white;
}
+
.term-bg-l-black {
background-color: $l-black;
}
+
.term-bg-l-red {
background-color: $l-red;
}
+
.term-bg-l-green {
background-color: $l-green;
}
+
.term-bg-l-yellow {
background-color: $l-yellow;
}
+
.term-bg-l-blue {
background-color: $l-blue;
}
+
.term-bg-l-magenta {
background-color: $l-magenta;
}
+
.term-bg-l-cyan {
background-color: $l-cyan;
}
+
.term-bg-l-white {
background-color: $l-white;
}
-
.xterm-fg-0 {
color: #000;
}
+
.xterm-fg-1 {
color: #800000;
}
+
.xterm-fg-2 {
color: #008000;
}
+
.xterm-fg-3 {
color: #808000;
}
+
.xterm-fg-4 {
color: #000080;
}
+
.xterm-fg-5 {
color: #800080;
}
+
.xterm-fg-6 {
color: #008080;
}
+
.xterm-fg-7 {
color: #c0c0c0;
}
+
.xterm-fg-8 {
color: #808080;
}
+
.xterm-fg-9 {
color: #f00;
}
+
.xterm-fg-10 {
color: #0f0;
}
+
.xterm-fg-11 {
color: #ff0;
}
+
.xterm-fg-12 {
color: #00f;
}
+
.xterm-fg-13 {
color: #f0f;
}
+
.xterm-fg-14 {
color: #0ff;
}
+
.xterm-fg-15 {
color: #fff;
}
+
.xterm-fg-16 {
color: #000;
}
+
.xterm-fg-17 {
color: #00005f;
}
+
.xterm-fg-18 {
color: #000087;
}
+
.xterm-fg-19 {
color: #0000af;
}
+
.xterm-fg-20 {
color: #0000d7;
}
+
.xterm-fg-21 {
color: #00f;
}
+
.xterm-fg-22 {
color: #005f00;
}
+
.xterm-fg-23 {
color: #005f5f;
}
+
.xterm-fg-24 {
color: #005f87;
}
+
.xterm-fg-25 {
color: #005faf;
}
+
.xterm-fg-26 {
color: #005fd7;
}
+
.xterm-fg-27 {
color: #005fff;
}
+
.xterm-fg-28 {
color: #008700;
}
+
.xterm-fg-29 {
color: #00875f;
}
+
.xterm-fg-30 {
color: #008787;
}
+
.xterm-fg-31 {
color: #0087af;
}
+
.xterm-fg-32 {
color: #0087d7;
}
+
.xterm-fg-33 {
color: #0087ff;
}
+
.xterm-fg-34 {
color: #00af00;
}
+
.xterm-fg-35 {
color: #00af5f;
}
+
.xterm-fg-36 {
color: #00af87;
}
+
.xterm-fg-37 {
color: #00afaf;
}
+
.xterm-fg-38 {
color: #00afd7;
}
+
.xterm-fg-39 {
color: #00afff;
}
+
.xterm-fg-40 {
color: #00d700;
}
+
.xterm-fg-41 {
color: #00d75f;
}
+
.xterm-fg-42 {
color: #00d787;
}
+
.xterm-fg-43 {
color: #00d7af;
}
+
.xterm-fg-44 {
color: #00d7d7;
}
+
.xterm-fg-45 {
color: #00d7ff;
}
+
.xterm-fg-46 {
color: #0f0;
}
+
.xterm-fg-47 {
color: #00ff5f;
}
+
.xterm-fg-48 {
color: #00ff87;
}
+
.xterm-fg-49 {
color: #00ffaf;
}
+
.xterm-fg-50 {
color: #00ffd7;
}
+
.xterm-fg-51 {
color: #0ff;
}
+
.xterm-fg-52 {
color: #5f0000;
}
+
.xterm-fg-53 {
color: #5f005f;
}
+
.xterm-fg-54 {
color: #5f0087;
}
+
.xterm-fg-55 {
color: #5f00af;
}
+
.xterm-fg-56 {
color: #5f00d7;
}
+
.xterm-fg-57 {
color: #5f00ff;
}
+
.xterm-fg-58 {
color: #5f5f00;
}
+
.xterm-fg-59 {
color: #5f5f5f;
}
+
.xterm-fg-60 {
color: #5f5f87;
}
+
.xterm-fg-61 {
color: #5f5faf;
}
+
.xterm-fg-62 {
color: #5f5fd7;
}
+
.xterm-fg-63 {
color: #5f5fff;
}
+
.xterm-fg-64 {
color: #5f8700;
}
+
.xterm-fg-65 {
color: #5f875f;
}
+
.xterm-fg-66 {
color: #5f8787;
}
+
.xterm-fg-67 {
color: #5f87af;
}
+
.xterm-fg-68 {
color: #5f87d7;
}
+
.xterm-fg-69 {
color: #5f87ff;
}
+
.xterm-fg-70 {
color: #5faf00;
}
+
.xterm-fg-71 {
color: #5faf5f;
}
+
.xterm-fg-72 {
color: #5faf87;
}
+
.xterm-fg-73 {
color: #5fafaf;
}
+
.xterm-fg-74 {
color: #5fafd7;
}
+
.xterm-fg-75 {
color: #5fafff;
}
+
.xterm-fg-76 {
color: #5fd700;
}
+
.xterm-fg-77 {
color: #5fd75f;
}
+
.xterm-fg-78 {
color: #5fd787;
}
+
.xterm-fg-79 {
color: #5fd7af;
}
+
.xterm-fg-80 {
color: #5fd7d7;
}
+
.xterm-fg-81 {
color: #5fd7ff;
}
+
.xterm-fg-82 {
color: #5fff00;
}
+
.xterm-fg-83 {
color: #5fff5f;
}
+
.xterm-fg-84 {
color: #5fff87;
}
+
.xterm-fg-85 {
color: #5fffaf;
}
+
.xterm-fg-86 {
color: #5fffd7;
}
+
.xterm-fg-87 {
color: #5fffff;
}
+
.xterm-fg-88 {
color: #870000;
}
+
.xterm-fg-89 {
color: #87005f;
}
+
.xterm-fg-90 {
color: #870087;
}
+
.xterm-fg-91 {
color: #8700af;
}
+
.xterm-fg-92 {
color: #8700d7;
}
+
.xterm-fg-93 {
color: #8700ff;
}
+
.xterm-fg-94 {
color: #875f00;
}
+
.xterm-fg-95 {
color: #875f5f;
}
+
.xterm-fg-96 {
color: #875f87;
}
+
.xterm-fg-97 {
color: #875faf;
}
+
.xterm-fg-98 {
color: #875fd7;
}
+
.xterm-fg-99 {
color: #875fff;
}
+
.xterm-fg-100 {
color: #878700;
}
+
.xterm-fg-101 {
color: #87875f;
}
+
.xterm-fg-102 {
color: #878787;
}
+
.xterm-fg-103 {
color: #8787af;
}
+
.xterm-fg-104 {
color: #8787d7;
}
+
.xterm-fg-105 {
color: #8787ff;
}
+
.xterm-fg-106 {
color: #87af00;
}
+
.xterm-fg-107 {
color: #87af5f;
}
+
.xterm-fg-108 {
color: #87af87;
}
+
.xterm-fg-109 {
color: #87afaf;
}
+
.xterm-fg-110 {
color: #87afd7;
}
+
.xterm-fg-111 {
color: #87afff;
}
+
.xterm-fg-112 {
color: #87d700;
}
+
.xterm-fg-113 {
color: #87d75f;
}
+
.xterm-fg-114 {
color: #87d787;
}
+
.xterm-fg-115 {
color: #87d7af;
}
+
.xterm-fg-116 {
color: #87d7d7;
}
+
.xterm-fg-117 {
color: #87d7ff;
}
+
.xterm-fg-118 {
color: #87ff00;
}
+
.xterm-fg-119 {
color: #87ff5f;
}
+
.xterm-fg-120 {
color: #87ff87;
}
+
.xterm-fg-121 {
color: #87ffaf;
}
+
.xterm-fg-122 {
color: #87ffd7;
}
+
.xterm-fg-123 {
color: #87ffff;
}
+
.xterm-fg-124 {
color: #af0000;
}
+
.xterm-fg-125 {
color: #af005f;
}
+
.xterm-fg-126 {
color: #af0087;
}
+
.xterm-fg-127 {
color: #af00af;
}
+
.xterm-fg-128 {
color: #af00d7;
}
+
.xterm-fg-129 {
color: #af00ff;
}
+
.xterm-fg-130 {
color: #af5f00;
}
+
.xterm-fg-131 {
color: #af5f5f;
}
+
.xterm-fg-132 {
color: #af5f87;
}
+
.xterm-fg-133 {
color: #af5faf;
}
+
.xterm-fg-134 {
color: #af5fd7;
}
+
.xterm-fg-135 {
color: #af5fff;
}
+
.xterm-fg-136 {
color: #af8700;
}
+
.xterm-fg-137 {
color: #af875f;
}
+
.xterm-fg-138 {
color: #af8787;
}
+
.xterm-fg-139 {
color: #af87af;
}
+
.xterm-fg-140 {
color: #af87d7;
}
+
.xterm-fg-141 {
color: #af87ff;
}
+
.xterm-fg-142 {
color: #afaf00;
}
+
.xterm-fg-143 {
color: #afaf5f;
}
+
.xterm-fg-144 {
color: #afaf87;
}
+
.xterm-fg-145 {
color: #afafaf;
}
+
.xterm-fg-146 {
color: #afafd7;
}
+
.xterm-fg-147 {
color: #afafff;
}
+
.xterm-fg-148 {
color: #afd700;
}
+
.xterm-fg-149 {
color: #afd75f;
}
+
.xterm-fg-150 {
color: #afd787;
}
+
.xterm-fg-151 {
color: #afd7af;
}
+
.xterm-fg-152 {
color: #afd7d7;
}
+
.xterm-fg-153 {
color: #afd7ff;
}
+
.xterm-fg-154 {
color: #afff00;
}
+
.xterm-fg-155 {
color: #afff5f;
}
+
.xterm-fg-156 {
color: #afff87;
}
+
.xterm-fg-157 {
color: #afffaf;
}
+
.xterm-fg-158 {
color: #afffd7;
}
+
.xterm-fg-159 {
color: #afffff;
}
+
.xterm-fg-160 {
color: #d70000;
}
+
.xterm-fg-161 {
color: #d7005f;
}
+
.xterm-fg-162 {
color: #d70087;
}
+
.xterm-fg-163 {
color: #d700af;
}
+
.xterm-fg-164 {
color: #d700d7;
}
+
.xterm-fg-165 {
color: #d700ff;
}
+
.xterm-fg-166 {
color: #d75f00;
}
+
.xterm-fg-167 {
color: #d75f5f;
}
+
.xterm-fg-168 {
color: #d75f87;
}
+
.xterm-fg-169 {
color: #d75faf;
}
+
.xterm-fg-170 {
color: #d75fd7;
}
+
.xterm-fg-171 {
color: #d75fff;
}
+
.xterm-fg-172 {
color: #d78700;
}
+
.xterm-fg-173 {
color: #d7875f;
}
+
.xterm-fg-174 {
color: #d78787;
}
+
.xterm-fg-175 {
color: #d787af;
}
+
.xterm-fg-176 {
color: #d787d7;
}
+
.xterm-fg-177 {
color: #d787ff;
}
+
.xterm-fg-178 {
color: #d7af00;
}
+
.xterm-fg-179 {
color: #d7af5f;
}
+
.xterm-fg-180 {
color: #d7af87;
}
+
.xterm-fg-181 {
color: #d7afaf;
}
+
.xterm-fg-182 {
color: #d7afd7;
}
+
.xterm-fg-183 {
color: #d7afff;
}
+
.xterm-fg-184 {
color: #d7d700;
}
+
.xterm-fg-185 {
color: #d7d75f;
}
+
.xterm-fg-186 {
color: #d7d787;
}
+
.xterm-fg-187 {
color: #d7d7af;
}
+
.xterm-fg-188 {
color: #d7d7d7;
}
+
.xterm-fg-189 {
color: #d7d7ff;
}
+
.xterm-fg-190 {
color: #d7ff00;
}
+
.xterm-fg-191 {
color: #d7ff5f;
}
+
.xterm-fg-192 {
color: #d7ff87;
}
+
.xterm-fg-193 {
color: #d7ffaf;
}
+
.xterm-fg-194 {
color: #d7ffd7;
}
+
.xterm-fg-195 {
color: #d7ffff;
}
+
.xterm-fg-196 {
color: #f00;
}
+
.xterm-fg-197 {
color: #ff005f;
}
+
.xterm-fg-198 {
color: #ff0087;
}
+
.xterm-fg-199 {
color: #ff00af;
}
+
.xterm-fg-200 {
color: #ff00d7;
}
+
.xterm-fg-201 {
color: #f0f;
}
+
.xterm-fg-202 {
color: #ff5f00;
}
+
.xterm-fg-203 {
color: #ff5f5f;
}
+
.xterm-fg-204 {
color: #ff5f87;
}
+
.xterm-fg-205 {
color: #ff5faf;
}
+
.xterm-fg-206 {
color: #ff5fd7;
}
+
.xterm-fg-207 {
color: #ff5fff;
}
+
.xterm-fg-208 {
color: #ff8700;
}
+
.xterm-fg-209 {
color: #ff875f;
}
+
.xterm-fg-210 {
color: #ff8787;
}
+
.xterm-fg-211 {
color: #ff87af;
}
+
.xterm-fg-212 {
color: #ff87d7;
}
+
.xterm-fg-213 {
color: #ff87ff;
}
+
.xterm-fg-214 {
color: #ffaf00;
}
+
.xterm-fg-215 {
color: #ffaf5f;
}
+
.xterm-fg-216 {
color: #ffaf87;
}
+
.xterm-fg-217 {
color: #ffafaf;
}
+
.xterm-fg-218 {
color: #ffafd7;
}
+
.xterm-fg-219 {
color: #ffafff;
}
+
.xterm-fg-220 {
color: #ffd700;
}
+
.xterm-fg-221 {
color: #ffd75f;
}
+
.xterm-fg-222 {
color: #ffd787;
}
+
.xterm-fg-223 {
color: #ffd7af;
}
+
.xterm-fg-224 {
color: #ffd7d7;
}
+
.xterm-fg-225 {
color: #ffd7ff;
}
+
.xterm-fg-226 {
color: #ff0;
}
+
.xterm-fg-227 {
color: #ffff5f;
}
+
.xterm-fg-228 {
color: #ffff87;
}
+
.xterm-fg-229 {
color: #ffffaf;
}
+
.xterm-fg-230 {
color: #ffffd7;
}
+
.xterm-fg-231 {
color: #fff;
}
+
.xterm-fg-232 {
color: #080808;
}
+
.xterm-fg-233 {
color: #121212;
}
+
.xterm-fg-234 {
color: #1c1c1c;
}
+
.xterm-fg-235 {
color: #262626;
}
+
.xterm-fg-236 {
color: #303030;
}
+
.xterm-fg-237 {
color: #3a3a3a;
}
+
.xterm-fg-238 {
color: #444;
}
+
.xterm-fg-239 {
color: #4e4e4e;
}
+
.xterm-fg-240 {
color: #585858;
}
+
.xterm-fg-241 {
color: #626262;
}
+
.xterm-fg-242 {
color: #6c6c6c;
}
+
.xterm-fg-243 {
color: #767676;
}
+
.xterm-fg-244 {
color: #808080;
}
+
.xterm-fg-245 {
color: #8a8a8a;
}
+
.xterm-fg-246 {
color: #949494;
}
+
.xterm-fg-247 {
color: #9e9e9e;
}
+
.xterm-fg-248 {
color: #a8a8a8;
}
+
.xterm-fg-249 {
color: #b2b2b2;
}
+
.xterm-fg-250 {
color: #bcbcbc;
}
+
.xterm-fg-251 {
color: #c6c6c6;
}
+
.xterm-fg-252 {
color: #d0d0d0;
}
+
.xterm-fg-253 {
color: #dadada;
}
+
.xterm-fg-254 {
color: #e4e4e4;
}
+
.xterm-fg-255 {
color: #eee;
}
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 82055006ac0..762e36ee2e9 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
def preview
- @message = broadcast_message_params[:message]
+ @broadcast_message = BroadcastMessage.new(broadcast_message_params)
end
protected
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index aed77d0358a..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
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/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 7c37f3155da..37a1a23178e 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController
private
def services_templates
- templates = []
-
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service_template = service_name.concat("_service").camelize.constantize
- templates << service_template.where(template: true).first_or_create
+ service_template.where(template: true).first_or_create
end
-
- templates
end
def service
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bd4ba384b29..37600ed875c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -45,6 +45,10 @@ class ApplicationController < ActionController::Base
redirect_to request.referer.present? ? :back : default, options
end
+ def not_found
+ render_404
+ end
+
protected
# This filter handles both private tokens and personal access tokens
@@ -114,7 +118,12 @@ class ApplicationController < ActionController::Base
end
def render_404
- render file: Rails.root.join("public", "404"), layout: false, status: "404"
+ respond_to do |format|
+ format.html do
+ render file: Rails.root.join("public", "404"), layout: false, status: "404"
+ end
+ format.any { head :not_found }
+ end
end
def no_cache_headers
@@ -173,7 +182,8 @@ class ApplicationController < ActionController::Base
end
def event_filter
- filters = cookies['event_filter'].split(',') if cookies['event_filter'].present?
+ # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
+ filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
@event_filter ||= EventFilter.new(filters)
end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
deleted file mode 100644
index 5bb7d499cdc..00000000000
--- a/app/controllers/ci/application_controller.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Ci
- class ApplicationController < ::ApplicationController
- def self.railtie_helpers_paths
- "app/helpers/ci"
- end
- end
-end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index e06d12cfce1..3eb485de9db 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -1,5 +1,5 @@
module Ci
- class LintsController < ApplicationController
+ class LintsController < ::ApplicationController
before_action :authenticate_user!
def show
@@ -14,6 +14,7 @@ module Ci
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
+ @jobs = @config_processor.jobs
end
rescue
@error = 'Undefined error'
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index aa894fde36b..ff297d6ff13 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,5 +1,5 @@
module Ci
- class ProjectsController < Ci::ApplicationController
+ class ProjectsController < ::ApplicationController
before_action :project
before_action :no_cache, only: [:badge]
before_action :authorize_read_project!, except: [:badge, :index]
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index d5a8a962662..4c497711fc0 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
+ return locked_user_redirect(user) if user.access_locked?
+
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
+ def locked_user_redirect(user)
+ flash.now[:alert] = 'Invalid Login or password'
+ render 'devise/sessions/new'
+ end
+
def authenticate_with_two_factor
user = self.resource = find_user
- if user_params[:otp_attempt].present? && session[:otp_user_id]
+ if user.access_locked?
+ locked_user_redirect(user)
+ elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
+ user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.'
- render :two_factor
+ prompt_for_two_factor(user)
end
end
@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
+ user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index bb32bc502e6..be86fa106f8 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -2,6 +2,7 @@ module IssuableActions
extend ActiveSupport::Concern
included do
+ before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
end
@@ -25,6 +26,10 @@ module IssuableActions
private
+ def labels
+ @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ end
+
def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index b8ed2c159a7..c13333641d3 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -15,18 +15,17 @@ module MembershipActions
end
def leave
- @member = membershipable.members.find_by(user_id: current_user) ||
- membershipable.requesters.find_by(user_id: current_user)
- Members::DestroyService.new(@member, current_user).execute
+ member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
+ execute(:all)
- source_type = @member.real_source_type.humanize(capitalize: false)
+ source_type = membershipable.class.to_s.humanize(capitalize: false)
notice =
- if @member.request?
+ if member.request?
"Your access request to the #{source_type} has been withdrawn."
else
- "You left the \"#{@member.source.human_name}\" #{source_type}."
+ "You left the \"#{membershipable.human_name}\" #{source_type}."
end
- redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
+ redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice
end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 29e243c66a3..99acd98ae13 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -7,7 +7,7 @@ module SpammableActions
def mark_as_spam
if SpamService.new(spammable).mark_as_spam!
- redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
+ redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
index 2a88350a4ca..d5031da867a 100644
--- a/app/controllers/dashboard/labels_controller.rb
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -1,9 +1,9 @@
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
- labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title)
+ labels = LabelsFinder.new(current_user).execute
respond_to do |format|
- format.json { render json: labels }
+ format.json { render json: labels.as_json(only: [:id, :title, :color]) }
end
end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 88a0c18180b..a62c6211372 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
- @projects = TrendingProjectsFinder.new.execute(current_user)
- @projects = filter_projects(@projects)
+ @projects = filter_projects(Project.trending)
@projects = @projects.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 272164cd0cc..940a3ad20ba 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -15,12 +15,16 @@ 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
def create
+ if params[:user_ids].blank?
+ return redirect_to(group_group_members_path(@group), alert: 'No users specified.')
+ end
+
@group.add_users(
params[:user_ids].split(','),
params[:access_level],
@@ -40,10 +44,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def destroy
- @group_member = @group.members.find_by(id: params[:id]) ||
- @group.requesters.find_by(id: params[:id])
-
- Members::DestroyService.new(@group_member, current_user).execute
+ Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
new file mode 100644
index 00000000000..29528b2cfaa
--- /dev/null
+++ b/app/controllers/groups/labels_controller.rb
@@ -0,0 +1,92 @@
+class Groups::LabelsController < Groups::ApplicationController
+ before_action :label, only: [:edit, :update, :destroy]
+ before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
+ before_action :save_previous_label_path, only: [:edit]
+
+ respond_to :html
+
+ def index
+ respond_to do |format|
+ format.html do
+ @labels = @group.labels.page(params[:page])
+ end
+
+ format.json do
+ available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
+ render json: available_labels.as_json(only: [:id, :title, :color])
+ end
+ end
+ end
+
+ def new
+ @label = @group.labels.new
+ @previous_labels_path = previous_labels_path
+ end
+
+ def create
+ @label = @group.labels.create(label_params)
+
+ if @label.valid?
+ redirect_to group_labels_path(@group)
+ else
+ render :new
+ end
+ end
+
+ def edit
+ @previous_labels_path = previous_labels_path
+ end
+
+ def update
+ if @label.update_attributes(label_params)
+ redirect_back_or_group_labels_path
+ else
+ render :edit
+ end
+ end
+
+ def destroy
+ @label.destroy
+
+ respond_to do |format|
+ format.html do
+ redirect_to group_labels_path(@group), notice: 'Label was removed'
+ end
+ format.js
+ end
+ end
+
+ protected
+
+ def authorize_admin_labels!
+ return render_404 unless can?(current_user, :admin_label, @group)
+ end
+
+ def authorize_read_labels!
+ return render_404 unless can?(current_user, :read_label, @group)
+ end
+
+ def label
+ @label ||= @group.labels.find(params[:id])
+ end
+
+ def label_params
+ params.require(:label).permit(:title, :description, :color)
+ end
+
+ def redirect_back_or_group_labels_path(options = {})
+ redirect_to previous_labels_path, options
+ end
+
+ def previous_labels_path
+ session.fetch(:previous_labels_path, fallback_path)
+ end
+
+ def fallback_path
+ group_labels_path(@group)
+ end
+
+ def save_previous_label_path
+ session[:previous_labels_path] = URI(request.referer || '').path
+ end
+end
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 8eda0753549..f22ac70bc23 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,10 +1,9 @@
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
- before_action :authenticate_admin!
def new
- @namespace_id = project_params[:namespace_id]
- @namespace_name = Namespace.find(project_params[:namespace_id]).name
+ @namespace = Namespace.find(project_params[:namespace_id])
+ return render_404 unless current_user.can?(:create_projects, @namespace)
@path = project_params[:path]
end
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
deleted file mode 100644
index 83eec1bf4a2..00000000000
--- a/app/controllers/namespaces_controller.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class NamespacesController < ApplicationController
- skip_before_action :authenticate_user!
-
- def show
- namespace = Namespace.find_by(path: params[:id])
-
- if namespace
- if namespace.is_a?(Group)
- group = namespace
- else
- user = namespace.owner
- end
- end
-
- if user
- redirect_to user_path(user)
- elsif group && can?(current_user, :read_group, group)
- redirect_to group_path(group)
- elsif current_user.nil?
- authenticate_user!
- else
- render_404
- end
- end
-end
diff --git a/app/controllers/projects/board_lists_controller.rb b/app/controllers/projects/board_lists_controller.rb
deleted file mode 100644
index 3cfb08d5822..00000000000
--- a/app/controllers/projects/board_lists_controller.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-class Projects::BoardListsController < Projects::ApplicationController
- respond_to :json
-
- before_action :authorize_admin_list!
-
- rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
-
- def create
- list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
-
- if list.valid?
- render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
- else
- render json: list.errors, status: :unprocessable_entity
- end
- end
-
- def update
- service = Boards::Lists::MoveService.new(project, current_user, move_params)
-
- if service.execute
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def destroy
- service = Boards::Lists::DestroyService.new(project, current_user, params)
-
- if service.execute
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def generate
- service = Boards::Lists::GenerateService.new(project, current_user)
-
- if service.execute
- render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
- else
- head :unprocessable_entity
- end
- end
-
- private
-
- def authorize_admin_list!
- return render_403 unless can?(current_user, :admin_list, project)
- end
-
- def list_params
- params.require(:list).permit(:label_id)
- end
-
- def move_params
- params.require(:list).permit(:position).merge(id: params[:id])
- end
-
- def record_not_found(exception)
- render json: { error: exception.message }, status: :not_found
- end
-end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 9404612a993..a2b01ff43dc 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -2,6 +2,7 @@ module Projects
module Boards
class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index]
+ before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update]
def index
@@ -9,16 +10,22 @@ module Projects
issues = issues.page(params[:page])
render json: {
- issues: issues.as_json(
- only: [:iid, :title, :confidential],
- include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
- }),
+ issues: serialize_as_json(issues),
size: issues.total_count
}
end
+ def create
+ service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
+ issue = service.execute
+
+ if issue.valid?
+ render json: serialize_as_json(issue)
+ else
+ render json: issue.errors, status: :unprocessable_entity
+ end
+ end
+
def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
@@ -33,7 +40,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!
@@ -43,16 +50,33 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project)
end
+ def authorize_create_issue!
+ return render_403 unless can?(current_user, :admin_issue, project)
+ end
+
def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue)
end
def filter_params
- params.merge(id: params[:list_id])
+ params.merge(board_id: params[:board_id], id: params[:list_id])
end
def move_params
- params.permit(:id, :from_list_id, :to_list_id)
+ params.permit(:board_id, :id, :from_list_id, :to_list_id)
+ end
+
+ def issue_params
+ params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(
+ labels: true,
+ only: [:iid, :title, :confidential],
+ include: {
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
+ })
end
end
end
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
index b995f586737..67e3c9add81 100644
--- a/app/controllers/projects/boards/lists_controller.rb
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -5,11 +5,11 @@ module Projects
before_action :authorize_read_list!, only: [:index]
def index
- render json: serialize_as_json(project.board.lists)
+ render json: serialize_as_json(board.lists)
end
def create
- list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
+ list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board)
if list.valid?
render json: serialize_as_json(list)
@@ -19,7 +19,7 @@ module Projects
end
def update
- list = project.board.lists.movable.find(params[:id])
+ list = board.lists.movable.find(params[:id])
service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
if service.execute(list)
@@ -30,8 +30,8 @@ module Projects
end
def destroy
- list = project.board.lists.destroyable.find(params[:id])
- service = ::Boards::Lists::DestroyService.new(project, current_user, params)
+ list = board.lists.destroyable.find(params[:id])
+ service = ::Boards::Lists::DestroyService.new(project, current_user)
if service.execute(list)
head :ok
@@ -43,8 +43,8 @@ module Projects
def generate
service = ::Boards::Lists::GenerateService.new(project, current_user)
- if service.execute
- render json: serialize_as_json(project.board.lists.movable)
+ if service.execute(board)
+ render json: serialize_as_json(board.lists.movable)
else
head :unprocessable_entity
end
@@ -60,6 +60,10 @@ module Projects
return render_403 unless can?(current_user, :read_list, project)
end
+ def board
+ @board ||= project.boards.find(params[:board_id])
+ end
+
def list_params
params.require(:list).permit(:label_id)
end
@@ -72,9 +76,8 @@ module Projects
resource.as_json(
only: [:id, :list_type, :position],
methods: [:title],
- include: {
- label: { only: [:id, :title, :description, :color, :priority] }
- })
+ label: true
+ )
end
end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 33206717089..808affa4f98 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -1,10 +1,28 @@
class Projects::BoardsController < Projects::ApplicationController
- respond_to :html
+ include IssuableCollections
- before_action :authorize_read_board!, only: [:show]
+ before_action :authorize_read_board!, only: [:index, :show]
+
+ def index
+ @boards = ::Boards::ListService.new(project, current_user).execute
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: serialize_as_json(@boards)
+ end
+ end
+ end
def show
- ::Boards::CreateService.new(project, current_user).execute
+ @board = project.boards.find(params[:id])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: serialize_as_json(@board)
+ end
+ end
end
private
@@ -12,4 +30,8 @@ class Projects::BoardsController < Projects::ApplicationController
def authorize_read_board!
return access_denied! unless can?(current_user, :read_board, project)
end
+
+ def serialize_as_json(resource)
+ resource.as_json(only: [:id])
+ end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 3b2e35a7a05..fbe391fc58c 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController
def trace
respond_to do |format|
format.json do
- render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
+ state = params[:state].presence
+ render json: @build.trace_with_state(state: state).
+ merge!(id: @build.id, status: @build.status)
end
end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index a52c614b259..c2e7bf1ffec 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController
@commits =
if search.present?
- @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact
+ @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
else
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 58678f96879..ea22b2dcc15 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
- before_action :authorize_update_environment!, only: [:edit, :update, :destroy]
- before_action :environment, only: [:show, :edit, :update, :destroy]
+ before_action :authorize_create_deployment!, only: [:stop]
+ before_action :authorize_update_environment!, only: [:edit, :update]
+ before_action :environment, only: [:show, :edit, :update, :stop]
def index
- @environments = project.environments
+ @scope = params[:scope]
+ @all_environments = project.environments
+ @environments =
+ if @scope == 'stopped'
+ @all_environments.stopped
+ else
+ @all_environments.available
+ end
end
def show
@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
- def destroy
- if @environment.destroy
- flash[:notice] = 'Environment was successfully removed.'
- else
- flash[:alert] = 'Failed to remove environment.'
- end
+ def stop
+ return render_404 unless @environment.stoppable?
- redirect_to namespace_project_environments_path(project.namespace, project)
+ new_action = @environment.stop!(current_user)
+ redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end
private
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 092ef32e6e3..923e7340e69 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -38,12 +38,12 @@ class Projects::GraphsController < Projects::ApplicationController
@languages = @languages.map do |language|
name, share = language
- color = Digest::SHA256.hexdigest(name)[0...6]
+ color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}"
{
value: (share.to_f * 100 / total).round(2),
label: name,
- color: "##{color}",
- highlight: "##{color}"
+ color: color,
+ highlight: color
}
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index d0c4550733c..ae060abee5c 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -1,27 +1,53 @@
class Projects::GroupLinksController < Projects::ApplicationController
layout 'project_settings'
before_action :authorize_admin_project!
+ before_action :authorize_admin_project_member!, only: [:update]
def index
@group_links = project.project_group_links.all
+
+ @skip_groups = @group_links.pluck(:group_id)
+ @skip_groups << project.group.try(:id)
end
def create
- group = Group.find(params[:link_group_id])
- return render_404 unless can?(current_user, :read_group, group)
+ group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
+
+ if group
+ return render_404 unless can?(current_user, :read_group, group)
- project.project_group_links.create(
- group: group,
- group_access: params[:link_group_access],
- expires_at: params[:expires_at]
- )
+ project.project_group_links.create(
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
+ )
+ else
+ flash[:alert] = 'Please select a group.'
+ end
redirect_to namespace_project_group_links_path(project.namespace, project)
end
+ def update
+ @group_link = @project.project_group_links.find(params[:id])
+
+ @group_link.update_attributes(group_link_params)
+ end
+
def destroy
project.project_group_links.find(params[:id]).destroy
- redirect_to namespace_project_group_links_path(project.namespace, project)
+ respond_to do |format|
+ format.html do
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+ format.js { head :ok }
+ end
+ end
+
+ protected
+
+ def group_link_params
+ params.require(:group_link).permit(:group_access, :expires_at)
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ef13e0677d2..cb649264146 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection
@issues = @issues.page(params[:page])
- @labels = @project.labels.where(title: params[:label_name])
+ if params[:label_name].present?
+ @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
+ end
respond_to do |format|
format.html
@@ -159,7 +161,8 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def issue
- @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old
+ # The Sortable default scope causes performance issues when used with find_by
+ @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 28fa4a5b141..4f855134368 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
+ before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
before_action :authorize_read_label!
- before_action :authorize_admin_labels!, only: [
- :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
- ]
+ before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
+ :generate, :destroy, :remove_priority,
+ :set_priorities]
respond_to :js, :html
def index
- @labels = @project.labels.unprioritized.page(params[:page])
- @prioritized_labels = @project.labels.prioritized
+ @prioritized_labels = @available_labels.prioritized(@project)
+ @labels = @available_labels.unprioritized(@project).page(params[:page])
respond_to do |format|
format.html
format.json do
- render json: @project.labels
+ render json: @available_labels.as_json(only: [:id, :title, :color])
end
end
end
@@ -30,9 +31,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params)
if @label.valid?
- redirect_to namespace_project_labels_path(@project.namespace, @project)
+ respond_to do |format|
+ format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
+ format.json { render json: @label }
+ end
else
- render 'new'
+ respond_to do |format|
+ format.html { render :new }
+ format.json { render json: { message: @label.errors.messages }, status: 400 }
+ end
end
end
@@ -43,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController
if @label.update_attributes(label_params)
redirect_to namespace_project_labels_path(@project.namespace, @project)
else
- render 'edit'
+ render :edit
end
end
@@ -62,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
def destroy
@label.destroy
+ @labels = find_labels
respond_to do |format|
format.html do
@@ -74,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController
def remove_priority
respond_to do |format|
- if label.update_attribute(:priority, nil)
+ label = @available_labels.find(params[:id])
+
+ if label.unprioritize!(project)
format.json { render json: label }
else
- message = label.errors.full_messages.uniq.join('. ')
- format.json { render json: { message: message }, status: :unprocessable_entity }
+ format.json { head :unprocessable_entity }
end
end
end
def set_priorities
Label.transaction do
- params[:label_ids].each_with_index do |label_id, index|
- label = @project.labels.find_by_id(label_id)
- label.update_attribute(:priority, index) if label
+ available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
+ label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) }
+
+ label_ids.each_with_index do |label_id, index|
+ label = @available_labels.find(label_id)
+ label.prioritize!(project, index)
end
end
@@ -113,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController
end
alias_method :subscribable_resource, :label
+ def find_labels
+ @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities)
+ end
+
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 935417d4ae8..2ee53f7ceda 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,15 +9,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
- :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
- :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
+ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check,
+ :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
- before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
+ before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
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 :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
+ before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :apply_diff_view_cookie!, only: [:new_diffs]
+ before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -28,14 +31,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
- before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
+ before_action :authenticate_user!, only: [:assign_related_issues]
+
+ before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
- @labels = @project.labels.where(title: params[:label_name])
+ if params[:label_name].present?
+ labels_params = { project_id: @project.id, title: params[:label_name] }
+ @labels = LabelsFinder.new(current_user, labels_params).execute
+ end
respond_to do |format|
format.html
@@ -165,6 +173,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def conflict_for_path
+ return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
+
+ file = @merge_request.conflicts.file_for_path(params[:old_path], params[:new_path])
+
+ return render_404 unless file
+
+ render json: file, full_content: true
+ end
+
def resolve_conflicts
return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
@@ -179,7 +197,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
- rescue Gitlab::Conflict::File::MissingResolution => e
+ rescue Gitlab::Conflict::ResolutionError => e
render status: :bad_request, json: { message: e.message }
end
end
@@ -209,29 +227,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def new
- apply_diff_view_cookie!
-
- build_merge_request
- @noteable = @merge_request
-
- @target_branches = if @merge_request.target_project
- @merge_request.target_project.repository.branch_names
- else
- []
- end
-
- @target_project = merge_request.target_project
- @source_project = merge_request.source_project
- @commits = @merge_request.compare_commits.reverse
- @commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit
- @diffs = @merge_request.diffs(diff_options) if @merge_request.compare
- @diff_notes_disabled = true
- @pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses.relevant if @pipeline
+ define_new_vars
+ end
- @note_counts = Note.where(commit_id: @commits.map(&:id)).
- group(:commit_id).count
+ def new_diffs
+ respond_to do |format|
+ format.html do
+ define_new_vars
+ render "new"
+ end
+ format.json do
+ @diffs = if @merge_request.can_be_created
+ @merge_request.diffs(diff_options)
+ else
+ []
+ end
+ @diff_notes_disabled = true
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ end
+ end
end
def create
@@ -275,7 +290,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def remove_wip
- MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
+ MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
notice: "The merge request can now be merged."
@@ -308,8 +323,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?
@@ -356,6 +369,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render layout: false
end
+ def assign_related_issues
+ result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute
+
+ respond_to do |format|
+ format.html do
+ case result[:count]
+ when 0
+ flash[:error] = "Failed to assign you issues related to the merge request"
+ when 1
+ flash[:notice] = "1 issue has been assigned to you"
+ else
+ flash[:notice] = "#{result[:count]} issues have been assigned to you"
+ end
+
+ redirect_to(merge_request_path(@merge_request))
+ end
+ end
+ end
+
def ci_status
pipeline = @merge_request.pipeline
if pipeline
@@ -366,7 +398,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
status ||= "preparing"
else
- ci_service = @merge_request.source_project.ci_service
+ ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
if ci_service.respond_to?(:commit_coverage)
@@ -384,6 +416,36 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
+ def ci_environments_status
+ environments =
+ begin
+ @merge_request.environments.map do |environment|
+ next unless can?(current_user, :read_environment, environment)
+
+ project = environment.project
+ deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
+
+ stop_url =
+ if environment.stoppable? && can?(current_user, :create_deployment, environment)
+ stop_namespace_project_environment_path(project.namespace, project, environment)
+ end
+
+ {
+ id: environment.id,
+ name: environment.name,
+ url: namespace_project_environment_path(project.namespace, project, environment),
+ stop_url: stop_url,
+ external_url: environment.external_url,
+ external_url_formatted: environment.formatted_external_url,
+ deployed_at: deployment.try(:created_at),
+ deployed_at_formatted: deployment.try(:formatted_deployment_time)
+ }
+ end.compact
+ end
+
+ render json: environments
+ end
+
protected
def selected_target_project
@@ -418,10 +480,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def validates_merge_request
- # If source project was removed and merge request for some reason
- # wasn't close (Ex. mr from fork to origin)
- return invalid_mr if !@merge_request.source_project && @merge_request.open?
-
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.commits.blank?
@@ -434,13 +492,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@noteable = @merge_request
@commits_count = @merge_request.commits.count
- @pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses.relevant if @pipeline
-
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
+
+ define_pipelines_vars
end
# Discussion tab data is rendered on html responses of actions
@@ -468,7 +525,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars
@pipeline = @merge_request.pipeline
- @pipelines = [@pipeline].compact
end
def define_commit_vars
@@ -495,8 +551,40 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
+ def define_pipelines_vars
+ @pipelines = @merge_request.all_pipelines
+
+ if @pipelines.present?
+ @pipeline = @pipelines.first
+ @statuses = @pipeline.statuses.relevant
+ end
+ end
+
+ def define_new_vars
+ @noteable = @merge_request
+
+ @target_branches = if @merge_request.target_project
+ @merge_request.target_project.repository.branch_names
+ else
+ []
+ end
+
+ @target_project = merge_request.target_project
+ @source_project = merge_request.source_project
+ @commits = @merge_request.compare_commits.reverse
+ @commit = @merge_request.diff_head_commit
+ @base_commit = @merge_request.diff_base_commit
+
+ @note_counts = Note.where(commit_id: @commits.map(&:id)).
+ group(:commit_id).count
+
+ @labels = LabelsFinder.new(current_user, project_id: @project.id).execute
+
+ define_pipelines_vars
+ 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
@@ -526,7 +614,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
def compared_diff_version
@@ -538,4 +626,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@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/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 42a7e5a2c30..d08f490de18 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -5,37 +5,30 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
+ @group_links = @project.project_group_links
+
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
@project_members = @project_members.where(user_id: users)
- end
-
- @project_members = @project_members.order('access_level DESC')
- @group = @project.group
-
- if @group
- @group_members = @group.group_members
- @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
-
- if params[:search].present?
- users = @group.users.search(params[:search]).to_a
- @group_members = @group_members.where(user_id: users)
- end
-
- @group_members = @group_members.order('access_level DESC')
+ @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- @requesters = @project.requesters if can?(current_user, :admin_project, @project)
+ @project_members = @project_members.order(access_level: :desc).page(params[:page])
+
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
- @project_group_links = @project.project_group_links
end
def create
+ if params[:user_ids].blank?
+ return redirect_to(namespace_project_project_members_path(@project.namespace, @project), alert: 'No users or groups specified.')
+ end
+
@project.team.add_users(
params[:user_ids].split(','),
params[:access_level],
@@ -43,7 +36,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
current_user: current_user
)
- redirect_to namespace_project_project_members_path(@project.namespace, @project)
+ redirect_to namespace_project_project_members_path(@project.namespace, @project), notice: 'Users were successfully added.'
end
def update
@@ -55,10 +48,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def destroy
- @project_member = @project.members.find_by(id: params[:id]) ||
- @project.requesters.find_by(id: params[:id])
-
- Members::DestroyService.new(@project_member, current_user).execute
+ Members::DestroyService.new(@project, current_user, params).
+ execute(:all)
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 6ea8ee62bc5..8fea20cefef 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController
def show
@tag = @repository.find_tag(params[:id])
+ return render_404 unless @tag
+
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
@commit = @repository.commit(@tag.target)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index eaa38fa6c98..76b730198d4 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,4 +1,5 @@
class ProjectsController < Projects::ApplicationController
+ include IssuableCollections
include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity, :refs]
@@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user
-
- if @project.repository_exists?
- if @project.empty_repo?
- render 'projects/empty'
- else
- render :show
- end
- else
- render 'projects/no_repo'
- end
+ render_landing_page
end
format.atom do
@@ -137,10 +129,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])
@@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController
private
+ # Render project landing depending of which features are available
+ # So if page is not availble in the list it renders the next page
+ #
+ # pages list order: repository readme, wiki home, issues list, customize workflow
+ def render_landing_page
+ if @project.feature_available?(:repository, current_user)
+ return render 'projects/no_repo' unless @project.repository_exists?
+ render 'projects/empty' if @project.empty_repo?
+ else
+ if @project.wiki_enabled?
+ @wiki_home = @project.wiki.find_page('home', params[:version_id])
+ elsif @project.feature_available?(:issues, current_user)
+ @issues = issues_collection
+ @issues = @issues.page(params[:page])
+ end
+
+ render :show
+ end
+ end
+
def determine_layout
if [:new, :create].include?(action_name.to_sym)
'application'
@@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController
project_feature_attributes:
[
:issues_access_level, :builds_access_level,
- :wiki_access_level, :merge_requests_access_level, :snippets_access_level
+ :wiki_access_level, :merge_requests_access_level,
+ :snippets_access_level, :repository_access_level
]
}
@@ -324,7 +337,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/snippets_controller.rb b/app/controllers/snippets_controller.rb
index d198782138a..dee57e4a388 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,10 +1,10 @@
class SnippetsController < ApplicationController
include ToggleAwardEmoji
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
# Allow read snippet
- before_action :authorize_read_snippet!, only: [:show, :raw]
+ before_action :authorize_read_snippet!, only: [:show, :raw, :download]
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
@@ -12,7 +12,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
- skip_before_action :authenticate_user!, only: [:index, :show, :raw]
+ skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
layout 'snippets'
respond_to :html
@@ -75,6 +75,14 @@ class SnippetsController < ApplicationController
)
end
+ def download
+ send_data(
+ @snippet.content,
+ type: 'text/plain; charset=utf-8',
+ filename: @snippet.sanitized_file_name
+ )
+ end
+
protected
def snippet
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 838ecc837e4..6a881b271d7 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,6 +1,6 @@
class UsersController < ApplicationController
skip_before_action :authenticate_user!
- before_action :user
+ before_action :user, except: [:exists]
before_action :authorize_read_user!, only: [:show]
def show
@@ -85,6 +85,10 @@ class UsersController < ApplicationController
render 'calendar_activities', layout: false
end
+ def exists
+ render json: { exists: Namespace.where(path: params[:username].downcase).any? }
+ end
+
private
def authorize_read_user!
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 8f9ef8f725c..e27986ef95b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -124,15 +124,12 @@ class IssuableFinder
def labels
return @labels if defined?(@labels)
- if labels? && !filter_by_no_label?
- @labels = Label.where(title: label_names)
-
- if projects
- @labels = @labels.where(project: projects)
+ @labels =
+ if labels? && !filter_by_no_label?
+ LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
+ else
+ Label.none
end
- else
- @labels = Label.none
- end
end
def assignee?
@@ -183,17 +180,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
@@ -279,8 +271,10 @@ class IssuableFinder
items = items.without_label
else
items = items.with_label(label_names, params[:sort])
+
if projects
- items = items.where(labels: { project_id: projects })
+ label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id)
+ items = items.where(labels: { id: label_ids })
end
end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
new file mode 100644
index 00000000000..6ace14a4bb5
--- /dev/null
+++ b/app/finders/labels_finder.rb
@@ -0,0 +1,92 @@
+class LabelsFinder < UnionFinder
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute(authorized_only: true)
+ @authorized_only = authorized_only
+
+ items = find_union(label_ids, Label)
+ items = with_title(items)
+ sort(items)
+ end
+
+ private
+
+ attr_reader :current_user, :params, :authorized_only
+
+ def label_ids
+ label_ids = []
+
+ if project
+ label_ids << project.group.labels if project.group.present?
+ label_ids << project.labels
+ else
+ label_ids << Label.where(group_id: projects.group_ids)
+ label_ids << Label.where(project_id: projects.select(:id))
+ end
+
+ label_ids
+ end
+
+ def sort(items)
+ items.reorder(title: :asc)
+ end
+
+ def with_title(items)
+ items = items.where(title: title) if title
+ items
+ end
+
+ def group_id
+ params[:group_id].presence
+ end
+
+ def project_id
+ params[:project_id].presence
+ end
+
+ def projects_ids
+ params[:project_ids].presence
+ end
+
+ def title
+ params[:title].presence || params[:name].presence
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ if project_id
+ @project = find_project
+ else
+ @project = nil
+ end
+
+ @project
+ end
+
+ def find_project
+ if authorized_only
+ available_projects.find_by(id: project_id)
+ else
+ Project.find_by(id: project_id)
+ end
+ end
+
+ def projects
+ return @projects if defined?(@projects)
+
+ @projects = authorized_only ? available_projects : Project.all
+ @projects = @projects.in_namespace(group_id) if group_id
+ @projects = @projects.where(id: projects_ids) if projects_ids
+ @projects = @projects.reorder(nil)
+
+ @projects
+ end
+
+ def available_projects
+ @available_projects ||= ProjectsFinder.new.execute(current_user)
+ end
+end
diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb
deleted file mode 100644
index 81a12403801..00000000000
--- a/app/finders/trending_projects_finder.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class TrendingProjectsFinder
- def execute(current_user, start_date = 1.month.ago)
- projects_for(current_user).trending(start_date)
- end
-
- private
-
- def projects_for(current_user)
- ProjectsFinder.new.execute(current_user)
- end
-end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index de13e7a1fc2..16136d02530 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -16,7 +16,7 @@ module AppearancesHelper
end
def brand_text
- markdown(brand_item.description)
+ markdown_field(brand_item, :description)
end
def brand_item
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 1df430e6279..ebd78bf9888 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 6de25bea654..6229384817b 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled?
end
- def extra_sign_in_text
- current_application_settings.sign_in_text
- end
-
- def after_sign_up_text
- current_application_settings.after_sign_up_text
- end
-
- def shared_runners_text
- current_application_settings.shared_runners_text
- end
-
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index df41473543b..b7e0ff8ecd0 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -4,15 +4,18 @@ module AvatarsHelper
user: commit_or_event.author,
user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email,
+ css_class: 'hidden-xs'
}))
end
def user_avatar(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
+ css_class = options[:css_class] || ''
+
avatar = image_tag(
avatar_icon(options[:user] || options[:user_email], avatar_size),
- class: "avatar has-tooltip hidden-xs s#{avatar_size}",
+ class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar",
title: user_name,
data: { container: 'body' }
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
index aa134cea31c..167b09e678f 100644
--- a/app/helpers/award_emoji_helper.rb
+++ b/app/helpers/award_emoji_helper.rb
@@ -1,9 +1,12 @@
module AwardEmojiHelper
def toggle_award_url(awardable)
- if @project
- url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
+ return url_for([:toggle_award_emoji, awardable]) unless @project
+
+ if awardable.is_a?(Note)
+ # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
+ toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
else
- url_for([:toggle_award_emoji, awardable])
+ url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end
end
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
new file mode 100644
index 00000000000..b7247ffa8b2
--- /dev/null
+++ b/app/helpers/boards_helper.rb
@@ -0,0 +1,12 @@
+module BoardsHelper
+ def board_data
+ board = @board || @boards.first
+
+ {
+ endpoint: namespace_project_boards_path(@project.namespace, @project),
+ board_id: board.id,
+ disabled: !can?(current_user, :admin_list, @project),
+ issue_link_base: namespace_project_issues_path(@project.namespace, @project)
+ }
+ end
+end
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 43a29c96bca..eb03ced67eb 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
- icon('bullhorn') << ' ' << render_broadcast_message(message.message)
+ icon('bullhorn') << ' ' << render_broadcast_message(message)
end
end
@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end
end
- def render_broadcast_message(message)
- Banzai.render(message, pipeline: :broadcast_message).html_safe
+ def render_broadcast_message(broadcast_message)
+ Banzai.render_field(broadcast_message, :message).html_safe
end
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
new file mode 100644
index 00000000000..f3aaff9140d
--- /dev/null
+++ b/app/helpers/builds_helper.rb
@@ -0,0 +1,8 @@
+module BuildsHelper
+ def sidebar_build_class(build, current_build)
+ build_class = ''
+ build_class += ' active' if build == current_build
+ build_class += ' retried' if build.retried?
+ build_class
+ end
+end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index b478580978b..85e1dc33ee8 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -15,13 +15,14 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
+ css_class = data[:class] || 'btn-clipboard btn-transparent'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
- class: "btn btn-clipboard",
+ class: "btn #{css_class}",
data: data,
type: :button,
- title: "Copy to Clipboard"
+ title: 'Copy to Clipboard'
end
def http_clone_button(project, placement = 'right', append_link: true)
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 4566f3782cc..81e0b6bb5ae 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -40,8 +40,9 @@ module DropdownsHelper
end
def dropdown_toggle(toggle_text, data_attr, options = {})
+ default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
- output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
+ output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down')
output.html_safe
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index bfedcb1c42b..f8ded05c31a 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -154,7 +154,7 @@ module EventsHelper
end
def event_commit_title(message)
- escape_once(truncate(message.split("\n").first, length: 70))
+ (message.split("\n").first || "").truncate(70)
rescue
"--broken encoding"
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 1a259656f31..0772d848289 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
- escaped_body = if body.start_with?('<img')
- body
- else
- escape_once(body)
- end
-
- user = current_user if defined?(current_user)
- gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
+ context = {
+ project: @project,
+ current_user: (current_user if defined?(current_user)),
+ pipeline: :single_line,
+ }
+ gfm_body = Banzai.render(body, context)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project
html = Banzai.render(text, context)
+ banzai_postprocess(html, context)
+ end
- context.merge!(
- current_user: (current_user if defined?(current_user)),
-
- # RelativeLinkFilter
- requested_path: @path,
- project_wiki: @project_wiki,
- ref: @ref
- )
+ def markdown_field(object, field)
+ object = object.for_display if object.respond_to?(:for_display)
+ return "" unless object.present?
- Banzai.post_process(html, context)
+ html = Banzai.render_field(object, field)
+ banzai_postprocess(html, object.banzai_render_context(field))
end
def asciidoc(text)
@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon])
end
end
+
+ # Calls Banzai.post_process with some common context options
+ def banzai_postprocess(html, context)
+ context.merge!(
+ current_user: (current_user if defined?(current_user)),
+
+ # RelativeLinkFilter
+ requested_path: @path,
+ project_wiki: @project_wiki,
+ ref: @ref
+ )
+
+ Banzai.post_process(html, context)
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 670a7ca36f4..bccf64d1aac 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -94,6 +94,22 @@ module GitlabRoutingHelper
namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
end
+ def pipeline_url(pipeline, *args)
+ namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
+ end
+
+ def pipeline_build_url(pipeline, build, *args)
+ namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args)
+ end
+
+ def commits_url(entity, *args)
+ namespace_project_commits_url(entity.project.namespace, entity.project, entity.ref, *args)
+ end
+
+ def commit_url(entity, *args)
+ namespace_project_commit_url(entity.project.namespace, entity.project, entity.sha, *args)
+ end
+
def project_snippet_url(entity, *args)
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 5c04bba323f..03b2db1bc91 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -8,18 +8,12 @@ module IssuablesHelper
end
def multi_label_name(current_labels, default_label)
- # current_labels may be a string from before
- if current_labels.is_a?(Array)
- if current_labels.count > 1
- "#{current_labels[0]} +#{current_labels.count - 1} more"
+ if current_labels && current_labels.any?
+ title = current_labels.first.try(:title)
+ if current_labels.size > 1
+ "#{title} +#{current_labels.size - 1} more"
else
- current_labels[0]
- end
- elsif current_labels.is_a?(String)
- if current_labels.nil? || current_labels.empty?
- default_label
- else
- current_labels
+ title
end
else
default_label
@@ -94,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?
@@ -111,4 +123,26 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
+
+ def issuable_filters_present
+ params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name]
+ 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/issues_helper.rb b/app/helpers/issues_helper.rb
index 8b212b0327a..1644c346dd8 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -113,14 +113,13 @@ module IssuesHelper
end
end
- def award_user_list(awards, current_user)
+ def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name
end
- # Take first 9 OR current user + first 9
current_user_name = names.delete('You')
- names = names.first(9).insert(0, current_user_name).compact
+ names = names.insert(0, current_user_name).compact.first(limit)
names << "#{awards.size - names.size} more." if awards.size > names.size
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 5e9f5837101..221a84b042f 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -4,9 +4,8 @@ module LabelsHelper
# Link to a Label
#
# label - Label object to link to
- # project - Project object which will be used as the context for the label's
- # link. If omitted, defaults to `@project`, or the label's own
- # project.
+ # subject - Project/Group object which will be used as the context for the
+ # label's link. If omitted, defaults to the label's own group/project.
# type - The type of item the link will point to (:issue or
# :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the
@@ -15,15 +14,14 @@ module LabelsHelper
#
# Examples:
#
- # # Allow the generated link to use the label's own project
+ # # Allow the generated link to use the label's own subject
# link_to_label(label)
#
- # # Force the generated link to use @project
- # @project = Project.first
- # link_to_label(label)
+ # # Force the generated link to use a provided group
+ # link_to_label(label, subject: Group.last)
#
# # Force the generated link to use a provided project
- # link_to_label(label, project: Project.last)
+ # link_to_label(label, subject: Project.last)
#
# # Force the generated link to point to merge requests instead of issues
# link_to_label(label, type: :merge_request)
@@ -32,9 +30,8 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block)
- project ||= @project || label.project
- link = label_filter_path(project, label, type: type)
+ def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
+ link = label_filter_path(subject || label.subject, label, type: type)
if block_given?
link_to link, class: css_class, &block
@@ -43,15 +40,40 @@ module LabelsHelper
end
end
- def label_filter_path(project, label, type: issue)
- send("namespace_project_#{type.to_s.pluralize}_path",
- project.namespace,
- project,
- label_name: [label.name])
+ def label_filter_path(subject, label, type: :issue)
+ case subject
+ when Group
+ send("#{type.to_s.pluralize}_group_path",
+ subject,
+ label_name: [label.name])
+ when Project
+ send("namespace_project_#{type.to_s.pluralize}_path",
+ subject.namespace,
+ subject,
+ label_name: [label.name])
+ end
+ end
+
+ def edit_label_path(label)
+ case label
+ when GroupLabel then edit_group_label_path(label.group, label)
+ when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label)
+ end
+ end
+
+ def destroy_label_path(label)
+ case label
+ when GroupLabel then group_label_path(label.group, label)
+ when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label)
+ end
end
- def project_label_names
- @project.labels.pluck(:title)
+ def toggle_subscription_data(label)
+ return unless label.is_a?(ProjectLabel)
+
+ {
+ url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
+ }
end
def render_colored_label(label, label_suffix = '', tooltip: true)
@@ -68,8 +90,8 @@ module LabelsHelper
span.html_safe
end
- def render_colored_cross_project_label(label, tooltip: true)
- label_suffix = label.project.name_with_namespace
+ def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
+ label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
render_colored_label(label, label_suffix, tooltip: tooltip)
end
@@ -115,19 +137,36 @@ module LabelsHelper
end
def labels_filter_path
- if @project
- namespace_project_labels_path(@project.namespace, @project, :json)
+ return group_labels_path(@group, :json) if @group
+
+ project = @target_project || @project
+
+ if project
+ namespace_project_labels_path(project.namespace, project, :json)
else
dashboard_labels_path(:json)
end
end
def label_subscription_status(label)
- label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ case label
+ when GroupLabel then 'Subscribing to group labels is currently not supported.'
+ when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ end
end
def label_subscription_toggle_button_text(label)
- label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ case label
+ when GroupLabel then 'Subscribing to group labels is currently not supported.'
+ when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ end
+ end
+
+ def label_deletion_confirm_text(label)
+ case label
+ when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
+ when ProjectLabel then 'Remove this label? Are you sure?'
+ end
end
# Required for Banzai::Filter::LabelReferenceFilter
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 8abe7865fed..a6659ea2fd1 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -72,12 +72,29 @@ module MergeRequestsHelper
)
end
+ def mr_assign_issues_link
+ issues = MergeRequests::AssignIssuesService.new(@project,
+ current_user,
+ merge_request: @merge_request,
+ closes_issues: mr_closes_issues
+ ).assignable_issues
+ path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ if issues.present?
+ pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
+ link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
+ end
+ end
+
def source_branch_with_namespace(merge_request)
- branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+ namespace = merge_request.source_project_namespace
+ branch = merge_request.source_branch
+
+ if merge_request.source_branch_exists?
+ namespace = link_to(namespace, project_path(merge_request.source_project))
+ branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+ end
if merge_request.for_fork?
- namespace = link_to(merge_request.source_project_namespace,
- project_path(merge_request.source_project))
namespace + ":" + branch
else
branch
@@ -110,4 +127,8 @@ module MergeRequestsHelper
def version_index(merge_request_diff)
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
+
+ def different_base?(version1, version2)
+ version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
+ end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index a11c313a6b8..83a2a4ad3ec 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -71,8 +71,9 @@ module MilestonesHelper
end
def milestones_filter_dropdown_path
- if @project
- namespace_project_milestones_path(@project.namespace, @project, :json)
+ project = @target_project || @project
+ if project
+ namespace_project_milestones_path(project.namespace, project, :json)
else
dashboard_milestones_path(:json)
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 22387d66451..7d4d049101a 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -92,12 +92,8 @@ module PageLayoutHelper
end
end
- def fluid_layout(enabled = false)
- if @fluid_layout.nil?
- @fluid_layout = (current_user && current_user.layout == "fluid") || enabled
- else
- @fluid_layout
- end
+ def fluid_layout
+ current_user && current_user.layout == "fluid"
end
def blank_container(enabled = false)
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index c3832cf5d65..a46f2c6e17d 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -50,6 +50,20 @@ module PreferencesHelper
end
def default_project_view
- current_user ? current_user.project_view : 'readme'
+ return 'readme' unless current_user
+
+ user_view = current_user.project_view
+
+ if @project.feature_available?(:repository, current_user)
+ user_view
+ elsif user_view == "activity"
+ "activity"
+ elsif @project.wiki_enabled?
+ "wiki"
+ elsif @project.feature_available?(:issues, current_user)
+ "projects/issues/issues"
+ else
+ "customize_workflow"
+ end
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 56477733ea2..d26b4018be6 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -134,16 +134,35 @@ module ProjectsHelper
options = project_feature_options
if @project.private?
+ level = @project.project_feature.send(field)
options.delete('Everyone with access')
- highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED
+ highest_available_option = options.values.max if level == 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
+
+ content_tag(
+ :select,
+ options,
+ name: "project[project_feature_attributes][#{field}]",
+ id: "project_project_feature_attributes_#{field}",
+ class: "pull-right form-control #{repo_children_classes(field)}",
+ data: { field: field }
+ ).html_safe
end
private
+ def repo_children_classes(field)
+ needs_repo_check = [:merge_requests_access_level, :builds_access_level]
+ return unless needs_repo_check.include?(field)
+
+ classes = "project-repo-select js-repo-select"
+ classes << " disabled" unless @project.feature_available?(:repository, current_user)
+
+ classes
+ end
+
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
@@ -155,12 +174,8 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if can?(current_user, :read_pipeline, project)
- nav_tabs << :pipelines
- end
-
if can?(current_user, :read_build, project)
- nav_tabs << :builds
+ nav_tabs << :pipelines
end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
@@ -435,4 +450,8 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED
}
end
+
+ def project_child_container_class(view_path)
+ view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 8a7446b7cc7..aba3a3f9c5d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -153,8 +153,18 @@ module SearchHelper
search_path(options)
end
- # Sanitize html generated after parsing markdown from issue description or comment
- def search_md_sanitize(html)
+ # Sanitize a HTML field for search display. Most tags are stripped out and the
+ # maximum length is set to 200 characters.
+ def search_md_sanitize(object, field)
+ html = markdown_field(object, field)
+ html = Truncato.truncate(
+ html,
+ count_tags: false,
+ count_tail: false,
+ max_length: 200
+ )
+
+ # Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code))
end
end
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 5f27e33c6ad..8706876ae4a 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -49,12 +49,10 @@ module SelectsHelper
end
def select2_tag(id, opts = {})
- css_class = ''
- css_class << 'multiselect ' if opts[:multiple]
- css_class << (opts[:class] || '')
+ opts[:class] << ' multiselect' if opts[:multiple]
value = opts[:selected] || ''
- hidden_field_tag(id, value, class: css_class)
+ hidden_field_tag(id, value, opts)
end
private
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 1e86f648203..a9db8bb2b82 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type
end
+ def todo_due_date(todo)
+ return unless todo.target.try(:due_date)
+
+ is_due_today = todo.target.due_date.today?
+ is_overdue = todo.target.overdue?
+ css_class =
+ if is_due_today
+ 'text-warning'
+ elsif is_overdue
+ 'text-danger'
+ else
+ ''
+ end
+
+ html = "&middot; ".html_safe
+ html << content_tag(:span, class: css_class) do
+ "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
+ end
+ end
+
private
def show_todo_state?(todo)
diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/mailers/.gitkeep
+++ /dev/null
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index 415f6e12885..f7ed61625f4 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer
default reply_to: Gitlab.config.gitlab.email_reply_to
layout 'devise_mailer'
+
+ protected
+
+ def subject_for(key)
+ subject = super
+ subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
+ subject
+ end
end
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 45311690293..7b617b359ea 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -45,7 +45,7 @@ module Emails
@token = token
mail(to: member.invite_email,
- subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
+ subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_invite_accepted_email(member_source_type, member_id)
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
new file mode 100644
index 00000000000..601c8b5cd62
--- /dev/null
+++ b/app/mailers/emails/pipelines.rb
@@ -0,0 +1,43 @@
+module Emails
+ module Pipelines
+ def pipeline_success_email(pipeline, to)
+ pipeline_mail(pipeline, to, 'succeeded')
+ end
+
+ def pipeline_failed_email(pipeline, to)
+ pipeline_mail(pipeline, to, 'failed')
+ end
+
+ private
+
+ def pipeline_mail(pipeline, to, status)
+ @project = pipeline.project
+ @pipeline = pipeline
+ @merge_request = pipeline.merge_requests.first
+ add_headers
+
+ mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
+ format.html { render layout: false }
+ format.text
+ end
+ end
+
+ def add_headers
+ add_project_headers
+ add_pipeline_headers
+ end
+
+ def add_pipeline_headers
+ headers['X-GitLab-Pipeline-Id'] = @pipeline.id
+ headers['X-GitLab-Pipeline-Ref'] = @pipeline.ref
+ headers['X-GitLab-Pipeline-Status'] = @pipeline.status
+ end
+
+ def pipeline_subject(status)
+ commit = @pipeline.short_sha
+ commit << " in #{@merge_request.to_reference}" if @merge_request
+
+ subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit)
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 29f1c527776..eca6ec29767 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::Projects
include Emails::Profile
include Emails::Builds
+ include Emails::Pipelines
include Emails::Members
add_template_helper MergeRequestsHelper
@@ -92,6 +93,7 @@ class Notify < BaseMailer
subject = ""
subject << "#{@project.name} | " if @project
subject << extra.join(' | ') if extra.present?
+ subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index b01a244032d..2340453831e 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -1,4 +1,8 @@
class AbuseReport < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :message, pipeline: :single_line
+
belongs_to :reporter, class_name: 'User'
belongs_to :user
@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
+ # For CacheMarkdownField
+ alias_method :author, :reporter
+
def remove_user(deleted_by:)
user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 4cf8dd9a8ce..e4106e1c2e9 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,4 +1,8 @@
class Appearance < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :description
+
validates :title, presence: true
validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte }
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 55d2e07de08..c99aa7772bb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -1,5 +1,7 @@
class ApplicationSetting < ActiveRecord::Base
+ include CacheMarkdownField
include TokenAuthenticatable
+
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
+ cache_markdown_field :sign_in_text
+ cache_markdown_field :help_page_text
+ cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
+ cache_markdown_field :after_sign_up_text
+
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay,
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 61498140f27..cb40f33932a 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -1,6 +1,9 @@
class BroadcastMessage < ActiveRecord::Base
+ include CacheMarkdownField
include Sortable
+ cache_markdown_field :message, pipeline: :broadcast_message
+
validates :message, presence: true
validates :starts_at, presence: true
validates :ends_at, presence: true
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 522e2264bb8..a6b606d13de 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,6 +1,7 @@
module Ci
class Build < CommitStatus
include TokenAuthenticatable
+ include AfterCommitQueue
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
@@ -75,25 +76,20 @@ module Ci
state_machine :status do
after_transition pending: :running do |build|
- build.execute_hooks
+ build.run_after_commit do
+ BuildHooksWorker.perform_async(id)
+ end
end
after_transition any => [:success, :failed, :canceled] do |build|
- build.update_coverage
- build.execute_hooks
+ build.run_after_commit do
+ BuildFinishedWorker.perform_async(id)
+ end
end
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,
- options: build.options.to_h[:environment],
- variables: build.variables)
- service.execute(build)
+ build.run_after_commit do
+ BuildSuccessWorker.perform_async(id)
end
end
end
@@ -137,13 +133,17 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx)
end
- def trace_html
- trace_with_state[:html] || ''
+ def trace_html(**args)
+ trace_with_state(**args)[:html] || ''
end
- def trace_with_state(state = nil)
- trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present?
- trace_with_state || {}
+ def trace_with_state(state: nil, last_lines: nil)
+ trace_ansi = trace(last_lines: last_lines)
+ if trace_ansi.present?
+ Ci::Ansi2html.convert(trace_ansi, state)
+ else
+ {}
+ end
end
def timeout
@@ -226,9 +226,10 @@ module Ci
raw_trace.present?
end
- def raw_trace
+ def raw_trace(last_lines: nil)
if File.exist?(trace_file_path)
- File.read(trace_file_path)
+ Gitlab::Ci::TraceReader.new(trace_file_path).
+ read(last_lines: last_lines)
else
# backward compatibility
read_attribute :trace
@@ -243,8 +244,8 @@ module Ci
project.ci_id && File.exist?(old_path_to_trace)
end
- def trace
- hide_secrets(raw_trace)
+ def trace(last_lines: nil)
+ hide_secrets(raw_trace(last_lines: last_lines))
end
def trace_length
@@ -373,7 +374,7 @@ module Ci
end
def artifacts?
- !artifacts_expired? && self[:artifacts_file].present?
+ !artifacts_expired? && artifacts_file.exists?
end
def artifacts_metadata?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 663c5b1e231..d5c1e03b461 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -3,6 +3,7 @@ module Ci
extend Ci::Model
include HasStatus
include Importable
+ include AfterCommitQueue
self.table_name = 'ci_commits'
@@ -18,7 +19,7 @@ module Ci
validates_presence_of :status, unless: :importing?
validate :valid_commit_sha, unless: :importing?
- after_save :keep_around_commits, unless: :importing?
+ after_create :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses
@@ -48,30 +49,37 @@ module Ci
transition any => :canceled
end
+ # IMPORTANT
+ # Do not add any operations to this state_machine
+ # Create a separate worker for each new operation
+
before_transition [:created, :pending] => :running do |pipeline|
pipeline.started_at = Time.now
end
before_transition any => [:success, :failed, :canceled] do |pipeline|
pipeline.finished_at = Time.now
+ pipeline.update_duration
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)
+ pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
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)
+ pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
end
- before_transition do |pipeline|
- pipeline.update_duration
+ after_transition [:created, :pending, :running] => :success do |pipeline|
+ pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
end
after_transition do |pipeline, transition|
- pipeline.execute_hooks unless transition.loopback?
+ next if transition.loopback?
+
+ pipeline.run_after_commit do
+ PipelineHooksWorker.perform_async(id)
+ end
end
end
@@ -143,7 +151,7 @@ module Ci
def retryable?
builds.latest.any? do |build|
- build.failed? && build.retryable?
+ (build.failed? || build.canceled?) && build.retryable?
end
end
@@ -196,7 +204,7 @@ module Ci
end
def has_warnings?
- builds.latest.ignored.any?
+ builds.latest.failed_but_allowed.any?
end
def config_processor
@@ -251,9 +259,8 @@ module Ci
Ci::ProcessPipelineService.new(project, user).execute(self)
end
- def build_updated
+ def update_status
with_lock do
- reload
case latest_builds_status
when 'pending' then enqueue
when 'running' then run
@@ -293,11 +300,9 @@ module Ci
# 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
+ @merge_requests ||= project.merge_requests
+ .where(source_branch: self.ref)
+ .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
end
private
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ed5d4b13b7e..44cb19ece3b 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 = 2.hours.ago
+ LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 656a242c265..ac2477fd973 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -80,7 +80,7 @@ class CommitRange
end
def inspect
- %(#<#{self.class}:#{object_id} #{to_s}>)
+ %(#<#{self.class}:#{object_id} #{self}>)
end
def to_s
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 736db1ab0f6..7b554be4f9a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,6 +1,7 @@
class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
+ include AfterCommitQueue
self.table_name = 'ci_builds'
@@ -24,7 +25,22 @@ class CommitStatus < ActiveRecord::Base
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
- scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
+
+ scope :failed_but_allowed, -> do
+ where(allow_failure: true, status: [:failed, :canceled])
+ end
+
+ scope :exclude_ignored, -> do
+ quoted_when = connection.quote_column_name('when')
+ # We want to ignore failed_but_allowed jobs
+ where("allow_failure = ? OR status IN (?)",
+ false, all_state_names - [:failed, :canceled]).
+ # We want to ignore skipped manual jobs
+ where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
+ # We want to ignore skipped on_failure
+ where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
+ end
+
scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
@@ -69,21 +85,25 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- after_transition any => [:success, :failed, :canceled] do |commit_status|
- commit_status.pipeline.try(:process!)
- true
- end
-
after_transition do |commit_status, transition|
- commit_status.pipeline.try(:build_updated) unless transition.loopback?
- end
-
- after_transition [:created, :pending, :running] => :success do |commit_status|
- MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
+ next if transition.loopback?
+
+ commit_status.run_after_commit do
+ pipeline.try do |pipeline|
+ if complete?
+ PipelineProcessWorker.perform_async(pipeline.id)
+ else
+ PipelineUpdateWorker.perform_async(pipeline.id)
+ end
+ end
+ end
end
after_transition any => :failed do |commit_status|
- MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
+ commit_status.run_after_commit do
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(pipeline.project, nil).execute(self)
+ end
end
end
@@ -111,7 +131,7 @@ class CommitStatus < ActiveRecord::Base
end
end
- def ignored?
+ def failed_but_allowed?
allow_failure? && (failed? || canceled?)
end
diff --git a/app/models/compare.rb b/app/models/compare.rb
index 4856510f526..3a8bbcb1acd 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -11,9 +11,10 @@ class Compare
end
end
- def initialize(compare, project)
+ def initialize(compare, project, straight: false)
@compare = compare
@project = project
+ @straight = straight
end
def commits
@@ -45,6 +46,18 @@ class Compare
end
end
+ def start_commit_sha
+ start_commit.try(:sha)
+ end
+
+ def base_commit_sha
+ base_commit.try(:sha)
+ end
+
+ def head_commit_sha
+ commit.try(:sha)
+ end
+
def raw_diffs(*args)
@compare.diffs(*args)
end
@@ -58,9 +71,9 @@ class Compare
def diff_refs
Gitlab::Diff::DiffRefs.new(
- base_sha: base_commit.try(:sha),
- start_sha: start_commit.try(:sha),
- head_sha: commit.try(:sha)
+ base_sha: @straight ? start_commit_sha : base_commit_sha,
+ start_sha: start_commit_sha,
+ head_sha: head_commit_sha
)
end
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/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
new file mode 100644
index 00000000000..90bd6490a02
--- /dev/null
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -0,0 +1,131 @@
+# This module takes care of updating cache columns for Markdown-containing
+# fields. Use like this in the body of your class:
+#
+# include CacheMarkdownField
+# cache_markdown_field :foo
+# cache_markdown_field :bar
+# cache_markdown_field :baz, pipeline: :single_line
+#
+# Corresponding foo_html, bar_html and baz_html fields should exist.
+module CacheMarkdownField
+ # Knows about the relationship between markdown and html field names, and
+ # stores the rendering contexts for the latter
+ class FieldData
+ extend Forwardable
+
+ def initialize
+ @data = {}
+ end
+
+ def_delegators :@data, :[], :[]=
+ def_delegator :@data, :keys, :markdown_fields
+
+ def html_field(markdown_field)
+ "#{markdown_field}_html"
+ end
+
+ def html_fields
+ markdown_fields.map {|field| html_field(field) }
+ end
+ end
+
+ # Dynamic registries don't really work in Rails as it's not guaranteed that
+ # every class will be loaded, so hardcode the list.
+ CACHING_CLASSES = %w[
+ AbuseReport
+ Appearance
+ ApplicationSetting
+ BroadcastMessage
+ Issue
+ Label
+ MergeRequest
+ Milestone
+ Namespace
+ Note
+ Project
+ Release
+ Snippet
+ ]
+
+ def self.caching_classes
+ CACHING_CLASSES.map(&:constantize)
+ end
+
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_reader :cached_markdown_fields do
+ FieldData.new
+ end
+
+ # Returns the default Banzai render context for the cached markdown field.
+ def banzai_render_context(field)
+ raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+ cached_markdown_fields.markdown_fields.include?(field)
+
+ # Always include a project key, or Banzai complains
+ project = self.project if self.respond_to?(:project)
+ context = cached_markdown_fields[field].merge(project: project)
+
+ # Banzai is less strict about authors, so don't always have an author key
+ context[:author] = self.author if self.respond_to?(:author)
+
+ context
+ end
+
+ # Allow callers to look up the cache field name, rather than hardcoding it
+ def markdown_cache_field_for(field)
+ raise ArgumentError.new("Unknown field: #{field}") unless
+ cached_markdown_fields.markdown_fields.include?(field)
+
+ cached_markdown_fields.html_field(field)
+ end
+
+ # Always exclude _html fields from attributes (including serialization).
+ # They contain unredacted HTML, which would be a security issue
+ alias_method :attributes_before_markdown_cache, :attributes
+ def attributes
+ attrs = attributes_before_markdown_cache
+
+ cached_markdown_fields.html_fields.each do |field|
+ attrs.delete(field)
+ end
+
+ attrs
+ end
+ end
+
+ class_methods do
+ private
+
+ # Specify that a field is markdown. Its rendered output will be cached in
+ # a corresponding _html field. Any custom rendering options may be provided
+ # as a context.
+ def cache_markdown_field(markdown_field, context = {})
+ raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
+ CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
+
+ cached_markdown_fields[markdown_field] = context
+
+ html_field = cached_markdown_fields.html_field(markdown_field)
+ cache_method = "#{markdown_field}_cache_refresh".to_sym
+ invalidation_method = "#{html_field}_invalidated?".to_sym
+
+ define_method(cache_method) do
+ html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
+ __send__("#{html_field}=", html)
+ true
+ end
+
+ # The HTML becomes invalid if any dependent fields change. For now, assume
+ # author and project invalidate the cache in all circumstances.
+ define_method(invalidation_method) do
+ changed_fields = changed_attributes.keys
+ invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
+ !invalidations.empty?
+ end
+
+ before_save cache_method, if: invalidation_method
+ end
+ end
+end
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
index be93435453b..b66ba08dc59 100644
--- a/app/models/concerns/expirable.rb
+++ b/app/models/concerns/expirable.rb
@@ -5,11 +5,15 @@ module Expirable
scope :expired, -> { where('expires_at <= ?', Time.current) }
end
+ def expired?
+ expires? && expires_at <= Time.current
+ end
+
def expires?
expires_at.present?
end
def expires_soon?
- expires_at < 7.days.from_now
+ expires? && expires_at < 7.days.from_now
end
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 0fa4df0fb56..ef3e73a4072 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -5,35 +5,36 @@ module HasStatus
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
COMPLETED_STATUSES = %w[success failed canceled]
+ ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do
def status_sql
- scope = all
+ scope = if respond_to?(:exclude_ignored)
+ exclude_ignored
+ else
+ all
+ end
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'
pending = scope.pending.select('count(*)').to_sql
running = scope.running.select('count(*)').to_sql
- canceled = scope.canceled.select('count(*)').to_sql
skipped = scope.skipped.select('count(*)').to_sql
+ canceled = scope.canceled.select('count(*)').to_sql
- deduce_status = "(CASE
+ "(CASE
+ WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
- WHEN (#{builds})=(#{skipped}) THEN 'skipped'
- WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
- WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
- WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
+ WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
+ WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
+ WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed'
END)"
-
- deduce_status
end
def status
- all.pluck(self.status_sql).first
+ all.pluck(status_sql).first
end
def started_at
@@ -43,6 +44,10 @@ module HasStatus
def finished_at
all.maximum(:finished_at)
end
+
+ def all_state_names
+ state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
+ end
end
included do
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index ff465d2c745..17c3b526c97 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
+ include CacheMarkdownField
include Participable
include Mentionable
include Subscribable
@@ -13,6 +14,9 @@ module Issuable
include Awardable
included do
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
@@ -141,8 +145,14 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
- condition_field = "#{table_name}.id"
- highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
+ params = {
+ target_type: name,
+ target_column: "#{table_name}.id",
+ project_column: "#{table_name}.#{project_foreign_key}",
+ excluded_labels: excluded_labels
+ }
+
+ highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
@@ -226,18 +236,6 @@ module Issuable
labels.order('title ASC').pluck(:title)
end
- def remove_labels
- labels.delete_all
- end
-
- def add_labels_by_names(label_names)
- label_names.each do |label_name|
- label = project.labels.create_with(color: Label::DEFAULT_COLOR).
- find_or_create_by(title: label_name.strip)
- self.labels << label
- end
- end
-
# Convert this Issuable class name to a format usable by Ability definitions
#
# Examples:
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index ec9e0f1b1d0..eb2ff0428f6 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -43,19 +43,15 @@ module Mentionable
self
end
- def all_references(current_user = nil, text = nil, extractor: nil)
+ def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user)
- if text
- extractor.analyze(text, author: author)
- else
- self.class.mentionable_attrs.each do |attr, options|
- text = __send__(attr)
- options = options.merge(cache_key: [self, attr], author: author)
+ self.class.mentionable_attrs.each do |attr, options|
+ text = __send__(attr)
+ options = options.merge(cache_key: [self, attr], author: author)
- extractor.analyze(text, options)
- end
+ extractor.analyze(text, options)
end
extractor
@@ -66,8 +62,8 @@ module Mentionable
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
- def referenced_mentionables(current_user = self.author, text = nil)
- refs = all_references(current_user, text)
+ def referenced_mentionables(current_user = self.author)
+ refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires
@@ -77,8 +73,8 @@ module Mentionable
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
- def create_cross_references!(author = self.author, without = [], text = nil)
- refs = referenced_mentionables(author, text)
+ def create_cross_references!(author = self.author, without = [])
+ refs = referenced_mentionables(author)
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
@@ -97,10 +93,7 @@ module Mentionable
return if changes.empty?
- original_text = changes.collect { |_, vals| vals.first }.join(' ')
-
- preexisting = referenced_mentionables(author, original_text)
- create_cross_references!(author, preexisting)
+ create_cross_references!(author)
end
private
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 5a7b36070e7..7fd0905ee81 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -1,6 +1,11 @@
module ProtectedBranchAccess
extend ActiveSupport::Concern
+ included do
+ scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
+ scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
+ end
+
def humanize
self.class.human_access_levels[self.access_level]
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 1ebecd86af9..12b23f00769 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -38,11 +38,13 @@ module Sortable
private
- def highest_label_priority(object_types, condition_field, excluded_labels: [])
- query = Label.select(Label.arel_table[:priority].minimum).
+ def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: [])
+ query = Label.select(LabelPriority.arel_table[:priority].minimum).
+ left_join_priorities.
joins(:label_links).
- where(label_links: { target_type: object_types }).
- where("label_links.target_id = #{condition_field}").
+ where("label_priorities.project_id = #{project_column}").
+ where(label_links: { target_type: target_type }).
+ where("label_links.target_id = #{target_column}").
reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present?
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index be295487fd2..8ed4a56b19b 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -2,6 +2,8 @@ class CycleAnalytics
include Gitlab::Database::Median
include Gitlab::Database::DateTime
+ DEPLOYMENT_METRIC_STAGES = %i[production staging]
+
def initialize(project, from:)
@project = project
@from = from
@@ -66,7 +68,7 @@ class CycleAnalytics
# cycle analytics stage.
interval_query = Arel::Nodes::As.new(
cte_table,
- subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s))
+ subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s))
median_datetime(cte_table, interval_query, name)
end
@@ -75,7 +77,7 @@ class CycleAnalytics
# 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
+ def base_query_for(name)
arel_table = MergeRequestsClosingIssues.arel_table
# Load issues
@@ -91,7 +93,11 @@ class CycleAnalytics
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))
+ if DEPLOYMENT_METRIC_STAGES.include?(name)
+ # 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
+
+ query
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 07d7e19e70d..91d85c2279b 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
- after_save :keep_around_commit
+ after_create :create_ref
def commit
project.commit(sha)
@@ -29,18 +29,25 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment
end
- def keep_around_commit
- project.repository.keep_around(self.sha)
+ def create_ref
+ project.repository.create_ref(ref, ref_path)
end
def manual_actions
- deployable.try(:other_actions)
+ @manual_actions ||= deployable.try(:other_actions)
end
def includes_commit?(commit)
return false unless commit
- project.repository.is_ancestor?(commit.id, sha)
+ # Before 8.10, deployments didn't have keep-around refs. Any deployment
+ # created before then could have a `sha` referring to a commit that no
+ # longer exists in the repository, so just ignore those.
+ begin
+ project.repository.is_ancestor?(commit.id, sha)
+ rescue Rugged::OdbError
+ false
+ end
end
def update_merge_request_metrics!
@@ -76,4 +83,25 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id).
take
end
+
+ def stop_action
+ return nil unless on_stop.present?
+ return nil unless manual_actions
+
+ @stop_action ||= manual_actions.find_by(name: on_stop)
+ end
+
+ def stoppable?
+ stop_action.present?
+ end
+
+ def formatted_deployment_time
+ created_at.to_time.in_time_zone.to_s(:medium)
+ end
+
+ private
+
+ def ref_path
+ File.join(environment.ref_path, 'deployments', iid.to_s)
+ end
end
diff --git a/app/models/email.rb b/app/models/email.rb
index 32a412ab878..826d4f16edb 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -7,10 +7,8 @@ class Email < ActiveRecord::Base
validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? }
- before_validation :cleanup_email
-
- def cleanup_email
- self.email = self.email.downcase.strip
+ def email=(value)
+ write_attribute(:email, value.downcase.strip)
end
def unique_email
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 49e0a20640c..73f415c0ef0 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base
allow_nil: true,
addressable_url: true
+ delegate :stop_action, to: :last_deployment, allow_nil: true
+
+ scope :available, -> { with_state(:available) }
+ scope :stopped, -> { with_state(:stopped) }
+
+ state_machine :state, initial: :available do
+ event :start do
+ transition stopped: :available
+ end
+
+ event :stop do
+ transition available: :stopped
+ end
+
+ state :available
+ state :stopped
+ end
+
def last_deployment
deployments.last
end
@@ -47,4 +65,33 @@ class Environment < ActiveRecord::Base
def update_merge_request_metrics?
self.name == "production"
end
+
+ def first_deployment_for(commit)
+ ref = project.repository.ref_name_for_sha(ref_path, commit.sha)
+
+ return nil unless ref
+
+ deployment_iid = ref.split('/').last
+ deployments.find_by(iid: deployment_iid)
+ end
+
+ def ref_path
+ "refs/environments/#{Shellwords.shellescape(name)}"
+ end
+
+ def formatted_external_url
+ return nil unless external_url
+
+ external_url.gsub(/\A.*?:\/\//, '')
+ end
+
+ def stoppable?
+ available? && stop_action.present?
+ end
+
+ def stop!(current_user)
+ return unless stoppable?
+
+ stop_action.play(current_user)
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 55a76e26f3c..3993b35f96d 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -12,6 +12,7 @@ class Event < ActiveRecord::Base
JOINED = 8 # User joined project
LEFT = 9 # User left project
DESTROYED = 10
+ EXPIRED = 11 # User left project due to expiry
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
@@ -68,8 +69,10 @@ class Event < ActiveRecord::Base
true
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
+ elsif merge_request? || merge_request_note?
+ Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
else
- ((merge_request? || note?) && target.present?) || milestone?
+ milestone?
end
end
@@ -113,6 +116,10 @@ class Event < ActiveRecord::Base
action == LEFT
end
+ def expired?
+ action == EXPIRED
+ end
+
def destroyed?
action == DESTROYED
end
@@ -122,7 +129,7 @@ class Event < ActiveRecord::Base
end
def membership_changed?
- joined? || left?
+ joined? || left? || expired?
end
def created_project?
@@ -182,6 +189,8 @@ class Event < ActiveRecord::Base
'joined'
elsif left?
'left'
+ elsif expired?
+ 'removed due to membership expiration from'
elsif destroyed?
'destroyed'
elsif commented?
@@ -280,6 +289,10 @@ class Event < ActiveRecord::Base
note? && target && target.for_issue?
end
+ def merge_request_note?
+ note? && target && target.for_merge_request?
+ end
+
def project_snippet_note?
target.for_snippet?
end
@@ -328,13 +341,15 @@ class Event < ActiveRecord::Base
def reset_project_activity
return unless project
- # Don't even bother obtaining a lock if the last update happened less than
- # 60 minutes ago.
+ # Don't bother updating if we know the project was updated recently.
return if recent_update?
- return unless try_obtain_lease
-
- project.update_column(:last_activity_at, created_at)
+ # At this point it's possible for multiple threads/processes to try to
+ # update the project. Only one query should actually perform the update,
+ # hence we add the extra WHERE clause for last_activity_at.
+ Project.unscoped.where(id: project_id).
+ where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
+ update_all(last_activity_at: created_at)
end
private
@@ -342,11 +357,4 @@ class Event < ActiveRecord::Base
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/external_issue.rb b/app/models/external_issue.rb
index b7894c99846..fd9a8c1b8b7 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -29,11 +29,6 @@ class ExternalIssue
@project
end
- # Pattern used to extract `JIRA-123` issue references from text
- def self.reference_pattern
- @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
- end
-
def to_reference(_from_project = nil)
id
end
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index ddd4bad5c21..698a7bbd327 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -4,6 +4,10 @@ class GlobalLabel
delegate :color, :description, to: :@first_label
+ def for_display
+ @first_label
+ end
+
def self.build_collection(labels)
labels = labels.group_by(&:title)
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index bda2b5c5d5d..cde4a568577 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -4,6 +4,10 @@ class GlobalMilestone
attr_accessor :title, :milestones
alias_attribute :name, :title
+ def for_display
+ @first_milestone
+ end
+
def self.build_collection(milestones)
milestones = milestones.group_by(&:title)
@@ -17,6 +21,7 @@ class GlobalMilestone
@title = title
@name = title
@milestones = milestones
+ @first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end
def safe_title
diff --git a/app/models/group.rb b/app/models/group.rb
index aefb94b2ada..00a595d2705 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -19,6 +19,7 @@ class Group < Namespace
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
+ has_many :labels, class_name: 'GroupLabel'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
@@ -102,40 +103,44 @@ class Group < Namespace
self[:lfs_enabled]
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 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/group_label.rb b/app/models/group_label.rb
new file mode 100644
index 00000000000..a698b532d19
--- /dev/null
+++ b/app/models/group_label.rb
@@ -0,0 +1,11 @@
+class GroupLabel < Label
+ belongs_to :group
+
+ validates :group, presence: true
+
+ alias_attribute :subject, :group
+
+ def to_reference(source_project = nil, target_project = nil, format: :id)
+ super(source_project, target_project, format: format)
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 20ddcc7bf32..9f29ecb0a86 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -139,6 +139,10 @@ class Issue < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
+ def self.project_foreign_key
+ 'project_id'
+ end
+
def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
@@ -208,7 +212,13 @@ class Issue < ActiveRecord::Base
note.all_references(current_user, extractor: ext)
end
- ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) }
+ merge_requests = ext.merge_requests.select(&:open?)
+ if merge_requests.any?
+ ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: id).pluck(:merge_request_id)
+ merge_requests.select { |mr| mr.id.in?(ids) }
+ else
+ []
+ end
end
def moved?
@@ -277,4 +287,16 @@ class Issue < ActiveRecord::Base
def check_for_spam?
project.public?
end
+
+ def as_json(options = {})
+ super(options).tap do |json|
+ if options.has_key?(:labels)
+ json[:labels] = labels.as_json(
+ project: project,
+ only: [:id, :title, :description, :color],
+ methods: [:text_color]
+ )
+ end
+ end
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index a23140b7d64..149fd98ecb3 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -1,4 +1,5 @@
class Label < ActiveRecord::Base
+ include CacheMarkdownField
include Referable
include Subscribable
@@ -8,38 +9,55 @@ class Label < ActiveRecord::Base
None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any Label', '')
+ cache_markdown_field :description, pipeline: :single_line
+
DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR
- belongs_to :project
-
has_many :lists, dependent: :destroy
+ has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false
- validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow ',' for label titles
- validates :title,
- presence: true,
- format: { with: /\A[^,]+\z/ },
- uniqueness: { scope: :project_id }
-
- before_save :nullify_priority
+ validates :title, presence: true, format: { with: /\A[^,]+\z/ }
+ validates :title, uniqueness: { scope: [:group_id, :project_id] }
default_scope { order(title: :asc) }
- scope :templates, -> { where(template: true) }
+ scope :templates, -> { where(template: true) }
+ scope :with_title, ->(title) { where(title: title) }
+
+ def self.prioritized(project)
+ joins(:priorities)
+ .where(label_priorities: { project_id: project })
+ .reorder('label_priorities.priority ASC, labels.title ASC')
+ end
+
+ def self.unprioritized(project)
+ labels = Label.arel_table
+ priorities = LabelPriority.arel_table
- def self.prioritized
- where.not(priority: nil).reorder(:priority, :title)
+ label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
+ on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))).
+ join_sources
+
+ joins(label_priorities).where(priorities[:priority].eq(nil))
end
- def self.unprioritized
- where(priority: nil)
+ def self.left_join_priorities
+ labels = Label.arel_table
+ priorities = LabelPriority.arel_table
+
+ label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
+ on(labels[:id].eq(priorities[:label_id])).
+ join_sources
+
+ joins(label_priorities)
end
alias_attribute :name, :title
@@ -74,6 +92,44 @@ class Label < ActiveRecord::Base
nil
end
+ def open_issues_count(user = nil, project = nil)
+ issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ end
+
+ def closed_issues_count(user = nil, project = nil)
+ issues_count(user, project_id: project.try(:id) || project_id, state: 'closed')
+ end
+
+ def open_merge_requests_count(user = nil, project = nil)
+ merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ end
+
+ def prioritize!(project, value)
+ label_priority = priorities.find_or_initialize_by(project_id: project.id)
+ label_priority.priority = value
+ label_priority.save!
+ end
+
+ def unprioritize!(project)
+ priorities.where(project: project).delete_all
+ end
+
+ def priority(project)
+ priorities.find_by(project: project).try(:priority)
+ end
+
+ def template?
+ template
+ end
+
+ def text_color
+ LabelsHelper.text_color_for_bg(self.color)
+ end
+
+ def title=(value)
+ write_attribute(:title, sanitize_title(value)) if value.present?
+ end
+
##
# Returns the String necessary to reference this Label in Markdown
#
@@ -81,49 +137,47 @@ class Label < ActiveRecord::Base
#
# Examples:
#
- # Label.first.to_reference # => "~1"
- # Label.first.to_reference(format: :name) # => "~\"bug\""
- # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
+ # Label.first.to_reference # => "~1"
+ # Label.first.to_reference(format: :name) # => "~\"bug\""
+ # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
#
- def to_reference(from_project = nil, format: :id)
+ def to_reference(source_project = nil, target_project = nil, format: :id)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if cross_project_reference?(from_project)
- project.to_reference + reference
+ if cross_project_reference?(source_project, target_project)
+ source_project.to_reference + reference
else
reference
end
end
- def open_issues_count(user = nil)
- issues.visible_to_user(user).opened.count
- end
-
- def closed_issues_count(user = nil)
- issues.visible_to_user(user).closed.count
+ def as_json(options = {})
+ super(options).tap do |json|
+ json[:priority] = priority(options[:project]) if options.has_key?(:project)
+ end
end
- def open_merge_requests_count
- merge_requests.opened.count
- end
+ private
- def template?
- template
+ def cross_project_reference?(source_project, target_project)
+ source_project && target_project && source_project != target_project
end
- def text_color
- LabelsHelper::text_color_for_bg(self.color)
+ def issues_count(user, params = {})
+ IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
+ .execute
+ .count
end
- def title=(value)
- write_attribute(:title, sanitize_title(value)) if value.present?
+ def merge_requests_count(user, params = {})
+ MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
+ .execute
+ .count
end
- private
-
def label_format_reference(format = :id)
raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
@@ -134,10 +188,6 @@ class Label < ActiveRecord::Base
end
end
- def nullify_priority
- self.priority = nil if priority.blank?
- end
-
def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
end
diff --git a/app/models/label_priority.rb b/app/models/label_priority.rb
new file mode 100644
index 00000000000..5b85e0b6533
--- /dev/null
+++ b/app/models/label_priority.rb
@@ -0,0 +1,8 @@
+class LabelPriority < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :label
+
+ validates :project, :label, :priority, presence: true
+ validates :label_id, uniqueness: { scope: :project_id }
+ validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+end
diff --git a/app/models/list.rb b/app/models/list.rb
index eb87decdbc8..065d75bd1dc 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -26,6 +26,17 @@ class List < ActiveRecord::Base
label? ? label.name : list_type.humanize
end
+ def as_json(options = {})
+ super(options).tap do |json|
+ if options.has_key?(:label)
+ json[:label] = label.as_json(
+ project: board.project,
+ only: [:id, :title, :description, :color]
+ )
+ end
+ end
+ end
+
private
def can_be_destroyed
diff --git a/app/models/member.rb b/app/models/member.rb
index 69406379948..b89ba8ecbb8 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -80,49 +80,75 @@ 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,
+ access_level: access_level
+ ).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..e4880973117 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
@@ -135,7 +121,11 @@ class ProjectMember < Member
end
def post_destroy_hook
- event_service.leave_project(self.project, self.user)
+ if expired?
+ event_service.expired_leave_project(self.project, self.user)
+ else
+ event_service.leave_project(self.project, self.user)
+ end
super
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index aec555dcec0..c476a3bb14e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
- attr_accessor :can_be_created, :compare_commits, :compare
+ attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do
event :close do
@@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
+ def self.project_foreign_key
+ 'target_project_id'
+ end
+
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
@@ -155,6 +159,20 @@ class MergeRequest < ActiveRecord::Base
where("merge_requests.id IN (#{union.to_sql})")
end
+ WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
+
+ def self.work_in_progress?(title)
+ !!(title =~ WIP_REGEX)
+ end
+
+ def self.wipless_title(title)
+ title.sub(WIP_REGEX, "")
+ end
+
+ def self.wip_title(title)
+ work_in_progress?(title) ? title : "WIP: #{title}"
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -182,7 +200,7 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
- merge_request_diff.size
+ diffs(diff_options).size
end
def diff_base_commit
@@ -308,21 +326,17 @@ class MergeRequest < ActiveRecord::Base
def validate_fork
return true unless target_project && source_project
return true if target_project == source_project
- return true unless forked_source_project_missing?
+ return true unless source_project_missing?
errors.add :validate_fork,
'Source project is not a fork of the target project'
end
def closed_without_fork?
- closed? && forked_source_project_missing?
+ closed? && source_project_missing?
end
- def closed_without_source_project?
- closed? && !source_project
- end
-
- def forked_source_project_missing?
+ def source_project_missing?
return false unless for_fork?
return true unless source_project
@@ -330,9 +344,7 @@ class MergeRequest < ActiveRecord::Base
end
def reopenable?
- return false if closed_without_fork? || closed_without_source_project? || merged?
-
- closed?
+ closed? && !source_project_missing? && source_branch_exists?
end
def ensure_merge_request_diff
@@ -389,14 +401,16 @@ class MergeRequest < ActiveRecord::Base
@closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
- WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
-
def work_in_progress?
- !!(title =~ WIP_REGEX)
+ self.class.work_in_progress?(title)
end
def wipless_title
- self.title.sub(WIP_REGEX, "")
+ self.class.wipless_title(self.title)
+ end
+
+ def wip_title
+ self.class.wip_title(self.title)
end
def mergeable?(skip_ci_check: false)
@@ -507,9 +521,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
+ # This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author)
+ return if project.has_external_issue_tracker?
+
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
@@ -638,7 +656,7 @@ class MergeRequest < ActiveRecord::Base
end
def has_ci?
- source_project.ci_service && commits.any?
+ source_project.try(:ci_service) && commits.any?
end
def branch_missing?
@@ -668,12 +686,12 @@ class MergeRequest < ActiveRecord::Base
def environments
return [] unless diff_head_commit
- 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
+ @environments ||=
+ begin
+ envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
+ envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
+ envs.uniq
+ end
end
def state_human_name
@@ -764,21 +782,21 @@ class MergeRequest < ActiveRecord::Base
def all_pipelines
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
+ @all_pipelines ||= source_project.pipelines
+ .where(sha: all_commits_sha, ref: source_branch)
+ .order(id: :desc)
end
# Note that this could also return SHA from now dangling commits
+ #
def all_commits_sha
- merge_request_diffs.flat_map(&:commits_sha).uniq
+ if persisted?
+ merge_request_diffs.flat_map(&:commits_sha).uniq
+ elsif compare_commits
+ compare_commits.to_a.reverse.map(&:id)
+ else
+ [diff_head_sha]
+ end
end
def merge_commit
@@ -848,7 +866,7 @@ class MergeRequest < ActiveRecord::Base
# files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
- rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 36b8b70870b..b8a10b7968e 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
+ # Valid types of serialized diffs allowed by Gitlab::Git::Diff
+ VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
+
belongs_to :merge_request
state_machine :state, initial: :empty do
@@ -164,12 +167,24 @@ class MergeRequestDiff < ActiveRecord::Base
self == merge_request.merge_request_diff
end
- def compare_with(sha)
- CompareService.new.execute(project, head_commit_sha, project, sha)
+ def compare_with(sha, straight: true)
+ # When compare merge request versions we want diff A..B instead of A...B
+ # so we handle cases when user does squash and rebase of the commits between versions.
+ # For this reason we set straight to true by default.
+ CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
end
private
+ # Old GitLab implementations may have generated diffs as ["--broken-diff"].
+ # Avoid an error 500 by ignoring bad elements. See:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
+ def valid_raw_diff?(raw)
+ return false unless raw.respond_to?(:each)
+
+ raw.any? { |element| VALID_CLASSES.include?(element.class) }
+ end
+
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -200,7 +215,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(raw, options)
- if raw.respond_to?(:each)
+ if valid_raw_diff?(raw)
if paths = options[:paths]
raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 2bd7f198030..23aecbfa3a6 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
+ include CacheMarkdownField
include InternalId
include Sortable
include Referable
include StripAttribute
include Milestoneish
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
belongs_to :project
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
@@ -158,7 +162,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 +208,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 919b3b1f095..b67049f0f55 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,9 +1,12 @@
class Namespace < ActiveRecord::Base
acts_as_paranoid
+ include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
+ cache_markdown_field :description, pipeline: :description
+
has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User"
@@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base
def clean_path(path)
path = path.dup
# Get the email username by removing everything after an `@` sign.
- path.gsub!(/@.*\z/, "")
- # Usernames can't end in .git, so remove it.
- path.gsub!(/\.git\z/, "")
- # Remove dashes at the start of the username.
- path.gsub!(/\A-+/, "")
- # Remove periods at the end of the username.
- path.gsub!(/\.+\z/, "")
+ path.gsub!(/@.*\z/, "")
# Remove everything that's not in the list of allowed characters.
- path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+ path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+ # Remove trailing violations ('.atom', '.git', or '.')
+ path.gsub!(/(\.atom|\.git|\.)*\z/, "")
+ # Remove leading violations ('-')
+ path.gsub!(/\A\-+/, "")
# Users with the great usernames of "." or ".." would end up with a blank username.
# Work around that by setting their username to "blank", followed by a counter.
diff --git a/app/models/note.rb b/app/models/note.rb
index f2656df028b..2d644b03e4d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
include Awardable
include Importable
include FasterCacheKeys
+ include CacheMarkdownField
+
+ cache_markdown_field :note, pipeline: :note
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
- attr_accessor :note_html
+ attr_accessor :redacted_note_html
# An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer
diff --git a/app/models/project.rb b/app/models/project.rb
index 7265cb55594..af117f0acb0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
include AccessRequestable
+ include CacheMarkdownField
include Referable
include Sortable
include AfterCommitQueue
@@ -15,8 +16,13 @@ class Project < ActiveRecord::Base
extend Gitlab::ConfigHelper
+ class BoardLimitExceeded < StandardError; end
+
+ NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'
+ cache_markdown_field :description, pipeline: :description
+
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false
@@ -26,8 +32,8 @@ class Project < ActiveRecord::Base
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist
+ after_create :create_project_feature, unless: :project_feature
after_save :ensure_dir_exist, if: :namespace_id_changed?
- after_initialize :setup_project_feature
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
@@ -62,8 +68,7 @@ class Project < ActiveRecord::Base
belongs_to :namespace
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
-
- has_one :board, dependent: :destroy
+ has_many :boards, before_add: :validate_board_limit, dependent: :destroy
# Project services
has_many :services
@@ -71,6 +76,7 @@ class Project < ActiveRecord::Base
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
has_one :builds_email_service, dependent: :destroy
+ has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
@@ -101,7 +107,7 @@ class Project < ActiveRecord::Base
# Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, dependent: :destroy
- has_many :labels, dependent: :destroy
+ has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy
@@ -146,6 +152,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
@@ -371,18 +378,9 @@ class Project < ActiveRecord::Base
%r{(?<project>#{name_pattern}/#{name_pattern})}
end
- def trending(since = 1.month.ago)
- # By counting in the JOIN we don't expose the GROUP BY to the outer query.
- # This means that calls such as "any?" and "count" just return a number of
- # the total count, instead of the counts grouped per project as a Hash.
- join_body = "INNER JOIN (
- SELECT project_id, COUNT(*) AS amount
- FROM notes
- WHERE created_at >= #{sanitize(since)}
- GROUP BY project_id
- ) join_note_counts ON projects.id = join_note_counts.project_id"
-
- joins(join_body).reorder('join_note_counts.amount DESC')
+ def trending
+ joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
+ reorder('trending_projects.id ASC')
end
def cached_count
@@ -390,6 +388,10 @@ class Project < ActiveRecord::Base
Project.count
end
end
+
+ def group_ids
+ joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
+ end
end
def lfs_enabled?
@@ -493,7 +495,7 @@ class Project < ActiveRecord::Base
end
def import_url
- if import_data && super
+ if import_data && super.present?
import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
import_url.full_url
else
@@ -666,6 +668,10 @@ class Project < ActiveRecord::Base
end
end
+ def issue_reference_pattern
+ issues_tracker.reference_pattern
+ end
+
def default_issues_tracker?
!external_issue_tracker
end
@@ -721,7 +727,7 @@ class Project < ActiveRecord::Base
if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service`
- self.send :"create_#{service_name}_service"
+ public_send("create_#{service_name}_service")
else
Service.create_from_template(self.id, template)
end
@@ -731,10 +737,8 @@ class Project < ActiveRecord::Base
def create_labels
Label.templates.each do |label|
- label = label.dup
- label.template = nil
- label.project_id = self.id
- label.save
+ params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
+ Labels::FindOrCreateService.new(owner, self, params).execute
end
end
@@ -832,11 +836,6 @@ class Project < ActiveRecord::Base
end
end
- def update_merge_requests(oldrev, newrev, ref, user)
- MergeRequests::RefreshService.new(self, user).
- execute(oldrev, newrev, ref)
- end
-
def valid_repo?
repository.exists?
rescue
@@ -1016,10 +1015,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
@@ -1304,7 +1299,7 @@ class Project < ActiveRecord::Base
environment_ids.where(ref: ref)
end
- environments.where(id: environment_ids).select do |environment|
+ environments.available.where(id: environment_ids).select do |environment|
environment.includes_commit?(commit)
end
end
@@ -1315,11 +1310,6 @@ class Project < ActiveRecord::Base
"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
- end
-
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
@@ -1348,4 +1338,15 @@ class Project < ActiveRecord::Base
shared_projects.any?
end
+
+ # Similar to the normal callbacks that hook into the life cycle of an
+ # Active Record object, you can also define callbacks that get triggered
+ # when you add an object to an association collection. If any of these
+ # callbacks throw an exception, the object will not be added to the
+ # collection. Before you add a new board to the boards collection if you
+ # already have 1, 2, or n it will fail, but it if you have 0 that is lower
+ # than the number of permitted boards per project it won't fail.
+ def validate_board_limit(board)
+ raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
+ end
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 8c9534c3565..b37ce1d3cf6 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -13,20 +13,26 @@ class ProjectFeature < ActiveRecord::Base
# Enabled: enabled for everyone able to access the project
#
- # Permision levels
+ # Permission levels
DISABLED = 0
PRIVATE = 10
ENABLED = 20
- FEATURES = %i(issues merge_requests wiki snippets builds)
+ FEATURES = %i(issues merge_requests wiki snippets builds repository)
- belongs_to :project
+ # Default scopes force us to unscope here since a service may need to check
+ # permissions for a project in pending_delete
+ # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
+ belongs_to :project, -> { unscope(where: :pending_delete) }
+
+ validate :repository_children_level
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
+ default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
@@ -54,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base
private
+ # Validates builds and merge requests access level
+ # which cannot be higher than repository access level
+ def repository_children_level
+ validator = lambda do |field|
+ level = public_send(field) || ProjectFeature::ENABLED
+ not_allowed = level > repository_access_level
+ self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
+ end
+
+ %i(merge_requests_access_level builds_access_level).each(&validator)
+ end
+
def get_permission(user, level)
case level
when DISABLED
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 7613cbdea93..db46def11eb 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base
belongs_to :group
validates :project_id, presence: true
- validates :group_id, presence: true
+ validates :group, presence: true
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
new file mode 100644
index 00000000000..33c2b617715
--- /dev/null
+++ b/app/models/project_label.rb
@@ -0,0 +1,34 @@
+class ProjectLabel < Label
+ MAX_NUMBER_OF_PRIORITIES = 1
+
+ belongs_to :project
+
+ validates :project, presence: true
+
+ validate :permitted_numbers_of_priorities
+ validate :title_must_not_exist_at_group_level
+
+ delegate :group, to: :project, allow_nil: true
+
+ alias_attribute :subject, :project
+
+ def to_reference(target_project = nil, format: :id)
+ super(project, target_project, format: format)
+ end
+
+ private
+
+ def title_must_not_exist_at_group_level
+ return unless group.present? && title_changed?
+
+ if group.labels.with_title(self.title).exists?
+ errors.add(:title, :label_already_exists_at_group_level, group: group.name)
+ end
+ end
+
+ def permitted_numbers_of_priorities
+ if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES
+ errors.add(:priorities, 'Number of permitted priorities exceeded')
+ end
+ end
+end
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index fa66e5864b8..201b94b065b 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -43,7 +43,7 @@ class BuildsEmailService < Service
end
def can_test?
- project.builds.count > 0
+ project.builds.any?
end
def disabled_title
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index afebd3b6a12..660a8ae3421 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -1,5 +1,12 @@
class HipchatService < Service
+ include ActionView::Helpers::SanitizeHelper
+
MAX_COMMITS = 3
+ HIPCHAT_ALLOWED_TAGS = %w[
+ a b i strong em br img pre code
+ table th tr td caption colgroup col thead tbody tfoot
+ ul ol li dl dt dd
+ ]
prop_accessor :token, :room, :server, :notify, :color, :api_version
boolean_accessor :notify_only_broken_builds
@@ -88,6 +95,10 @@ class HipchatService < Service
end
end
+ def render_line(text)
+ markdown(text.lines.first.chomp, pipeline: :single_line) if text
+ end
+
def create_push_message(push)
ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch'
ref = Gitlab::Git.ref_name(push[:ref])
@@ -110,7 +121,7 @@ class HipchatService < Service
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
push[:commits].take(MAX_COMMITS).each do |commit|
- message << "<br /> - #{commit[:message].lines.first} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
+ message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
end
if push[:commits].count > MAX_COMMITS
@@ -121,12 +132,22 @@ class HipchatService < Service
message
end
- def format_body(body)
- if body
- body = body.truncate(200, separator: ' ', omission: '...')
- end
+ def markdown(text, options = {})
+ return "" unless text
+
+ context = {
+ project: project,
+ pipeline: :email
+ }
+
+ Banzai.render(text, context)
- "<pre>#{body}</pre>"
+ context.merge!(options)
+
+ html = Banzai.post_process(Banzai.render(text, context), context)
+ sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
+
+ sanitized_html.truncate(200, separator: ' ', omission: '...')
end
def create_issue_message(data)
@@ -134,7 +155,7 @@ class HipchatService < Service
obj_attr = data[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
- title = obj_attr[:title]
+ title = render_line(obj_attr[:title])
state = obj_attr[:state]
issue_iid = obj_attr[:iid]
issue_url = obj_attr[:url]
@@ -143,10 +164,7 @@ class HipchatService < Service
issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>"
message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"
- if description
- description = format_body(description)
- message << description
- end
+ message << "<pre>#{markdown(description)}</pre>"
message
end
@@ -159,23 +177,20 @@ class HipchatService < Service
merge_request_id = obj_attr[:iid]
state = obj_attr[:state]
description = obj_attr[:description]
- title = obj_attr[:title]
+ title = render_line(obj_attr[:title])
merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
message = "#{user_name} #{state} #{merge_request_link} in " \
"#{project_link}: <b>#{title}</b>"
- if description
- description = format_body(description)
- message << description
- end
+ message << "<pre>#{markdown(description)}</pre>"
message
end
def format_title(title)
- "<b>" + title.lines.first.chomp + "</b>"
+ "<b>#{render_line(title)}</b>"
end
def create_note_message(data)
@@ -186,11 +201,13 @@ class HipchatService < Service
note = obj_attr[:note]
note_url = obj_attr[:url]
noteable_type = obj_attr[:noteable_type]
+ commit_id = nil
case noteable_type
when "Commit"
commit_attr = HashWithIndifferentAccess.new(data[:commit])
- subject_desc = commit_attr[:id]
+ commit_id = commit_attr[:id]
+ subject_desc = commit_id
subject_desc = Commit.truncate_sha(subject_desc)
subject_type = "commit"
title = format_title(commit_attr[:message])
@@ -218,10 +235,7 @@ class HipchatService < Service
message = "#{user_name} commented on #{subject_html} in #{project_link}: "
message << title
- if note
- note = format_body(note)
- message << note
- end
+ message << "<pre>#{markdown(note, ref: commit_id)}</pre>"
message
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index d1df6d0292f..b26ddd518d7 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -3,6 +3,12 @@ class IssueTrackerService < Service
default_value_for :category, 'issue_tracker'
+ # Pattern used to extract links from comments
+ # Override this method on services that uses different patterns
+ def reference_pattern
+ @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
+ end
+
def default?
default
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 97bcbacf2b9..f81b66fd219 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -13,6 +13,11 @@ class JiraService < IssueTrackerService
before_update :reset_password
+ # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
+ def reference_pattern
+ @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
+ end
+
def reset_password
# don't reset the password if a new one is provided
if api_url_changed? && !password_touched?
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
new file mode 100644
index 00000000000..ec3c1bc85ee
--- /dev/null
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -0,0 +1,96 @@
+class PipelinesEmailService < Service
+ prop_accessor :recipients
+ boolean_accessor :add_pusher
+ boolean_accessor :notify_only_broken_pipelines
+ validates :recipients,
+ presence: true,
+ if: ->(s) { s.activated? && !s.add_pusher? }
+
+ def initialize_properties
+ self.properties ||= { notify_only_broken_pipelines: true }
+ end
+
+ def title
+ 'Pipelines emails'
+ end
+
+ def description
+ 'Email the pipelines status to a list of recipients.'
+ end
+
+ def to_param
+ 'pipelines_email'
+ end
+
+ def supported_events
+ %w[pipeline]
+ end
+
+ def execute(data, force: false)
+ return unless supported_events.include?(data[:object_kind])
+ return unless force || should_pipeline_be_notified?(data)
+
+ all_recipients = retrieve_recipients(data)
+
+ return unless all_recipients.any?
+
+ pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
+ Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
+ end
+
+ def can_test?
+ project.pipelines.any?
+ end
+
+ def disabled_title
+ 'Please setup a pipeline on your repository.'
+ end
+
+ def test_data(project, user)
+ data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
+ data[:user] = user.hook_attrs
+ data
+ end
+
+ def fields
+ [
+ { type: 'textarea',
+ name: 'recipients',
+ placeholder: 'Emails separated by comma' },
+ { type: 'checkbox',
+ name: 'add_pusher',
+ label: 'Add pusher to recipients list' },
+ { type: 'checkbox',
+ name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def test(data)
+ result = execute(data, force: true)
+
+ { success: true, result: result }
+ rescue StandardError => error
+ { success: false, result: error }
+ 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
+
+ def retrieve_recipients(data)
+ all_recipients = recipients.to_s.split(',').reject(&:blank?)
+
+ if add_pusher? && data[:user].try(:[], :email)
+ all_recipients << data[:user][:email]
+ end
+
+ all_recipients
+ end
+end
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb
index 88e053ec192..cd87a79d0c6 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/slack_service/issue_message.rb
@@ -11,7 +11,7 @@ class SlackService
attr_reader :description
def initialize(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb
index 11fc691022b..b7615c96068 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/slack_service/merge_message.rb
@@ -10,7 +10,7 @@ class SlackService
attr_reader :title
def initialize(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index 89ba51cb662..9e84e90f38c 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -10,7 +10,7 @@ class SlackService
def initialize(params)
params = HashWithIndifferentAccess.new(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb
index f336d9e7691..160ca3ac115 100644
--- a/app/models/project_services/slack_service/wiki_page_message.rb
+++ b/app/models/project_services/slack_service/wiki_page_message.rb
@@ -9,7 +9,7 @@ class SlackService
attr_reader :description
def initialize(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d9ce5088903..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
diff --git a/app/models/release.rb b/app/models/release.rb
index e196b84eb18..c936899799e 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -1,4 +1,8 @@
class Release < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :description
+
belongs_to :project
validates :description, :project, :tag, presence: true
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 51557228ab9..4ae9c20726f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -109,15 +109,20 @@ class Repository
end
def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
+ unless exists? && has_visible_content? && query.present?
+ return []
+ end
+
ref ||= root_ref
- # Limited to 1000 commits for now, could be parameterized?
- args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query})
+ args = %W(
+ #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset}
+ --max-count #{limit} --grep=#{query} --regexp-ignore-case
+ )
args = args.concat(%W(-- #{path})) if path.present?
- git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
- commits = git_log_results.map { |c| commit(c) }
- commits
+ git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
+ git_log_results.map { |c| commit(c.chomp) }.compact
end
def find_branch(name, fresh_repo: true)
@@ -414,6 +419,17 @@ class Repository
@exists = nil
end
+ # expire cache that doesn't depend on repository data (when expiring)
+ def expire_content_cache
+ expire_tags_cache
+ expire_tag_count_cache
+ expire_branches_cache
+ expire_branch_count_cache
+ expire_root_ref_cache
+ expire_emptiness_caches
+ expire_exists_cache
+ end
+
# Runs code after a repository has been created.
def after_create
expire_exists_cache
@@ -429,14 +445,7 @@ class Repository
expire_cache if exists?
- # expire cache that don't depend on repository data (when expiring)
- expire_tags_cache
- expire_tag_count_cache
- expire_branches_cache
- expire_branch_count_cache
- expire_root_ref_cache
- expire_emptiness_caches
- expire_exists_cache
+ expire_content_cache
repository_event(:remove_repository)
end
@@ -468,14 +477,13 @@ class Repository
end
def before_import
- expire_emptiness_caches
- expire_exists_cache
+ expire_content_cache
end
# Runs code after a repository has been forked/imported.
def after_import
- expire_emptiness_caches
- expire_exists_cache
+ expire_content_cache
+ build_cache
end
# Runs code after a new commit has been pushed.
@@ -717,6 +725,14 @@ class Repository
end
end
+ def ref_name_for_sha(ref_path, sha)
+ args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
+
+ # Not found -> ["", 0]
+ # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
+ Gitlab::Popen.popen(args, path_to_repo).first.split.last
+ end
+
def refs_contains_sha(ref_type, sha)
args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
names = Gitlab::Popen.popen(args, path_to_repo).first
@@ -838,6 +854,52 @@ class Repository
end
end
+ def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
+ update_branch_with_hooks(user, branch) do |ref|
+ index = rugged.index
+ parents = []
+ branch = find_branch(ref)
+
+ if branch
+ last_commit = branch.target
+ index.read_tree(last_commit.raw_commit.tree)
+ parents = [last_commit.sha]
+ end
+
+ actions.each do |action|
+ case action[:action]
+ when :create, :update, :move
+ mode =
+ case action[:action]
+ when :update
+ index.get(action[:file_path])[:mode]
+ when :move
+ index.get(action[:previous_path])[:mode]
+ end
+ mode ||= 0o100644
+
+ index.remove(action[:previous_path]) if action[:action] == :move
+
+ content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
+ oid = rugged.write(content, :blob)
+
+ index.add(path: action[:file_path], oid: oid, mode: mode)
+ when :delete
+ index.remove(action[:file_path])
+ end
+ end
+
+ options = {
+ tree: index.write_tree(rugged),
+ message: message,
+ parents: parents
+ }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+
+ Rugged::Commit.create(rugged, options)
+ end
+ end
+
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
@@ -968,7 +1030,8 @@ class Repository
root_ref_commit = commit(root_ref)
if branch_commit
- is_ancestor?(branch_commit.id, root_ref_commit.id)
+ same_head = branch_commit.id == root_ref_commit.id
+ !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
@@ -997,6 +1060,10 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
+ def create_ref(ref, ref_path)
+ fetch_ref(path_to_repo, ref, ref_path)
+ end
+
def update_branch_with_hooks(current_user, branch)
update_autocrlf_option
diff --git a/app/models/service.rb b/app/models/service.rb
index 80de7175565..625fbc48302 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end
def #{arg}=(value)
+ self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value
end
@@ -195,12 +196,13 @@ class Service < ActiveRecord::Base
end
def self.available_services_names
- %w(
+ %w[
asana
assembla
bamboo
buildkite
builds_email
+ pipelines_email
bugzilla
campfire
custom_issue_tracker
@@ -217,7 +219,7 @@ class Service < ActiveRecord::Base
redmine
slack
teamcity
- )
+ ]
end
def self.create_from_template(project_id, template)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 8a1730f3f36..2373b445009 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -1,11 +1,21 @@
class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Linguist::BlobHelper
+ include CacheMarkdownField
include Participable
include Referable
include Sortable
include Awardable
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :content
+
+ # If file_name changes, it invalidates content
+ alias_method :default_content_html_invalidator, :content_html_invalidated?
+ def content_html_invalidated?
+ default_content_html_invalidator || file_name_changed?
+ end
+
default_value_for :visibility_level, Snippet::PRIVATE
belongs_to :author, class_name: 'User'
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 6ae9956ade5..11c072dd000 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base
# Todos with highest priority first then oldest todos
# Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
def order_by_labels_priority
- highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
+ params = {
+ target_type: ['Issue', 'MergeRequest'],
+ target_column: "todos.target_id",
+ project_column: "todos.project_id"
+ }
+
+ highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
diff --git a/app/models/trending_project.rb b/app/models/trending_project.rb
new file mode 100644
index 00000000000..27e3732da17
--- /dev/null
+++ b/app/models/trending_project.rb
@@ -0,0 +1,35 @@
+class TrendingProject < ActiveRecord::Base
+ belongs_to :project
+
+ # The number of months to include in the trending calculation.
+ MONTHS_TO_INCLUDE = 1
+
+ # The maximum number of projects to include in the trending set.
+ PROJECTS_LIMIT = 100
+
+ # Populates the trending projects table with the current list of trending
+ # projects.
+ def self.refresh!
+ # The calculation **must** run in a transaction. If the removal of data and
+ # insertion of new data were to run separately a user might end up with an
+ # empty list of trending projects for a short period of time.
+ transaction do
+ delete_all
+
+ timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago)
+
+ connection.execute <<-EOF.strip_heredoc
+ INSERT INTO #{table_name} (project_id)
+ SELECT project_id
+ FROM notes
+ INNER JOIN projects ON projects.id = notes.project_id
+ WHERE notes.created_at >= #{timestamp}
+ AND notes.system IS FALSE
+ AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}
+ GROUP BY project_id
+ ORDER BY count(*) DESC
+ LIMIT #{PROJECTS_LIMIT};
+ EOF
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 582c0301195..26823907024 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -286,6 +286,11 @@ class User < ActiveRecord::Base
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
+ # Returns a user for the given SSH key.
+ def find_by_ssh_key_id(key_id)
+ find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
+ end
+
def build_user(attrs = {})
User.new(attrs)
end
@@ -587,6 +592,11 @@ class User < ActiveRecord::Base
end
def set_projects_limit
+ # `User.select(:id)` raises
+ # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
+ # without this safeguard!
+ return unless self.has_attribute?(:projects_limit)
+
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined
@@ -830,6 +840,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true)
end
+ # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
+ # flow means we don't call that automatically (and can't conveniently do so).
+ #
+ # See:
+ # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
+ #
+ def increment_failed_attempts!
+ self.failed_attempts ||= 0
+ self.failed_attempts += 1
+ if attempts_exceeded?
+ lock_access! unless access_locked?
+ else
+ save(validate: false)
+ end
+ end
+
private
def projects_union(min_access_level = nil)
@@ -884,7 +910,7 @@ class User < ActiveRecord::Base
if domain_matches?(allowed_domains, self.email)
valid = true
else
- error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}"
+ error = "domain is not authorized for sign-up"
valid = false
end
end
diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb
new file mode 100644
index 00000000000..7b34aa182eb
--- /dev/null
+++ b/app/policies/group_label_policy.rb
@@ -0,0 +1,5 @@
+class GroupLabelPolicy < BasePolicy
+ def rules
+ delegate! @subject.group
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 8202b58b5c9..2c68ad00d65 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -20,6 +20,7 @@ class GroupPolicy < BasePolicy
if master
can! :create_projects
can! :admin_milestones
+ can! :admin_label
end
# Only group owner and administrators can admin group
diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb
new file mode 100644
index 00000000000..b12b4c5166b
--- /dev/null
+++ b/app/policies/project_label_policy.rb
@@ -0,0 +1,5 @@
+class ProjectLabelPolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index af1d1f784a4..27e2f55721b 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -42,7 +42,6 @@ class ProjectPolicy < BasePolicy
can! :read_milestone
can! :read_project_snippet
can! :read_project_member
- can! :read_merge_request
can! :read_note
can! :create_project
can! :create_issue
@@ -65,6 +64,7 @@ class ProjectPolicy < BasePolicy
can! :read_pipeline
can! :read_environment
can! :read_deployment
+ can! :read_merge_request
end
# Permissions given when an user is team member of a project
@@ -100,7 +100,6 @@ class ProjectPolicy < BasePolicy
can! :admin_milestone
can! :admin_project_snippet
can! :admin_project_member
- can! :admin_merge_request
can! :admin_note
can! :admin_wiki
can! :admin_project
@@ -120,6 +119,7 @@ class ProjectPolicy < BasePolicy
can! :read_container_image
can! :build_download_code
can! :build_read_container_image
+ can! :read_merge_request
end
def owner_access!
@@ -141,11 +141,18 @@ 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
- team_member_reporter_access! if access >= Gitlab::Access::REPORTER
- developer_access! if access >= Gitlab::Access::DEVELOPER
- master_access! if access >= Gitlab::Access::MASTER
+ return if access < Gitlab::Access::GUEST
+ guest_access!
+
+ return if access < Gitlab::Access::REPORTER
+ reporter_access!
+ team_member_reporter_access!
+
+ return if access < Gitlab::Access::DEVELOPER
+ developer_access!
+
+ return if access < Gitlab::Access::MASTER
+ master_access!
end
def archived_access!
@@ -157,11 +164,13 @@ class ProjectPolicy < BasePolicy
end
def disabled_features!
+ repository_enabled = project.feature_available?(:repository, user)
+
unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue))
end
- unless project.feature_available?(:merge_requests, user)
+ unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request))
end
@@ -178,13 +187,21 @@ class ProjectPolicy < BasePolicy
cannot!(*named_abilities(:wiki))
end
- unless project.feature_available?(:builds, user)
+ unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment))
end
+ unless repository_enabled
+ cannot! :push_code
+ cannot! :push_code_to_protected_branches
+ cannot! :download_code
+ cannot! :fork_project
+ cannot! :read_commit_status
+ end
+
unless project.container_registry_enabled
cannot!(*named_abilities(:container_image))
end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 0c208150fb8..1a2bad77a02 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -56,9 +56,8 @@ class BaseService
result
end
- def success
- {
- status: :success
- }
+ def success(pass_back = {})
+ pass_back[:status] = :success
+ pass_back
end
end
diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb
deleted file mode 100644
index b2069ca825a..00000000000
--- a/app/services/boards/base_service.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Boards
- class BaseService < ::BaseService
- delegate :board, to: :project
- end
-end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 072a0749285..9bdd7b6f0cf 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -1,16 +1,21 @@
module Boards
- class CreateService < Boards::BaseService
+ class CreateService < BaseService
def execute
- create_board! unless project.board.present?
- project.board
+ if project.boards.empty?
+ create_board!
+ else
+ project.boards.first
+ end
end
private
def create_board!
- project.create_board
- project.board.lists.create(list_type: :backlog)
- project.board.lists.create(list_type: :done)
+ board = project.boards.create
+ board.lists.create(list_type: :backlog)
+ board.lists.create(list_type: :done)
+
+ board
end
end
end
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
new file mode 100644
index 00000000000..c0d7ff5b585
--- /dev/null
+++ b/app/services/boards/issues/create_service.rb
@@ -0,0 +1,23 @@
+module Boards
+ module Issues
+ class CreateService < BaseService
+ def execute
+ create_issue(params.merge(label_ids: [list.label_id]))
+ end
+
+ private
+
+ def board
+ @board ||= project.boards.find(params.delete(:board_id))
+ end
+
+ def list
+ @list ||= board.lists.find(params.delete(:list_id))
+ end
+
+ def create_issue(params)
+ ::Issues::CreateService.new(project, current_user, params).execute
+ end
+ end
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 34efd09ed9f..fd4a462c7b2 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -1,6 +1,6 @@
module Boards
module Issues
- class ListService < Boards::BaseService
+ class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless list.movable?
@@ -10,6 +10,10 @@ module Boards
private
+ def board
+ @board ||= project.boards.find(params[:board_id])
+ end
+
def list
@list ||= board.lists.find(params[:id])
end
@@ -36,12 +40,7 @@ module Boards
end
def set_state
- params[:state] =
- case list.list_type.to_sym
- when :backlog then 'opened'
- when :done then 'closed'
- else 'all'
- end
+ params[:state] = list.done? ? 'closed' : 'opened'
end
def board_label_ids
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 84dc3f70e76..96554a92a02 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -1,6 +1,6 @@
module Boards
module Issues
- class MoveService < Boards::BaseService
+ class MoveService < BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false unless valid_move?
@@ -10,6 +10,10 @@ module Boards
private
+ def board
+ @board ||= project.boards.find(params[:board_id])
+ end
+
def valid_move?
moving_from_list.present? && moving_to_list.present? &&
moving_from_list != moving_to_list
@@ -49,7 +53,7 @@ module Boards
if moving_to_list.movable?
moving_from_list.label_id
else
- board.lists.movable.pluck(:label_id)
+ project.boards.joins(:lists).merge(List.movable).pluck(:label_id)
end
Array(label_ids).compact
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
new file mode 100644
index 00000000000..84f1fc3a4e2
--- /dev/null
+++ b/app/services/boards/list_service.rb
@@ -0,0 +1,14 @@
+module Boards
+ class ListService < BaseService
+ def execute
+ create_board! if project.boards.empty?
+ project.boards
+ end
+
+ private
+
+ def create_board!
+ Boards::CreateService.new(project, current_user).execute
+ end
+ end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index b1887820bd4..fe0d762ccd2 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -1,23 +1,27 @@
module Boards
module Lists
- class CreateService < Boards::BaseService
- def execute
+ class CreateService < BaseService
+ def execute(board)
List.transaction do
- label = project.labels.find(params[:label_id])
- position = next_position
+ label = available_labels.find(params[:label_id])
+ position = next_position(board)
- create_list(label, position)
+ create_list(board, label, position)
end
end
private
- def next_position
+ def available_labels
+ LabelsFinder.new(current_user, project_id: project.id).execute
+ end
+
+ def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ
end
- def create_list(label, position)
+ def create_list(board, label, position)
board.lists.create(label: label, list_type: :label, position: position)
end
end
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
index 25da3bfb56d..f986e05944c 100644
--- a/app/services/boards/lists/destroy_service.rb
+++ b/app/services/boards/lists/destroy_service.rb
@@ -1,9 +1,11 @@
module Boards
module Lists
- class DestroyService < Boards::BaseService
+ class DestroyService < BaseService
def execute(list)
return false unless list.destroyable?
+ @board = list.board
+
list.with_lock do
decrement_higher_lists(list)
remove_list(list)
@@ -12,6 +14,8 @@ module Boards
private
+ attr_reader :board
+
def decrement_higher_lists(list)
board.lists.movable.where('position > ?', list.position)
.update_all('position = position - 1')
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
index 1c48b9786e4..939f9bfd068 100644
--- a/app/services/boards/lists/generate_service.rb
+++ b/app/services/boards/lists/generate_service.rb
@@ -1,11 +1,11 @@
module Boards
module Lists
- class GenerateService < Boards::BaseService
- def execute
+ class GenerateService < BaseService
+ def execute(board)
return false unless board.lists.movable.empty?
List.transaction do
- label_params.each { |params| create_list(params) }
+ label_params.each { |params| create_list(board, params) }
end
true
@@ -13,22 +13,19 @@ module Boards
private
- def create_list(params)
+ def create_list(board, params)
label = find_or_create_label(params)
- Lists::CreateService.new(project, current_user, label_id: label.id).execute
+ Lists::CreateService.new(project, current_user, label_id: label.id).execute(board)
end
def find_or_create_label(params)
- project.labels.create_with(color: params[:color])
- .find_or_create_by(name: params[:name])
+ ::Labels::FindOrCreateService.new(current_user, project, params).execute
end
def label_params
[
- { name: 'Development', color: '#5CB85C' },
- { name: 'Testing', color: '#F0AD4E' },
- { name: 'Production', color: '#FF5F00' },
- { name: 'Ready', color: '#FF0000' }
+ { name: 'To Do', color: '#F0AD4E' },
+ { name: 'Doing', color: '#5CB85C' }
]
end
end
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
new file mode 100644
index 00000000000..c579ed4c869
--- /dev/null
+++ b/app/services/boards/lists/list_service.rb
@@ -0,0 +1,9 @@
+module Boards
+ module Lists
+ class ListService < BaseService
+ def execute(board)
+ board.lists
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
index 020ff69f4a7..f2a68865f7b 100644
--- a/app/services/boards/lists/move_service.rb
+++ b/app/services/boards/lists/move_service.rb
@@ -1,7 +1,8 @@
module Boards
module Lists
- class MoveService < Boards::BaseService
+ class MoveService < BaseService
def execute(list)
+ @board = list.board
@old_position = list.position
@new_position = params[:position]
@@ -16,7 +17,7 @@ module Boards
private
- attr_reader :old_position, :new_position
+ attr_reader :board, :old_position, :new_position
def valid_move?
new_position.present? && new_position != old_position &&
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 36c93dddadb..d3dd30b2588 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -16,6 +16,8 @@ module Ci
process_stage(index)
end
+ @pipeline.update_status
+
# Return a flag if a when builds got enqueued
new_builds.flatten.any?
end
diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb
new file mode 100644
index 00000000000..ceb182801f7
--- /dev/null
+++ b/app/services/ci/send_pipeline_notification_service.rb
@@ -0,0 +1,19 @@
+module Ci
+ class SendPipelineNotificationService
+ attr_reader :pipeline
+
+ def initialize(new_pipeline)
+ @pipeline = new_pipeline
+ end
+
+ def execute(recipients)
+ email_template = "pipeline_#{pipeline.status}_email"
+
+ return unless Notify.respond_to?(email_template)
+
+ recipients.each do |to|
+ Notify.public_send(email_template, pipeline, to).deliver_later
+ end
+ end
+ end
+end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 6d6075628af..5e8fafca98c 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch)
+ def execute(source_project, source_branch, target_project, target_branch, straight: false)
source_commit = source_project.commit(source_branch)
return unless source_commit
@@ -23,9 +23,10 @@ class CompareService
raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
- source_sha
+ source_sha,
+ straight
)
- Compare.new(raw_compare, target_project)
+ Compare.new(raw_compare, target_project, straight: straight)
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index 799ad3e1bd0..8ae15ad32f4 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -2,28 +2,43 @@ require_relative 'base_service'
class CreateDeploymentService < BaseService
def execute(deployable = nil)
- environment = find_or_create_environment
+ return unless executable?
- deployment = project.deployments.create(
- environment: environment,
- ref: params[:ref],
- tag: params[:tag],
- sha: params[:sha],
- user: current_user,
- deployable: deployable
- )
+ ActiveRecord::Base.transaction do
+ @deployable = deployable
+
+ @environment = environment
+ @environment.external_url = expanded_url if expanded_url
+ @environment.fire_state_event(action)
- deployment.update_merge_request_metrics!
+ return unless @environment.save
+ return if @environment.stopped?
- deployment
+ deploy.tap do |deployment|
+ deployment.update_merge_request_metrics!
+ end
+ end
end
private
- def find_or_create_environment
- project.environments.find_or_create_by(name: expanded_name) do |environment|
- environment.external_url = expanded_url
- end
+ def executable?
+ project && name.present?
+ end
+
+ def deploy
+ project.deployments.create(
+ environment: @environment,
+ ref: params[:ref],
+ tag: params[:tag],
+ sha: params[:sha],
+ user: current_user,
+ deployable: @deployable,
+ on_stop: options[:on_stop])
+ end
+
+ def environment
+ @environment ||= project.environments.find_or_create_by(name: expanded_name)
end
def expanded_name
@@ -51,4 +66,8 @@ class CreateDeploymentService < BaseService
def variables
params[:variables] || []
end
+
+ def action
+ options[:action] || 'start'
+ end
end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 07fc77001a5..e24cc66e0fe 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -62,6 +62,10 @@ class EventCreateService
create_event(project, current_user, Event::LEFT)
end
+ def expired_leave_project(project, current_user)
+ create_event(project, current_user, Event::EXPIRED)
+ end
+
def create_project(project, current_user)
create_event(project, current_user, Event::CREATED)
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index e8465729d06..9bd4bd464f7 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -27,8 +27,9 @@ module Files
create_target_branch
end
- if commit
- success
+ result = commit
+ if result
+ success(result: result)
else
error('Something went wrong. Your changes were not committed')
end
@@ -42,6 +43,12 @@ module Files
@source_branch != @target_branch || @source_project != @project
end
+ def file_has_changed?
+ return false unless @last_commit_sha && last_commit
+
+ @last_commit_sha != last_commit.sha
+ end
+
def raise_error(message)
raise ValidationError.new(message)
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
new file mode 100644
index 00000000000..d28912e1301
--- /dev/null
+++ b/app/services/files/multi_service.rb
@@ -0,0 +1,124 @@
+require_relative "base_service"
+
+module Files
+ class MultiService < Files::BaseService
+ class FileChangedError < StandardError; end
+
+ def commit
+ repository.multi_action(
+ user: current_user,
+ branch: @target_branch,
+ message: @commit_message,
+ actions: params[:actions],
+ author_email: @author_email,
+ author_name: @author_name
+ )
+ end
+
+ private
+
+ def validate
+ super
+
+ params[:actions].each_with_index do |action, index|
+ unless action[:file_path].present?
+ raise_error("You must specify a file_path.")
+ end
+
+ regex_check(action[:file_path])
+ regex_check(action[:previous_path]) if action[:previous_path]
+
+ if project.empty_repo? && action[:action] != :create
+ raise_error("No files to #{action[:action]}.")
+ end
+
+ validate_file_exists(action)
+
+ case action[:action]
+ when :create
+ validate_create(action)
+ when :update
+ validate_update(action)
+ when :delete
+ validate_delete(action)
+ when :move
+ validate_move(action, index)
+ else
+ raise_error("Unknown action type `#{action[:action]}`.")
+ end
+ end
+ end
+
+ def validate_file_exists(action)
+ return if action[:action] == :create
+
+ file_path = action[:file_path]
+ file_path = action[:previous_path] if action[:action] == :move
+
+ blob = repository.blob_at_branch(params[:branch_name], file_path)
+
+ unless blob
+ raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
+ end
+ end
+
+ def last_commit
+ Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
+ end
+
+ def regex_check(file)
+ if file =~ Gitlab::Regex.directory_traversal_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name, `' +
+ file +
+ '` ' +
+ Gitlab::Regex.directory_traversal_regex_message
+ )
+ end
+
+ unless file =~ Gitlab::Regex.file_path_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name, `' +
+ file +
+ '` ' +
+ Gitlab::Regex.file_path_regex_message
+ )
+ end
+ end
+
+ def validate_create(action)
+ return if project.empty_repo?
+
+ if repository.blob_at_branch(params[:branch_name], action[:file_path])
+ raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
+ end
+ end
+
+ def validate_delete(action)
+ end
+
+ def validate_move(action, index)
+ if action[:previous_path].nil?
+ raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
+ end
+
+ blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
+
+ if blob
+ raise_error("Move destination `#{action[:file_path]}` already exists.")
+ end
+
+ if action[:content].nil?
+ blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
+ blob.load_all_data!(repository) if blob.truncated?
+ params[:actions][index][:content] = blob.data
+ end
+ end
+
+ def validate_update(action)
+ if file_has_changed?
+ raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
+ end
+ end
+ end
+end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 9e9b5b63f26..c17fdb8d1f1 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -23,12 +23,6 @@ module Files
end
end
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
def last_commit
@last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path)
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index c499427605a..e8415862de5 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -63,13 +63,12 @@ class GitPushService < BaseService
protected
def update_merge_requests
- @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
+ UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
EventCreateService.new.push(@project, current_user, build_push_data)
- SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
- Ci::CreatePipelineService.new(project, current_user, build_push_data).execute
+ Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
ProjectCacheWorker.perform_async(@project.id)
end
@@ -148,16 +147,6 @@ class GitPushService < BaseService
push_commits)
end
- def build_push_data_system_hook
- @push_data_system ||= Gitlab::DataBuilder::Push.build(
- @project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- [])
- end
-
def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 57d521f2fea..bb92cd80cc9 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -80,17 +80,18 @@ class IssuableBaseService < BaseService
def filter_labels_in_param(key)
return if params[key].to_a.empty?
- params[key] = project.labels.where(id: params[key]).pluck(:id)
+ params[key] = available_labels.where(id: params[key]).pluck(:id)
end
def find_or_create_label_ids
labels = params.delete(:labels)
return unless labels
- params[:label_ids] = labels.split(",").map do |label_name|
- project.labels.create_with(color: Label::DEFAULT_COLOR)
- .find_or_create_by(title: label_name.strip)
- .id
+ params[:label_ids] = labels.split(',').map do |label_name|
+ service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
+ label = service.execute
+
+ label.id
end
end
@@ -111,6 +112,10 @@ class IssuableBaseService < BaseService
new_label_ids
end
+ def available_labels
+ LabelsFinder.new(current_user, project_id: @project.id).execute
+ end
+
def merge_slash_commands_into_params!(issuable)
description, command_params =
SlashCommands::InterpretService.new(project, current_user).
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index ab667456db7..a2a5f57d069 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -52,8 +52,12 @@ module Issues
end
def cloneable_label_ids
- @new_project.labels
- .where(title: @old_issue.labels.pluck(:title)).pluck(:id)
+ params = {
+ project_id: @new_project.id,
+ title: @old_issue.labels.pluck(:title)
+ }
+
+ LabelsFinder.new(current_user, params).execute.pluck(:id)
end
def cloneable_milestone_id
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
new file mode 100644
index 00000000000..74291312c4e
--- /dev/null
+++ b/app/services/labels/find_or_create_service.rb
@@ -0,0 +1,33 @@
+module Labels
+ class FindOrCreateService
+ def initialize(current_user, project, params = {})
+ @current_user = current_user
+ @group = project.group
+ @project = project
+ @params = params.dup
+ end
+
+ def execute
+ find_or_create_label
+ end
+
+ private
+
+ attr_reader :current_user, :group, :project, :params
+
+ def available_labels
+ @available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute
+ end
+
+ def find_or_create_label
+ new_label = available_labels.find_by(title: title)
+ new_label ||= project.labels.create(params)
+
+ new_label
+ end
+
+ def title
+ params[:title] || params[:name]
+ end
+ end
+end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
new file mode 100644
index 00000000000..514679ed29d
--- /dev/null
+++ b/app/services/labels/transfer_service.rb
@@ -0,0 +1,78 @@
+# Labels::TransferService class
+#
+# User for recreate the missing group labels at project level
+#
+module Labels
+ class TransferService
+ def initialize(current_user, old_group, project)
+ @current_user = current_user
+ @old_group = old_group
+ @project = project
+ end
+
+ def execute
+ return unless old_group.present?
+
+ Label.transaction do
+ labels_to_transfer.find_each do |label|
+ new_label_id = find_or_create_label!(label)
+
+ next if new_label_id == label.id
+
+ update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id)
+ update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id)
+ update_label_priorities(old_label_id: label.id, new_label_id: new_label_id)
+ end
+ end
+ end
+
+ private
+
+ attr_reader :current_user, :old_group, :project
+
+ def labels_to_transfer
+ label_ids = []
+ label_ids << group_labels_applied_to_issues.select(:id)
+ label_ids << group_labels_applied_to_merge_requests.select(:id)
+
+ union = Gitlab::SQL::Union.new(label_ids)
+
+ Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq
+ end
+
+ def group_labels_applied_to_issues
+ Label.joins(:issues).
+ where(
+ issues: { project_id: project.id },
+ labels: { type: 'GroupLabel', group_id: old_group.id }
+ )
+ end
+
+ def group_labels_applied_to_merge_requests
+ Label.joins(:merge_requests).
+ where(
+ merge_requests: { target_project_id: project.id },
+ labels: { type: 'GroupLabel', group_id: old_group.id }
+ )
+ end
+
+ def find_or_create_label!(label)
+ params = label.attributes.slice('title', 'description', 'color')
+ new_label = FindOrCreateService.new(current_user, project, params).execute
+
+ new_label.id
+ end
+
+ def update_label_links(labels, old_label_id:, new_label_id:)
+ LabelLink.joins(:label).
+ merge(labels).
+ where(label_id: old_label_id).
+ update_all(label_id: new_label_id)
+ end
+
+ def update_label_priorities(old_label_id:, new_label_id:)
+ LabelPriority.where(project_id: project.id, label_id: old_label_id).
+ update_all(label_id: new_label_id)
+ end
+ end
+end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
index ca9db59cac7..b7a244c2029 100644
--- a/app/services/members/authorized_destroy_service.rb
+++ b/app/services/members/authorized_destroy_service.rb
@@ -14,6 +14,8 @@ module Members
if member.request? && member.user != user
notification_service.decline_access_request(member)
end
+
+ member
end
end
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 9a2bf82ef51..431da8372c9 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -1,17 +1,42 @@
module Members
class DestroyService < BaseService
- attr_accessor :member, :current_user
+ include MembersHelper
- def initialize(member, current_user)
- @member = member
+ attr_accessor :source
+
+ ALLOWED_SCOPES = %i[members requesters all]
+
+ def initialize(source, current_user, params = {})
+ @source = source
@current_user = current_user
+ @params = params
end
- def execute
- unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
- raise Gitlab::Access::AccessDeniedError
- end
+ def execute(scope = :members)
+ raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
+
+ member = find_member!(scope)
+
+ raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
+
AuthorizedDestroyService.new(member, current_user).execute
end
+
+ private
+
+ def find_member!(scope)
+ condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
+ case scope
+ when :all
+ source.members.find_by(condition) ||
+ source.requesters.find_by!(condition)
+ else
+ source.public_send(scope).find_by!(condition)
+ end
+ end
+
+ def can_destroy_member?(member)
+ member && can?(current_user, action_member_permission(:destroy, member), member)
+ 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/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index 566049525cb..d572a928a42 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -2,14 +2,14 @@ module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
def execute(commit_status)
- each_merge_request(commit_status) do |merge_request|
+ commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
# Closes any pending build failed todos for the parent MRs when a build is retried
def close(commit_status)
- each_merge_request(commit_status) do |merge_request|
+ commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
new file mode 100644
index 00000000000..066efa1acc3
--- /dev/null
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -0,0 +1,35 @@
+module MergeRequests
+ class AssignIssuesService < BaseService
+ def assignable_issues
+ @assignable_issues ||= begin
+ if current_user == merge_request.author
+ closes_issues.select do |issue|
+ !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue)
+ end
+ else
+ []
+ end
+ end
+ end
+
+ def execute
+ assignable_issues.each do |issue|
+ Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue)
+ end
+
+ {
+ count: assignable_issues.count
+ }
+ end
+
+ private
+
+ def merge_request
+ params[:merge_request]
+ end
+
+ def closes_issues
+ @closes_issues ||= params[:closes_issues] || merge_request.closes_issues(current_user)
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index ba424b09463..58f69a41e14 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -5,16 +5,17 @@ module MergeRequests
end
def create_title_change_note(issuable, old_title)
- removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress?
- added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress?
+ removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress?
+ added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress?
+ changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title
if removed_wip
SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
elsif added_wip
SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
- else
- super
end
+
+ super if changed_title
end
def hook_data(merge_request, action, oldrev = nil)
@@ -41,28 +42,33 @@ module MergeRequests
super(:merge_request)
end
- def merge_request_from(commit_status)
- branches = commit_status.ref
+ def merge_requests_for(branch)
+ origin_merge_requests = @project.origin_merge_requests
+ .opened.where(source_branch: branch).to_a
- # This is for ref-less builds
- branches ||= @project.repository.branch_names_contains(commit_status.sha)
+ fork_merge_requests = @project.fork_merge_requests
+ .opened.where(source_branch: branch).to_a
- return [] if branches.blank?
+ (origin_merge_requests + fork_merge_requests)
+ .uniq.select(&:source_project)
+ end
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
- merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
+ def pipeline_merge_requests(pipeline)
+ merge_requests_for(pipeline.ref).each do |merge_request|
+ next unless pipeline == merge_request.pipeline
- merge_requests.uniq.select(&:source_project)
+ yield merge_request
+ end
end
- def each_merge_request(commit_status)
- merge_request_from(commit_status).each do |merge_request|
+ def commit_status_merge_requests(commit_status)
+ merge_requests_for(commit_status.ref).each do |merge_request|
pipeline = merge_request.pipeline
next unless pipeline
next unless pipeline.sha == commit_status.sha
- yield merge_request, pipeline
+ yield merge_request
end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index e57791f6818..404f75616b5 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
merge_request = MergeRequest.new(params)
# Set MR attributes
- merge_request.can_be_created = false
+ merge_request.can_be_created = true
merge_request.compare_commits = []
merge_request.source_project = project unless merge_request.source_project
@@ -22,6 +22,12 @@ module MergeRequests
return build_failed(merge_request, message)
end
+ if merge_request.source_project == merge_request.target_project &&
+ merge_request.target_branch == merge_request.source_branch
+
+ return build_failed(merge_request, 'You must select different branches')
+ end
+
compare = CompareService.new.execute(
merge_request.source_project,
merge_request.source_branch,
@@ -29,17 +35,8 @@ module MergeRequests
merge_request.target_branch,
)
- commits = compare.commits
-
- # At this point we decide if merge request can be created
- # If we have at least one commit to merge -> creation allowed
- if commits.present?
- merge_request.compare_commits = commits
- merge_request.can_be_created = true
- merge_request.compare = compare
- else
- merge_request.can_be_created = false
- end
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
set_title_and_description(merge_request)
end
@@ -89,6 +86,8 @@ module MergeRequests
end
end
+ merge_request.title = merge_request.wip_title if commits.empty?
+
merge_request
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index b037780c431..ab9056a3250 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -11,14 +11,14 @@ module MergeRequests
def execute(merge_request)
@merge_request = merge_request
- return error('Merge request is not mergeable') unless @merge_request.mergeable?
+ return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
merge_request.in_locked_state do
if commit
after_merge
success
else
- error('Can not merge changes')
+ log_merge_error('Can not merge changes', true)
end
end
end
@@ -46,8 +46,8 @@ module MergeRequests
merge_request.update(merge_error: e.message)
false
rescue StandardError => e
- merge_request.update(merge_error: "Something went wrong during merge")
- Rails.logger.error(e.message)
+ merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
+ log_merge_error(e.message)
false
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
@@ -65,5 +65,17 @@ module MergeRequests
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
+
+ def log_merge_error(message, http_error = false)
+ Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
+
+ error(message) if http_error
+ end
+
+ def merge_request_info
+ project = merge_request.project
+
+ "#{project.to_reference}#{merge_request.to_reference}"
+ end
end
end
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index 4ad5fb08311..dc159de0058 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -18,12 +18,13 @@ module MergeRequests
merge_request.save
end
- # Triggers the automatic merge of merge_request once the build succeeds
- def trigger(commit_status)
- each_merge_request(commit_status) do |merge_request, pipeline|
+ # Triggers the automatic merge of merge_request once the pipeline succeeds
+ def trigger(pipeline)
+ return unless pipeline.success?
+
+ pipeline_merge_requests(pipeline) do |merge_request|
next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable?
- next unless pipeline.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
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/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
index 19caa038c44..d22a1d3e0ad 100644
--- a/app/services/merge_requests/resolve_service.rb
+++ b/app/services/merge_requests/resolve_service.rb
@@ -1,5 +1,8 @@
module MergeRequests
class ResolveService < MergeRequests::BaseService
+ class MissingFiles < Gitlab::Conflict::ResolutionError
+ end
+
attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request)
@@ -10,8 +13,16 @@ module MergeRequests
fetch_their_commit!
- conflicts.files.each do |file|
- write_resolved_file_to_index(file, params[:sections])
+ params[:files].each do |file_params|
+ conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path])
+
+ write_resolved_file_to_index(conflict_file, file_params)
+ end
+
+ unless merge_index.conflicts.empty?
+ missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
+
+ raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end
commit_params = {
@@ -23,8 +34,13 @@ module MergeRequests
project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
end
- def write_resolved_file_to_index(file, resolutions)
- new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
+ def write_resolved_file_to_index(file, params)
+ new_file = if params[:sections]
+ file.resolve_lines(params[:sections]).map(&:text).join("\n")
+ elsif params[:content]
+ file.resolve_content(params[:content])
+ end
+
our_path = file.our_path
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index f14f9e4b327..a37cc3fdf21 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -15,8 +15,11 @@ module MergeRequests
params.except!(:target_branch, :force_remove_source_branch)
end
- merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+ if params[:force_remove_source_branch].present?
+ merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+ end
+ handle_wip_event(merge_request)
update(merge_request)
end
@@ -81,5 +84,18 @@ module MergeRequests
def after_update(issuable)
issuable.cache_merge_request_closes_issues!(current_user)
end
+
+ private
+
+ def handle_wip_event(merge_request)
+ if wip_event = params.delete(:wip_event)
+ # We update the title that is provided in the params or we use the mr title
+ title = params[:title] || merge_request.title
+ params[:title] = case wip_event
+ when 'wip' then MergeRequest.wip_title(title)
+ when 'unwip' then MergeRequest.wipless_title(title)
+ end
+ end
+ end
end
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index a36008c3ef5..723cc0e6834 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -7,8 +7,10 @@ module Notes
if note.award_emoji?
noteable = note.noteable
- todo_service.new_award_emoji(noteable, current_user)
- return noteable.create_award_emoji(note.award_emoji_name, current_user)
+ if noteable.user_can_award?(current_user, note.award_emoji_name)
+ todo_service.new_award_emoji(noteable, current_user)
+ return noteable.create_award_emoji(note.award_emoji_name, current_user)
+ end
end
# We execute commands (extracted from `params[:note]`) on the noteable
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 6139ed56e25..72712afc07e 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
@@ -474,10 +475,12 @@ class NotificationService
end
def reject_users_without_access(recipients, target)
- return recipients unless target.is_a?(Issue)
+ return recipients unless target.is_a?(Issuable)
+
+ ability = :"read_#{target.to_ability_name}"
recipients.select do |user|
- user.can?(:read_issue, target)
+ user.can?(ability, target)
end
end
@@ -514,9 +517,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 +567,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 +596,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/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index f578f8dbea2..015f2828921 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -13,7 +13,7 @@ module Projects
end
def labels
- @project.labels.select([:title, :color])
+ LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
end
def commands(noteable, type)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index be749ba4a1c..15d7918e7fd 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -7,6 +7,8 @@ module Projects
def execute
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
+ @skip_wiki = params.delete(:skip_wiki)
+
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
@@ -15,6 +17,11 @@ module Projects
return @project
end
+ unless allowed_fork?(forked_from_project_id)
+ @project.errors.add(:forked_from_project_id, 'is forbidden')
+ return @project
+ end
+
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
@@ -71,6 +78,13 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
+ def allowed_fork?(source_project_id)
+ return true if source_project_id.nil?
+
+ source_project = Project.find_by(id: source_project_id)
+ current_user.can?(:fork_project, source_project)
+ end
+
def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace)
@@ -80,7 +94,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
- @project.create_wiki if @project.feature_available?(:wiki, current_user)
+ @project.create_wiki unless skip_wiki?
@project.build_missing_services
@project.create_labels
@@ -94,6 +108,10 @@ module Projects
end
end
+ def skip_wiki?
+ !@project.feature_available?(:wiki, current_user) || @skip_wiki
+ end
+
def save_project_and_import_data(import_data)
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index a2de4dccece..a2b23ea6171 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -16,6 +16,8 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
+ return new_project unless new_project.persisted?
+
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
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/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index bc7f8bf433b..28470f59807 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -28,6 +28,7 @@ module Projects
Project.transaction do
old_path = project.path_with_namespace
old_namespace = project.namespace
+ old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
@@ -57,6 +58,9 @@ module Projects
# Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
+ # Move missing group labels to project
+ Labels::TransferService.new(current_user, old_group, project).execute
+
# clear project cached events
project.reset_events_cache
diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/api_create_service.rb
new file mode 100644
index 00000000000..f2040dfa03a
--- /dev/null
+++ b/app/services/protected_branches/api_create_service.rb
@@ -0,0 +1,29 @@
+# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+ class ApiCreateService < BaseService
+ def execute
+ push_access_level =
+ if params.delete(:developers_can_push)
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MASTER
+ end
+
+ merge_access_level =
+ if params.delete(:developers_can_merge)
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MASTER
+ end
+
+ @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
+ merge_access_levels_attributes: [{ access_level: merge_access_level }])
+
+ service = ProtectedBranches::CreateService.new(@project, @current_user, @params)
+ service.execute
+ end
+ end
+end
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb
new file mode 100644
index 00000000000..050cb3b738b
--- /dev/null
+++ b/app/services/protected_branches/api_update_service.rb
@@ -0,0 +1,47 @@
+# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+ class ApiUpdateService < BaseService
+ def execute(protected_branch)
+ @developers_can_push = params.delete(:developers_can_push)
+ @developers_can_merge = params.delete(:developers_can_merge)
+
+ @protected_branch = protected_branch
+
+ protected_branch.transaction do
+ delete_redundant_access_levels
+
+ case @developers_can_push
+ when true
+ params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ when false
+ params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ end
+
+ case @developers_can_merge
+ when true
+ params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ when false
+ params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ end
+
+ service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
+ service.execute(protected_branch)
+ end
+ end
+
+ private
+
+ def delete_redundant_access_levels
+ unless @developers_can_merge.nil?
+ @protected_branch.merge_access_levels.destroy_all
+ end
+
+ unless @developers_can_push.nil?
+ @protected_branch.push_access_levels.destroy_all
+ end
+ end
+ end
+end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index ffcad5b3a87..5a81194a5f4 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -116,13 +116,20 @@ module SlashCommands
desc 'Add label(s)'
params '~label1 ~"label 2"'
condition do
+ available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
+
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
- project.labels.any?
+ available_labels.any?
end
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
- @updates[:add_label_ids] = label_ids unless label_ids.empty?
+ if label_ids.any?
+ @updates[:add_label_ids] ||= []
+ @updates[:add_label_ids] += label_ids
+
+ @updates[:add_label_ids].uniq!
+ end
end
desc 'Remove all or specific label(s)'
@@ -136,7 +143,12 @@ module SlashCommands
if labels_param.present?
label_ids = find_label_ids(labels_param)
- @updates[:remove_label_ids] = label_ids unless label_ids.empty?
+ if label_ids.any?
+ @updates[:remove_label_ids] ||= []
+ @updates[:remove_label_ids] += label_ids
+
+ @updates[:remove_label_ids].uniq!
+ end
else
@updates[:label_ids] = []
end
@@ -152,7 +164,12 @@ module SlashCommands
command :relabel do |labels_param|
label_ids = find_label_ids(labels_param)
- @updates[:label_ids] = label_ids unless label_ids.empty?
+ if label_ids.any?
+ @updates[:label_ids] ||= []
+ @updates[:label_ids] += label_ids
+
+ @updates[:label_ids].uniq!
+ end
end
desc 'Add a todo'
@@ -214,6 +231,18 @@ module SlashCommands
@updates[:due_date] = nil
end
+ desc do
+ "Toggle the Work In Progress status"
+ end
+ condition do
+ issuable.persisted? &&
+ issuable.respond_to?(:work_in_progress?) &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :wip do
+ @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
+ end
+
# This is a dummy command, so that it appears in the autocomplete commands
desc 'CC'
params '@user'
@@ -221,7 +250,7 @@ module SlashCommands
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
- labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id)
+ labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
label_ids_by_reference | labels_ids_by_name
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 1fb72cf89e9..a2bfa422c9d 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -72,7 +72,7 @@ class SystemHooksService
return 'user_add_to_group' if event == :create
return 'user_remove_from_group' if event == :destroy
else
- "#{model.class.name.downcase}_#{event.to_s}"
+ "#{model.class.name.downcase}_#{event}"
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 0c8446e7c3d..1ce66d50368 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
@@ -245,7 +246,7 @@ module SystemNoteService
'deleted'
end
- body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize
+ body = "#{verb} #{branch_type} branch `#{branch}`".capitalize
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)
@@ -347,7 +347,7 @@ module SystemNoteService
notes = notes.where(noteable_id: noteable.id)
end
- notes_for_mentioner(mentioner, noteable, notes).count > 0
+ notes_for_mentioner(mentioner, noteable, notes).exists?
end
# Build an Array of lines detailing each commit added in a merge request
@@ -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 776530ac0a5..f8e6b2ef094 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -273,12 +273,12 @@ class TodoService
end
def reject_users_without_access(users, project, target)
- if target.is_a?(Note) && target.for_issue?
+ if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
target = target.noteable
end
- if target.is_a?(Issue)
- select_users(users, :read_issue, target)
+ if target.is_a?(Issuable)
+ select_users(users, :"read_#{target.to_ability_name}", target)
else
select_users(users, :read_project, project)
end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 7a35958cc5f..2821ecf0a88 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
@@ -23,6 +24,7 @@ class NamespaceValidator < ActiveModel::EachValidator
projects
public
repository
+ robots.txt
s
search
services
@@ -31,7 +33,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/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 56bf6194914..05f3d9a3b50 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -21,7 +21,7 @@
%td
%strong.subheading.visible-xs-block.visible-sm-block Message
.message
- = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
+ = markdown_field(abuse_report, :message)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
index 6c51639b840..acbe17036f7 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview.html.haml
@@ -1,9 +1,12 @@
-- page_title "Preview | Appearance"
+= render 'devise/shared/tab_single', tab_title: 'Sign in preview'
.login-box
- .login-heading
- %h3 Existing user? Sign in
- %form
- = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
- = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
- = button_tag "Sign in", class: "btn-create btn"
+ %form.show-gl-field-errors
+ .form-group
+ = label_tag :login
+ = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
+ .form-group
+ = label_tag :password
+ = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.'
+ .form-group
+ = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 0d79ca7dc52..c4c68cd7891 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -221,7 +221,11 @@
%fieldset
%legend Metrics
%p
- These settings require a restart to take effect.
+ Setup InfluxDB to measure a wide variety of statistics like the time spent
+ in running SQL queries. These settings require a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
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/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 6b157abf842..3132d157f29 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -1,7 +1,10 @@
.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
= icon('bullhorn')
.js-broadcast-message-preview
- = render_broadcast_message(@broadcast_message.message.presence || "Your message here")
+ - if @broadcast_message.message.present?
+ = render_broadcast_message(@broadcast_message)
+ - else
+ = "Your message here"
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
@@ -18,11 +21,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/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml
index fbc9453c72e..c72e59640d7 100644
--- a/app/views/admin/broadcast_messages/preview.js.haml
+++ b/app/views/admin/broadcast_messages/preview.js.haml
@@ -1 +1 @@
-$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}");
+$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
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/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e6687f43816..90798c47d97 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -63,6 +63,11 @@
Reply by email
%span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
+ %p
+ Container Registry
+ %span.light.pull-right
+ = boolean_to_icon Gitlab.config.registry.enabled
+
.col-md-4
%h4
Components
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 77a11e49e20..adfa1eaafc9 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -23,4 +23,4 @@
- if group.description.present?
.description
- = markdown(group.description, pipeline: :description)
+ = markdown_field(group, :description)
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/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index f417b2e44a4..be224d66855 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,7 +1,7 @@
%li{id: dom_id(label)}
.label-row
= render_colored_label(label, tooltip: false)
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
.pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
= link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 1e755785d90..339cfc613fe 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -87,7 +87,7 @@
- if project.description.present?
.description
- = markdown(project.description, pipeline: :description)
+ = markdown_field(project, :description)
= paginate @projects, theme: 'gitlab'
- else
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index a53876d6757..37bb6a3b0e0 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
@@ -73,4 +75,4 @@
- @runners.each do |runner|
= render "admin/runners/runner", runner: runner
- = paginate @runners
+ = paginate @runners, theme: "gitlab"
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 61abfc6ecbe..73038164056 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
@@ -67,11 +67,11 @@
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: @runner.id
= f.submit 'Enable', class: 'btn btn-xs'
- = paginate @projects
+ = paginate @projects, theme: "gitlab"
.col-md-6
- %h4 Recent builds served by this runner
- %table.table.builds.runner-builds
+ %h4 Recent builds served by this Runner
+ %table.table.ci-table.runner-builds
%thead
%tr
%th Build
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index f7875e68b7e..61c7cce20b2 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -16,18 +16,20 @@
%tr
%td #{stage.capitalize} Job - #{build[:name]}
%td
- %pre
- = simple_format build[:commands]
+ %pre= build[:commands]
%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/groups/_empty_state.html.haml b/app/views/dashboard/groups/_empty_state.html.haml
new file mode 100644
index 00000000000..f5222fe631e
--- /dev/null
+++ b/app/views/dashboard/groups/_empty_state.html.haml
@@ -0,0 +1,7 @@
+.groups-empty-state
+ = custom_icon("icon_empty_groups")
+
+ .text-content
+ %h4 A group is a collection of several projects.
+ %p If you organize your projects under a group, it works like a folder.
+ %p You can manage your group member’s permissions and access to each project in the group.
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index caca91af536..1a679c51774 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -2,9 +2,12 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
-%ul.content-list
- - @group_members.each do |group_member|
- - group = group_member.group
- = render 'shared/groups/group', group: group, group_member: group_member
+- if @group_members.empty?
+ = render 'empty_state'
+- else
+ %ul.content-list
+ - @group_members.each do |group_member|
+ - group = group_member.group
+ = render 'shared/groups/group', group: group, group_member: group_member
-= paginate @group_members, theme: 'gitlab'
+ = paginate @group_members, theme: 'gitlab'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index b40395c74de..cc077fad32a 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -19,6 +19,7 @@
(removed)
&middot; #{time_ago_with_tooltip(todo.created_at)}
+ = todo_due_date(todo)
.todo-body
.todo-note
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 9d31f31c639..2a0302638ba 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -55,7 +55,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to todos_filter_path(sort: sort_value_priority) do
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 73c3a3dd2eb..20cd7b0179d 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -3,9 +3,9 @@
Almost there...
%p.lead
Please check your email to confirm your account
-- if after_sign_up_text.present?
+- if current_application_settings.after_sign_up_text.present?
.well-confirmation.text-center
- = markdown(after_sign_up_text)
+ = markdown_field(current_application_settings, :after_sign_up_text)
%p.confirmation-content.text-center
No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
index 970ba147111..5d25dd398d6 100644
--- a/app/views/devise/confirmations/new.html.haml
+++ b/app/views/devise/confirmations/new.html.haml
@@ -1,14 +1,14 @@
+= render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions'
.login-box
- .login-heading
- %h3 Resend confirmation instructions
.login-body
- = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f|
+ = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
- .clearfix.append-bottom-20
- = f.email_field :email, placeholder: 'Email', class: "form-control", required: true
+ .form-group
+ = f.label :email
+ = f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.'
.clearfix
- = f.submit "Resend confirmation instructions", class: 'btn btn-success'
+ = f.submit "Resend", class: 'btn btn-success'
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 56048e99c17..b518fae7c95 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -1,19 +1,21 @@
+= render 'devise/shared/tab_single', tab_title:'Change your password'
.login-box
- .login-heading
- %h3 Change your password
.login-body
- = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
+ = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'show-gl-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
= f.hidden_field :reset_password_token
- %div
- = f.password_field :password, class: "form-control top", placeholder: "New password", required: true
- %div
- = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true
+ .form-group
+ = f.label 'New password', for: :password
+ = f.password_field :password, class: "form-control top", required: true, title: 'This field is required'
+ .form-group
+ = f.label 'Confirm new password', for: :password_confirmation
+ = f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true
.clearfix
= f.submit "Change your password", class: "btn btn-primary"
.clearfix.prepend-top-20
%p
- = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
- = render 'devise/shared/sign_in_link'
+ %span.light Didn't receive a confirmation email?
+ = link_to "Request a new one", new_confirmation_path(resource_name)
+= render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 535e85869e5..1fcfd06419a 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -1,12 +1,12 @@
+= render 'devise/shared/tab_single', tab_title: 'Reset Password'
.login-box
- .login-heading
- %h3 Reset password
.login-body
- = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f|
+ = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
- .clearfix.append-bottom-20
- = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true
+ .form-group
+ = f.label :email
+ = f.email_field :email, class: "form-control", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.'
.clearfix
= f.submit "Reset password", class: "btn-primary btn"
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 9f5520603cd..5fd896f6835 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,12 +1,16 @@
-= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
- = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off"
- = f.password_field :password, class: "form-control bottom", placeholder: "Password"
+= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-field-errors', 'aria-live' => 'assertive'}) do |f|
+ %div.form-group
+ = f.label "Username or email", for: :login
+ = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
+ %div.form-group
+ = f.label :password
+ = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
+ %div.submit-container.move-submit-down
+ = f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{for: "user_remember_me"}
= f.check_box :remember_me
%span Remember me
- .pull-right
+ .pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
- %div
- = f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index b7d3acac2b1..1d381ad7893 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,9 +1,13 @@
-= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do
- = text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"}
- = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
+= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do
+ .form-group
+ = label_tag :username, 'Username or email'
+ = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
+ .form-group
+ = label_tag :password
+ = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{for: "remember_me"}
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
- = button_tag "Sign in", class: "btn-save btn"
+ = submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 2ef383960f4..c18bc2ac413 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,9 +1,13 @@
-= 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"}
+= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do
+ .form-group
+ = label_tag :username, "#{server['label']} Username"
+ = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
+ .form-group
+ = label_tag :password
+ = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{for: "remember_me"}
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
- = button_tag "Sign in", class: "btn-save btn"
+ = submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 28194506acc..fa8e7979461 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,19 +1,22 @@
- page_title "Sign in"
%div
- - if signin_enabled? || ldap_enabled? || crowd_enabled?
- = render 'devise/shared/signin_box'
+ - if form_based_providers.any?
+ = render 'devise/shared/tabs_ldap'
+ - else
+ = render 'devise/shared/tabs_normal'
+ .tab-content
+ - if signin_enabled? || ldap_enabled? || crowd_enabled?
+ = render 'devise/shared/signin_box'
- -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled?
- .clearfix.prepend-top-20
- = render 'devise/shared/omniauth_box'
-
- -# Signup only makes sense if you can also sign-in
- - if signin_enabled? && signup_enabled?
- .prepend-top-20
+ -# Signup only makes sense if you can also sign-in
+ - if signin_enabled? && signup_enabled?
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
+
+ - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled?
+ .clearfix
+ = render 'devise/shared/omniauth_box'
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index e623f7cff88..fd77cdbee2e 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -3,20 +3,19 @@
= page_specific_javascript_tag('u2f.js')
%div
+ = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
.login-box
- .login-heading
- %h3 Two-Factor Authentication
.login-body
- if @user.two_factor_otp_enabled?
- %h5 Authenticate via Two-Factor App
- = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
+ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
- = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
- %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
- .prepend-top-20
- = f.submit "Verify code", class: "btn btn-save"
+ %div
+ = f.label 'Two-Factor Authentication code', name: :otp_attempt
+ = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
+ %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
+ .prepend-top-20
+ = f.submit "Verify code", class: "btn btn-save"
- if @user.two_factor_u2f_enabled?
- %hr
= render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 2e7da2747d0..8908b64cdac 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,8 +1,9 @@
-%p
- %span.light
- Sign in with &nbsp;
- - providers = enabled_button_based_providers
- - providers.each do |provider|
+%div.omniauth-container
+ %p
%span.light
- - has_icon = provider_has_icon?(provider)
- = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
+ Sign in with &nbsp;
+ - providers = enabled_button_based_providers
+ - providers.each do |provider|
+ %span.light
+ - has_icon = provider_has_icon?(provider)
+ = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml
index fafc4b82f53..289bf40f3de 100644
--- a/app/views/devise/shared/_sign_in_link.html.haml
+++ b/app/views/devise/shared/_sign_in_link.html.haml
@@ -1,5 +1,4 @@
%p
%span.light
Already have login and password?
- %strong
= link_to "Sign in", new_session_path(resource_name)
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 2c15e2c4891..86edaf14e43 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -1,32 +1,18 @@
-.login-box
- - if signup_enabled?
- .login-heading
- %h3 Existing user? Sign in
- - else
- .login-heading
- %h3 Sign in
- .login-body
- - if form_based_providers.any?
- %ul.nav-links
- - if crowd_enabled?
- %li.active
- = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
- - @ldap_servers.each_with_index do |server, i|
- %li{class: (:active if i.zero? && !crowd_enabled?)}
- = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab'
- - if signin_enabled?
- %li
- = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab'
- .tab-content
- - if crowd_enabled?
- %div.tab-pane.active{id: "tab-crowd"}
- = render 'devise/sessions/new_crowd'
- - @ldap_servers.each_with_index do |server, i|
- %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)}
- = render 'devise/sessions/new_ldap', server: server
- - if signin_enabled?
- %div#tab-signin.tab-pane
- = render 'devise/sessions/new_base'
+- if form_based_providers.any?
+ - if crowd_enabled?
+ .login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'}
+ .login-body
+ = render 'devise/sessions/new_crowd'
+ - @ldap_servers.each_with_index do |server, i|
+ .login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)}
+ .login-body
+ = render 'devise/sessions/new_ldap', server: server
+ - if signin_enabled?
+ .login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'}
+ .login-body
+ = render 'devise/sessions/new_base'
- - elsif signin_enabled?
+- elsif signin_enabled?
+ .login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'}
+ .login-body
= render 'devise/sessions/new_base'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 905a8dbcd84..d0bbcf3115e 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,29 +1,30 @@
-.login-box
- - if signin_enabled?
- .login-heading
- %h3 New user? Create an account
- - else
- .login-heading
- %h3 Create an account
+#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
.login-body
- = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name)) do |f|
+ = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user show-gl-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors
= devise_error_messages!
- %div
- = f.text_field :name, class: "form-control top", placeholder: "Name", required: true
- %div
- = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
- %div
- = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
+ %div.form-group
+ = f.label :name
+ = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
+ %div.username.form-group
+ = f.label :username
+ = f.text_field :username, class: "form-control middle no-gl-field-error", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
+ %p.validation-error.hide Username is already taken.
+ %p.validation-success.hide Username is available.
+ %p.validation-pending.hide Checking username availability...
+ %div.form-group
+ = f.label :email
+ = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
.form-group.append-bottom-20#password-strength
- = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters"
+ = f.label :password
+ = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
+ %p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div
- if current_application_settings.recaptcha_enabled
= recaptcha_tags
%div
- = f.submit "Sign up", class: "btn-create btn"
-
-.clearfix.prepend-top-20
+ = f.submit "Register", class: "btn-register btn"
+.clearfix.submit-container
%p
%span.light Didn't receive a confirmation email?
= succeed '.' do
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
new file mode 100644
index 00000000000..f943d25e41a
--- /dev/null
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -0,0 +1,3 @@
+%ul.nav-links.nav-tabs.new-session-tabs.single-tab
+ %li.active
+ %a= tab_title
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
new file mode 100644
index 00000000000..1e957f0935f
--- /dev/null
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -0,0 +1,10 @@
+%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
+ - if crowd_enabled?
+ %li.active
+ = link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
+ - @ldap_servers.each_with_index do |server, i|
+ %li{class: (:active if i.zero? && !crowd_enabled?)}
+ = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
+ - if signin_enabled?
+ %li
+ = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
new file mode 100644
index 00000000000..79b1d447a92
--- /dev/null
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -0,0 +1,5 @@
+%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'}
+ %li.active{ role: 'presentation' }
+ %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in
+ %li{ role: 'presentation'}
+ %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register
diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml
index 49c087c0646..49b2f77111f 100644
--- a/app/views/devise/unlocks/new.html.haml
+++ b/app/views/devise/unlocks/new.html.haml
@@ -1,12 +1,12 @@
+= render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions'
.login-box
- .login-heading
- %h3 Resend unlock email
.login-body
- = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f|
+ = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
- .clearfix.append-bottom-20
- = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off'
+ .form-group.append-bottom-20
+ = f.label :email
+ = f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: 'Please provide a valid email address.'
.clearfix
= f.submit 'Resend unlock instructions', class: 'btn btn-success'
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index b8248a80a27..a1b39d9e1a0 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -23,7 +23,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index cd485da5104..4cff14b096b 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -7,8 +7,8 @@
= visibility_level_label(params[:visibility_level].to_i)
- else
Any
- %b.caret
- %ul.dropdown-menu
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(visibility_level: nil) do
Any
@@ -27,8 +27,8 @@
= params[:tag]
- else
Any
- %b.caret
- %ul.dropdown-menu
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
Any
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 2fb3190ab11..b185b81db7f 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -1,27 +1,22 @@
-= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
- .form-group
- = f.label :user_ids, "People", class: 'control-label'
- .col-sm-10
- = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
- .help-block
+= form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f|
+ .row
+ .col-md-4.col-lg-6
+ = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
+ .help-block.append-bottom-10
Search for users by name, username, or email, or invite new ones using their email address.
- .form-group
- = f.label :access_level, "Group Access", class: 'control-label'
- .col-sm-10
- = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2"
- .help-block
- Read more about role permissions
- %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .col-md-3.col-lg-2
+ = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
+ .help-block.append-bottom-10
+ = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ about role permissions
- .form-group
- = f.label :expires_at, 'Access expiration date', class: 'control-label'
- .col-sm-10
+ .col-md-3.col-lg-2
.clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
- .help-block
+ .help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this group and all of its projects.
- .form-actions
- = f.submit 'Add users to group', class: "btn btn-create"
+ .col-md-2
+ = f.submit 'Add to group', class: "btn btn-create btn-block"
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index f789796e942..ebf9aca7700 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,35 +1,31 @@
- page_title "Members"
-.group-members-page.prepend-top-default
+.project-members-page.prepend-top-default
+ %h4
+ Members
+ %hr
- if can?(current_user, :admin_group_member, @group)
- .panel.panel-default
- .panel-heading
- Add new user to group
- .panel-body
- %p.light
- Members of group have access to all group projects.
- .new-group-member-holder
- = render "new_group_member"
+ .project-members-new.append-bottom-default
+ %p.clearfix
+ Add new user to
+ %strong= @group.name
+ = render "new_group_member"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters
+ .append-bottom-default.clearfix
+ %h5.member.existing-title
+ Existing users
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
.panel.panel-default
.panel-heading
+ Users with access to
%strong #{@group.name}
- group members
%span.badge= @members.total_count
- .controls
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
- = button_tag class: 'btn', title: 'Search' do
- = icon("search")
%ul.content-list
= render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab'
-
-:javascript
- $('form.member-search-form').on('submit', function(event) {
- event.preventDefault();
- Turbolinks.visit(this.action + '?' + $(this).serialize());
- });
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index 3be7ed8432c..de8f53b6b52 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 gl.MemberExpirationDate();
+ var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+ $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
diff --git a/app/views/groups/labels/destroy.js.haml b/app/views/groups/labels/destroy.js.haml
new file mode 100644
index 00000000000..3dfbfc77c0d
--- /dev/null
+++ b/app/views/groups/labels/destroy.js.haml
@@ -0,0 +1,2 @@
+- if @group.labels.empty?
+ $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml
new file mode 100644
index 00000000000..836981fc6fd
--- /dev/null
+++ b/app/views/groups/labels/edit.html.haml
@@ -0,0 +1,7 @@
+- page_title 'Edit', @label.name, 'Labels'
+
+%h3.page-title
+ Edit Label
+%hr
+
+= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
new file mode 100644
index 00000000000..70783a63409
--- /dev/null
+++ b/app/views/groups/labels/index.html.haml
@@ -0,0 +1,20 @@
+- page_title 'Labels'
+
+.top-area.adjust
+ .nav-text
+ Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
+
+ .nav-controls
+ - if can?(current_user, :admin_label, @group)
+ = link_to new_group_label_path(@group), class: "btn btn-new" do
+ New label
+
+.labels
+ .other-labels
+ - if @labels.present?
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', collection: @labels, as: :label
+ = paginate @labels, theme: 'gitlab'
+ - else
+ .nothing-here-block
+ No labels created yet.
diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml
new file mode 100644
index 00000000000..2be87460b1d
--- /dev/null
+++ b/app/views/groups/labels/new.html.haml
@@ -0,0 +1,8 @@
+- page_title 'New Label'
+- header_title group_title(@group, 'Labels', group_labels_path(@group))
+
+%h3.page-title
+ New Label
+%hr
+
+= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index ca6c4326d1c..23d438b2aa1 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -33,8 +33,8 @@
.form-group
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
- = f.collection_select :project_ids, @group.projects, :id, :name,
- { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
+ = f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
+ { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
.col-md-6
.form-group
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 31db6ee0cad..fab61f447c2 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -21,7 +21,7 @@
- if @group.description.present?
.cover-desc.description
- = markdown(@group.description, pipeline: :description)
+ = markdown_field(@group, :description)
%div.groups-header{ class: container_class }
.top-area
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 57601ae9be0..31631887317 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -20,7 +20,7 @@
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
- if current_application_settings.help_page_text.present?
%hr
- = markdown(current_application_settings.help_page_text)
+ = markdown_field(current_application_settings, :help_page_text)
%hr
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 44e2653ca4a..767dffb5589 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -9,12 +9,12 @@
%p
Project will be imported as
%strong
- #{@namespace_name}/#{@path}
+ #{@namespace.name}/#{@path}
%p
To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
.form-group
- = hidden_field_tag :namespace_id, @namespace_id
+ = hidden_field_tag :namespace_id, @namespace.id
= hidden_field_tag :path, @path
= label_tag :file, class: 'control-label' do
%span GitLab project export
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/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 67ff4b272b9..e138ebab018 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,7 +1,8 @@
- project = @target_project || @project
- noteable_type = @noteable.class if @noteable.present?
-:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
- GitLab.GfmAutoComplete.cachedData = undefined;
- GitLab.GfmAutoComplete.setup();
+- if project
+ :javascript
+ GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
+ GitLab.GfmAutoComplete.cachedData = undefined;
+ GitLab.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 4f7839a881f..8aefdcb3d9b 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,10 +1,11 @@
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll
.sidebar-action-buttons
- = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
+ .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" }
%span.sr-only Toggle navigation
= icon('bars')
- = link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
+
+ %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } }
%span.sr-only Toggle navigation pinning
= icon('fw thumb-tack')
@@ -20,6 +21,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/application.html.haml b/app/views/layouts/application.html.haml
index 15a94ac23c5..6c2285fa2b6 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -11,3 +11,4 @@
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
+ = render "layouts/init_auto_complete" if @gfm_form
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 3d28eec84ef..6922f1e153f 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,36 +1,37 @@
!!! 5
-%html{ lang: "en"}
+%html{ lang: "en", class: "devise-layout-html"}
= render "layouts/head"
- %body.ui_charcoal.login-page.application.navless
- = Gon::Base.render_data
- = render "layouts/header/empty"
- = render "layouts/broadcast"
- .container.navless-container
- .content
- = render "layouts/flash"
- .row
- .col-sm-5.pull-right
- = yield
- .col-sm-7.brand-holder.pull-left
- %h1
- = brand_title
- - if brand_item
- = brand_image
- = brand_text
- - else
- %h3 Open source software to collaborate on code
+ %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }}
+ .page-wrap
+ = Gon::Base.render_data
+ = render "layouts/header/empty"
+ = render "layouts/broadcast"
+ .container.navless-container
+ .content
+ = render "layouts/flash"
+ .row
+ .col-sm-5.pull-right.new-session-forms-container
+ = yield
+ .col-sm-7.brand-holder.pull-left
+ %h1
+ = brand_title
+ - if brand_item
+ = brand_image
+ = brand_text
+ - else
+ %h3 Open source software to collaborate on code
- %p
- Manage git repositories with fine grained access controls that keep your code secure.
- Perform code reviews and enhance collaboration with merge requests.
- Each project can also have an issue tracker and a wiki.
+ %p
+ Manage Git repositories with fine-grained access controls that keep your code secure.
+ Perform code reviews and enhance collaboration with merge requests.
+ Each project can also have an issue tracker and a wiki.
- - if extra_sign_in_text.present?
- = markdown(extra_sign_in_text)
+ - if current_application_settings.sign_in_text.present?
+ = markdown_field(current_application_settings, :sign_in_text)
- %hr
- .container
- .footer-links
- = link_to "Explore", explore_root_path
- = link_to "Help", help_path
- = link_to "About GitLab", "https://about.gitlab.com/"
+ %hr.footer-fixed
+ .container.footer-container
+ .footer-links
+ = link_to "Explore", explore_root_path
+ = link_to "Help", help_path
+ = link_to "About GitLab", "https://about.gitlab.com/"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 9d6af4d0b51..dde27fd270d 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,5 +1,5 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
+ %div{ class: "container-fluid" }
.header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation
@@ -41,7 +41,7 @@
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- %span.caret
+ = icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 27ac1760166..f7edb47b666 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -13,6 +13,10 @@
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
+ = nav_link(controller: [:group, :labels]) do
+ = link_to group_labels_path(@group), title: 'Labels' do
+ %span
+ Labels
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index e44a2bfed9d..99a58bbb676 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -116,4 +116,4 @@
-# Shortcut to issue boards
%li.hidden
- = link_to 'Issue Boards', namespace_project_board_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
+ = link_to 'Issue Boards', namespace_project_boards_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
new file mode 100644
index 00000000000..0995826775a
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -0,0 +1,177 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
+ %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
+ %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+ %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %tbody
+ %tr.line
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %tr.header
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr.alert
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
+ %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ Your pipeline has failed.
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ = namespace_name
+ \/
+ %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ = @project.name
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ = @pipeline.ref
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ = @merge_request.to_reference
+ .commit{style: "color:#5c5c5c;font-weight:300;"}
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ - commit = @pipeline.commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ - if commit.author
+ %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ - failed = @pipeline.statuses.latest.failed
+ %tr.pre-section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"}
+ Pipeline
+ %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = "\##{@pipeline.id}"
+ had
+ = failed.size
+ failed
+ = "#{'build'.pluralize(failed.size)}."
+ %tr.warning
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"}
+ Logs may contain sensitive data. Please consider before forwarding this email.
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"}
+ %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"}
+ %tbody
+ - failed.each do |build|
+ %tr.build-state
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"}
+ %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
+ = build.stage
+ %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
+ %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"}
+ = build.name
+ %tr.build-log
+ %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
+ %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"}
+ = build.trace_html(last_lines: 10).html_safe
+ %tr.footer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %div
+ %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ &middot;
+ %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %div
+ You're receiving this email because of your account on
+ = succeed "." do
+ %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
new file mode 100644
index 00000000000..8f8084b58e1
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -0,0 +1,31 @@
+Your pipeline has failed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% failed = @pipeline.statuses.latest.failed -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
+
+<% failed.each do |build| -%>
+Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> )
+Stage: <%= build.stage %>
+Name: <%= build.name %>
+Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
+
+<% end -%>
+
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
+Manage all notifications: <%= profile_notifications_url %>
+Help: <%= help_url %>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
new file mode 100644
index 00000000000..cf9c1d4d72c
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -0,0 +1,154 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
+ %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
+ %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+ %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %tbody
+ %tr.line
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %tr.header
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr.success
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
+ %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ Your pipeline has passed.
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ = namespace_name
+ \/
+ %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ = @project.name
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ = @pipeline.ref
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ = @merge_request.to_reference
+ .commit{style: "color:#5c5c5c;font-weight:300;"}
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ - commit = @pipeline.commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ - if commit.author
+ %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ %tr.success-message
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
+ - build_count = @pipeline.statuses.latest.size
+ - stage_count = @pipeline.stages.size
+ Pipeline
+ %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = "\##{@pipeline.id}"
+ successfully completed
+ = "#{build_count} #{'build'.pluralize(build_count)}"
+ in
+ = "#{stage_count} #{'stage'.pluralize(stage_count)}."
+ %tr.footer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %div
+ %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ &middot;
+ %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %div
+ You're receiving this email because of your account on
+ = succeed "." do
+ %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
new file mode 100644
index 00000000000..ae22d474f2c
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -0,0 +1,24 @@
+Your pipeline has passed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% build_count = @pipeline.statuses.latest.size -%>
+<% stage_count = @pipeline.stages.size -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
+
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
+Manage all notifications: <%= profile_notifications_url %>
+Help: <%= help_url %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index c80f22457b4..e2e974ba072 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -86,11 +86,11 @@
= f.label :username, "Path", class: "label-light"
.input-group
.input-group-addon
- = "#{root_url}u/"
+ = root_url
= f.text_field :username, required: true, class: 'form-control'
.help-block
Current path:
- = "#{root_url}u/#{current_user.username}"
+ = "#{root_url}#{current_user.username}"
.prepend-top-default
= f.button class: "btn btn-warning", type: "submit" do
= icon "spinner spin", class: "hidden loading-username"
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 4433cab7782..8966dd3fd86 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
- $('.content-wrapper').find('.container-fluid').removeClass('container-limited')
+ $('.content-wrapper .container-fluid').removeClass('container-limited')
} else {
- $('.content-wrapper').find('.container-fluid').addClass('container-limited')
+ $('.content-wrapper .container-fluid').addClass('container-limited')
}
// Re-enable the "Save" button
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/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml
new file mode 100644
index 00000000000..d2c1e943db1
--- /dev/null
+++ b/app/views/projects/_customize_workflow.html.haml
@@ -0,0 +1,8 @@
+.row-content-block.project-home-empty
+ %div.text-center{ class: container_class }
+ %h4
+ Customize your workflow!
+ %p
+ Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production!
+ - if can?(current_user, :admin_project, @project)
+ = link_to "Get started", edit_project_path(@project), class: "btn btn-success"
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 8ef31ca3bda..d3987fc9c4f 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -9,7 +9,7 @@
.project-home-desc
- if @project.description.present?
- = markdown(@project.description, pipeline: :description)
+ = markdown_field(@project, :description)
- if forked_from_project = @project.forked_from_project
%p
@@ -22,5 +22,6 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- .project-clone-holder
- = render "shared/clone_panel"
+ - if @project.feature_available?(:repository, current_user)
+ .project-clone-holder
+ = render "shared/clone_panel"
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/_wiki.html.haml b/app/views/projects/_wiki.html.haml
new file mode 100644
index 00000000000..f00422dd7c0
--- /dev/null
+++ b/app/views/projects/_wiki.html.haml
@@ -0,0 +1,19 @@
+- if @wiki_home.present?
+ %div{ class: container_class }
+ .wiki-holder.prepend-top-default.append-bottom-default
+ .wiki
+ = preserve do
+ = render_wiki_content(@wiki_home)
+- else
+ - can_create_wiki = can?(current_user, :create_wiki, @project)
+ .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
+ %div.text-center{ class: container_class }
+ %h4
+ This project does not have a wiki homepage yet
+ - if can_create_wiki
+ %p
+ Add a homepage to your wiki that contains information about your project
+ %p
+ We recommend you
+ = link_to "add a homepage", namespace_project_wiki_path(@project.namespace, @project, :home)
+ to your project's wiki and GitLab will show it here instead of this message.
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index cb97181b9e1..0c8241053e7 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,3 +1,4 @@
+- @gfm_form = true
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area'
@@ -7,6 +8,3 @@
= 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 5a98e258b22..dfb96305f48 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,45 +1,48 @@
+- @no_container = true
- page_title "Blame", @blob.path, @ref
+= render "projects/commits/head"
-%h3.page-title Blame view
+%div{ class: container_class }
+ %h3.page-title Blame view
-#blob-content-holder.tree-holder
- .file-holder
- .file-title
- = blob_icon @blob.mode, @blob.name
- %strong
- = @path
- %small= number_to_human_size @blob.size
- .file-actions
- = render "projects/blob/actions"
- .table-responsive.file-content.blame.code.js-syntax-highlight
- %table
- - current_line = 1
- - @blame_groups.each do |blame_group|
- %tr
- %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"
- .pull-right
- = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace"
- &nbsp;
- .light
- = commit_author_link(commit, avatar: false)
- authored
- #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
- %td.line-numbers
- - line_count = blame_group[:lines].count
- - (current_line...(current_line + line_count)).each do |i|
- %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
- = icon("link")
- = i
- \
- - current_line += line_count
- %td.lines
- %pre.code.highlight
- %code
- - blame_group[:lines].each do |line|
- #{line}
+ #blob-content-holder.tree-holder
+ .file-holder
+ .file-title
+ = blob_icon @blob.mode, @blob.name
+ %strong
+ = @path
+ %small= number_to_human_size @blob.size
+ .file-actions
+ = render "projects/blob/actions"
+ .table-responsive.file-content.blame.code.js-syntax-highlight
+ %table
+ - current_line = 1
+ - @blame_groups.each do |blame_group|
+ %tr
+ %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"
+ .pull-right
+ = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace"
+ &nbsp;
+ .light
+ = commit_author_link(commit, avatar: false)
+ authored
+ #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
+ %td.line-numbers
+ - line_count = blame_group[:lines].count
+ - (current_line...(current_line + line_count)).each do |i|
+ %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ = icon("link")
+ = i
+ \
+ - current_line += line_count
+ %td.lines
+ %pre.code.highlight
+ %code
+ - blame_group[:lines].each do |line|
+ #{line}
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/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 680e95ac6b5..2a0352a71b7 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,28 +1,31 @@
+- @no_container = true
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+= render "projects/commits/head"
-- if @conflict
- .alert.alert-danger
- Someone edited the file the same time you did. Please check out
- = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
- and make sure your changes will not unintentionally remove theirs.
+%div{ class: container_class }
+ - if @conflict
+ .alert.alert-danger
+ Someone edited the file the same time you did. Please check out
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+ and make sure your changes will not unintentionally remove theirs.
-.file-editor
- %ul.nav-links.no-bottom.js-edit-mode
- %li.active
- = link_to '#editor' do
- Edit File
+ .file-editor
+ %ul.nav-links.no-bottom.js-edit-mode
+ %li.active
+ = link_to '#editor' do
+ Edit File
- %li
- = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
- = editing_preview_title(@blob.name)
+ %li
+ = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
+ = editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
- = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
- = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
- = hidden_field_tag 'last_commit_sha', @last_commit_sha
- = hidden_field_tag 'content', '', id: "file-content"
- = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
- = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
+ = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
+ = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
+ = hidden_field_tag 'last_commit_sha', @last_commit_sha
+ = hidden_field_tag 'content', '', id: "file-content"
+ = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
+ = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 73066150fb3..ba1502c97b6 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -12,8 +12,17 @@
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }}
- %span.pull-right{ "v-if" => "list.type !== 'blank'" }
- {{ list.issuesSize }}
+ .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
+ %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
+ {{ list.issuesSize }}
+ - if can?(current_user, :admin_issue, @project)
+ %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => "list.type !== 'done'",
+ "aria-label" => "Add an issue",
+ "title" => "Add an issue",
+ data: { placement: "top", container: "body" } }
+ = icon("plus")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
@@ -26,12 +35,38 @@
":issues" => "list.issues",
":loading" => "list.loading",
":disabled" => "disabled",
+ ":show-issue-form.sync" => "showIssueForm",
":issue-link-base" => "issueLinkBase" }
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
+ - if can? current_user, :create_issue, @project
+ %board-new-issue{ "inline-template" => true,
+ ":list" => "list",
+ ":show-issue-form.sync" => "showIssueForm",
+ "v-show" => "list.type !== 'done' && showIssueForm" }
+ .card.board-new-issue-form
+ %form{ "@submit" => "submit($event)" }
+ .flash-container{ "v-if" => "error" }
+ .flash-alert
+ An error occured. Please try again.
+ %label.label-light{ ":for" => "list.id + '-title'" }
+ Title
+ %input.form-control{ type: "text",
+ "v-model" => "title",
+ "v-el:input" => true,
+ ":id" => "list.id + '-title'" }
+ .clearfix.prepend-top-10
+ %button.btn.btn-success.pull-left{ type: "submit",
+ ":disabled" => "title === ''",
+ "v-el:submit-button" => true }
+ Submit issue
+ %button.btn.btn-default.pull-right{ type: "button",
+ "@click" => "cancel" }
+ Cancel
%ul.board-list{ "v-el:list" => true,
"v-show" => "!loading",
- ":data-board" => "list.id" }
+ ":data-board" => "list.id",
+ ":class" => "{ 'is-smaller': showIssueForm }" }
= render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index e8b60b54d80..c6d718a1cd1 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -7,7 +7,7 @@
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled }",
+ %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
":index" => "index" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
@@ -15,7 +15,7 @@
":title" => "issue.title" }
{{ issue.title }}
.card-footer
- %span.card-number
+ %span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
@@ -26,7 +26,7 @@
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- %a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username",
+ %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
":title" => "'Assigned to ' + issue.assignee.name",
"v-if" => "issue.assignee",
data: { container: 'body' } }
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
new file mode 100644
index 00000000000..885f8e34b55
--- /dev/null
+++ b/app/views/projects/boards/index.html.haml
@@ -0,0 +1,16 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('boards/boards_bundle.js')
+ = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+.boards-list#board-app{ "v-cloak" => true, data: board_data }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index edbbd3f3d2a..885f8e34b55 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -10,10 +10,7 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true,
- "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}",
- "data-disabled" => "#{!can?(current_user, :admin_list, @project)}",
- "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" }
+.boards-list#board-app{ "v-cloak" => true, data: board_data }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 5217b8bf028..4480b2f22c3 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -30,8 +30,8 @@
= render 'projects/buttons/download', project: @project, ref: branch.name
- - if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ - if can?(current_user, :push_code, @project)
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: "btn btn-remove remove-row has-tooltip #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index e889f29c816..84f38575e84 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -15,7 +15,7 @@
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
= projects_sort_options_hash[@sort]
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_branches_path(sort: sort_value_name) do
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index c57b07e549e..fd3e3ca8d83 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,5 +1,4 @@
-- builds = @build.pipeline.builds.latest.to_a
-- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+- builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
@@ -8,7 +7,7 @@
%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
@@ -95,7 +94,7 @@
- @build.trigger_request.variables.each do |key, value|
.hide.js-build
- .js-build-variable= key
+ .js-build-variable= key
.js-build-value= value
.block
@@ -124,22 +123,16 @@
%a.stage-item= stage
.builds-container
- - statuses.each do |build_status|
+ - HasStatus::ORDERED_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}}
+ .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}}
= link_to namespace_project_build_path(@project.namespace, @project, build) do
- = icon('check')
+ = icon('arrow-right')
= 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.
+ - if build.retried?
+ %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'}
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index c2bcfb773a6..36294c89fa8 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -1,11 +1,11 @@
- admin = local_assigns.fetch(:admin, false)
- if builds.blank?
- %li
+ %div
.nothing-here-block No builds to show
- else
.table-holder
- %table.table.builds
+ %table.table.ci-table.builds-page
%thead
%tr
%th Status
diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml
index 2642de8021d..83f299da651 100644
--- a/app/views/projects/builds/_user.html.haml
+++ b/app/views/projects/builds/_user.html.haml
@@ -1,4 +1,7 @@
by
%a{ href: user_path(@build.user) }
- = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
- %strong= @build.user.to_reference
+ %span.hidden-xs
+ = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
+ %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
+ = @build.user.name
+ %strong.visible-xs-inline= @build.user.to_reference
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 5c60b7a7364..06070f12bbd 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -19,5 +19,5 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
- %ul.content-list.builds-content-list
+ %div.content-list.builds-content-list
= render "table", builds: @builds, project: @project
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index e4d41288aa6..b5e8b0bf6eb 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,56 +1,59 @@
+- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
- trace_with_state = @build.trace_with_state
- header_title project_title(@project, "Builds", project_builds_path(@project))
+= render "projects/pipelines/head", build_subnav: true
-.build-page
- = render "header"
+%div{ class: container_class }
+ .build-page
+ = render "header"
- - if @build.stuck?
- - unless @build.any_runners_online?
- .bs-callout.bs-callout-warning
- %p
- - if no_runners_for_project?(@build.project)
- This build is stuck, because the project doesn't have any runners online assigned to it.
- - elsif @build.tags.any?
- This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
- - @build.tags.each do |tag|
- %span.label.label-primary
- = tag
- - else
- This build is stuck, because you don't have any active runners that can run this build.
+ - if @build.stuck?
+ - unless @build.any_runners_online?
+ .bs-callout.bs-callout-warning
+ %p
+ - if no_runners_for_project?(@build.project)
+ This build is stuck, because the project doesn't have any runners online assigned to it.
+ - elsif @build.tags.any?
+ This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
+ - @build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - else
+ This build is stuck, because you don't have any active runners that can run this build.
- %br
- Go to
- = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
- Runners page
+ %br
+ Go to
+ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
+ Runners page
- .prepend-top-default
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- - if @build.erased?
- .erased.alert.alert-warning
- - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
- Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- - else
- #js-build-scroll.scroll-controls
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
- %pre.build-trace#build-trace
- %code.bash.js-build-output
- = icon("refresh spin", class: "js-build-refresh")
+ .prepend-top-default
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+ - if @build.erased?
+ .erased.alert.alert-warning
+ - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
+ Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+ - else
+ #js-build-scroll.scroll-controls
+ = link_to '#build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
+ %pre.build-trace#build-trace
+ %code.bash.js-build-output
+ = icon("refresh spin", class: "js-build-refresh")
- #down-build-trace
+ #down-build-trace
-= render "sidebar"
+ = render "sidebar"
-:javascript
- new Build({
- page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
- build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
- build_status: "#{@build.status}",
- build_stage: "#{@build.stage}",
- state1: "#{trace_with_state[:state]}"
- })
+ :javascript
+ new Build({
+ page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
+ build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
+ build_status: "#{@build.status}",
+ build_stage: "#{@build.stage}",
+ state1: "#{trace_with_state[:state]}"
+ })
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 24de020917a..7e83a88913a 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,9 +1,9 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'}
+ %span{class: 'hidden-xs hidden-sm download-button'}
.dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
- %span.caret
+ = icon("caret-down")
%span.sr-only
Select Archive Format
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index ca907077c2b..6cd9b98a706 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,7 +1,8 @@
- if current_user
- .btn-group
+ .dropdown.inline.project-dropdown
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
+ = icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 22db33498f1..29d549a60f5 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -5,10 +5,10 @@
= custom_icon('icon_fork')
%span Fork
- else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do
= custom_icon('icon_fork')
%span Fork
%div.count-with-arrow
%span.arrow
- = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
+ = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do
= @project.forks_count
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 75192c48188..94632056b15 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -6,52 +6,50 @@
- coverage = local_assigns.fetch(:coverage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
-%tr.build.commit
+%tr.build.commit{class: ('retried' if retried)}
%td.status
- if can?(current_user, :read_build, build)
= ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
- else
= ci_status_with_icon(build.status)
- %td
- .branch-commit
- - if can?(current_user, :read_build, build)
- = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
- %span.build-link ##{build.id}
- - else
+ %td.branch-commit
+ - if can?(current_user, :read_build, build)
+ = 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 ref
- - 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
+ - if ref
+ - if build.ref
.icon-container
- = custom_icon("icon_commit")
+ = 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.commit-icon
+ = custom_icon("icon_commit")
+
+ - if commit_sha
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
- - 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 build.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- - if retried
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
+ - if retried
+ = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
- .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
- - if retried
- %span.label.label-warning retried
- - if build.manual?
- %span.label.label-info manual
+ .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
+ - if build.manual?
+ %span.label.label-info manual
- if admin
%td
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 547bc0c9c19..55965172d3f 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,12 +1,14 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
- 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
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } 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)
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
+ %span.ci-status-icon
+ = render_status_with_link('build', subject.status)
.ci-status-text= subject.name
- else
- = render_status_with_link('build', subject.status)
+ %span.ci-status-icon
+ = 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 04e48a4dc17..c6f359f5679 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -9,44 +9,50 @@
= ci_icon_for_status(status)
- else
= ci_status_with_icon(status)
+
%td
- .branch-commit
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- %span ##{pipeline.id}
- - 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?
- %span.label.label-primary triggered
- - if pipeline.yaml_errors.present?
- %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- - if pipeline.builds.any?(&:stuck?)
- %span.label.label-warning stuck
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
+ %span.pipeline-id ##{pipeline.id}
+ %span by
+ - if pipeline.user
+ = user_avatar(user: pipeline.user, size: 20)
+ - else
+ %span.api.monospace API
+ - if pipeline.latest?
+ %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
+ - if pipeline.triggered?
+ %span.label.label-primary triggered
+ - if pipeline.yaml_errors.present?
+ %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
+ - if pipeline.builds.any?(&:stuck?)
+ %span.label.label-warning stuck
- %p.commit-title
- - if commit = pipeline.commit
- = author_avatar(commit, size: 20)
- = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- - else
- Cant find HEAD commit for this branch
+ %td.branch-commit
+ - 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.commit-icon
+ = custom_icon("icon_commit")
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
+ %p.commit-title
+ - if commit = pipeline.commit
+ = author_avatar(commit, size: 20)
+ = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
+ - else
+ Cant find HEAD commit for this branch
- - stages_status = pipeline.statuses.relevant.latest.stages_status
- %td.stage-cell
- - stages.each do |stage|
- - status = stages_status[stage]
- - tooltip = "#{stage.titleize}: #{status || 'not found'}"
- - if status
- .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)
+ - stages_status = pipeline.statuses.relevant.latest.stages_status
+ %td.stage-cell
+ - stages.each do |stage|
+ - status = stages_status[stage]
+ - tooltip = "#{stage.titleize}: #{status || 'not found'}"
+ - if status
+ .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
@@ -58,8 +64,8 @@
= icon("calendar")
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
- %td.pipeline-actions
- .controls.hidden-xs.pull-right
+ %td.pipeline-actions.hidden-xs
+ .controls.pull-right
- artifacts = pipeline.builds.latest.with_artifacts_not_expired
- actions = pipeline.manual_actions
- if artifacts.present? || actions.any?
@@ -68,7 +74,7 @@
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
@@ -79,7 +85,7 @@
.btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download")
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 29d767e7769..6c82a4e5600 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -14,7 +14,7 @@
.dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
%span.hidden-xs Options
- %span.caret.commit-options-dropdown-caret
+ = icon('caret-down', class: ".commit-options-dropdown-caret")
%ul.dropdown-menu.dropdown-menu-align-right
%li.visible-xs-block.visible-sm-block
= link_to namespace_project_tree_path(@project.namespace, @project, @commit) do
@@ -24,6 +24,8 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
+ %li.clearfix
+ = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider
%li.dropdown-header
Download
@@ -63,10 +65,10 @@
.commit-box.content-block
%h3.commit-title
- = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author
+ = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author))
+ = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
:javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 9258f4b3c25..d6916fb7f1a 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,45 +1,46 @@
-.row-content-block.build-content.middle-block.pipeline-actions
- .pull-right
- .btn.btn-grouped.btn-white.toggle-pipeline-btn
- %span.toggle-btn-text Hide
- %span pipeline graph
- %span.caret
- - if can?(current_user, :update_pipeline, pipeline.project)
- - if pipeline.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
+.pipeline-graph-container
+ .row-content-block.build-content.middle-block.pipeline-actions
+ .pull-right
+ .btn.btn-grouped.btn-white.toggle-pipeline-btn
+ %span.toggle-btn-text Hide
+ %span pipeline graph
+ %span.caret
+ - if can?(current_user, :update_pipeline, pipeline.project)
+ - if pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
- - if pipeline.builds.running_or_pending.any?
- = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+ - if pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
- .oneline.clearfix
- - if defined?(pipeline_details) && pipeline_details
- Pipeline
- = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
- with
- = pluralize pipeline.statuses.count(:id), "build"
- - if pipeline.ref
- for
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
- - if defined?(link_to_commit) && link_to_commit
- for commit
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
- - if pipeline.duration
- in
- = time_interval_in_words pipeline.duration
+ .oneline.clearfix
+ - if defined?(pipeline_details) && pipeline_details
+ Pipeline
+ = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
+ with
+ = pluralize pipeline.statuses.count(:id), "build"
+ - if pipeline.ref
+ for
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
+ - if defined?(link_to_commit) && link_to_commit
+ for commit
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
+ - if pipeline.duration
+ in
+ = time_interval_in_words pipeline.duration
-.row-content-block.build-content.middle-block.pipeline-graph
- .pipeline-visualization
- %ul.stage-column-list
- - stages = pipeline.stages_with_latest_statuses
- - stages.each do |stage, statuses|
- %li.stage-column
- .stage-name
- %a{name: stage}
- - if stage
- = stage.titleize
- .builds-container
- %ul
- = render "projects/commit/pipeline_stage", statuses: statuses
+ .row-content-block.build-content.middle-block.pipeline-graph.hidden
+ .pipeline-visualization
+ %ul.stage-column-list
+ - stages = pipeline.stages_with_latest_statuses
+ - stages.each do |stage, statuses|
+ %li.stage-column
+ .stage-name
+ %a{name: stage}
+ - if stage
+ = stage.titleize
+ .builds-container
+ %ul
+ = render "projects/commit/pipeline_stage", statuses: statuses
- if pipeline.yaml_errors.present?
@@ -55,7 +56,7 @@
\.gitlab-ci.yml not found in this commit
.table-holder.pipeline-holder
- %table.table.builds.pipeline
+ %table.table.ci-table.pipeline
%thead
%tr
%th Status
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
index 23c5c51fbc2..289aa5178b1 100644
--- a/app/views/projects/commit/_pipeline_stage.html.haml
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -10,5 +10,5 @@
- else
%li.build
.curve
- .build-content
+ .dropdown.inline.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
index 4e7a6f1af08..f2d71fa6989 100644
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -1,11 +1,13 @@
- 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
+%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
+ %span.ci-status-icon
+ = render_status_with_link('build', group_status)
+ %span.ci-status-text
+ = name
+ %span.badge= subject.size
+.dropdown-menu.grouped-pipeline-dropdown
+ .arrow
+ %ul
- subject.each do |status|
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ %li
+ = 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 998812793a2..ac451441eec 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -4,10 +4,11 @@
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.builds
+ %table.table.ci-table
%tbody
%th Status
%th Pipeline
+ %th Commit
%th Stages
%th
%th
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
index 2f051fb90e0..f9d7eac3542 100644
--- a/app/views/projects/commit/builds.html.haml
+++ b/app/views/projects/commit/builds.html.haml
@@ -1,7 +1,10 @@
+- @no_container = true
- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
+= render "projects/commits/head"
-.prepend-top-default
- = render "commit_box"
+%div{ class: container_class }
+ .prepend-top-default
+ = render "commit_box"
-= render "ci_menu"
-= render "builds"
+ = render "ci_menu"
+ = render "builds"
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index ed44d86a687..cebf58d63df 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,14 +1,17 @@
+- @no_container = true
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
+= render "projects/commits/head"
-.prepend-top-default
- = render "commit_box"
-- if @commit.status
- = render "ci_menu"
-- else
- %div.block-connector
-= render "projects/diffs/diffs", diffs: @diffs
-= render "projects/notes/notes_with_form"
-- if can_collaborate_with_project?
- - %w(revert cherry-pick).each do |type|
- = render "projects/commit/change", type: type, commit: @commit, title: @commit.title
+%div{ class: container_class }
+ .prepend-top-default
+ = render "commit_box"
+ - if @commit.status
+ = render "ci_menu"
+ - else
+ %div.block-connector
+ = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/notes/notes_with_form"
+ - if can_collaborate_with_project?
+ - %w(revert cherry-pick).each do |type|
+ = render "projects/commit/change", type: type, commit: @commit, title: @commit.title
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 389477d0927..fb48aef0559 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -33,7 +33,7 @@
- if commit.description?
%pre.commit-row-description.js-toggle-content
- = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
+ = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
.commit-row-info
= commit_author_link(commit, avatar: false, size: 24)
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..7bde20c3286 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.inline ...
+ .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..05fb37cdc0f 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_title "Select Git revision"
+ = dropdown_filter "Filter by Git revision"
= dropdown_content
= dropdown_loading
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index e9ff8e90dd5..45be6581cfc 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -4,7 +4,7 @@
%div{ class: container_class }
.sub-header-block
- Compare branches, tags or commit ranges.
+ Compare Git revisions.
%br
Fill input field with commit id like
%code.label-branch 4eedf23
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 7f346df8797..b647882efa0 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -2,10 +2,10 @@
- page_title "Cycle Analytics"
= render "projects/pipelines/head"
-#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
+#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()")
+ = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
.row
.col-sm-3.col-xs-12.svg-container
= custom_icon('icon_cycle_analytics_splash')
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 16d134eb6b6..58a214bdbd1 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,22 +1,15 @@
-- if can?(current_user, :create_deployment, deployment) && deployment.deployable
- .pull-right
- - actions = deployment.manual_actions
- - if actions.present?
- .inline
- .dropdown
- %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
- = custom_icon('icon_play')
- %b.caret
- %ul.dropdown-menu.dropdown-menu-align-right
- - actions.each do |action|
- %li
- = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
- = custom_icon('icon_play')
- %span= action.name.humanize
+- if can?(current_user, :create_deployment, deployment)
+ - actions = deployment.manual_actions
+ - if actions.present?
+ .inline
+ .dropdown
+ %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ = custom_icon('icon_play')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - actions.each do |action|
+ %li
+ = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
+ = custom_icon('icon_play')
+ %span= action.name.humanize
- - if local_assigns.fetch(:allow_rollback, false)
- = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- - if deployment.last?
- Re-deploy
- - else
- Rollback
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 28813babd7b..ff250eeca50 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -3,7 +3,7 @@
.icon-container
= deployment.tag? ? icon('tag') : icon('code-fork')
= link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name"
- .icon-container
+ .icon-container.commit-icon
= custom_icon("icon_commit")
= link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index cd95841ca5a..9238f232c7e 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -5,14 +5,18 @@
%td
= render 'projects/deployments/commit', deployment: deployment
- %td
+ %td.build-column
- if deployment.deployable
- = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
- = user_avatar(user: deployment.user, size: 20)
+ = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
= "#{deployment.deployable.name} (##{deployment.deployable.id})"
+ - if deployment.user
+ by
+ = user_avatar(user: deployment.user, size: 20)
%td
#{time_ago_with_tooltip(deployment.created_at)}
- %td
- = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
+ %td.hidden-xs
+ .pull-right
+ = render 'projects/deployments/actions', deployment: deployment
+ = render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
new file mode 100644
index 00000000000..5941e01c6f1
--- /dev/null
+++ b/app/views/projects/deployments/_rollback.haml
@@ -0,0 +1,6 @@
+- if can?(current_user, :create_deployment, deployment) && deployment.deployable
+ = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
+ - if deployment.last?
+ Re-deploy
+ - else
+ Rollback
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/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 62aff36aadd..067cf595da3 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,7 +1,6 @@
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
+- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
-- if diff_view == :parallel
- - fluid_layout true
.content-block.oneline-block.files-changed
.inline-parallel-buttons
@@ -22,7 +21,7 @@
- if diff_files.overflow?
= render 'projects/diffs/warning', diff_files: diff_files
-.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}}
+.files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file, index|
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 1a51ccd4c7d..257e0a855bd 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -5,10 +5,10 @@
- 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 files", disabled: @diff_notes_disabled 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')
\
-
+ = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option')
- if editable_diff?(diff_file)
- link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
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 a04d53e02bf..30473d14b9b 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -46,61 +46,70 @@
%h5.prepend-top-0
Feature Visibility
- = f.fields_for :project_feature do |feature_fields|
- .form_group.prepend-top-20
- .row
- .col-md-9
- = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
- %span.help-block Lightweight issue tracking system for this project
- .col-md-3
- = project_feature_access_select(:issues_access_level)
+ = f.fields_for :project_feature do |feature_fields|
+ .form_group.prepend-top-20
+ .row
+ .col-md-9
+ = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
+ %span.help-block Push files to be stored in this project
+ .col-md-3.js-repo-access-level
+ = project_feature_access_select(:repository_access_level)
- .row
- .col-md-9
- = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
- %span.help-block Submit changes to be merged upstream
- .col-md-3
- = project_feature_access_select(:merge_requests_access_level)
+ .col-sm-12
+ .row
+ .col-md-9.project-feature-nested
+ = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
+ %span.help-block Submit changes to be merged upstream
+ .col-md-3
+ = project_feature_access_select(:merge_requests_access_level)
- .row
- .col-md-9
- = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
- %span.help-block Submit Test and deploy your changes before merge
- .col-md-3
- = project_feature_access_select(:builds_access_level)
+ .row
+ .col-md-9.project-feature-nested
+ = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
+ %span.help-block Submit, test and deploy your changes before merge
+ .col-md-3
+ = project_feature_access_select(:builds_access_level)
- .row
- .col-md-9
- = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
- %span.help-block Pages for project documentation
- .col-md-3
- = project_feature_access_select(:wiki_access_level)
+ .row
+ .col-md-9
+ = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
+ %span.help-block Share code pastes with others out of Git repository
+ .col-md-3
+ = project_feature_access_select(:snippets_access_level)
- .row
- .col-md-9
- = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
- %span.help-block Share code pastes with others out of Git repository
- .col-md-3
- = project_feature_access_select(:snippets_access_level)
+ .row
+ .col-md-9
+ = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
+ %span.help-block Lightweight issue tracking system for this project
+ .col-md-3
+ = project_feature_access_select(:issues_access_level)
- - if Gitlab.config.lfs.enabled && current_user.admin?
.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')
+ = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
+ %span.help-block Pages for project documentation
.col-md-3
- = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control'
+ = project_feature_access_select(:wiki_access_level)
- - if Gitlab.config.registry.enabled
- .form-group
- .checkbox
- = f.label :container_registry_enabled do
- = f.check_box :container_registry_enabled
- %strong Container Registry
- %br
- %span.descr Enable Container Registry for this repository
+ - if Gitlab.config.lfs.enabled && current_user.admin?
+ .checkbox
+ = f.label :lfs_enabled do
+ = f.check_box :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')
+
+ - if Gitlab.config.registry.enabled
+ .form-group
+ .checkbox
+ = f.label :container_registry_enabled do
+ = f.check_box :container_registry_enabled
+ %strong Container Registry
+ %br
+ %span.descr Enable Container Registry for this project
+ = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
= render 'merge_request_settings', f: f
%hr
@@ -279,4 +288,4 @@
Saving project.
%p Please wait a moment, this page will automatically refresh when ready.
-= render 'shared/confirm_modal', phrase: @project.path
+= render 'shared/confirm_modal', phrase: @project.path \ No newline at end of file
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/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index 36a6162a5a8..b75d5df4150 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -4,10 +4,17 @@
%td
= link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
- %td
+ %td.deployment-column
- if last_deployment
- = user_avatar(user: last_deployment.user, size: 20)
- %strong ##{last_deployment.id}
+ %span ##{last_deployment.iid}
+ - if last_deployment.user
+ by
+ = user_avatar(user: last_deployment.user, size: 20)
+
+ %td
+ - if last_deployment && last_deployment.deployable
+ = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
+ = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
%td
- if last_deployment
@@ -20,5 +27,9 @@
- if last_deployment
#{time_ago_with_tooltip(last_deployment.created_at)}
- %td
- = render 'projects/deployments/actions', deployment: last_deployment
+ %td.hidden-xs
+ .pull-right
+ = render 'projects/environments/external_url', environment: environment
+ = render 'projects/deployments/actions', deployment: last_deployment
+ = render 'projects/environments/stop', environment: environment
+ = render 'projects/deployments/rollback', deployment: last_deployment
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
new file mode 100644
index 00000000000..4c8fe1c271b
--- /dev/null
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -0,0 +1,3 @@
+- if environment.external_url && can?(current_user, :read_environment, environment)
+ = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+ = icon('external-link')
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
new file mode 100644
index 00000000000..69848123c17
--- /dev/null
+++ b/app/views/projects/environments/_stop.html.haml
@@ -0,0 +1,5 @@
+- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+ .inline
+ = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
+ class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
+ = icon('stop', class: 'stop-env-icon')
diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml
index 6d1bdb9320f..3871165763c 100644
--- a/app/views/projects/environments/edit.html.haml
+++ b/app/views/projects/environments/edit.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
- page_title "Edit", @environment.name, "Environments"
+= render "projects/pipelines/head"
-%h3.page-title
- Edit environment
-%hr
-= render 'form'
+%div{ class: container_class }
+ %h3.page-title
+ Edit environment
+ %hr
+ = render 'form'
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index b3eb5b0011a..8f555afcf11 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,31 +3,46 @@
= render "projects/pipelines/head"
%div{ class: container_class }
- - if can?(current_user, :create_environment, @project) && !@environments.blank?
- .top-area
+ .top-area
+ %ul.nav-links
+ %li{class: ('active' if @scope.nil?)}
+ = link_to project_environments_path(@project) do
+ Available
+ %span.badge.js-available-environments-count
+ = number_with_delimiter(@all_environments.available.count)
+
+ %li{class: ('active' if @scope == 'stopped')}
+ = link_to project_environments_path(@project, scope: :stopped) do
+ Stopped
+ %span.badge.js-stopped-environments-count
+ = number_with_delimiter(@all_environments.stopped.count)
+
+ - if can?(current_user, :create_environment, @project) && !@all_environments.blank?
.nav-controls
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
New environment
- - if @environments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any environments right now.
- %p.blank-state-text
- Environments are places where code gets deployed, such as staging or production.
- %br
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci/environments")
- - if can?(current_user, :create_environment, @project)
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
- - else
- .table-holder
- %table.table.builds.environments
- %tbody
- %th Environment
- %th Last Deployment
- %th Commit
- %th
- %th
- = render @environments
+ .environments-container
+ - if @all_environments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any environments right now.
+ %p.blank-state-text
+ Environments are places where code gets deployed, such as staging or production.
+ %br
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci/environments")
+ - if can?(current_user, :create_environment, @project)
+ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+ New environment
+ - else
+ .table-holder
+ %table.table.ci-table.environments
+ %tbody
+ %th Environment
+ %th Last Deployment
+ %th Build
+ %th Commit
+ %th
+ %th.hidden-xs
+ = render @environments
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
index e51667ade2d..24638c77cbb 100644
--- a/app/views/projects/environments/new.html.haml
+++ b/app/views/projects/environments/new.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
- page_title 'New Environment'
+= render "projects/pipelines/head"
-%h3.page-title
- New environment
-%hr
-= render 'form'
+%div{ class: container_class }
+ %h3.page-title
+ New environment
+ %hr
+ = render 'form'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 8f8c1c4ce22..bcac73d3698 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -3,35 +3,38 @@
= render "projects/pipelines/head"
%div{ class: container_class }
- .top-area
+ .top-area.adjust
.col-md-9
%h3.page-title= @environment.name.capitalize
.col-md-3
.nav-controls
+ = render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
+ - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+ = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
- - if @deployments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any deployments right now.
- %p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- - else
- .table-holder
- %table.table.builds.environments
- %thead
- %tr
- %th ID
- %th Commit
- %th Build
- %th
- %th
+ .deployments-container
+ - if @deployments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ - else
+ .table-holder
+ %table.table.ci-table.environments
+ %thead
+ %tr
+ %th ID
+ %th Commit
+ %th Build
+ %th
+ %th.hidden-xs
- = render @deployments
+ = render @deployments
- = paginate @deployments, theme: 'gitlab'
+ = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index bacc5708e4b..abf4f697f86 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -15,7 +15,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 331dc1fcc29..80fe6be49b0 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -62,5 +62,3 @@
%td.coverage
- if generic_commit_status.try(:coverage)
#{generic_commit_status.coverage}%
-
- %td
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 409f4701e4b..c45b73e4225 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,7 +1,10 @@
-- if subject.target_url
- = link_to subject.target_url do
- = render_status_with_link('commit status', subject.status)
+%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
+ - if subject.target_url
+ = link_to subject.target_url do
+ %span.ci-status-icon
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
+ - else
+ %span.ci-status-icon
+ = 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/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index ca700cb3a3b..1b0dbbb8111 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -8,15 +8,15 @@
.col-lg-9
%h5.prepend-top-0
Set a group to share
- = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post do
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do
.form-group
= label_tag :link_group_id, "Group", class: "label-light"
- = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+ = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true)
.form-group
= label_tag :link_group_access, "Max access level", class: "label-light"
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
- %span.caret
+ = icon('caret-down')
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input
diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml
new file mode 100644
index 00000000000..af9a5b19060
--- /dev/null
+++ b/app/views/projects/group_links/update.js.haml
@@ -0,0 +1,3 @@
+:plain
+ var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
+ $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index f88b33018d0..4825820c4d9 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_boards_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 8b1a8a8a2d9..c80210d6ff4 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -50,7 +50,7 @@
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
- = link_to_label(label, project: issue.project)
+ = link_to_label(label, subject: issue.project)
- if issue.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index a2c31c0b4c5..a4b752ad86d 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,5 +1,5 @@
%ul.content-list.issues-list.issuable-list
- = render @issues
+ = render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
%li
.nothing-here-block No issues to show
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 7cf1923456e..1b7d878c38c 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
%h3.page-title
Edit Issue ##{@issue.iid}
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 8da9f2100e9..cc57cfdb342 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -3,7 +3,8 @@
- 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/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 3fb4191c60e..bd629b5c519 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
@@ -23,8 +23,8 @@
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- %span.caret
Options
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can?(current_user, :create_issue, @project)
@@ -53,14 +53,14 @@
.issue-details.issuable-details
- .detail-page-description.content-block
+ .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
%h2.title
- = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author
+ = markdown_field(@issue, :title)
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
= preserve do
- = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author)
+ = markdown_field(@issue, :description)
%textarea.hidden.js-task-list-field
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
deleted file mode 100644
index 73c6f2a046c..00000000000
--- a/app/views/projects/labels/_label.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-- label_css_id = dom_id(label)
-%li{id: label_css_id, data: { id: label.id } }
- = render "shared/label_row", label: label
-
- .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
- %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
- Options
- %span.caret
- .dropdown-menu.dropdown-menu-align-right
- %ul
- %li
- = link_to_label(label, type: :merge_request) do
- = pluralize label.open_merge_requests_count, 'merge request'
- %li
- = link_to_label(label) do
- = pluralize label.open_issues_count(current_user), 'open issue'
- - if current_user
- %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
- %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span= label_subscription_toggle_button_text(label)
- - if can? current_user, :admin_label, @project
- %li
- = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label)
- %li
- = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
-
- .pull-right.hidden-xs.hidden-sm.hidden-md
- = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
- = pluralize label.open_merge_requests_count, 'merge request'
- = link_to_label(label, css_class: 'btn btn-transparent btn-action') do
- = pluralize label.open_issues_count(current_user), 'open issue'
-
- - if current_user
- .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
- %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span.sr-only= label_subscription_toggle_button_text(label)
- = icon('eye', class: 'label-subscribe-button-icon')
- = icon('spinner spin', class: 'label-subscribe-button-loading')
-
- - if can? current_user, :admin_label, @project
- = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
- %span.sr-only Edit
- = icon('pencil-square-o')
- = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
- %span.sr-only Delete
- = icon('trash-o')
-
- - if current_user
- :javascript
- new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml
index d59563b122a..8d09e2bda11 100644
--- a/app/views/projects/labels/destroy.js.haml
+++ b/app/views/projects/labels/destroy.js.haml
@@ -1,2 +1,2 @@
-- if @project.labels.size == 0
+- if @labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 6901ba13ab7..a80a07b52e6 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
- page_title "Edit", @label.name, "Labels"
+= render "projects/issues/head"
-%h3.page-title
- Edit Label
-%hr
-= render 'form'
+%div{ class: container_class }
+ %h3.page-title
+ Edit Label
+ %hr
+ = render 'shared/labels/form', url: namespace_project_label_path(@project.namespace.becomes(Namespace), @project, @label), back_path: namespace_project_labels_path(@project.namespace, @project)
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index db66a0edbd8..f135bf6f6b4 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -16,21 +16,22 @@
.labels
- if can?(current_user, :admin_label, @project)
-# Only show it in the first page
- - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1')
+ - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
%h5 Prioritized Labels
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
%p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- if @prioritized_labels.present?
- = render @prioritized_labels
+ = render partial: 'shared/label', collection: @prioritized_labels, as: :label
+
.other-labels
- if can?(current_user, :admin_label, @project)
%h5{ class: ('hide' if hide) } Other Labels
- - if @labels.present?
- %ul.content-list.manage-labels-list.js-other-labels
- = render @labels
+ %ul.content-list.manage-labels-list.js-other-labels
+ - if @labels.present?
+ = render partial: 'shared/label', collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- - else
+ - if @labels.blank?
.nothing-here-block
- if can?(current_user, :admin_label, @project)
Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 49ddf901619..f0d9be744d1 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
- page_title "New Label"
+= render "projects/issues/head"
-%h3.page-title
- New Label
-%hr
-= render 'form'
+%div{ class: container_class }
+ %h3.page-title
+ New Label
+ %hr
+ = render 'shared/labels/form', url: namespace_project_labels_path(@project.namespace.becomes(Namespace), @project), back_path: namespace_project_labels_path(@project.namespace, @project)
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 68fb7d5a414..12408068834 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -62,7 +62,7 @@
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
- = link_to_label(label, project: merge_request.project, type: 'merge_request')
+ = link_to_label(label, subject: merge_request.project, type: :merge_request)
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index de39964fca8..466ec1475d8 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -65,19 +65,6 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
- - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
- .light-well.append-bottom-default
- .center
- %h4
- There isn't anything to merge.
- %p.slead
- - if @merge_request.source_branch == @merge_request.target_branch
- You'll need to use different branch names to get a valid comparison.
- - else
- %span.label-branch #{@merge_request.source_branch}
- and
- %span.label-branch #{@merge_request.target_branch}
- are the same.
= f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
:javascript
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
new file mode 100644
index 00000000000..74367ab9b7b
--- /dev/null
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -0,0 +1 @@
+= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 00bd4e143df..9c6f562f7db 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,34 +18,44 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
- %li.commits-tab
- = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
- Commits
- %span.badge= @commits.size
- - if @pipeline
- %li.builds-tab.active
- = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
- Builds
- %span.badge= @statuses.size
- %li.diffs-tab.active
- = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
- Changes
- %span.badge= @diffs.real_size
+ - if @commits.empty?
+ .commits-empty
+ %h4
+ There are no commits yet.
+ = custom_icon ('illustration_no_commits')
+ - else
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ %li.commits-tab.active
+ = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
+ Commits
+ %span.badge= @commits.size
+ - if @pipelines.any?
+ %li.builds-tab
+ = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
+ Pipelines
+ %span.badge= @pipelines.size
+ %li.builds-tab
+ = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
+ Builds
+ %span.badge= @statuses.size
+ %li.diffs-tab
+ = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
+ Changes
+ %span.badge= @merge_request.diff_size
- .tab-content
- #commits.commits.tab-pane
- = render "projects/merge_requests/show/commits"
- #diffs.diffs.tab-pane.active
- - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
- .alert.alert-danger
- %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
- %p To preserve performance the line changes are not shown.
- - else
- = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
- - if @pipeline
- #builds.builds.tab-pane
- = render "projects/merge_requests/show/builds"
+ .tab-content
+ #commits.commits.tab-pane.active
+ = render "projects/merge_requests/show/commits"
+ #diffs.diffs.tab-pane
+ - # This tab is always loaded via AJAX
+ - if @pipelines.any?
+ #builds.builds.tab-pane
+ = render "projects/merge_requests/show/builds"
+ #pipelines.pipelines.tab-pane
+ = render "projects/merge_requests/show/pipelines"
+
+ .mr-loading-status
+ = spinner
:javascript
$('.assign-to-me-link').on('click', function(e){
@@ -54,6 +64,6 @@
});
:javascript
var merge_request = new MergeRequest({
- action: "#{(@show_changes_tab ? 'diffs' : 'new')}",
- setUrl: false
+ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
+ buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index d03ff9ec7e8..f57abe73977 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -4,9 +4,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
-- if diff_view == :parallel
- - fluid_layout true
-
.merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title"
@@ -25,23 +22,23 @@
%span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as
- %span.caret
+ = icon('caret-down')
%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)
- - 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)
+ .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_source_project?
+ - if @merge_request.source_branch_exists?
= render "projects/merge_requests/show/how_to_merge"
- = render "projects/merge_requests/widget/show.html.haml"
+
+ = 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
@@ -50,39 +47,41 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits_count.nonzero?
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
- %li.notes-tab
- = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
- Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.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
- Pipelines
- %span.badge= @merge_request.all_pipelines.size
- %li.builds-tab
- = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
- Builds
- %span.badge= @statuses.size
- %li.diffs-tab
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
- Changes
- %span.badge= @merge_request.diff_size
- %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
- = render "discussions/jump_to_next"
+ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+ %div{ class: container_class }
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ %li.notes-tab
+ = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
+ Discussion
+ %span.badge= @merge_request.mr_and_commit_notes.user.count
+ - if @merge_request.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
+ Pipelines
+ %span.badge= @pipelines.size
+ %li.builds-tab
+ = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
+ Builds
+ %span.badge= @statuses.size
+ %li.diffs-tab
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
+ Changes
+ %span.badge= @merge_request.diff_size
+ %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
+ %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ = render "shared/icons/icon_status_success.svg"
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
+ = render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index a524936f73c..d9f74d2cbfb 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,11 +1,7 @@
-- class_bindings = "{ |
- 'head': line.isHead, |
- 'origin': line.isOrigin, |
- 'match': line.hasMatch, |
- 'selected': line.isSelected, |
- 'unselected': line.isUnselected }"
-
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
+ = page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details
@@ -24,6 +20,21 @@
= render partial: "projects/merge_requests/conflicts/commit_stats"
.files-wrapper{"v-if" => "!isLoading && !hasError"}
- = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
- = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
+ .files
+ .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"}
+ .file-title
+ %i.fa.fa-fw{":class" => "file.iconClass"}
+ %strong {{file.filePath}}
+ = render partial: 'projects/merge_requests/conflicts/file_actions'
+ .diff-content.diff-wrap-lines
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
+ %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
+ = render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
+
= render partial: "projects/merge_requests/conflicts/submit_form"
+
+-# Components
+= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
index 457c467fba9..5ab3cd96163 100644
--- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -1,20 +1,16 @@
.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
- .inline-parallel-buttons
+ .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
.btn-group
- %a.btn{ |
- ":class" => "{'active': !isParallel}", |
- "@click" => "handleViewTypeChange('inline')"}
+ %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
Inline
- %a.btn{ |
- ":class" => "{'active': isParallel}", |
- "@click" => "handleViewTypeChange('parallel')"}
+ %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
Side-by-side
.js-toggle-container
.commit-stat-summary
Showing
- %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}}
+ %strong.cred {{conflictsCountText}}
between
- %strong {{conflictsData.source_branch}}
+ %strong {{conflictsData.sourceBranch}}
and
- %strong {{conflictsData.target_branch}}
+ %strong {{conflictsData.targetBranch}}
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
new file mode 100644
index 00000000000..05af57acf03
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -0,0 +1,12 @@
+.file-actions
+ .btn-group{"v-if" => "file.type === 'text'"}
+ %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
+ '@click' => "onClickResolveModeButton(file, 'interactive')",
+ type: 'button' }
+ Interactive mode
+ %button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
+ '@click' => "onClickResolveModeButton(file, 'edit')",
+ type: 'button' }
+ Edit inline
+ %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
deleted file mode 100644
index 19c7da4b5e3..00000000000
--- a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-.files{"v-show" => "!isParallel"}
- .diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"}
- .file-title
- %i.fa.fa-fw{":class" => "file.iconClass"}
- %strong {{file.filePath}}
- .file-actions
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
- View file @{{conflictsData.shortCommitSha}}
-
- .diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight
- %table
- %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
- %template{"v-if" => "!line.isHeader"}
- %td.diff-line-num.new_line{":class" => class_bindings}
- %a {{line.new_line}}
- %td.diff-line-num.old_line{":class" => class_bindings}
- %a {{line.old_line}}
- %td.line_content{":class" => class_bindings}
- {{{line.richText}}}
-
- %template{"v-if" => "line.isHeader"}
- %td.diff-line-num.header{":class" => class_bindings}
- %td.diff-line-num.header{":class" => class_bindings}
- %td.line_content.header{":class" => class_bindings}
- %strong {{{line.richText}}}
- %button.btn{"@click" => "handleSelected(line.id, line.section)"}
- {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
deleted file mode 100644
index 2e6f67c2eaf..00000000000
--- a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-.files{"v-show" => "isParallel"}
- .diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"}
- .file-title
- %i.fa.fa-fw{":class" => "file.iconClass"}
- %strong {{file.filePath}}
- .file-actions
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
- View file @{{conflictsData.shortCommitSha}}
-
- .diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight
- %table
- %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
- %template{"v-for" => "line in section"}
-
- %template{"v-if" => "line.isHeader"}
- %td.diff-line-num.header{":class" => class_bindings}
- %td.line_content.header{":class" => class_bindings}
- %strong {{line.richText}}
- %button.btn{"@click" => "handleSelected(line.id, line.section)"}
- {{line.buttonTitle}}
-
- %template{"v-if" => "!line.isHeader"}
- %td.diff-line-num.old_line{":class" => class_bindings}
- {{line.lineNumber}}
- %td.line_content.parallel{":class" => class_bindings}
- {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 78bd4133ea2..6ffaa9ad4d2 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -1,15 +1,16 @@
-.content-block.oneline-block.files-changed
- %strong.resolved-count {{resolvedCount}}
- of
- %strong.total-count {{conflictsCount}}
- conflicts have been resolved
-
- .commit-message-container.form-group
- .max-width-marker
- %textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
- {{{conflictsData.commitMessage}}}
-
- %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
- %span {{commitButtonText}}
-
- = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
+.form-horizontal.resolve-conflicts-form
+ .form-group
+ %label.col-sm-2.control-label{ "for" => "commit-message" }
+ Commit message
+ .col-sm-10
+ .commit-message-container
+ .max-width-marker
+ %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .row
+ .col-xs-6
+ %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
+ %span {{commitButtonText}}
+ .col-xs-6.text-right
+ = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
new file mode 100644
index 00000000000..3c927d362c2
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
@@ -0,0 +1,13 @@
+%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"}
+ .diff-editor-wrap{ "v-show" => "file.showEditor" }
+ .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
+ .discard-changes-alert
+ Are you sure you want to discard your changes?
+ .discard-actions
+ %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes
+ %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
+ .editor-wrap{ ":class" => "classObject" }
+ .loading
+ %i.fa.fa-spinner.fa-spin
+ .editor
+ %pre{ "style" => "height: 350px" }
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
new file mode 100644
index 00000000000..f094df7fcaa
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -0,0 +1,15 @@
+%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"}
+ %table
+ %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
+ %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %a {{line.new_line}}
+ %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %a {{line.old_line}}
+ %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ {{{line.richText}}}
+ %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %strong {{{line.richText}}}
+ %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
+ {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
new file mode 100644
index 00000000000..5690bf7419c
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
@@ -0,0 +1,10 @@
+%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
+ %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %strong {{line.richText}}
+ %button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
+ {{line.buttonTitle}}
+ %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ {{line.lineNumber}}
+ %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
new file mode 100644
index 00000000000..a8ecdf59393
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
@@ -0,0 +1,4 @@
+%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
+ %table
+ %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
+ %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index ebf18f6ac85..ed23d06ee5e 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,13 +1,13 @@
.detail-page-description.content-block
%h2.title
- = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author
+ = markdown_field(@merge_request, :title)
%div
- if @merge_request.description.present?
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki
= preserve do
- = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author)
+ = markdown_field(@merge_request, :description)
%textarea.hidden.js-task-list-field
= @merge_request.description
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index e35291dff7d..e7c5bca6a37 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -19,8 +19,8 @@
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- %span.caret
Options
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
%li{ class: merge_request_button_visibility(@merge_request, true) }
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 904452fcc4f..eab48b78cb3 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -9,7 +9,7 @@
latest version
- else
version #{version_index(@merge_request_diff)}
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
@@ -39,7 +39,7 @@
version #{version_index(@start_version)}
- else
#{@merge_request.target_branch}
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
@@ -64,6 +64,16 @@
#{@merge_request.target_branch} (base)
.monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ - if different_base?(@start_version, @merge_request_diff)
+ .content-block
+ = icon('info-circle')
+ Selected versions have different base commits.
+ Changes will include
+ = link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
+ new commits
+ from
+ %code #{@merge_request.target_branch}
+
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
= icon('info-circle')
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 44e645a7e81..a82c846baa7 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -4,14 +4,15 @@
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
= ci_icon_for_status(status)
%span
- CI build
+ Pipeline
+ = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
for
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
%span.ci-coverage
- = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
+ = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'}
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
@@ -43,16 +44,5 @@
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
-- @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)
+.js-success-icon.hidden
+ = ci_icon_for_status('success')
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 6f5ee5f16c5..842b6df310d 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -35,3 +35,4 @@
Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
= succeed '.' do
!= markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
+ = mr_assign_issues_link
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index ea618263a4a..608fdf1c5f5 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -12,6 +12,7 @@
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}",
ci_message: {
@@ -33,4 +34,4 @@
merge_request_widget.clearEventListeners();
}
- merge_request_widget = new MergeRequestWidget(opts);
+ merge_request_widget = new window.gl.MergeRequestWidget(opts);
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index bf2e76f0083..ce43ca3a286 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -12,7 +12,7 @@
Merge When Build Succeeds
- unless @project.only_allow_merge_if_build_succeeds?
= button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
- %span.caret
+ = icon('caret-down')
%span.sr-only
Select Merge Moment
%ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index be682226ab6..11f41e75e63 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,8 +1,12 @@
+- @no_container = true
- page_title "Edit", @milestone.title, "Milestones"
+= render "projects/issues/head"
-%h3.page-title
- Edit Milestone ##{@milestone.iid}
+%div{ class: container_class }
-%hr
+ %h3.page-title
+ Edit Milestone ##{@milestone.iid}
-= render "form"
+ %hr
+
+ = render "form"
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 7f372b41698..cda093ade81 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,8 +1,11 @@
+- @no_container = true
- page_title "New Milestone"
+= render "projects/issues/head"
-%h3.page-title
- New Milestone
+%div{ class: container_class }
+ %h3.page-title
+ New Milestone
-%hr
+ %hr
-= render "form"
+ = render "form"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 73772cc0e32..f9ba77e87b5 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,49 +1,52 @@
+- @no_container = true
- page_title @milestone.title, "Milestones"
- page_description @milestone.description
+= render "projects/issues/head"
-.detail-page-header
- .status-box{ class: status_box_class(@milestone) }
- - if @milestone.closed?
- Closed
- - elsif @milestone.expired?
- Past due
- - else
- Open
- %span.identifier
- Milestone ##{@milestone.iid}
- - if @milestone.expires_at
- %span.creator
- &middot;
- = @milestone.expires_at
- .pull-right
- - if can?(current_user, :admin_milestone, @project)
- - if @milestone.active?
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
+%div{ class: container_class }
+ .detail-page-header
+ .status-box{ class: status_box_class(@milestone) }
+ - if @milestone.closed?
+ Closed
+ - elsif @milestone.expired?
+ Past due
- else
- = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
+ Open
+ %span.identifier
+ Milestone ##{@milestone.iid}
+ - if @milestone.expires_at
+ %span.creator
+ &middot;
+ = @milestone.expires_at
+ .pull-right
+ - if can?(current_user, :admin_milestone, @project)
+ - if @milestone.active?
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
+ - else
+ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
- Edit
+ = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
+ Edit
- = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
- Delete
+ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
+ Delete
-.detail-page-description.milestone-detail
- %h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
- %div
- - if @milestone.description.present?
- .description
- .wiki
- = preserve do
- = markdown @milestone.description
+ .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
+ %h2.title
+ = markdown_field(@milestone, :title)
+ %div
+ - if @milestone.description.present?
+ .description
+ .wiki
+ = preserve do
+ = markdown_field(@milestone, :description)
-- if @milestone.total_items_count(current_user).zero?
- .alert.alert-success.prepend-top-default
- %span Assign some issues to this milestone.
-- elsif @milestone.complete?(current_user) && @milestone.active?
- .alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close this milestone now.
+ - if @milestone.total_items_count(current_user).zero?
+ .alert.alert-success.prepend-top-default
+ %span Assign some issues to this milestone.
+ - elsif @milestone.complete?(current_user) && @milestone.active?
+ .alert.alert-success.prepend-top-default
+ %span All issues for this milestone are closed. You may close this milestone now.
-= render 'shared/milestones/summary', milestone: @milestone, project: @project
-= render 'shared/milestones/tabs', milestone: @milestone
+ = render 'shared/milestones/summary', milestone: @milestone, project: @project
+ = render 'shared/milestones/tabs', milestone: @milestone
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index b2ece44d966..29df1bab04e 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -8,7 +8,7 @@
.project-network
.controls
= form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f|
- = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha'
+ = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Git revision", class: 'search-input form-control input-mx-250 search-sha'
= button_tag class: 'btn btn-success' do
= icon('search')
.inline.prepend-left-20
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index ffe99d0d0c6..932603f03b0 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -27,6 +27,7 @@
- else
.input-group-addon.static-namespace
#{root_url}#{current_user.username}/
+ = f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= f.label :namespace_id, class: 'label-light' do
%span
@@ -72,7 +73,7 @@
= link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL')
%div{ class: 'import_gitlab_project' }
- - if gitlab_project_import_enabled? && current_user.admin?
+ - if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
@@ -126,6 +127,11 @@
}
});
+ $('#new_project').submit(function(){
+ var $path = $('#project_path');
+ $path.val($path.val().trim());
+ });
+
$('#project_path').keyup(function(){
if($(this).val().length !=0) {
$('.btn_import_gitlab_project').attr('disabled', false);
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 788be4a0047..ab719e38904 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -57,11 +57,11 @@
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
- = icon('trash-o')
+ = icon('trash-o', class: 'danger-highlight')
.note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text.md
= preserve do
- = note.note_html
+ = note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 8352eba7446..00b62a595ff 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -14,7 +14,7 @@
.disabled-comment.text-center
.disabled-comment-text.inline
Please
- = link_to "sign up", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
or
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
to post a comment
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index 5f571499e80..b10dd47709f 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_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) }
- - 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{ class: ('build' if local_assigns.fetch(:build_subnav, false)) }
+ %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
+ - 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 5800ef7de48..d288efc546f 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -33,7 +33,7 @@
- if @commit
.commit-box.content-block
%h3.commit-title
- = markdown escape_once(@commit.title), pipeline: :single_line
+ = markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
+ = preserve(markdown(@commit.description, pipeline: :single_line))
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 50c7e5044b2..4bc49072f35 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -36,20 +36,21 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
- %ul.content-list.pipelines
+ %div.content-list.pipelines
- stages = @pipelines.stages
- if @pipelines.blank?
- %li
+ %div
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.builds
- %tbody
+ %table.table.ci-table
+ %thead
%th Status
%th Pipeline
+ %th Commit
%th Stages
%th
- %th
+ %th.hidden-xs
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
= paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 75943c64276..688535ad764 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -1,8 +1,11 @@
+- @no_container = true
- page_title "Pipeline"
+= render "projects/pipelines/head"
-.prepend-top-default
- - if @commit
- = render "projects/pipelines/info"
- %div.block-connector
+%div{ class: container_class }
+ .prepend-top-default
+ - if @commit
+ = render "projects/pipelines/info"
+ %div.block-connector
-= render "projects/commit/pipeline", pipeline: @pipeline
+ = render "projects/commit/pipeline", pipeline: @pipeline
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index 8c7222bfe3d..bebf0ccd54d 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -7,7 +7,7 @@
.col-lg-9
%h5.prepend-top-0
Pipelines
- = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project), remote: true, authenticity_token: true do |f|
+ = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
%fieldset.builds-feature
- unless @repository.gitlab_ci_yml
.form-group
@@ -64,8 +64,8 @@
.checkbox
= f.label :public_builds do
= f.check_box :public_builds
- %strong Public pipelines
- .help-block Allow everyone to access pipelines for Public and Internal projects
+ %strong Public builds
+ .help-block Allow everyone to access builds traces for Public and Internal projects
.form-group.append-bottom-default
= f.label :runners_token, "Runners token", class: 'label-light'
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index e783d8c72c5..9738f369a35 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -1,7 +1,7 @@
.panel.panel-default
.panel-heading
+ Group members with access to
%strong #{@group.name}
- group members
%span.badge= members.size
- if can?(current_user, :admin_group_member, @group)
.controls
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
new file mode 100644
index 00000000000..d7f5fa96527
--- /dev/null
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -0,0 +1,7 @@
+.panel.panel-default.project-members-groups
+ .panel-heading
+ Groups with access to
+ %strong #{@project.name}
+ %span.badge= group_links.size
+ %ul.content-list
+ = render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index fa8cbf71733..79dcd7a6ee9 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,27 +1,22 @@
-= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f|
- .form-group
- = f.label :user_ids, "People", class: 'control-label'
- .col-sm-10
- = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
- .help-block
+= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
+ .row
+ .col-md-4.col-lg-6
+ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
+ .help-block.append-bottom-10
Search for users by name, username, or email, or invite new ones using their email address.
- .form-group
- = f.label :access_level, "Project Access", class: 'control-label'
- .col-sm-10
- = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
- .help-block
- Read more about role permissions
- %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .col-md-3.col-lg-2
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
+ .help-block.append-bottom-10
+ = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ about role permissions
- .form-group
- = f.label :expires_at, 'Access expiration date', class: 'control-label'
- .col-sm-10
+ .col-md-3.col-lg-2
.clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
- .help-block
+ .help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this project.
- .form-actions
- = f.submit 'Add users to project', class: "btn btn-create"
+ .col-md-2
+ = f.submit "Add to project", class: "btn btn-create btn-block"
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index b0bfdd235f7..c1e894d8f40 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,19 +1,7 @@
.panel.panel-default
.panel-heading
+ Users with access to
%strong #{@project.name}
- project members
- %span.badge= members.size
- .controls
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
- = button_tag class: 'btn', title: 'Search' do
- = icon("search")
+ %span.badge= @project_members.total_count
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
-
-:javascript
- $('form.member-search-form').on('submit', function (event) {
- event.preventDefault();
- Turbolinks.visit(this.action + '?' + $(this).serialize());
- });
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9d063b3081f..bdeb704b6da 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,24 +1,28 @@
- page_title "Members"
-.project-members-page.js-project-members-page.prepend-top-default
+.project-members-page.prepend-top-default
+ %h4.project-members-title.clearfix
+ Members
+ = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
- if can?(current_user, :admin_project_member, @project)
- .panel.panel-default
- .panel-heading
- Add new user to project
- .controls
- = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
- Import members
- .panel-body
- %p.light
- Users with access to this project are listed below.
- = render "new_project_member"
+ .project-members-new.append-bottom-default
+ %p.clearfix
+ Add new user to
+ %strong= @project.name
+ = render "new_project_member"
- = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+ = render 'shared/members/requests', membership_source: @project, requesters: @requesters
- = render 'team', members: @project_members
-
- - if @group
- = render "group_members", members: @group_members
+ .append-bottom-default.clearfix
+ %h5.member.existing-title
+ Existing users and groups
+ = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
+ - if @group_links.any?
+ = render 'groups', group_links: @group_links
- - if @project_group_links.any? && @project.allowed_to_share_with_group?
- = render "shared_group_members"
+ = render 'team', members: @project_members
+ = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 37e55dc72a3..91927181efb 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 gl.MemberExpirationDate();
+ var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+ $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 49dcc9a6ba4..42e9bdbd30e 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -1,4 +1,6 @@
- page_title "Protected branches"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js')
.row.prepend-top-default.append-bottom-default
.col-lg-3
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index 43a6fdfd103..d9c39fb87b7 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -12,7 +12,7 @@
= link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
%code= commit.short_id
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
- = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author
+ = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
%td
%span.pull-right.cgray
= time_ago_with_tooltip(commit.committed_date)
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..5afa193357e 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')
+ - if current_application_settings.shared_runners_text.present?
+ = markdown_field(current_application_settings, :shared_runners_text)
- 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..51b0939564e 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?
@@ -26,4 +26,4 @@
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
= render partial: 'runner', collection: @assignable_runners, as: :runner
- = paginate @assignable_runners
+ = paginate @assignable_runners, theme: "gitlab"
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/show.html.haml b/app/views/projects/show.html.haml
index 9adce776c1c..ba16c641462 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -12,73 +12,74 @@
= render 'projects/last_push'
= render "home_panel"
-%nav.project-stats{ class: (container_class) }
- %ul.nav
- %li
- = link_to project_files_path(@project) do
- Files (#{repository_size})
- %li
- = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
- %li
- = link_to namespace_project_branches_path(@project.namespace, @project) do
- #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
- %li
- = link_to namespace_project_tags_path(@project.namespace, @project) do
- #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
-
- - if default_project_view != 'readme' && @repository.readme
+- if @project.feature_available?(:repository, current_user)
+ %nav.project-stats{ class: container_class }
+ %ul.nav
%li
- = link_to 'Readme', readme_path(@project)
-
- - if @repository.changelog
+ = link_to project_files_path(@project) do
+ Files (#{repository_size})
%li
- = link_to 'Changelog', changelog_path(@project)
-
- - if @repository.license_blob
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
+ #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
%li
- = link_to license_short_name(@project), license_path(@project)
-
- - if @repository.contribution_guide
+ = link_to namespace_project_branches_path(@project.namespace, @project) do
+ #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
- = link_to 'Contribution guide', contribution_guide_path(@project)
+ = link_to namespace_project_tags_path(@project.namespace, @project) do
+ #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- - if @repository.gitlab_ci_yml
- %li
- = link_to 'CI configuration', ci_configuration_path(@project)
-
- - if current_user && can_push_branch?(@project, @project.default_branch)
- - unless @repository.changelog
- %li.missing
- = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
- Add Changelog
- - unless @repository.license_blob
- %li.missing
- = link_to add_special_file_path(@project, file_name: 'LICENSE') do
- Add License
- - unless @repository.contribution_guide
- %li.missing
- = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
- Add Contribution guide
- - unless @repository.gitlab_ci_yml
- %li.missing
- = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
- Set Up CI
-
- %li.project-repo-buttons-right
- .project-repo-buttons.project-right-buttons
- - if current_user
- = render 'shared/members/access_request_buttons', source: @project
- = render "projects/buttons/koding"
-
- .btn-group.project-repo-btn-group
- = render 'projects/buttons/download', project: @project, ref: @ref
- = render 'projects/buttons/dropdown'
-
- = render 'shared/notifications/button', notification_setting: @notification_setting
-- if @repository.commit
- .project-last-commit{ class: container_class }
- = render 'projects/last_commit', commit: @repository.commit, project: @project
+ - if default_project_view != 'readme' && @repository.readme
+ %li
+ = link_to 'Readme', readme_path(@project)
+
+ - if @repository.changelog
+ %li
+ = link_to 'Changelog', changelog_path(@project)
+
+ - if @repository.license_blob
+ %li
+ = link_to license_short_name(@project), license_path(@project)
+
+ - if @repository.contribution_guide
+ %li
+ = link_to 'Contribution guide', contribution_guide_path(@project)
+
+ - if @repository.gitlab_ci_yml
+ %li
+ = link_to 'CI configuration', ci_configuration_path(@project)
+
+ - if current_user && can_push_branch?(@project, @project.default_branch)
+ - unless @repository.changelog
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
+ Add Changelog
+ - unless @repository.license_blob
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: 'LICENSE') do
+ Add License
+ - unless @repository.contribution_guide
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
+ Add Contribution guide
+ - unless @repository.gitlab_ci_yml
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
+ Set Up CI
+
+ %li.project-repo-buttons-right
+ .project-repo-buttons.project-right-buttons
+ - if current_user
+ = render 'shared/members/access_request_buttons', source: @project
+ = render "projects/buttons/koding"
+
+ .btn-group.project-repo-btn-group
+ = render 'projects/buttons/download', project: @project, ref: @ref
+ = render 'projects/buttons/dropdown'
+
+ = render 'shared/notifications/button', notification_setting: @notification_setting
+ - if @repository.commit
+ .project-last-commit{ class: container_class }
+ = render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
- if @project.archived?
@@ -87,5 +88,7 @@
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
- %div{class: "project-show-#{default_project_view}"}
- = render default_project_view
+ - view_path = default_project_view
+
+ %div{ class: project_child_container_class(view_path) }
+ = render view_path
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 9773b8438ec..32e1f8a21b0 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -12,7 +12,7 @@
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
- if can?(current_user, :create_project_snippet, @project)
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index a156d98bab8..05fccb4f976 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -30,4 +30,4 @@
.description.prepend-top-default
.wiki
= preserve do
- = markdown release.description
+ = markdown_field(release, :description)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 6adbe9351dc..7a0d9dcc94f 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -14,7 +14,7 @@
%button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= @sort.humanize
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_tags_path(sort: nil) do
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 4dd7439b2d0..155af755759 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -33,6 +33,6 @@
.description
.wiki
= preserve do
- = markdown @release.description
+ = markdown_field(@release, :description)
- else
This tag has no release notes.
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/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/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 8f68d6d1b87..e010f21de5a 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -7,7 +7,7 @@
- if issue.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author }))
+ = search_md_sanitize(issue, :description)
%span.light
#{issue.project.name_with_namespace}
- if issue.closed?
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 6331c2bd6b0..07b17bc69c0 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -6,7 +6,7 @@
- if merge_request.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author }))
+ = search_md_sanitize(merge_request, :description)
%span.light
#{merge_request.project.name_with_namespace}
.pull-right
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index b31595d8d1c..9664f65a36e 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -6,4 +6,4 @@
- if milestone.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(milestone.description))
+ = search_md_sanitize(milestone, :description)
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index e0400083870..f3701b89bb4 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -23,4 +23,4 @@
.note-search-result
.term
= preserve do
- = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author}))
+ = search_md_sanitize(note, :note)
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 8824bcc158e..3480800369a 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,5 @@
%ul.nav-links.event-filter.scrolling-tabs
+ = event_filter_link EventFilter.all, 'All'
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
new file mode 100644
index 00000000000..40c8d2af226
--- /dev/null
+++ b/app/views/shared/_label.html.haml
@@ -0,0 +1,53 @@
+- label_css_id = dom_id(label)
+- open_issues_count = label.open_issues_count(current_user, @project)
+- open_merge_requests_count = label.open_merge_requests_count(current_user, @project)
+
+%li{id: label_css_id, data: { id: label.id } }
+ = render "shared/label_row", label: label
+
+ .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
+ %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-align-right
+ %ul
+ %li
+ = link_to_label(label, subject: @project, type: :merge_request) do
+ = pluralize open_merge_requests_count, 'merge request'
+ %li
+ = link_to_label(label, subject: @project) do
+ = pluralize open_issues_count, 'open issue'
+ - if current_user
+ %li.label-subscription{ data: toggle_subscription_data(label) }
+ %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
+ %span= label_subscription_toggle_button_text(label)
+ - if can?(current_user, :admin_label, label)
+ %li
+ = link_to 'Edit', edit_label_path(label)
+ %li
+ = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
+
+ .pull-right.hidden-xs.hidden-sm.hidden-md
+ = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
+ = pluralize open_merge_requests_count, 'merge request'
+ = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do
+ = pluralize open_issues_count, 'open issue'
+
+ - if current_user
+ .label-subscription.inline{ data: toggle_subscription_data(label) }
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
+ %span.sr-only= label_subscription_toggle_button_text(label)
+ = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+
+ - if can?(current_user, :admin_label, label)
+ = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
+ %span.sr-only Edit
+ = icon('pencil-square-o')
+ = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
+ %span.sr-only Delete
+ = icon('trash-o')
+
+ - if current_user && label.is_a?(ProjectLabel)
+ :javascript
+ new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 77676454b57..d28f9421ecf 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -3,13 +3,16 @@
.draggable-handler
= icon('bars')
.js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label),
- dom_id: dom_id(label) } }
+ dom_id: dom_id(label), type: label.type } }
%button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' }
= icon('star-o')
%button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' }
= icon('star')
%span.label-name
- = link_to_label(label, tooltip: false)
+ = link_to_label(label, subject: @project, tooltip: false)
+ - if defined?(@project) && @project.group.present?
+ %span.label-type
+ = label.model_name.human.titleize
- if label.description
%span.label-description
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
index e324d0e5203..21b37a7c9ae 100644
--- a/app/views/shared/_labels_row.html.haml
+++ b/app/views/shared/_labels_row.html.haml
@@ -1,5 +1,5 @@
- labels.each do |label|
%span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
- = link_to_label(label, css_class: 'btn btn-transparent')
+ = link_to_label(label, subject: @project, css_class: 'btn btn-transparent')
%button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
= icon("times")
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 51622931e24..fbbf6f358c5 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -3,7 +3,7 @@
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }
%a.btn.btn-new.new-project-item-select-button
= local_assigns[:label]
- %b.caret
+ = icon('caret-down')
:javascript
$('.new-project-item-select-button').on('click', function() {
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 249bce926ce..68e05cb72e1 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -5,29 +5,29 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%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 add4536a0a2..b11257ee0e6 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -6,7 +6,7 @@
- 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/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 1ad95351005..dc4ee3074d2 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -35,4 +35,4 @@
- if group.description.present?
.description
- = markdown(group.description, pipeline: :description)
+ = markdown_field(group, :description)
diff --git a/app/views/shared/icons/_icon_empty_groups.svg b/app/views/shared/icons/_icon_empty_groups.svg
new file mode 100644
index 00000000000..9228be05f03
--- /dev/null
+++ b/app/views/shared/icons/_icon_empty_groups.svg
@@ -0,0 +1 @@
+<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg> \ No newline at end of file
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/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg
new file mode 100644
index 00000000000..4f9d9add60d
--- /dev/null
+++ b/app/views/shared/icons/_illustration_no_commits.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index cf26197f7d7..ed93857e6d4 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,3 +1,4 @@
+- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters
@@ -14,22 +15,23 @@
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
- = render "shared/issuable/milestone_dropdown"
+ = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown"
+ = 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
+ - if issuable_filters_present
+ .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 boards_page
@@ -37,7 +39,7 @@
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
.dropdown.pull-right
- %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+ %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Create new list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
@@ -76,11 +78,10 @@
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= 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?) }
- - if @labels.any?
- = render "shared/labels_row", labels: @labels
+ - has_labels = @labels && @labels.any?
+ .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
+ - if has_labels
+ = render 'shared/labels_row', labels: @labels
:javascript
new UsersSelect();
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 04373684ee9..d410755cad1 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,3 +1,4 @@
+- project = @target_project || @project
= form_errors(issuable)
- if @conflict
@@ -22,6 +23,8 @@
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.no-template
+ No template
%a.reset-template
Reset template
%div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
@@ -82,38 +85,22 @@
= f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
- placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
- selected: issuable.assignee_id, project: @target_project || @project,
- first_user: true, current_user: true, include_blank: true)
- %div
- = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline'
+ - if issuable.assignee_id
+ = f.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
.form-group.issue-milestone
= f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
- - if milestone_options(issuable).present?
- .issuable-form-select-holder
- = f.select(:milestone_id, milestone_options(issuable),
- { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
- - else
- .prepend-top-10
- %span.light No open milestones available.
- - if can? current_user, :admin_milestone, issuable.project
- %div
- = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group
- - has_labels = issuable.project.labels.any?
+ - has_labels = @labels && @labels.any?
= f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = f.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
- - if has_labels
- .issuable-form-select-holder
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- - else
- %span.light No labels yet.
- - if can? current_user, :admin_label, issuable.project
- %div
- = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
- if has_due_date
.col-lg-6
.form-group
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index d34d28f6736..22b5a6aa11b 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,25 +1,30 @@
+- project = @target_project || @project
- show_create = local_assigns.fetch(:show_create, true)
- extra_options = local_assigns.fetch(:extra_options, true)
- filter_submit = local_assigns.fetch(:filter_submit, true)
- show_footer = local_assigns.fetch(:show_footer, true)
+- use_id = local_assigns.fetch(:use_id, true)
- data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, [])
-- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}
+- selected = local_assigns.fetch(:selected, nil)
+- selected_toggle = local_assigns.fetch(:selected_toggle, nil)
+- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
-- if params[:label_name].present?
- - if params[:label_name].respond_to?('any?')
- - params[:label_name].each do |label|
- = hidden_field_tag "label_name[]", label, id: nil
+- if selected
+ - selected.each do |label|
+ = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : 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
- = h(multi_label_name(params[:label_name], "Label"))
+ %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
+ = multi_label_name(selected, "Labels")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create }
- - if show_create and @project and can?(current_user, :admin_label, @project)
+ = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
+ - if show_create && project && can?(current_user, :admin_label, project)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 2fcf40ece99..f27a9002ec2 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,16 +1,21 @@
-- if params[:milestone_title].present?
- = hidden_field_tag(:milestone_title, params[:milestone_title])
-= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
- placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- - if @project
+- project = @target_project || @project
+- extra_class = extra_class || ''
+- show_menu_above = show_menu_above || false
+- selected_text = selected.try(:title)
+- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
+- if selected.present?
+ = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id)
+= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
+ placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ - if project
%ul.dropdown-footer-list
- - if can? current_user, :admin_milestone, @project
+ - if can? current_user, :admin_milestone, project
%li
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ = link_to new_namespace_project_milestone_path(project.namespace, project), title: "New Milestone" do
Create new
%li
- = link_to namespace_project_milestones_path(@project.namespace, @project) do
- - if can? current_user, :admin_milestone, @project
+ = link_to namespace_project_milestones_path(project.namespace, project) do
+ - if can? current_user, :admin_milestone, project
Manage milestones
- else
View milestones
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/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index b13daaf43c9..7363ead09ff 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -107,30 +107,31 @@
= dropdown_content do
.js-due-date-calendar
- - if issuable.project.labels.any?
+ - if @labels && @labels.any?
+ - selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
= icon('tags')
%span
- = issuable.labels_array.size
+ = selected_labels.size
.title.hide-collapsed
Labels
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) }
- - if issuable.labels_array.any?
- - issuable.labels_array.each do |label|
+ .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
+ - if selected_labels.any?
+ - selected_labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
%span.no-value None
.selectbox.hide-collapsed
- - issuable.labels_array.each do |label|
+ - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
- %span.dropdown-toggle-text
- Label
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
+ %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)}
+ = multi_label_name(selected_labels, "Labels")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
@@ -170,5 +171,5 @@
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
new Subscription('.subscription')
- new DueDateSelect();
+ new gl.DueDateSelectors();
sidebar = new Sidebar();
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index aa143e54ffe..647e05e5ff7 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
+= form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
= form_errors(@label)
.form-group
@@ -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
@@ -30,4 +30,4 @@
= f.submit 'Save changes', class: 'btn btn-save js-save-button'
- else
= f.submit 'Create Label', class: 'btn btn-create js-save-button'
- = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel'
+ = link_to 'Cancel', back_path, class: 'btn btn-cancel'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
new file mode 100644
index 00000000000..1c0346bbc78
--- /dev/null
+++ b/app/views/shared/members/_group.html.haml
@@ -0,0 +1,29 @@
+- group_link = local_assigns[:group_link]
+- group = group_link.group
+- can_admin_member = can?(current_user, :admin_project_member, @project)
+%li.member.group_member{ id: "group_member_#{group_link.id}" }
+ %span{ class: "list-item-name" }
+ = image_tag group_icon(group), class: "avatar s40", alt: ''
+ %strong
+ = link_to group.name, group_path(group)
+ .cgray
+ Joined #{time_ago_with_tooltip(group.created_at)}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
+ .controls.member-controls
+ = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
+ = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
+ .prepend-left-5.clearable-input.member-form-control
+ = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
+ %i.clear-icon.js-clear-input
+ - if can_admin_member
+ = link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
+ remote: true,
+ method: :delete,
+ data: { confirm: "Are you sure you want to remove #{group.name}?" },
+ class: 'btn btn-remove prepend-left-10' do
+ %span.visible-xs-block
+ Delete
+ = icon('trash', class: 'hidden-xs')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 5f20e4bd42a..432047a1c4e 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,59 +1,29 @@
- show_roles = local_assigns.fetch(:show_roles, true)
- show_controls = local_assigns.fetch(:show_controls, true)
-- user = member.user
+- user = local_assigns.fetch(:user, member.user)
+- source = member.source
+- can_admin_member = can?(current_user, action_member_permission(:update, member), member)
-%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
- - if show_roles
- .controls
- %strong.control-text= member.human_access
- - if show_controls
- - if !user && can?(current_user, action_member_permission(:admin, member), member.source)
- = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
- method: :post,
- class: 'btn'
-
- - if can?(current_user, action_member_permission(:update, member), member)
- = button_tag icon('pencil'),
- type: 'button',
- class: 'btn inline js-toggle-button',
- title: 'Edit'
-
- - if member.request?
- = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
- method: :post,
- class: 'btn btn-success',
- title: 'Grant access'
-
- - if can?(current_user, action_member_permission(:destroy, member), member)
- - if current_user == user
- = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
- method: :delete,
- data: { confirm: leave_confirmation_message(member.source) },
- class: 'btn btn-remove'
- - else
- = link_to icon('trash'), member,
- remote: true,
- method: :delete,
- data: { confirm: remove_member_message(member) },
- class: 'btn btn-remove',
- title: remove_member_title(member)
-
-
- %span{ class: ("list-item-name" if show_controls) }
+%li.member{ class: dom_class(member), id: dom_id(member) }
+ %span.list-item-name
- if user
= image_tag avatar_icon(user, 40), class: "avatar s40", alt: ''
%strong
= link_to user.name, user_path(user)
- %span.cgray= user.username
+ %span.cgray= user.to_reference
- if user == current_user
- %span.label.label-success It's you
+ %span.label.label-success.prepend-left-5 It's you
- if user.blocked?
%label.label.label-danger
%strong Blocked
- .cgray
+ - if source.instance_of?(Group) && !@group
+ = link_to source, class: "member-group-link prepend-left-5" do
+ = "· #{source.name}"
+
+ .hidden-xs.cgray
- if member.request?
Requested
= time_ago_with_tooltip(member.requested_at)
@@ -73,20 +43,44 @@
by
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
-
- if show_roles
- .edit-member.hide.js-toggle-content
- %br
- = form_for member, remote: true, html: { class: 'form-horizontal' } do |f|
- .form-group
- = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
- .col-sm-10
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
- .form-group
- = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
- .col-sm-10
- .clearable-input
- = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
+ .controls.member-controls
+ - if show_controls
+ - if user != current_user
+ = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
+ .prepend-left-5.clearable-input.member-form-control
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
- .prepend-top-10
- = f.submit 'Save', class: 'btn btn-save btn-sm'
+ - else
+ %span.member-access-text= member.human_access
+
+ - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source)
+ = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
+ method: :post,
+ class: 'btn btn-default prepend-left-10'
+
+ - elsif member.request? && can_admin_member
+ = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
+ method: :post,
+ class: 'btn btn-success prepend-left-10',
+ title: 'Grant access'
+
+ - if can?(current_user, action_member_permission(:destroy, member), member)
+ - if current_user == user
+ = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(member.source) },
+ class: 'btn btn-remove prepend-left-10'
+ - else
+ = link_to member,
+ remote: true,
+ method: :delete,
+ data: { confirm: remove_member_message(member) },
+ class: 'btn btn-remove prepend-left-10',
+ title: remove_member_title(member) do
+ %span.visible-xs-block
+ Delete
+ = icon('trash', class: 'hidden-xs')
+ - else
+ %span.member-access-text= member.human_access
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
index 40b39e850b0..10050adfda5 100644
--- a/app/views/shared/members/_requests.html.haml
+++ b/app/views/shared/members/_requests.html.haml
@@ -1,8 +1,8 @@
- if requesters.any?
.panel.panel-default
.panel-heading
+ Users requesting access to
%strong= membership_source.name
- access requests
%span.badge= requesters.size
%ul.content-list
= render partial: 'shared/members/member', collection: requesters, as: :member
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index b15e8ea73fe..33f93dccd3c 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -8,7 +8,7 @@
= link_to milestones_label_path(options) do
- render_colored_label(label, tooltip: false)
%span.prepend-description-left
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
.pull-info-right
%span.append-right-20
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 7ff947a51db..548215243db 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -26,7 +26,7 @@
.detail-page-description.milestone-detail
%h2.title
- = markdown escape_once(milestone.title), pipeline: :single_line
+ = markdown_field(milestone, :title)
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
@@ -55,4 +55,3 @@
Open
%td
= ms.expires_at
-
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index ff1cf966a9b..feaa5570c21 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -11,7 +11,7 @@
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
- %span.caret
+ = icon('caret-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 66c309644a7..e8668048703 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -50,4 +50,4 @@
class: "commit-row-message"
- elsif project.description.present?
.description
- = markdown(project.description, pipeline: :description)
+ = markdown_field(project, :description)
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 773ce8ac240..dcdba01aee9 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,9 +1,12 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
%textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
- = @snippet.data
+ = @snippet.content
.file-content.wiki
- = render_markup(@snippet.file_name, @snippet.data)
+ - if gitlab_markdown?(@snippet.file_name)
+ = preserve(markdown_field(@snippet, :content))
+ - else
+ = render_markup(@snippet.file_name, @snippet.content)
- else
= render 'shared/file_highlight', blob: @snippet
- else
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 7ae4211ddfd..d7506e07ff6 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -21,4 +21,4 @@
= render "snippets/actions"
%h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
+ = markdown_field(@snippet, :title)
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index c446dc3bdc1..1d0e549ed3d 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -12,7 +12,7 @@
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
%li
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index cd89155c616..27d7a6c5bb6 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -9,6 +9,7 @@
.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"
+ = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
= 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 9657101ace5..232ca26c1af 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -6,7 +6,7 @@
%script#js-authenticate-u2f-setup{ type: "text/template" }
%div
%p Insert your security key (if you haven't already), and press the button below.
- %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device
+ %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index f736f98ade5..6b60787fd32 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -41,11 +41,11 @@
.user-info
.cover-title
= @user.name
- %span.handle
- @#{@user.username}
.cover-desc.member-date
%span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
Member since #{@user.created_at.to_s(:medium)}
.cover-desc
@@ -83,7 +83,7 @@
%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
+ = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
Activity
%li.js-groups-tab
= link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index 667fff031dd..c2dc955b27c 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -1,7 +1,6 @@
class AdminEmailWorker
include Sidekiq::Worker
-
- sidekiq_options retry: false # this job auto-repeats via sidekiq-cron
+ include CronjobQueue
def perform
repository_check_failed_count = Project.where(last_repository_check_failed: true).count
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
new file mode 100644
index 00000000000..def0ab1dde1
--- /dev/null
+++ b/app/workers/build_coverage_worker.rb
@@ -0,0 +1,9 @@
+class BuildCoverageWorker
+ include Sidekiq::Worker
+ include BuildQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id)
+ .try(:update_coverage)
+ end
+end
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
index 1c7a04a66a8..5fdb1f2baa0 100644
--- a/app/workers/build_email_worker.rb
+++ b/app/workers/build_email_worker.rb
@@ -1,5 +1,6 @@
class BuildEmailWorker
include Sidekiq::Worker
+ include BuildQueue
def perform(build_id, recipients, push_data)
recipients.each do |recipient|
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
new file mode 100644
index 00000000000..466410bf08c
--- /dev/null
+++ b/app/workers/build_finished_worker.rb
@@ -0,0 +1,11 @@
+class BuildFinishedWorker
+ include Sidekiq::Worker
+ include BuildQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id).try do |build|
+ BuildCoverageWorker.new.perform(build.id)
+ BuildHooksWorker.new.perform(build.id)
+ end
+ end
+end
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
new file mode 100644
index 00000000000..9965af935d4
--- /dev/null
+++ b/app/workers/build_hooks_worker.rb
@@ -0,0 +1,9 @@
+class BuildHooksWorker
+ include Sidekiq::Worker
+ include BuildQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id)
+ .try(:execute_hooks)
+ end
+end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
new file mode 100644
index 00000000000..e0ad5268664
--- /dev/null
+++ b/app/workers/build_success_worker.rb
@@ -0,0 +1,27 @@
+class BuildSuccessWorker
+ include Sidekiq::Worker
+ include BuildQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id).try do |build|
+ create_deployment(build)
+ end
+ end
+
+ private
+
+ def create_deployment(build)
+ return if build.environment.blank?
+
+ 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
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
new file mode 100644
index 00000000000..c4cb4733482
--- /dev/null
+++ b/app/workers/clear_database_cache_worker.rb
@@ -0,0 +1,24 @@
+# This worker clears all cache fields in the database, working in batches.
+class ClearDatabaseCacheWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ BATCH_SIZE = 1000
+
+ def perform
+ CacheMarkdownField.caching_classes.each do |kls|
+ fields = kls.cached_markdown_fields.html_fields
+ clear_cache_fields = fields.each_with_object({}) do |field, memo|
+ memo[field] = nil
+ end
+
+ Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
+
+ kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
+ relation.update_all(clear_cache_fields)
+ end
+ end
+
+ nil
+ end
+end
diff --git a/app/workers/concerns/build_queue.rb b/app/workers/concerns/build_queue.rb
new file mode 100644
index 00000000000..cf0ead40a8b
--- /dev/null
+++ b/app/workers/concerns/build_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various CI build workers.
+module BuildQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :build
+ end
+end
diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb
new file mode 100644
index 00000000000..e918bb011e0
--- /dev/null
+++ b/app/workers/concerns/cronjob_queue.rb
@@ -0,0 +1,9 @@
+# Concern that sets various Sidekiq settings for workers executed using a
+# cronjob.
+module CronjobQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :cronjob, retry: false
+ end
+end
diff --git a/app/workers/concerns/dedicated_sidekiq_queue.rb b/app/workers/concerns/dedicated_sidekiq_queue.rb
new file mode 100644
index 00000000000..132bae6022b
--- /dev/null
+++ b/app/workers/concerns/dedicated_sidekiq_queue.rb
@@ -0,0 +1,9 @@
+# Concern that sets the queue of a Sidekiq worker based on the worker's class
+# name/namespace.
+module DedicatedSidekiqQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: name.sub(/Worker\z/, '').underscore.tr('/', '_')
+ end
+end
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
new file mode 100644
index 00000000000..ca3860e1d38
--- /dev/null
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various CI pipeline workers.
+module PipelineQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :pipeline
+ end
+end
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
new file mode 100644
index 00000000000..a597321ccf4
--- /dev/null
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various repository check workers.
+module RepositoryCheckQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :repository_check, retry: false
+ end
+end
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 6ff361e4d80..3194c389b3d 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -1,5 +1,6 @@
class DeleteUserWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
def perform(current_user_id, delete_user_id, options = {})
delete_user = User.find(delete_user_id)
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 842eebdea9e..d3f7e479a8d 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -1,7 +1,6 @@
class EmailReceiverWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :incoming_email
+ include DedicatedSidekiqQueue
def perform(raw)
return unless Gitlab::IncomingEmail.enabled?
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 1dc7e0adef7..b9cd49985dc 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,7 +1,7 @@
class EmailsOnPushWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :mailers
attr_reader :email, :skip_premailer
def perform(project_id, recipients, push_data, options = {})
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index c64ea108d52..a27585fd389 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -1,13 +1,13 @@
class ExpireBuildArtifactsWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
- Rails.logger.info 'Cleaning old build artifacts'
+ Rails.logger.info 'Scheduling removal of build artifacts'
- builds = Ci::Build.with_expired_artifacts
- builds.find_each(batch_size: 50).each do |build|
- Rails.logger.debug "Removing artifacts build #{build.id}..."
- build.erase_artifacts!
- end
+ build_ids = Ci::Build.with_expired_artifacts.pluck(:id)
+ build_ids = build_ids.map { |build_id| [build_id] }
+
+ Sidekiq::Client.push_bulk('class' => ExpireBuildInstanceArtifactsWorker, 'args' => build_ids )
end
end
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
new file mode 100644
index 00000000000..eb403c134d1
--- /dev/null
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -0,0 +1,16 @@
+class ExpireBuildInstanceArtifactsWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(build_id)
+ build = Ci::Build
+ .with_expired_artifacts
+ .reorder(nil)
+ .find_by(id: build_id)
+
+ return unless build.try(:project)
+
+ Rails.logger.info "Removing artifacts for build #{build.id}..."
+ build.erase_artifacts!
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index a6cefd4d601..65f8093b5b0 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,8 +1,9 @@
class GitGarbageCollectWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :gitlab_shell, retry: false
+ sidekiq_options retry: false
def perform(project_id)
project = Project.find(project_id)
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index cfeda88bbc5..964287a1793 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -1,8 +1,7 @@
class GitlabShellWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
-
- sidekiq_options queue: :gitlab_shell
+ include DedicatedSidekiqQueue
def perform(action, *arg)
gitlab_shell.send(action, *arg)
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 5048746f09b..a49a5fd0855 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -1,7 +1,6 @@
class GroupDestroyWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(group_id, user_id)
begin
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index 72e3a9ae734..7957ed807ab 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -1,7 +1,6 @@
class ImportExportProjectCleanupWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include CronjobQueue
def perform
ImportExportCleanUpService.new.execute
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 19f38358eb5..7e44b241743 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -3,6 +3,7 @@ require 'socket'
class IrkerWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
def perform(project_id, chans, colors, push_data, settings)
project = Project.find(project_id)
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index c87c0a252b1..79efca4f2f9 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -1,7 +1,6 @@
class MergeWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(merge_request_id, current_user_id, params)
params = params.with_indifferent_access
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 1b3232cd365..c3e62bb88c0 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -1,7 +1,6 @@
class NewNoteWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(note_id, note_params)
note = Note.find(note_id)
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
new file mode 100644
index 00000000000..7e36eacebf8
--- /dev/null
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -0,0 +1,9 @@
+class PipelineHooksWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id)
+ .try(:execute_hooks)
+ end
+end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
new file mode 100644
index 00000000000..34f6ef161fb
--- /dev/null
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -0,0 +1,29 @@
+class PipelineMetricsWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+ update_metrics_for_active_pipeline(pipeline) if pipeline.active?
+ update_metrics_for_succeeded_pipeline(pipeline) if pipeline.success?
+ end
+ end
+
+ private
+
+ def update_metrics_for_active_pipeline(pipeline)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ end
+
+ def update_metrics_for_succeeded_pipeline(pipeline)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at)
+ end
+
+ def metrics(pipeline)
+ MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline))
+ end
+
+ def merge_requests(pipeline)
+ pipeline.merge_requests.map(&:id)
+ end
+end
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
new file mode 100644
index 00000000000..357e4a9a1c3
--- /dev/null
+++ b/app/workers/pipeline_process_worker.rb
@@ -0,0 +1,9 @@
+class PipelineProcessWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id)
+ .try(:process!)
+ end
+end
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
new file mode 100644
index 00000000000..2aa6fff24da
--- /dev/null
+++ b/app/workers/pipeline_success_worker.rb
@@ -0,0 +1,12 @@
+class PipelineSuccessWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+ MergeRequests::MergeWhenBuildSucceedsService
+ .new(pipeline.project, nil)
+ .trigger(pipeline)
+ end
+ end
+end
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
new file mode 100644
index 00000000000..96c4152c674
--- /dev/null
+++ b/app/workers/pipeline_update_worker.rb
@@ -0,0 +1,9 @@
+class PipelineUpdateWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id)
+ .try(:update_status)
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index a9a2b716005..eee0ca12af9 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,7 +1,6 @@
class PostReceive
include Sidekiq::Worker
-
- sidekiq_options queue: :post_receive
+ include DedicatedSidekiqQueue
def perform(repo_path, identifier, changes)
if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index ccefd0f71a0..71b274e0c99 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,9 +1,29 @@
+# Worker for updating any project specific caches.
+#
+# This worker runs at most once every 15 minutes per project. This is to ensure
+# that multiple instances of jobs for this worker don't hammer the underlying
+# storage engine as much.
class ProjectCacheWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :default
+ LEASE_TIMEOUT = 15.minutes.to_i
def perform(project_id)
+ if try_obtain_lease_for(project_id)
+ Rails.logger.
+ info("Obtained ProjectCacheWorker lease for project #{project_id}")
+ else
+ Rails.logger.
+ info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
+
+ return
+ end
+
+ update_caches(project_id)
+ end
+
+ def update_caches(project_id)
project = Project.find(project_id)
return unless project.repository.exists?
@@ -15,4 +35,10 @@ class ProjectCacheWorker
project.repository.build_cache
end
end
+
+ def try_obtain_lease_for(project_id)
+ Gitlab::ExclusiveLease.
+ new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT).
+ try_obtain
+ end
end
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 3062301a9b1..b462327490e 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -1,7 +1,6 @@
class ProjectDestroyWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(project_id, user_id, params)
begin
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 615311e63f5..6009aa1b191 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,7 +1,8 @@
class ProjectExportWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :gitlab_shell, retry: 3
+ sidekiq_options retry: 3
def perform(current_user_id, project_id)
current_user = User.find(current_user_id)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 64d39c4d3f7..fdfdeab7b41 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -1,7 +1,6 @@
class ProjectServiceWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :project_web_hook
+ include DedicatedSidekiqQueue
def perform(hook_id, data)
data = data.with_indifferent_access
diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb
index fb878965288..efb85eafd15 100644
--- a/app/workers/project_web_hook_worker.rb
+++ b/app/workers/project_web_hook_worker.rb
@@ -1,7 +1,6 @@
class ProjectWebHookWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :project_web_hook
+ include DedicatedSidekiqQueue
def perform(hook_id, data, hook_name)
data = data.with_indifferent_access
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index 5883cafe1d1..392abb9c21b 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -1,5 +1,6 @@
class PruneOldEventsWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
# Contribution calendar shows maximum 12 months of events.
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 246c8b6650a..2a619f83410 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -1,5 +1,6 @@
class RemoveExpiredGroupLinksWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
ProjectGroupLink.expired.destroy_all
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index cf765af97ce..31f652e5f9b 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -1,5 +1,6 @@
class RemoveExpiredMembersWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
Member.expired.find_each do |member|
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index a2e49c61f59..e47069df189 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -1,7 +1,6 @@
class RepositoryArchiveCacheWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include CronjobQueue
def perform
RepositoryArchiveCleanUpService.new.execute
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index a3e16fa5212..c3e7491ec4e 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -1,14 +1,13 @@
module RepositoryCheck
class BatchWorker
include Sidekiq::Worker
-
+ include CronjobQueue
+
RUN_TIME = 3600
-
- sidekiq_options retry: false
-
+
def perform
start = Time.now
-
+
# This loop will break after a little more than one hour ('a little
# more' because `git fsck` may take a few minutes), or if it runs out of
# projects to check. By default sidekiq-cron will start a new
@@ -17,15 +16,15 @@ module RepositoryCheck
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
break unless current_settings.repository_checks_enabled
-
+
next unless try_obtain_lease(project_id)
-
+
SingleRepositoryWorker.new.perform(project_id)
end
end
-
+
private
-
+
# Project.find_each does not support WHERE clauses and
# Project.find_in_batches does not support ordering. So we just build an
# array of ID's. This is OK because we do it only once an hour, because
@@ -39,7 +38,7 @@ module RepositoryCheck
reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
never_checked_projects + old_check_projects
end
-
+
def try_obtain_lease(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is
# super slow we definitely do not want to run it twice in parallel.
@@ -48,7 +47,7 @@ module RepositoryCheck
timeout: 24.hours
).try_obtain
end
-
+
def current_settings
# No caching of the settings! If we cache them and an admin disables
# this feature, an active RepositoryCheckWorker would keep going for up
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index b7202ddff34..1f1b38540ee 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -1,8 +1,7 @@
module RepositoryCheck
class ClearWorker
include Sidekiq::Worker
-
- sidekiq_options retry: false
+ include RepositoryCheckQueue
def perform
# Do small batched updates because these updates will be slow and locking
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 98ddf5d0688..3d8bfc6fc6c 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -1,8 +1,7 @@
module RepositoryCheck
class SingleRepositoryWorker
include Sidekiq::Worker
-
- sidekiq_options retry: false
+ include RepositoryCheckQueue
def perform(project_id)
project = Project.find(project_id)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 61ed1c38ac4..efc99ec962a 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,8 +1,7 @@
class RepositoryForkWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
-
- sidekiq_options queue: :gitlab_shell
+ include DedicatedSidekiqQueue
def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
Gitlab::Metrics.add_event(:fork_repository,
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d2ca8813ab9..c8a77e21c12 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,8 +1,7 @@
class RepositoryImportWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
-
- sidekiq_options queue: :gitlab_shell
+ include DedicatedSidekiqQueue
attr_accessor :project, :current_user
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 9dd228a2483..703b025d76e 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -1,7 +1,6 @@
class RequestsProfilesWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include CronjobQueue
def perform
Gitlab::RequestProfiler.remove_all_profiles
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
index 6828013b377..b70df5a1afa 100644
--- a/app/workers/stuck_ci_builds_worker.rb
+++ b/app/workers/stuck_ci_builds_worker.rb
@@ -1,5 +1,6 @@
class StuckCiBuildsWorker
include Sidekiq::Worker
+ include CronjobQueue
BUILD_STUCK_TIMEOUT = 1.day
diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb
index a122c274763..baf2f12eeac 100644
--- a/app/workers/system_hook_worker.rb
+++ b/app/workers/system_hook_worker.rb
@@ -1,7 +1,6 @@
class SystemHookWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :system_hook
+ include DedicatedSidekiqQueue
def perform(hook_id, data, hook_name)
SystemHook.find(hook_id).execute(data, hook_name)
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
new file mode 100644
index 00000000000..0531630d13a
--- /dev/null
+++ b/app/workers/trending_projects_worker.rb
@@ -0,0 +1,10 @@
+class TrendingProjectsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ def perform
+ Rails.logger.info('Refreshing trending projects')
+
+ TrendingProject.refresh!
+ end
+end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
new file mode 100644
index 00000000000..acc4d858136
--- /dev/null
+++ b/app/workers/update_merge_requests_worker.rb
@@ -0,0 +1,17 @@
+class UpdateMergeRequestsWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(project_id, user_id, oldrev, newrev, ref)
+ project = Project.find_by(id: project_id)
+ return unless project
+
+ user = User.find_by(id: user_id)
+ return unless user
+
+ MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
+
+ push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, [])
+ SystemHooksService.new.execute_hooks(push_data, :push_hooks)
+ end
+end
diff --git a/bin/background_jobs b/bin/background_jobs
index 25a578a1c49..f28e2f722dc 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -4,6 +4,7 @@ cd $(dirname $0)/..
app_root=$(pwd)
sidekiq_pidfile="$app_root/tmp/pids/sidekiq.pid"
sidekiq_logfile="$app_root/log/sidekiq.log"
+sidekiq_config="$app_root/config/sidekiq_queues.yml"
gitlab_user=$(ls -l config.ru | awk '{print $3}')
warn()
@@ -37,7 +38,7 @@ start_no_deamonize()
start_sidekiq()
{
- exec bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@"
+ exec bundle exec sidekiq -C "${sidekiq_config}" -e $RAILS_ENV -P $sidekiq_pidfile "$@"
}
load_ok()
diff --git a/config/application.rb b/config/application.rb
index 4792f6670a8..92c8467e7f4 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -24,7 +24,8 @@ module Gitlab
#{config.root}/app/models/ci
#{config.root}/app/models/hooks
#{config.root}/app/models/members
- #{config.root}/app/models/project_services))
+ #{config.root}/app/models/project_services
+ #{config.root}/app/workers/concerns))
config.generators.templates.push("#{config.root}/generator_templates")
@@ -50,6 +51,7 @@ module Gitlab
# - Build variables (:variables)
# - GitLab Pages SSL cert/key info (:certificate, :encrypted_key)
# - Webhook URLs (:hook)
+ # - GitLab-shell secret token (:secret_token)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
config.filter_parameters += %i(
@@ -62,6 +64,7 @@ module Gitlab
password
password_confirmation
private_token
+ secret_token
sentry_dsn
variables
)
@@ -85,8 +88,10 @@ module Gitlab
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"
config.assets.precompile << "profile/profile_bundle.js"
+ config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
+ config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
@@ -99,13 +104,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 +132,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/gitlab.yml.example b/config/gitlab.yml.example
index 1470a6e2550..114ceac8e1f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -70,6 +70,7 @@ production: &base
email_from: example@example.com
email_display_name: GitLab
email_reply_to: noreply@example.com
+ email_subject_suffix: ''
# Email server smtp settings are in config/initializers/smtp_settings.rb.sample
@@ -111,7 +112,7 @@ production: &base
## Reply by email
# Allow users to comment on issues and merge requests by replying to notification emails.
- # For documentation on how to set this up, see http://doc.gitlab.com/ce/incoming_email/README.html
+ # For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html
incoming_email:
enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 195108b921b..efe0ac9c965 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -186,6 +186,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni
Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
+Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || ""
Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url)
Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
@@ -303,6 +304,10 @@ 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'
+Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'
+Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker'
+
#
# 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/ar5_batching.rb b/config/initializers/ar5_batching.rb
new file mode 100644
index 00000000000..35e8b3808e2
--- /dev/null
+++ b/config/initializers/ar5_batching.rb
@@ -0,0 +1,41 @@
+# Port ActiveRecord::Relation#in_batches from ActiveRecord 5.
+# https://github.com/rails/rails/blob/ac027338e4a165273607dccee49a3d38bc836794/activerecord/lib/active_record/relation/batches.rb#L184
+# TODO: this can be removed once we're using AR5.
+raise "Vendored ActiveRecord 5 code! Delete #{__FILE__}!" if ActiveRecord::VERSION::MAJOR >= 5
+
+module ActiveRecord
+ module Batches
+ # Differences from upstream: enumerator support was removed, and custom
+ # order/limit clauses are ignored without a warning.
+ def in_batches(of: 1000, start: nil, finish: nil, load: false)
+ raise "Must provide a block" unless block_given?
+
+ relation = self.reorder(batch_order).limit(of)
+ relation = relation.where(arel_table[primary_key].gteq(start)) if start
+ relation = relation.where(arel_table[primary_key].lteq(finish)) if finish
+ batch_relation = relation
+
+ loop do
+ if load
+ records = batch_relation.records
+ ids = records.map(&:id)
+ yielded_relation = self.where(primary_key => ids)
+ yielded_relation.load_records(records)
+ else
+ ids = batch_relation.pluck(primary_key)
+ yielded_relation = self.where(primary_key => ids)
+ end
+
+ break if ids.empty?
+
+ primary_key_offset = ids.last
+ raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
+
+ yield yielded_relation
+
+ break if ids.length < of
+ batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
+ end
+ end
+ end
+end
diff --git a/config/initializers/ar_speed_up_migration_checking.rb b/config/initializers/ar_speed_up_migration_checking.rb
new file mode 100644
index 00000000000..1fe5defc01d
--- /dev/null
+++ b/config/initializers/ar_speed_up_migration_checking.rb
@@ -0,0 +1,18 @@
+if Rails.env.test?
+ require 'active_record/migration'
+
+ module ActiveRecord
+ class Migrator
+ class << self
+ alias_method :migrations_unmemoized, :migrations
+
+ # This method is called a large number of times per rspec example, and
+ # it reads + parses `db/migrate/*` each time. Memoizing it can save 0.5
+ # seconds per spec.
+ def migrations(paths)
+ @migrations ||= migrations_unmemoized(paths)
+ end
+ end
+ end
+ end
+end
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/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb
index 7454c33c9dd..529dcdd4644 100644
--- a/config/initializers/gitlab_shell_secret_token.rb
+++ b/config/initializers/gitlab_shell_secret_token.rb
@@ -1 +1 @@
-Gitlab::Shell.new.generate_and_link_secret_token
+Gitlab::Shell.ensure_secret_token!
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index be22085b0df..3b8771543e4 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled?
['app', 'finders'] => ['app', 'finders'],
['app', 'mailers', 'emails'] => ['app', 'mailers'],
['app', 'services', '**'] => ['app', 'services'],
+ ['lib', 'gitlab', 'conflicts'] => ['lib'],
['lib', 'gitlab', 'diff'] => ['lib'],
['lib', 'gitlab', 'email', 'message'] => ['lib'],
['lib', 'gitlab', 'checks'] => ['lib']
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/initializers/sentry.rb b/config/initializers/sentry.rb
index 5892c1de024..4f30d1265c8 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -18,6 +18,8 @@ if Rails.env.production?
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
+ # Sanitize authentication headers
+ config.sanitize_http_headers = %w[Authorization Private-Token]
config.tags = { program: Gitlab::Sentry.program_context }
end
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index cedb5e207bd..12a59be79f0 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -5,6 +5,7 @@ en:
hello: "Hello world"
errors:
messages:
+ label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
wrong_size: "is the wrong size (should be %{file_size})"
size_too_small: "is too small (should be at least %{file_size})"
size_too_big: "is too big (should be at most %{file_size})"
diff --git a/config/mail_room.yml b/config/mail_room.yml
index c639f8260aa..68697bd1dc4 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -25,7 +25,7 @@
:delivery_options:
:redis_url: <%= config[:redis_url].to_json %>
:namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %>
- :queue: incoming_email
+ :queue: email_receiver
:worker: EmailReceiverWorker
:arbitration_method: redis
diff --git a/config/routes.rb b/config/routes.rb
index ba3864b92be..659ea51bc75 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,34 +2,13 @@ require 'sidekiq/web'
require 'sidekiq/cron/web'
require 'api/api'
-Rails.application.routes.draw do
- if Gitlab::Sherlock.enabled?
- namespace :sherlock do
- resources :transactions, only: [:index, :show] do
- resources :queries, only: [:show]
- resources :file_samples, only: [:show]
-
- collection do
- delete :destroy_all
- end
- end
- end
- end
-
- if Rails.env.development?
- # Make the built-in Rails routes available in development, otherwise they'd
- # get swallowed by the `namespace/project` route matcher below.
- #
- # See https://git.io/va79N
- get '/rails/mailers' => 'rails/mailers#index'
- get '/rails/mailers/:path' => 'rails/mailers#preview'
- get '/rails/info/properties' => 'rails/info#properties'
- get '/rails/info/routes' => 'rails/info#routes'
- get '/rails/info' => 'rails/info#index'
-
- mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
+class ActionDispatch::Routing::Mapper
+ def draw(routes_name)
+ instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
end
+end
+Rails.application.routes.draw do
concern :access_requestable do
post :request_access, on: :collection
post :approve_access_request, on: :member
@@ -39,21 +18,9 @@ Rails.application.routes.draw do
post :toggle_award_emoji, on: :member
end
- namespace :ci do
- # CI API
- Ci::API::API.logger Rails.logger
- mount Ci::API::API => '/api'
-
- resource :lint, only: [:show, :create]
-
- resources :projects, only: [:index, :show] do
- member do
- get :status, to: 'projects#badge'
- end
- end
-
- root to: 'projects#index'
- end
+ draw :sherlock
+ draw :development
+ draw :ci
use_doorkeeper do
controllers applications: 'oauth/applications',
@@ -76,44 +43,18 @@ Rails.application.routes.draw do
# JSON Web Token
get 'jwt/auth' => 'jwt#auth'
- # API
- API::API.logger Rails.logger
- mount API::API => '/api'
-
- constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
- constraints constraint do
- mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
- end
-
# Health check
get 'health_check(/:checks)' => 'health_check#index', as: :health_check
- # Help
- get 'help' => 'help#index'
- get 'help/shortcuts' => 'help#shortcuts'
- get 'help/ui' => 'help#ui'
- get 'help/*path' => 'help#show', as: :help_page
-
- #
# Koding route
- #
get 'koding' => 'koding#index'
- #
- # Global snippets
- #
- resources :snippets, concerns: :awardable do
- member do
- get 'raw'
- end
- end
-
- get '/s/:username', to: redirect('/u/%{username}/snippets'),
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
+ draw :api
+ draw :sidekiq
+ draw :help
+ draw :snippets
- #
# Invites
- #
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
@@ -127,810 +68,26 @@ Rails.application.routes.draw do
end
end
- #
# Spam reports
- #
resources :abuse_reports, only: [:new, :create]
- #
# Notification settings
- #
resources :notification_settings, only: [:create, :update]
- #
- # Import
- #
- namespace :import do
- resource :github, only: [:create, :new], controller: :github do
- post :personal_access_token
- get :status
- get :callback
- get :jobs
- end
-
- resource :gitlab, only: [:create], controller: :gitlab do
- get :status
- get :callback
- get :jobs
- end
-
- resource :bitbucket, only: [:create], controller: :bitbucket do
- get :status
- get :callback
- get :jobs
- end
-
- resource :google_code, only: [:create, :new], controller: :google_code do
- get :status
- post :callback
- get :jobs
-
- get :new_user_map, path: :user_map
- post :create_user_map, path: :user_map
- end
-
- resource :fogbugz, only: [:create, :new], controller: :fogbugz do
- get :status
- post :callback
- get :jobs
-
- get :new_user_map, path: :user_map
- post :create_user_map, path: :user_map
- end
-
- resource :gitlab_project, only: [:create, :new] do
- post :create
- end
- end
-
- #
- # Uploads
- #
-
- scope path: :uploads do
- # Note attachments and User/Group/Project avatars
- get ":model/:mounted_as/:id/:filename",
- to: "uploads#show",
- constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
-
- # Appearance
- get ":model/:mounted_as/:id/:filename",
- to: "uploads#show",
- constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
-
- # Project markdown uploads
- get ":namespace_id/:project_id/:secret/:filename",
- to: "projects/uploads#show",
- constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
- end
-
- # Redirect old note attachments path to new uploads path.
- get "files/note/:id/:filename",
- to: redirect("uploads/note/attachment/%{id}/%{filename}"),
- constraints: { filename: /[^\/]+/ }
-
- #
- # Explore area
- #
- namespace :explore do
- resources :projects, only: [:index] do
- collection do
- get :trending
- get :starred
- end
- end
-
- resources :groups, only: [:index]
- resources :snippets, only: [:index]
- root to: 'projects#trending'
- end
-
- # Compatibility with old routing
- get 'public' => 'explore/projects#index'
- get 'public/projects' => 'explore/projects#index'
-
- #
- # Admin Area
- #
- namespace :admin do
- resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
- resources :keys, only: [:show, :destroy]
- resources :identities, except: [:show]
-
- member do
- get :projects
- get :keys
- get :groups
- put :block
- put :unblock
- put :unlock
- put :confirm
- post :impersonate
- patch :disable_two_factor
- delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
- end
- end
-
- resource :impersonation, only: :destroy
-
- resources :abuse_reports, only: [:index, :destroy]
- resources :spam_logs, only: [:index, :destroy] do
- member do
- post :mark_as_ham
- end
- end
-
- resources :applications
-
- resources :groups, constraints: { id: /[^\/]+/ } do
- member do
- put :members_update
- end
- end
-
- resources :deploy_keys, only: [:index, :new, :create, :destroy]
-
- resources :hooks, only: [:index, :create, :destroy] do
- get :test
- end
-
- resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
- post :preview, on: :collection
- end
-
- resource :logs, only: [:show]
- resource :health_check, controller: 'health_check', only: [:show]
- resource :background_jobs, controller: 'background_jobs', only: [:show]
- resource :system_info, controller: 'system_info', only: [:show]
- resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
-
- resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- root to: 'projects#index', as: :projects
-
- resources(:projects,
- path: '/',
- constraints: { id: /[a-zA-Z.0-9_\-]+/ },
- only: [:index, :show]) do
- root to: 'projects#show'
-
- member do
- put :transfer
- post :repository_check
- end
-
- resources :runner_projects, only: [:create, :destroy]
- end
- end
-
- resource :appearances, only: [:show, :create, :update], path: 'appearance' do
- member do
- get :preview
- delete :logo
- delete :header_logos
- end
- end
-
- resource :application_settings, only: [:show, :update] do
- resources :services, only: [:index, :edit, :update]
- put :reset_runners_token
- put :reset_health_check_token
- put :clear_repository_check_states
- end
-
- resources :labels
-
- resources :runners, only: [:index, :show, :update, :destroy] do
- member do
- get :resume
- get :pause
- end
- end
-
- resources :builds, only: :index do
- collection do
- post :cancel_all
- end
- end
-
- root to: 'dashboard#index'
- end
-
- #
- # Profile Area
- #
- resource :profile, only: [:show, :update] do
- member do
- get :audit_log
- get :applications, to: 'oauth/applications#index'
-
- put :reset_private_token
- put :update_username
- end
-
- scope module: :profiles do
- resource :account, only: [:show] do
- member do
- delete :unlink
- end
- end
- resource :notifications, only: [:show, :update]
- resource :password, only: [:new, :create, :edit, :update] do
- member do
- put :reset
- end
- end
- resource :preferences, only: [:show, :update]
- resources :keys, only: [:index, :show, :new, :create, :destroy]
- resources :emails, only: [:index, :create, :destroy]
- resource :avatar, only: [:destroy]
-
- resources :personal_access_tokens, only: [:index, :create] do
- member do
- put :revoke
- end
- end
-
- resource :two_factor_auth, only: [:show, :create, :destroy] do
- member do
- post :create_u2f
- post :codes
- patch :skip
- end
- end
-
- resources :u2f_registrations, only: [:destroy]
- end
- end
-
- scope(path: 'u/:username',
- as: :user,
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
- controller: :users) do
- get :calendar
- get :calendar_activities
- get :groups
- get :projects
- get :contributed, as: :contributed_projects
- get :snippets
- get '/', action: :show
- end
-
- #
- # Dashboard Area
- #
- resource :dashboard, controller: 'dashboard', only: [] do
- get :issues
- get :merge_requests
- get :activity
-
- scope module: :dashboard do
- resources :milestones, only: [:index, :show]
- resources :labels, only: [:index]
-
- resources :groups, only: [:index]
- resources :snippets, only: [:index]
-
- resources :todos, only: [:index, :destroy] do
- collection do
- delete :destroy_all
- end
- end
-
- resources :projects, only: [:index] do
- collection do
- get :starred
- end
- end
- end
-
- root to: "dashboard/projects#index"
- end
-
- #
- # Groups Area
- #
- resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
- member do
- get :issues
- get :merge_requests
- get :projects
- get :activity
- end
-
- scope module: :groups do
- resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
- post :resend_invite, on: :member
- delete :leave, on: :collection
- end
-
- resource :avatar, only: [:destroy]
- resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
- end
- end
-
- resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
-
- devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
- registrations: :registrations,
- passwords: :passwords,
- sessions: :sessions,
- confirmations: :confirmations }
-
- devise_scope :user do
- get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
- get '/users/almost_there' => 'confirmations#almost_there'
- end
-
- root to: "root#index"
-
- #
- # Project Area
- #
- resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
- [:new, :create, :index], path: "/") do
- member do
- put :transfer
- delete :remove_fork
- post :archive
- post :unarchive
- post :housekeeping
- post :toggle_star
- post :preview_markdown
- post :export
- post :remove_export
- post :generate_new_export
- get :download_export
- get :autocomplete_sources
- get :activity
- get :refs
- end
-
- scope module: :projects do
- scope constraints: { id: /.+\.git/, format: nil } do
- # Git HTTP clients ('git clone' etc.)
- get '/info/refs', to: 'git_http#info_refs'
- post '/git-upload-pack', to: 'git_http#git_upload_pack'
- post '/git-receive-pack', to: 'git_http#git_receive_pack'
-
- # Git LFS API (metadata)
- post '/info/lfs/objects/batch', to: 'lfs_api#batch'
- post '/info/lfs/objects', to: 'lfs_api#deprecated'
- get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
-
- # GitLab LFS object storage
- scope constraints: { oid: /[a-f0-9]{64}/ } do
- get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
-
- scope constraints: { size: /[0-9]+/ } do
- put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
- put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
- end
- end
- end
-
- # Allow /info/refs, /info/refs?service=git-upload-pack, and
- # /info/refs?service=git-receive-pack, but nothing else.
- #
- git_http_handshake = lambda do |request|
- request.query_string.blank? ||
- request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
- end
-
- ref_redirect = redirect do |params, request|
- path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
- path << "?#{request.query_string}" unless request.query_string.blank?
- path
- end
-
- get '/info/refs', constraints: git_http_handshake, to: ref_redirect
-
- # Blob routes:
- get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
- post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
- get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
- put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
- post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
-
- #
- # Templates
- #
- get '/templates/:template_type/:key' => 'templates#show', as: :template
-
- scope do
- get(
- '/blob/*id/diff',
- to: 'blob#diff',
- constraints: { id: /.+/, format: false },
- as: :blob_diff
- )
- get(
- '/blob/*id',
- to: 'blob#show',
- constraints: { id: /.+/, format: false },
- as: :blob
- )
- delete(
- '/blob/*id',
- to: 'blob#destroy',
- constraints: { id: /.+/, format: false }
- )
- put(
- '/blob/*id',
- to: 'blob#update',
- constraints: { id: /.+/, format: false }
- )
- post(
- '/blob/*id',
- to: 'blob#create',
- constraints: { id: /.+/, format: false }
- )
- end
-
- scope do
- get(
- '/raw/*id',
- to: 'raw#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :raw
- )
- end
-
- scope do
- get(
- '/tree/*id',
- to: 'tree#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :tree
- )
- end
-
- scope do
- get(
- '/find_file/*id',
- to: 'find_file#show',
- constraints: { id: /.+/, format: /html/ },
- as: :find_file
- )
- end
-
- scope do
- get(
- '/files/*id',
- to: 'find_file#list',
- constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
- as: :files
- )
- end
-
- scope do
- post(
- '/create_dir/*id',
- to: 'tree#create_dir',
- constraints: { id: /.+/ },
- as: 'create_dir'
- )
- end
-
- scope do
- get(
- '/blame/*id',
- to: 'blame#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :blame
- )
- end
-
- scope do
- get(
- '/commits/*id',
- to: 'commits#show',
- constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
- as: :commits
- )
- end
-
- resource :avatar, only: [:show, :destroy]
- resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
- member do
- get :branches
- get :builds
- get :pipelines
- post :cancel_builds
- post :retry_builds
- post :revert
- post :cherry_pick
- get :diff_for_path
- end
- end
-
- resources :compare, only: [:index, :create] do
- collection do
- get :diff_for_path
- end
- end
-
- get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
-
- # Don't use format parameter as file extension (old 3.0.x behavior)
- # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
- scope format: false do
- resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
-
- resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
- member do
- get :commits
- get :ci
- get :languages
- end
- end
- end
-
- resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- get 'raw'
- end
- end
-
- WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
-
- scope do
- # Order matters to give priority to these matches
- get '/wikis/git_access', to: 'wikis#git_access'
- get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
- post '/wikis', to: 'wikis#create'
-
- get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
- get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
-
- get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
- delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
- put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
- post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
- end
-
- resource :repository, only: [:create] do
- member do
- get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
- end
- end
-
- resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
- member do
- get :test
- end
- end
-
- resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
- member do
- put :enable
- put :disable
- end
- end
-
- resources :forks, only: [:index, :new, :create]
- resource :import, only: [:new, :create, :show]
-
- resources :refs, only: [] do
- collection do
- get 'switch'
- end
-
- member do
- # tree viewer logs
- get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
- # Directories with leading dots erroneously get rejected if git
- # ref regex used in constraints. Regex verification now done in controller.
- get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
- id: /.*/,
- path: /.*/
- }
- end
- end
-
- resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- get :commits
- get :diffs
- get :conflicts
- get :builds
- get :pipelines
- get :merge_check
- post :merge
- post :cancel_merge_when_build_succeeds
- get :ci_status
- post :toggle_subscription
- post :remove_wip
- get :diff_for_path
- post :resolve_conflicts
- end
-
- collection do
- get :branch_from
- get :branch_to
- get :update_branches
- get :diff_for_path
- post :bulk_update
- end
-
- resources :discussions, only: [], constraints: { id: /\h{40}/ } do
- member do
- post :resolve
- delete :resolve, action: :unresolve
- end
- end
- end
-
- resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
- resource :release, only: [:edit, :update]
- end
-
- resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resources :variables, only: [:index, :show, :update, :create, :destroy]
- resources :triggers, only: [:index, :create, :destroy]
-
- resources :pipelines, only: [:index, :new, :create, :show] do
- collection do
- resource :pipelines_settings, path: 'settings', only: [:show, :update]
- end
-
- member do
- post :cancel
- post :retry
- end
- end
-
- resources :environments
-
- resource :cycle_analytics, only: [:show]
-
- resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
- collection do
- post :cancel_all
-
- resources :artifacts, only: [] do
- collection do
- get :latest_succeeded,
- path: '*ref_name_and_path',
- format: false
- end
- end
- end
-
- member do
- get :status
- post :cancel
- post :retry
- post :play
- post :erase
- get :trace
- get :raw
- end
-
- resource :artifacts, only: [] do
- get :download
- get :browse, path: 'browse(/*path)', format: false
- get :file, path: 'file/*path', format: false
- post :keep
- end
- end
-
- resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
- member do
- get :test
- end
- end
-
- resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
-
- resources :milestones, constraints: { id: /\d+/ } do
- member do
- put :sort_issues
- put :sort_merge_requests
- end
- end
-
- resources :labels, except: [:show], constraints: { id: /\d+/ } do
- collection do
- post :generate
- post :set_priorities
- end
-
- member do
- post :toggle_subscription
- delete :remove_priority
- end
- end
-
- resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- post :toggle_subscription
- post :mark_as_spam
- get :referenced_merge_requests
- get :related_branches
- get :can_create_branch
- end
- collection do
- post :bulk_update
- end
- end
-
- resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
- collection do
- delete :leave
-
- # Used for import team
- # from another project
- get :import
- post :apply_import
- end
-
- member do
- post :resend_invite
- end
- end
-
- resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
-
- resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- delete :delete_attachment
- post :resolve
- delete :resolve, action: :unresolve
- end
- end
-
- resource :board, only: [:show] do
- scope module: :boards do
- resources :issues, only: [:update]
-
- resources :lists, only: [:index, :create, :update, :destroy] do
- collection do
- post :generate
- end
-
- resources :issues, only: [:index]
- end
- end
- end
-
- resources :todos, only: [:create]
-
- resources :uploads, only: [:create] do
- collection do
- get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
- end
- end
-
- resources :runners, only: [:index, :edit, :update, :destroy, :show] do
- member do
- get :resume
- get :pause
- end
-
- collection do
- post :toggle_shared_runners
- end
- end
-
- resources :runner_projects, only: [:create, :destroy]
- resources :badges, only: [:index] do
- collection do
- scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
- constraints format: /svg/ do
- get :build
- get :coverage
- end
- end
- end
- end
- end
- end
- end
+ draw :import
+ draw :uploads
+ draw :explore
+ draw :admin
+ draw :profile
+ draw :dashboard
+ draw :group
+ draw :user
+ draw :project
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
- get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
+ root to: "root#index"
+
+ get '*unmatched_route', to: 'application#not_found'
end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
new file mode 100644
index 00000000000..5ae985da561
--- /dev/null
+++ b/config/routes/admin.rb
@@ -0,0 +1,102 @@
+namespace :admin do
+ resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
+ resources :keys, only: [:show, :destroy]
+ resources :identities, except: [:show]
+
+ member do
+ get :projects
+ get :keys
+ get :groups
+ put :block
+ put :unblock
+ put :unlock
+ put :confirm
+ post :impersonate
+ patch :disable_two_factor
+ delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
+ end
+ end
+
+ resource :impersonation, only: :destroy
+
+ resources :abuse_reports, only: [:index, :destroy]
+ resources :spam_logs, only: [:index, :destroy] do
+ member do
+ post :mark_as_ham
+ end
+ end
+
+ resources :applications
+
+ resources :groups, constraints: { id: /[^\/]+/ } do
+ member do
+ put :members_update
+ end
+ end
+
+ resources :deploy_keys, only: [:index, :new, :create, :destroy]
+
+ resources :hooks, only: [:index, :create, :destroy] do
+ get :test
+ end
+
+ resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
+ post :preview, on: :collection
+ end
+
+ resource :logs, only: [:show]
+ resource :health_check, controller: 'health_check', only: [:show]
+ resource :background_jobs, controller: 'background_jobs', only: [:show]
+ resource :system_info, controller: 'system_info', only: [:show]
+ resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
+
+ resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
+ root to: 'projects#index', as: :projects
+
+ resources(:projects,
+ path: '/',
+ constraints: { id: /[a-zA-Z.0-9_\-]+/ },
+ only: [:index, :show]) do
+ root to: 'projects#show'
+
+ member do
+ put :transfer
+ post :repository_check
+ end
+
+ resources :runner_projects, only: [:create, :destroy]
+ end
+ end
+
+ resource :appearances, only: [:show, :create, :update], path: 'appearance' do
+ member do
+ get :preview
+ delete :logo
+ delete :header_logos
+ end
+ end
+
+ resource :application_settings, only: [:show, :update] do
+ resources :services, only: [:index, :edit, :update]
+ put :reset_runners_token
+ put :reset_health_check_token
+ put :clear_repository_check_states
+ end
+
+ resources :labels
+
+ resources :runners, only: [:index, :show, :update, :destroy] do
+ member do
+ get :resume
+ get :pause
+ end
+ end
+
+ resources :builds, only: :index do
+ collection do
+ post :cancel_all
+ end
+ end
+
+ root to: 'dashboard#index'
+end
diff --git a/config/routes/api.rb b/config/routes/api.rb
new file mode 100644
index 00000000000..69c8efc151c
--- /dev/null
+++ b/config/routes/api.rb
@@ -0,0 +1,2 @@
+API::API.logger Rails.logger
+mount API::API => '/api'
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
new file mode 100644
index 00000000000..47a049d5b20
--- /dev/null
+++ b/config/routes/ci.rb
@@ -0,0 +1,15 @@
+namespace :ci do
+ # CI API
+ Ci::API::API.logger Rails.logger
+ mount Ci::API::API => '/api'
+
+ resource :lint, only: [:show, :create]
+
+ resources :projects, only: [:index, :show] do
+ member do
+ get :status, to: 'projects#badge'
+ end
+ end
+
+ root to: 'projects#index'
+end
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
new file mode 100644
index 00000000000..fb20c63bc63
--- /dev/null
+++ b/config/routes/dashboard.rb
@@ -0,0 +1,27 @@
+resource :dashboard, controller: 'dashboard', only: [] do
+ get :issues
+ get :merge_requests
+ get :activity
+
+ scope module: :dashboard do
+ resources :milestones, only: [:index, :show]
+ resources :labels, only: [:index]
+
+ resources :groups, only: [:index]
+ resources :snippets, only: [:index]
+
+ resources :todos, only: [:index, :destroy] do
+ collection do
+ delete :destroy_all
+ end
+ end
+
+ resources :projects, only: [:index] do
+ collection do
+ get :starred
+ end
+ end
+ end
+
+ root to: "dashboard/projects#index"
+end
diff --git a/config/routes/development.rb b/config/routes/development.rb
new file mode 100644
index 00000000000..9b2b47c6a21
--- /dev/null
+++ b/config/routes/development.rb
@@ -0,0 +1,13 @@
+if Rails.env.development?
+ # Make the built-in Rails routes available in development, otherwise they'd
+ # get swallowed by the `namespace/project` route matcher below.
+ #
+ # See https://git.io/va79N
+ get '/rails/mailers' => 'rails/mailers#index'
+ get '/rails/mailers/:path' => 'rails/mailers#preview'
+ get '/rails/info/properties' => 'rails/info#properties'
+ get '/rails/info/routes' => 'rails/info#routes'
+ get '/rails/info' => 'rails/info#index'
+
+ mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
+end
diff --git a/config/routes/explore.rb b/config/routes/explore.rb
new file mode 100644
index 00000000000..42ec5e8abec
--- /dev/null
+++ b/config/routes/explore.rb
@@ -0,0 +1,16 @@
+namespace :explore do
+ resources :projects, only: [:index] do
+ collection do
+ get :trending
+ get :starred
+ end
+ end
+
+ resources :groups, only: [:index]
+ resources :snippets, only: [:index]
+ root to: 'projects#trending'
+end
+
+# Compatibility with old routing
+get 'public' => 'explore/projects#index'
+get 'public/projects' => 'explore/projects#index'
diff --git a/config/routes/group.rb b/config/routes/group.rb
new file mode 100644
index 00000000000..4838c9d91c6
--- /dev/null
+++ b/config/routes/group.rb
@@ -0,0 +1,34 @@
+require 'constraints/group_url_constrainer'
+
+constraints(GroupUrlConstrainer.new) do
+ scope(path: ':id',
+ as: :group,
+ constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ controller: :groups) do
+ get '/', action: :show
+ patch '/', action: :update
+ put '/', action: :update
+ delete '/', action: :destroy
+ end
+end
+
+resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
+ member do
+ get :issues
+ get :merge_requests
+ get :projects
+ get :activity
+ end
+
+ scope module: :groups do
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+ post :resend_invite, on: :member
+ delete :leave, on: :collection
+ end
+
+ resource :avatar, only: [:destroy]
+ resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
+
+ resources :labels, except: [:show], constraints: { id: /\d+/ }
+ end
+end
diff --git a/config/routes/help.rb b/config/routes/help.rb
new file mode 100644
index 00000000000..d53822da9ec
--- /dev/null
+++ b/config/routes/help.rb
@@ -0,0 +1,4 @@
+get 'help' => 'help#index'
+get 'help/shortcuts' => 'help#shortcuts'
+get 'help/ui' => 'help#ui'
+get 'help/*path' => 'help#show', as: :help_page
diff --git a/config/routes/import.rb b/config/routes/import.rb
new file mode 100644
index 00000000000..89f3b3f6378
--- /dev/null
+++ b/config/routes/import.rb
@@ -0,0 +1,42 @@
+namespace :import do
+ resource :github, only: [:create, :new], controller: :github do
+ post :personal_access_token
+ get :status
+ get :callback
+ get :jobs
+ end
+
+ resource :gitlab, only: [:create], controller: :gitlab do
+ get :status
+ get :callback
+ get :jobs
+ end
+
+ resource :bitbucket, only: [:create], controller: :bitbucket do
+ get :status
+ get :callback
+ get :jobs
+ end
+
+ resource :google_code, only: [:create, :new], controller: :google_code do
+ get :status
+ post :callback
+ get :jobs
+
+ get :new_user_map, path: :user_map
+ post :create_user_map, path: :user_map
+ end
+
+ resource :fogbugz, only: [:create, :new], controller: :fogbugz do
+ get :status
+ post :callback
+ get :jobs
+
+ get :new_user_map, path: :user_map
+ post :create_user_map, path: :user_map
+ end
+
+ resource :gitlab_project, only: [:create, :new] do
+ post :create
+ end
+end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
new file mode 100644
index 00000000000..4cb68c9b34a
--- /dev/null
+++ b/config/routes/profile.rb
@@ -0,0 +1,43 @@
+resource :profile, only: [:show, :update] do
+ member do
+ get :audit_log
+ get :applications, to: 'oauth/applications#index'
+
+ put :reset_private_token
+ put :update_username
+ end
+
+ scope module: :profiles do
+ resource :account, only: [:show] do
+ member do
+ delete :unlink
+ end
+ end
+ resource :notifications, only: [:show, :update]
+ resource :password, only: [:new, :create, :edit, :update] do
+ member do
+ put :reset
+ end
+ end
+ resource :preferences, only: [:show, :update]
+ resources :keys, only: [:index, :show, :new, :create, :destroy]
+ resources :emails, only: [:index, :create, :destroy]
+ resource :avatar, only: [:destroy]
+
+ resources :personal_access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
+
+ resource :two_factor_auth, only: [:show, :create, :destroy] do
+ member do
+ post :create_u2f
+ post :codes
+ patch :skip
+ end
+ end
+
+ resources :u2f_registrations, only: [:destroy]
+ end
+end
diff --git a/config/routes/project.rb b/config/routes/project.rb
new file mode 100644
index 00000000000..8142e231621
--- /dev/null
+++ b/config/routes/project.rb
@@ -0,0 +1,472 @@
+resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
+
+resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
+ resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
+ [:new, :create, :index], path: "/") do
+ member do
+ put :transfer
+ delete :remove_fork
+ post :archive
+ post :unarchive
+ post :housekeeping
+ post :toggle_star
+ post :preview_markdown
+ post :export
+ post :remove_export
+ post :generate_new_export
+ get :download_export
+ get :autocomplete_sources
+ get :activity
+ get :refs
+ end
+
+ scope module: :projects do
+ scope constraints: { id: /.+\.git/, format: nil } do
+ # Git HTTP clients ('git clone' etc.)
+ get '/info/refs', to: 'git_http#info_refs'
+ post '/git-upload-pack', to: 'git_http#git_upload_pack'
+ post '/git-receive-pack', to: 'git_http#git_receive_pack'
+
+ # Git LFS API (metadata)
+ post '/info/lfs/objects/batch', to: 'lfs_api#batch'
+ post '/info/lfs/objects', to: 'lfs_api#deprecated'
+ get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
+
+ # GitLab LFS object storage
+ scope constraints: { oid: /[a-f0-9]{64}/ } do
+ get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
+
+ scope constraints: { size: /[0-9]+/ } do
+ put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
+ put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
+ end
+ end
+ end
+
+ # Allow /info/refs, /info/refs?service=git-upload-pack, and
+ # /info/refs?service=git-receive-pack, but nothing else.
+ #
+ git_http_handshake = lambda do |request|
+ request.query_string.blank? ||
+ request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
+ end
+
+ ref_redirect = redirect do |params, request|
+ path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
+ path << "?#{request.query_string}" unless request.query_string.blank?
+ path
+ end
+
+ get '/info/refs', constraints: git_http_handshake, to: ref_redirect
+
+ # Blob routes:
+ get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
+ post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
+ get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
+ put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
+ post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+
+ #
+ # Templates
+ #
+ get '/templates/:template_type/:key' => 'templates#show', as: :template
+
+ scope do
+ get(
+ '/blob/*id/diff',
+ to: 'blob#diff',
+ constraints: { id: /.+/, format: false },
+ as: :blob_diff
+ )
+ get(
+ '/blob/*id',
+ to: 'blob#show',
+ constraints: { id: /.+/, format: false },
+ as: :blob
+ )
+ delete(
+ '/blob/*id',
+ to: 'blob#destroy',
+ constraints: { id: /.+/, format: false }
+ )
+ put(
+ '/blob/*id',
+ to: 'blob#update',
+ constraints: { id: /.+/, format: false }
+ )
+ post(
+ '/blob/*id',
+ to: 'blob#create',
+ constraints: { id: /.+/, format: false }
+ )
+ end
+
+ scope do
+ get(
+ '/raw/*id',
+ to: 'raw#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :raw
+ )
+ end
+
+ scope do
+ get(
+ '/tree/*id',
+ to: 'tree#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :tree
+ )
+ end
+
+ scope do
+ get(
+ '/find_file/*id',
+ to: 'find_file#show',
+ constraints: { id: /.+/, format: /html/ },
+ as: :find_file
+ )
+ end
+
+ scope do
+ get(
+ '/files/*id',
+ to: 'find_file#list',
+ constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
+ as: :files
+ )
+ end
+
+ scope do
+ post(
+ '/create_dir/*id',
+ to: 'tree#create_dir',
+ constraints: { id: /.+/ },
+ as: 'create_dir'
+ )
+ end
+
+ scope do
+ get(
+ '/blame/*id',
+ to: 'blame#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :blame
+ )
+ end
+
+ scope do
+ get(
+ '/commits/*id',
+ to: 'commits#show',
+ constraints: { id: /.+/, format: false },
+ as: :commits
+ )
+ end
+
+ resource :avatar, only: [:show, :destroy]
+ resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
+ member do
+ get :branches
+ get :builds
+ get :pipelines
+ post :cancel_builds
+ post :retry_builds
+ post :revert
+ post :cherry_pick
+ get :diff_for_path
+ end
+ end
+
+ resources :compare, only: [:index, :create] do
+ collection do
+ get :diff_for_path
+ end
+ end
+
+ get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
+
+ # Don't use format parameter as file extension (old 3.0.x behavior)
+ # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
+ scope format: false do
+ resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
+
+ resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
+ member do
+ get :commits
+ get :ci
+ get :languages
+ end
+ end
+ end
+
+ resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ get 'raw'
+ end
+ end
+
+ WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
+
+ scope do
+ # Order matters to give priority to these matches
+ get '/wikis/git_access', to: 'wikis#git_access'
+ get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
+ post '/wikis', to: 'wikis#create'
+
+ get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
+ get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
+
+ get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
+ delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
+ put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
+ post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
+ end
+
+ resource :repository, only: [:create] do
+ member do
+ get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
+ end
+ end
+
+ resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
+ member do
+ get :test
+ end
+ end
+
+ resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
+ member do
+ put :enable
+ put :disable
+ end
+ end
+
+ resources :forks, only: [:index, :new, :create]
+ resource :import, only: [:new, :create, :show]
+
+ resources :refs, only: [] do
+ collection do
+ get 'switch'
+ end
+
+ member do
+ # tree viewer logs
+ get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
+ # Directories with leading dots erroneously get rejected if git
+ # ref regex used in constraints. Regex verification now done in controller.
+ get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
+ id: /.*/,
+ path: /.*/
+ }
+ end
+ end
+
+ resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ get :commits
+ get :diffs
+ get :conflicts
+ get :conflict_for_path
+ get :builds
+ get :pipelines
+ get :merge_check
+ post :merge
+ post :cancel_merge_when_build_succeeds
+ get :ci_status
+ get :ci_environments_status
+ post :toggle_subscription
+ post :remove_wip
+ get :diff_for_path
+ post :resolve_conflicts
+ post :assign_related_issues
+ end
+
+ collection do
+ get :branch_from
+ get :branch_to
+ get :update_branches
+ get :diff_for_path
+ post :bulk_update
+ get :new_diffs, path: 'new/diffs'
+ end
+
+ resources :discussions, only: [], constraints: { id: /\h{40}/ } do
+ member do
+ post :resolve
+ delete :resolve, action: :unresolve
+ end
+ end
+ end
+
+ resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
+ resource :release, only: [:edit, :update]
+ end
+
+ resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resources :variables, only: [:index, :show, :update, :create, :destroy]
+ resources :triggers, only: [:index, :create, :destroy]
+
+ resources :pipelines, only: [:index, :new, :create, :show] do
+ collection do
+ resource :pipelines_settings, path: 'settings', only: [:show, :update]
+ end
+
+ member do
+ post :cancel
+ post :retry
+ end
+ end
+
+ resources :environments, except: [:destroy] do
+ member do
+ post :stop
+ end
+ end
+
+ resource :cycle_analytics, only: [:show]
+
+ resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
+ collection do
+ post :cancel_all
+
+ resources :artifacts, only: [] do
+ collection do
+ get :latest_succeeded,
+ path: '*ref_name_and_path',
+ format: false
+ end
+ end
+ end
+
+ member do
+ get :status
+ post :cancel
+ post :retry
+ post :play
+ post :erase
+ get :trace
+ get :raw
+ end
+
+ resource :artifacts, only: [] do
+ get :download
+ get :browse, path: 'browse(/*path)', format: false
+ get :file, path: 'file/*path', format: false
+ post :keep
+ end
+ end
+
+ resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
+ member do
+ get :test
+ end
+ end
+
+ resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
+
+ resources :milestones, constraints: { id: /\d+/ } do
+ member do
+ put :sort_issues
+ put :sort_merge_requests
+ end
+ end
+
+ resources :labels, except: [:show], constraints: { id: /\d+/ } do
+ collection do
+ post :generate
+ post :set_priorities
+ end
+
+ member do
+ post :toggle_subscription
+ delete :remove_priority
+ end
+ end
+
+ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ post :toggle_subscription
+ post :mark_as_spam
+ get :referenced_merge_requests
+ get :related_branches
+ get :can_create_branch
+ end
+ collection do
+ post :bulk_update
+ end
+ end
+
+ resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
+ collection do
+ delete :leave
+
+ # Used for import team
+ # from another project
+ get :import
+ post :apply_import
+ end
+
+ member do
+ post :resend_invite
+ end
+ end
+
+ resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
+
+ resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ delete :delete_attachment
+ post :resolve
+ delete :resolve, action: :unresolve
+ end
+ end
+
+ resources :boards, only: [:index, :show] do
+ scope module: :boards do
+ resources :issues, only: [:update]
+
+ resources :lists, only: [:index, :create, :update, :destroy] do
+ collection do
+ post :generate
+ end
+
+ resources :issues, only: [:index, :create]
+ end
+ end
+ end
+
+ resources :todos, only: [:create]
+
+ resources :uploads, only: [:create] do
+ collection do
+ get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
+ end
+ end
+
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ member do
+ get :resume
+ get :pause
+ end
+
+ collection do
+ post :toggle_shared_runners
+ end
+ end
+
+ resources :runner_projects, only: [:create, :destroy]
+ resources :badges, only: [:index] do
+ collection do
+ scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ constraints format: /svg/ do
+ get :build
+ get :coverage
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/routes/sherlock.rb b/config/routes/sherlock.rb
new file mode 100644
index 00000000000..c9969f91c36
--- /dev/null
+++ b/config/routes/sherlock.rb
@@ -0,0 +1,12 @@
+if Gitlab::Sherlock.enabled?
+ namespace :sherlock do
+ resources :transactions, only: [:index, :show] do
+ resources :queries, only: [:show]
+ resources :file_samples, only: [:show]
+
+ collection do
+ delete :destroy_all
+ end
+ end
+ end
+end
diff --git a/config/routes/sidekiq.rb b/config/routes/sidekiq.rb
new file mode 100644
index 00000000000..d3e6bc4c292
--- /dev/null
+++ b/config/routes/sidekiq.rb
@@ -0,0 +1,4 @@
+constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
+constraints constraint do
+ mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
+end
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
new file mode 100644
index 00000000000..3ca096f31ba
--- /dev/null
+++ b/config/routes/snippets.rb
@@ -0,0 +1,9 @@
+resources :snippets, concerns: :awardable do
+ member do
+ get 'raw'
+ get 'download'
+ end
+end
+
+get '/s/:username', to: redirect('/u/%{username}/snippets'),
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
new file mode 100644
index 00000000000..2b22148a134
--- /dev/null
+++ b/config/routes/uploads.rb
@@ -0,0 +1,21 @@
+scope path: :uploads do
+ # Note attachments and User/Group/Project avatars
+ get ":model/:mounted_as/:id/:filename",
+ to: "uploads#show",
+ constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
+
+ # Appearance
+ get ":model/:mounted_as/:id/:filename",
+ to: "uploads#show",
+ constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
+
+ # Project markdown uploads
+ get ":namespace_id/:project_id/:secret/:filename",
+ to: "projects/uploads#show",
+ constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
+end
+
+# Redirect old note attachments path to new uploads path.
+get "files/note/:id/:filename",
+ to: redirect("uploads/note/attachment/%{id}/%{filename}"),
+ constraints: { filename: /[^\/]+/ }
diff --git a/config/routes/user.rb b/config/routes/user.rb
new file mode 100644
index 00000000000..0a9c924863d
--- /dev/null
+++ b/config/routes/user.rb
@@ -0,0 +1,44 @@
+require 'constraints/user_url_constrainer'
+
+devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
+ registrations: :registrations,
+ passwords: :passwords,
+ sessions: :sessions,
+ confirmations: :confirmations }
+
+devise_scope :user do
+ get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
+ get '/users/almost_there' => 'confirmations#almost_there'
+end
+
+constraints(UserUrlConstrainer.new) do
+ scope(path: ':username',
+ as: :user,
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ controller: :users) do
+ get '/', action: :show
+ end
+end
+
+scope(path: 'users/:username',
+ as: :user,
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ controller: :users) do
+ get :calendar
+ get :calendar_activities
+ get :groups
+ get :projects
+ get :contributed, as: :contributed_projects
+ get :snippets
+ get :exists
+ get '/', to: redirect('/%{username}')
+end
+
+# Compatibility with old routing
+# TODO (dzaporozhets): remove in 10.0
+get '/u/:username', to: redirect('/%{username}'), constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
+# TODO (dzaporozhets): remove in 9.0
+get '/u/:username/groups', to: redirect('/users/%{username}/groups'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
+get '/u/:username/projects', to: redirect('/users/%{username}/projects'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
+get '/u/:username/snippets', to: redirect('/users/%{username}/snippets'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
+get '/u/:username/contributed', to: redirect('/users/%{username}/contributed'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
new file mode 100644
index 00000000000..f36fe893fd0
--- /dev/null
+++ b/config/sidekiq_queues.yml
@@ -0,0 +1,47 @@
+# This configuration file should be exclusively used to set queue settings for
+# Sidekiq. Any other setting should be specified using the Sidekiq CLI or the
+# Sidekiq Ruby API (see config/initializers/sidekiq.rb).
+---
+# All the queues to process and their weights. Every queue _must_ have a weight
+# defined.
+#
+# The available weights are as follows
+#
+# 1: low priority
+# 2: medium priority
+# 3: high priority
+# 5: _super_ high priority, this should only be used for _very_ important queues
+#
+# As per http://stackoverflow.com/a/21241357/290102 the formula for calculating
+# the likelihood of a job being popped off a queue (given all queues have work
+# to perform) is:
+#
+# chance = (queue weight / total weight of all queues) * 100
+:queues:
+ - [post_receive, 5]
+ - [merge, 5]
+ - [update_merge_requests, 3]
+ - [new_note, 2]
+ - [build, 2]
+ - [pipeline, 2]
+ - [gitlab_shell, 2]
+ - [email_receiver, 2]
+ - [emails_on_push, 2]
+ - [mailers, 2]
+ - [repository_fork, 1]
+ - [repository_import, 1]
+ - [project_service, 1]
+ - [clear_database_cache, 1]
+ - [delete_user, 1]
+ - [expire_build_instance_artifacts, 1]
+ - [group_destroy, 1]
+ - [irker, 1]
+ - [project_cache, 1]
+ - [project_destroy, 1]
+ - [project_export, 1]
+ - [project_web_hook, 1]
+ - [repository_check, 1]
+ - [system_hook, 1]
+ - [git_garbage_collect, 1]
+ - [cronjob, 1]
+ - [default, 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 650b410595c..08ad3097d34 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -16,7 +16,8 @@ class Gitlab::Seeder::Pipelines
{ name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
{ name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
{ name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
- { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success },
+ { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } },
+ { name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
@@ -34,7 +35,7 @@ class Gitlab::Seeder::Pipelines
rescue ActiveRecord::RecordInvalid
print 'F'
ensure
- pipeline.build_updated
+ pipeline.update_status
end
end
end
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
new file mode 100644
index 00000000000..8753e55e058
--- /dev/null
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.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 AddMarkdownCacheColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ COLUMNS = {
+ abuse_reports: [:message],
+ appearances: [:description],
+ application_settings: [
+ :sign_in_text,
+ :help_page_text,
+ :shared_runners_text,
+ :after_sign_up_text
+ ],
+ broadcast_messages: [:message],
+ issues: [:title, :description],
+ labels: [:description],
+ merge_requests: [:title, :description],
+ milestones: [:title, :description],
+ namespaces: [:description],
+ notes: [:note],
+ projects: [:description],
+ releases: [:description],
+ snippets: [:title, :content],
+ }
+
+ def change
+ COLUMNS.each do |table, columns|
+ columns.each do |column|
+ add_column table, "#{column}_html", :text
+ end
+ end
+ end
+end
diff --git a/db/migrate/20160919144305_add_type_to_labels.rb b/db/migrate/20160919144305_add_type_to_labels.rb
new file mode 100644
index 00000000000..66172bda6ff
--- /dev/null
+++ b/db/migrate/20160919144305_add_type_to_labels.rb
@@ -0,0 +1,14 @@
+class AddTypeToLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Labels will not work as expected until this migration is complete.'
+
+ def change
+ add_column :labels, :type, :string
+
+ update_column_in_batches(:labels, :type, 'ProjectLabel') do |table, query|
+ query.where(table[:project_id].not_eq(nil))
+ end
+ end
+end
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
new file mode 100644
index 00000000000..05e21af0584
--- /dev/null
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -0,0 +1,13 @@
+class AddGroupIdToLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_column :labels, :group_id, :integer
+ add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade
+ add_concurrent_index :labels, :group_id
+ end
+end
diff --git a/db/migrate/20161006104309_add_state_to_environment.rb b/db/migrate/20161006104309_add_state_to_environment.rb
new file mode 100644
index 00000000000..ccb546654f9
--- /dev/null
+++ b/db/migrate/20161006104309_add_state_to_environment.rb
@@ -0,0 +1,15 @@
+class AddStateToEnvironment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:environments, :state, :string, default: :available)
+ end
+
+ def down
+ remove_column(:environments, :state)
+ end
+end
diff --git a/db/migrate/20161007133303_precalculate_trending_projects.rb b/db/migrate/20161007133303_precalculate_trending_projects.rb
new file mode 100644
index 00000000000..b324cd94268
--- /dev/null
+++ b/db/migrate/20161007133303_precalculate_trending_projects.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 PrecalculateTrendingProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :trending_projects do |t|
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
+ end
+
+ timestamp = connection.quote(1.month.ago)
+
+ # We're hardcoding the visibility level (public) here so that if it ever
+ # changes this query doesn't suddenly use the new value (which may break
+ # later migrations).
+ visibility = 20
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO trending_projects (project_id)
+ SELECT project_id
+ FROM notes
+ INNER JOIN projects ON projects.id = notes.project_id
+ WHERE notes.created_at >= #{timestamp}
+ AND notes.system IS FALSE
+ AND projects.visibility_level = #{visibility}
+ GROUP BY project_id
+ ORDER BY count(*) DESC
+ LIMIT 100;
+ EOF
+ end
+
+ def down
+ drop_table :trending_projects
+ end
+end
diff --git a/db/migrate/20161012180455_add_repository_access_level_to_project_feature.rb b/db/migrate/20161012180455_add_repository_access_level_to_project_feature.rb
new file mode 100644
index 00000000000..7b33da3ea11
--- /dev/null
+++ b/db/migrate/20161012180455_add_repository_access_level_to_project_feature.rb
@@ -0,0 +1,14 @@
+class AddRepositoryAccessLevelToProjectFeature < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:project_features, :repository_access_level, :integer, default: ProjectFeature::ENABLED)
+ end
+
+ def down
+ remove_column :project_features, :repository_access_level
+ end
+end
diff --git a/db/migrate/20161014173530_create_label_priorities.rb b/db/migrate/20161014173530_create_label_priorities.rb
new file mode 100644
index 00000000000..2c22841c28a
--- /dev/null
+++ b/db/migrate/20161014173530_create_label_priorities.rb
@@ -0,0 +1,25 @@
+class CreateLabelPriorities < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration adds foreign keys'
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :label_priorities do |t|
+ t.references :project, foreign_key: { on_delete: :cascade }, null: false
+ t.references :label, foreign_key: { on_delete: :cascade }, null: false
+ t.integer :priority, null: false
+
+ t.timestamps null: false
+ end
+
+ add_concurrent_index :label_priorities, [:project_id, :label_id], unique: true
+ add_concurrent_index :label_priorities, :priority
+ end
+
+ def down
+ drop_table :label_priorities
+ end
+end
diff --git a/db/migrate/20161017095000_add_properties_to_deployment.rb b/db/migrate/20161017095000_add_properties_to_deployment.rb
new file mode 100644
index 00000000000..f620ee0de1c
--- /dev/null
+++ b/db/migrate/20161017095000_add_properties_to_deployment.rb
@@ -0,0 +1,9 @@
+class AddPropertiesToDeployment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :deployments, :on_stop, :string
+ end
+end
diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb
new file mode 100644
index 00000000000..f2b56ebfb7b
--- /dev/null
+++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb
@@ -0,0 +1,32 @@
+class AddUniqueIndexToLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration removes duplicated labels.'
+
+ disable_ddl_transaction!
+
+ def up
+ select_all('SELECT title, project_id, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label|
+ label_title = quote_string(label['title'])
+ duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] }
+ label_id = duplicated_ids.first
+ duplicated_ids.delete(label_id)
+
+ execute("UPDATE label_links SET label_id = #{label_id} WHERE label_id IN(#{duplicated_ids.join(",")})")
+ execute("DELETE FROM labels WHERE id IN(#{duplicated_ids.join(",")})")
+ end
+
+ remove_index :labels, column: :project_id if index_exists?(:labels, :project_id)
+ remove_index :labels, column: :title if index_exists?(:labels, :title)
+
+ add_concurrent_index :labels, [:group_id, :project_id, :title], unique: true
+ end
+
+ def down
+ remove_index :labels, column: [:group_id, :project_id, :title] if index_exists?(:labels, [:group_id, :project_id, :title], unique: true)
+
+ add_concurrent_index :labels, :project_id
+ add_concurrent_index :labels, :title
+ end
+end
diff --git a/db/migrate/20161018024215_migrate_labels_priority.rb b/db/migrate/20161018024215_migrate_labels_priority.rb
new file mode 100644
index 00000000000..22bec2382f4
--- /dev/null
+++ b/db/migrate/20161018024215_migrate_labels_priority.rb
@@ -0,0 +1,36 @@
+class MigrateLabelsPriority < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Prioritized labels will not work as expected until this migration is complete.'
+
+ disable_ddl_transaction!
+
+ def up
+ execute <<-EOF.strip_heredoc
+ INSERT INTO label_priorities (project_id, label_id, priority, created_at, updated_at)
+ SELECT labels.project_id, labels.id, labels.priority, NOW(), NOW()
+ FROM labels
+ WHERE labels.project_id IS NOT NULL
+ AND labels.priority IS NOT NULL;
+ EOF
+ end
+
+ def down
+ if Gitlab::Database.mysql?
+ execute <<-EOF.strip_heredoc
+ UPDATE labels
+ INNER JOIN label_priorities ON labels.id = label_priorities.label_id AND labels.project_id = label_priorities.project_id
+ SET labels.priority = label_priorities.priority;
+ EOF
+ else
+ execute <<-EOF.strip_heredoc
+ UPDATE labels
+ SET priority = label_priorities.priority
+ FROM label_priorities
+ WHERE labels.id = label_priorities.label_id
+ AND labels.project_id = label_priorities.project_id;
+ EOF
+ end
+ end
+end
diff --git a/db/migrate/20161018024550_remove_priority_from_labels.rb b/db/migrate/20161018024550_remove_priority_from_labels.rb
new file mode 100644
index 00000000000..b7416cca664
--- /dev/null
+++ b/db/migrate/20161018024550_remove_priority_from_labels.rb
@@ -0,0 +1,17 @@
+class RemovePriorityFromLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration removes an existing column'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_column :labels, :priority, :integer, index: true
+ end
+
+ def down
+ add_column :labels, :priority, :integer
+ add_concurrent_index :labels, :priority
+ end
+end
diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb
new file mode 100644
index 00000000000..a576bb7b622
--- /dev/null
+++ b/db/migrate/20161018124658_make_project_owners_masters.rb
@@ -0,0 +1,15 @@
+class MakeProjectOwnersMasters < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:members, :access_level, 40) do |table, query|
+ query.where(table[:access_level].eq(50).and(table[:source_type].eq('Project')))
+ end
+ end
+
+ def down
+ # do nothing
+ end
+end
diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
new file mode 100644
index 00000000000..e875213ab96
--- /dev/null
+++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
@@ -0,0 +1,109 @@
+require 'json'
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+
+ DOWNTIME_REASON = <<-EOF
+ Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping
+ Sidekiq will result in the loss of jobs that are scheduled after this
+ migration completes.
+ EOF
+
+ disable_ddl_transaction!
+
+ # Jobs for which the queue names have been changed (e.g. multiple workers
+ # using the same non-default queue).
+ #
+ # The keys are the old queue names, the values the jobs to move and their new
+ # queue names.
+ RENAMED_QUEUES = {
+ gitlab_shell: {
+ 'GitGarbageCollectorWorker' => :git_garbage_collector,
+ 'ProjectExportWorker' => :project_export,
+ 'RepositoryForkWorker' => :repository_fork,
+ 'RepositoryImportWorker' => :repository_import
+ },
+ project_web_hook: {
+ 'ProjectServiceWorker' => :project_service
+ },
+ incoming_email: {
+ 'EmailReceiverWorker' => :email_receiver
+ },
+ mailers: {
+ 'EmailsOnPushWorker' => :emails_on_push
+ },
+ default: {
+ 'AdminEmailWorker' => :cronjob,
+ 'BuildCoverageWorker' => :build,
+ 'BuildEmailWorker' => :build,
+ 'BuildFinishedWorker' => :build,
+ 'BuildHooksWorker' => :build,
+ 'BuildSuccessWorker' => :build,
+ 'ClearDatabaseCacheWorker' => :clear_database_cache,
+ 'DeleteUserWorker' => :delete_user,
+ 'ExpireBuildArtifactsWorker' => :cronjob,
+ 'ExpireBuildInstanceArtifactsWorker' => :expire_build_instance_artifacts,
+ 'GroupDestroyWorker' => :group_destroy,
+ 'ImportExportProjectCleanupWorker' => :cronjob,
+ 'IrkerWorker' => :irker,
+ 'MergeWorker' => :merge,
+ 'NewNoteWorker' => :new_note,
+ 'PipelineHooksWorker' => :pipeline,
+ 'PipelineMetricsWorker' => :pipeline,
+ 'PipelineProcessWorker' => :pipeline,
+ 'PipelineSuccessWorker' => :pipeline,
+ 'PipelineUpdateWorker' => :pipeline,
+ 'ProjectCacheWorker' => :project_cache,
+ 'ProjectDestroyWorker' => :project_destroy,
+ 'PruneOldEventsWorker' => :cronjob,
+ 'RemoveExpiredGroupLinksWorker' => :cronjob,
+ 'RemoveExpiredMembersWorker' => :cronjob,
+ 'RepositoryArchiveCacheWorker' => :cronjob,
+ 'RepositoryCheck::BatchWorker' => :cronjob,
+ 'RepositoryCheck::ClearWorker' => :repository_check,
+ 'RepositoryCheck::SingleRepositoryWorker' => :repository_check,
+ 'RequestsProfilesWorker' => :cronjob,
+ 'StuckCiBuildsWorker' => :cronjob,
+ 'UpdateMergeRequestsWorker' => :update_merge_requests
+ }
+ }
+
+ def up
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |queue, jobs|
+ migrate_from_queue(redis, queue, jobs)
+ end
+ end
+ end
+
+ def down
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |dest_queue, jobs|
+ jobs.each do |worker, from_queue|
+ migrate_from_queue(redis, from_queue, worker => dest_queue)
+ end
+ end
+ end
+ end
+
+ def migrate_from_queue(redis, queue, job_mapping)
+ while job = redis.lpop("queue:#{queue}")
+ payload = JSON.load(job)
+ new_queue = job_mapping[payload['class']]
+
+ # If we have no target queue to migrate to we're probably dealing with
+ # some ancient job for which the worker no longer exists. In that case
+ # there's no sane option we can take, other than just dropping the job.
+ next unless new_queue
+
+ payload['queue'] = new_queue
+
+ redis.lpush("queue:#{new_queue}", JSON.dump(payload))
+ end
+ end
+end
diff --git a/db/migrate/20161019213545_generate_project_feature_for_projects.rb b/db/migrate/20161019213545_generate_project_feature_for_projects.rb
new file mode 100644
index 00000000000..4554e14b0df
--- /dev/null
+++ b/db/migrate/20161019213545_generate_project_feature_for_projects.rb
@@ -0,0 +1,28 @@
+class GenerateProjectFeatureForProjects < ActiveRecord::Migration
+ DOWNTIME = true
+
+ DOWNTIME_REASON = <<-HEREDOC
+ Application was eager loading project_feature for all projects generating an extra query
+ everytime a project was fetched. We removed that behavior to avoid the extra query, this migration
+ makes sure all projects have a project_feature record associated.
+ HEREDOC
+
+ def up
+ # Generate enabled values for each project feature 20, 20, 20, 20, 20
+ # All features are enabled by default
+ enabled_values = [ProjectFeature::ENABLED] * 5
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO project_features
+ (project_id, merge_requests_access_level, builds_access_level,
+ issues_access_level, snippets_access_level, wiki_access_level)
+ (SELECT projects.id, #{enabled_values.join(',')} FROM projects LEFT OUTER JOIN project_features
+ ON project_features.project_id = projects.id
+ WHERE project_features.id IS NULL)
+ EOF
+ end
+
+ def down
+ "Not needed"
+ end
+end
diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
new file mode 100644
index 00000000000..06d07bdb835
--- /dev/null
+++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
@@ -0,0 +1,63 @@
+require 'json'
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+
+ DOWNTIME_REASON = <<-EOF
+ Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping
+ Sidekiq will result in the loss of jobs that are scheduled after this
+ migration completes.
+ EOF
+
+ disable_ddl_transaction!
+
+ # Jobs for which the queue names have been changed (e.g. multiple workers
+ # using the same non-default queue).
+ #
+ # The keys are the old queue names, the values the jobs to move and their new
+ # queue names.
+ RENAMED_QUEUES = {
+ incoming_email: {
+ 'EmailReceiverWorker' => :email_receiver
+ }
+ }
+
+ def up
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |queue, jobs|
+ migrate_from_queue(redis, queue, jobs)
+ end
+ end
+ end
+
+ def down
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |dest_queue, jobs|
+ jobs.each do |worker, from_queue|
+ migrate_from_queue(redis, from_queue, worker => dest_queue)
+ end
+ end
+ end
+ end
+
+ def migrate_from_queue(redis, queue, job_mapping)
+ while job = redis.lpop("queue:#{queue}")
+ payload = JSON.load(job)
+ new_queue = job_mapping[payload['class']]
+
+ # If we have no target queue to migrate to we're probably dealing with
+ # some ancient job for which the worker no longer exists. In that case
+ # there's no sane option we can take, other than just dropping the job.
+ next unless new_queue
+
+ payload['queue'] = new_queue
+
+ redis.lpush("queue:#{new_queue}", JSON.dump(payload))
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3a24fd66630..63a61ebcf7d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,95 +11,101 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160926145521) do
+ActiveRecord::Schema.define(version: 20161024042317) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
create_table "abuse_reports", force: :cascade do |t|
- t.integer "reporter_id"
- t.integer "user_id"
- t.text "message"
+ t.integer "reporter_id"
+ t.integer "user_id"
+ t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
+ t.text "message_html"
end
create_table "appearances", force: :cascade do |t|
- t.string "title"
- t.text "description"
- t.string "header_logo"
- t.string "logo"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.string "title"
+ t.text "description"
+ t.string "header_logo"
+ t.string "logo"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "description_html"
end
create_table "application_settings", force: :cascade do |t|
- t.integer "default_projects_limit"
- t.boolean "signup_enabled"
- t.boolean "signin_enabled"
- t.boolean "gravatar_enabled"
- t.text "sign_in_text"
+ t.integer "default_projects_limit"
+ t.boolean "signup_enabled"
+ t.boolean "signin_enabled"
+ t.boolean "gravatar_enabled"
+ t.text "sign_in_text"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "home_page_url"
- t.integer "default_branch_protection", default: 2
- t.text "restricted_visibility_levels"
- t.boolean "version_check_enabled", default: true
- t.integer "max_attachment_size", default: 10, null: false
- t.integer "default_project_visibility"
- t.integer "default_snippet_visibility"
- t.text "domain_whitelist"
- t.boolean "user_oauth_applications", default: true
- t.string "after_sign_out_path"
- t.integer "session_expire_delay", default: 10080, null: false
- t.text "import_sources"
- t.text "help_page_text"
- t.string "admin_notification_email"
- t.boolean "shared_runners_enabled", default: true, null: false
- t.integer "max_artifacts_size", default: 100, null: false
- t.string "runners_registration_token"
- t.boolean "require_two_factor_authentication", default: false
- t.integer "two_factor_grace_period", default: 48
- t.boolean "metrics_enabled", default: false
- t.string "metrics_host", default: "localhost"
- t.integer "metrics_pool_size", default: 16
- t.integer "metrics_timeout", default: 10
- t.integer "metrics_method_call_threshold", default: 10
- t.boolean "recaptcha_enabled", default: false
- t.string "recaptcha_site_key"
- t.string "recaptcha_private_key"
- t.integer "metrics_port", default: 8089
- t.boolean "akismet_enabled", default: false
- t.string "akismet_api_key"
- t.integer "metrics_sample_interval", default: 15
- t.boolean "sentry_enabled", default: false
- t.string "sentry_dsn"
- t.boolean "email_author_in_body", default: false
- t.integer "default_group_visibility"
- t.boolean "repository_checks_enabled", default: false
- t.text "shared_runners_text"
- t.integer "metrics_packet_size", default: 1
- t.text "disabled_oauth_sign_in_sources"
- t.string "health_check_access_token"
- t.boolean "send_user_confirmation_email", default: false
- t.integer "container_registry_token_expire_delay", default: 5
- t.text "after_sign_up_text"
- t.boolean "user_default_external", default: false, null: false
- t.string "repository_storage", default: "default"
- t.string "enabled_git_access_protocol"
- t.boolean "domain_blacklist_enabled", default: false
- t.text "domain_blacklist"
- t.boolean "koding_enabled"
- t.string "koding_url"
+ t.string "home_page_url"
+ t.integer "default_branch_protection", default: 2
+ t.text "restricted_visibility_levels"
+ t.boolean "version_check_enabled", default: true
+ t.integer "max_attachment_size", default: 10, null: false
+ t.integer "default_project_visibility"
+ t.integer "default_snippet_visibility"
+ t.text "domain_whitelist"
+ t.boolean "user_oauth_applications", default: true
+ t.string "after_sign_out_path"
+ t.integer "session_expire_delay", default: 10080, null: false
+ t.text "import_sources"
+ t.text "help_page_text"
+ t.string "admin_notification_email"
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.integer "max_artifacts_size", default: 100, null: false
+ t.string "runners_registration_token"
+ t.boolean "require_two_factor_authentication", default: false
+ t.integer "two_factor_grace_period", default: 48
+ t.boolean "metrics_enabled", default: false
+ t.string "metrics_host", default: "localhost"
+ t.integer "metrics_pool_size", default: 16
+ t.integer "metrics_timeout", default: 10
+ t.integer "metrics_method_call_threshold", default: 10
+ t.boolean "recaptcha_enabled", default: false
+ t.string "recaptcha_site_key"
+ t.string "recaptcha_private_key"
+ t.integer "metrics_port", default: 8089
+ t.boolean "akismet_enabled", default: false
+ t.string "akismet_api_key"
+ t.integer "metrics_sample_interval", default: 15
+ t.boolean "sentry_enabled", default: false
+ t.string "sentry_dsn"
+ t.boolean "email_author_in_body", default: false
+ t.integer "default_group_visibility"
+ t.boolean "repository_checks_enabled", default: false
+ t.text "shared_runners_text"
+ t.integer "metrics_packet_size", default: 1
+ t.text "disabled_oauth_sign_in_sources"
+ t.string "health_check_access_token"
+ t.boolean "send_user_confirmation_email", default: false
+ t.integer "container_registry_token_expire_delay", default: 5
+ t.text "after_sign_up_text"
+ t.boolean "user_default_external", default: false, null: false
+ t.string "repository_storage", default: "default"
+ t.string "enabled_git_access_protocol"
+ t.boolean "domain_blacklist_enabled", default: false
+ t.text "domain_blacklist"
+ t.boolean "koding_enabled"
+ t.string "koding_url"
+ t.text "sign_in_text_html"
+ t.text "help_page_text_html"
+ t.text "shared_runners_text_html"
+ t.text "after_sign_up_text_html"
end
create_table "audit_events", force: :cascade do |t|
- t.integer "author_id", null: false
- t.string "type", null: false
- t.integer "entity_id", null: false
- t.string "entity_type", null: false
- t.text "details"
+ t.integer "author_id", null: false
+ t.string "type", null: false
+ t.integer "entity_id", null: false
+ t.string "entity_type", null: false
+ t.text "details"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -107,10 +113,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
create_table "award_emoji", force: :cascade do |t|
- t.string "name"
- t.integer "user_id"
- t.integer "awardable_id"
- t.string "awardable_type"
+ t.string "name"
+ t.integer "user_id"
+ t.integer "awardable_id"
+ t.string "awardable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -120,7 +126,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
create_table "boards", force: :cascade do |t|
- t.integer "project_id", null: false
+ t.integer "project_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
@@ -128,60 +134,61 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t|
- t.text "message", null: false
+ t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "color"
- t.string "font"
+ t.string "color"
+ t.string "font"
+ t.text "message_html"
end
create_table "ci_application_settings", force: :cascade do |t|
- t.boolean "all_broken_builds"
- t.boolean "add_pusher"
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "ci_builds", force: :cascade do |t|
- t.integer "project_id"
- t.string "status"
+ t.integer "project_id"
+ t.string "status"
t.datetime "finished_at"
- t.text "trace"
+ t.text "trace"
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "started_at"
- t.integer "runner_id"
- t.float "coverage"
- t.integer "commit_id"
- t.text "commands"
- t.integer "job_id"
- t.string "name"
- t.boolean "deploy", default: false
- t.text "options"
- t.boolean "allow_failure", default: false, null: false
- t.string "stage"
- t.integer "trigger_request_id"
- t.integer "stage_idx"
- t.boolean "tag"
- t.string "ref"
- t.integer "user_id"
- t.string "type"
- t.string "target_url"
- t.string "description"
- t.text "artifacts_file"
- t.integer "gl_project_id"
- t.text "artifacts_metadata"
- t.integer "erased_by_id"
+ t.integer "runner_id"
+ t.float "coverage"
+ t.integer "commit_id"
+ t.text "commands"
+ t.integer "job_id"
+ t.string "name"
+ t.boolean "deploy", default: false
+ t.text "options"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage"
+ t.integer "trigger_request_id"
+ t.integer "stage_idx"
+ t.boolean "tag"
+ t.string "ref"
+ t.integer "user_id"
+ t.string "type"
+ t.string "target_url"
+ t.string "description"
+ t.text "artifacts_file"
+ t.integer "gl_project_id"
+ t.text "artifacts_metadata"
+ t.integer "erased_by_id"
t.datetime "erased_at"
t.datetime "artifacts_expire_at"
- t.string "environment"
- t.integer "artifacts_size", limit: 8
- t.string "when"
- t.text "yaml_variables"
+ t.string "environment"
+ t.integer "artifacts_size", limit: 8
+ t.string "when"
+ t.text "yaml_variables"
t.datetime "queued_at"
- t.string "token"
+ 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
@@ -196,22 +203,22 @@ ActiveRecord::Schema.define(version: 20160926145521) do
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"
- t.string "ref"
- t.string "sha"
- t.string "before_sha"
- t.text "push_data"
+ t.integer "project_id"
+ t.string "ref"
+ t.string "sha"
+ t.string "before_sha"
+ t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "tag", default: false
- t.text "yaml_errors"
+ t.boolean "tag", default: false
+ t.text "yaml_errors"
t.datetime "committed_at"
- t.integer "gl_project_id"
- t.string "status"
+ t.integer "gl_project_id"
+ t.string "status"
t.datetime "started_at"
t.datetime "finished_at"
- t.integer "duration"
- t.integer "user_id"
+ t.integer "duration"
+ t.integer "user_id"
end
add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
@@ -221,140 +228,140 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
create_table "ci_events", force: :cascade do |t|
- t.integer "project_id"
- t.integer "user_id"
- t.integer "is_admin"
- t.text "description"
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "ci_jobs", force: :cascade do |t|
- t.integer "project_id", null: false
- t.text "commands"
- t.boolean "active", default: true, null: false
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "name"
- t.boolean "build_branches", default: true, null: false
- t.boolean "build_tags", default: false, null: false
- t.string "job_type", default: "parallel"
- t.string "refs"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
t.datetime "deleted_at"
end
create_table "ci_projects", force: :cascade do |t|
- t.string "name"
- t.integer "timeout", default: 3600, null: false
+ t.string "name"
+ t.integer "timeout", default: 3600, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "token"
- t.string "default_ref"
- t.string "path"
- t.boolean "always_build", default: false, null: false
- t.integer "polling_interval"
- t.boolean "public", default: false, null: false
- t.string "ssh_url_to_repo"
- t.integer "gitlab_id"
- t.boolean "allow_git_fetch", default: true, null: false
- t.string "email_recipients", default: "", null: false
- t.boolean "email_add_pusher", default: true, null: false
- t.boolean "email_only_broken_builds", default: true, null: false
- t.string "skip_refs"
- t.string "coverage_regex"
- t.boolean "shared_runners_enabled", default: false
- t.text "generated_yaml_config"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
end
create_table "ci_runner_projects", force: :cascade do |t|
- t.integer "runner_id", null: false
- t.integer "project_id"
+ t.integer "runner_id", null: false
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "gl_project_id"
end
add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
create_table "ci_runners", force: :cascade do |t|
- t.string "token"
+ t.string "token"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "description"
+ t.string "description"
t.datetime "contacted_at"
- t.boolean "active", default: true, null: false
- t.boolean "is_shared", default: false
- t.string "name"
- t.string "version"
- t.string "revision"
- t.string "platform"
- t.string "architecture"
- t.boolean "run_untagged", default: true, null: false
- t.boolean "locked", default: false, null: false
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name"
+ t.string "version"
+ t.string "revision"
+ t.string "platform"
+ t.string "architecture"
+ t.boolean "run_untagged", default: true, null: false
+ t.boolean "locked", default: false, null: false
end
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
create_table "ci_sessions", force: :cascade do |t|
- t.string "session_id", null: false
- t.text "data"
+ t.string "session_id", null: false
+ t.text "data"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "ci_taggings", force: :cascade do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context", limit: 128
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
t.datetime "created_at"
end
add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "ci_tags", force: :cascade do |t|
- t.string "name"
+ t.string "name"
t.integer "taggings_count", default: 0
end
create_table "ci_trigger_requests", force: :cascade do |t|
- t.integer "trigger_id", null: false
- t.text "variables"
+ t.integer "trigger_id", null: false
+ t.text "variables"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "commit_id"
+ t.integer "commit_id"
end
create_table "ci_triggers", force: :cascade do |t|
- t.string "token"
- t.integer "project_id"
+ t.string "token"
+ t.integer "project_id"
t.datetime "deleted_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "gl_project_id"
end
add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
create_table "ci_variables", force: :cascade do |t|
t.integer "project_id"
- t.string "key"
- t.text "value"
- t.text "encrypted_value"
- t.string "encrypted_value_salt"
- t.string "encrypted_value_iv"
+ t.string "key"
+ t.text "value"
+ t.text "encrypted_value"
+ t.string "encrypted_value_salt"
+ t.string "encrypted_value_iv"
t.integer "gl_project_id"
end
add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
create_table "deploy_keys_projects", force: :cascade do |t|
- t.integer "deploy_key_id", null: false
- t.integer "project_id", null: false
+ t.integer "deploy_key_id", null: false
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -362,17 +369,18 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
create_table "deployments", force: :cascade do |t|
- t.integer "iid", null: false
- t.integer "project_id", null: false
- t.integer "environment_id", null: false
- t.string "ref", null: false
- t.boolean "tag", null: false
- t.string "sha", null: false
- t.integer "user_id"
- t.integer "deployable_id"
- t.string "deployable_type"
+ t.integer "iid", null: false
+ t.integer "project_id", null: false
+ t.integer "environment_id", null: false
+ t.string "ref", null: false
+ t.boolean "tag", null: false
+ t.string "sha", null: false
+ t.integer "user_id"
+ t.integer "deployable_id"
+ t.string "deployable_type"
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "on_stop"
end
add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
@@ -381,8 +389,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree
create_table "emails", force: :cascade do |t|
- t.integer "user_id", null: false
- t.string "email", null: false
+ t.integer "user_id", null: false
+ t.string "email", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -391,26 +399,27 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
create_table "environments", force: :cascade do |t|
- t.integer "project_id"
- t.string "name", null: false
+ t.integer "project_id"
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "external_url"
- t.string "environment_type"
+ t.string "external_url"
+ t.string "environment_type"
+ t.string "state", default: "available", null: false
end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
create_table "events", force: :cascade do |t|
- t.string "target_type"
- t.integer "target_id"
- t.string "title"
- t.text "data"
- t.integer "project_id"
+ t.string "target_type"
+ t.integer "target_id"
+ t.string "title"
+ t.text "data"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "action"
- t.integer "author_id"
+ t.integer "action"
+ t.integer "author_id"
end
add_index "events", ["action"], name: "index_events_on_action", using: :btree
@@ -421,8 +430,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
create_table "forked_project_links", force: :cascade do |t|
- t.integer "forked_to_project_id", null: false
- t.integer "forked_from_project_id", null: false
+ t.integer "forked_to_project_id", null: false
+ t.integer "forked_from_project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -430,9 +439,9 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
create_table "identities", force: :cascade do |t|
- t.string "extern_uid"
- t.string "provider"
- t.integer "user_id"
+ t.string "extern_uid"
+ t.string "provider"
+ t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -440,35 +449,37 @@ ActiveRecord::Schema.define(version: 20160926145521) 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.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
+ 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"
- t.integer "author_id"
- t.integer "project_id"
+ t.string "title"
+ t.integer "assignee_id"
+ t.integer "author_id"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "position", default: 0
- t.string "branch_name"
- t.text "description"
- t.integer "milestone_id"
- t.string "state"
- t.integer "iid"
- t.integer "updated_by_id"
- t.boolean "confidential", default: false
+ t.integer "position", default: 0
+ t.string "branch_name"
+ t.text "description"
+ t.integer "milestone_id"
+ t.string "state"
+ t.integer "iid"
+ t.integer "updated_by_id"
+ t.boolean "confidential", default: false
t.datetime "deleted_at"
- t.date "due_date"
- t.integer "moved_to_id"
- t.integer "lock_version"
+ t.date "due_date"
+ t.integer "moved_to_id"
+ t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -484,23 +495,23 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t|
- t.integer "user_id"
+ t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.text "key"
- t.string "title"
- t.string "type"
- t.string "fingerprint"
- t.boolean "public", default: false, null: false
+ t.text "key"
+ t.string "title"
+ t.string "type"
+ t.string "fingerprint"
+ t.boolean "public", default: false, null: false
end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
create_table "label_links", force: :cascade do |t|
- t.integer "label_id"
- t.integer "target_id"
- t.string "target_type"
+ t.integer "label_id"
+ t.integer "target_id"
+ t.string "target_type"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -508,34 +519,46 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
+ create_table "label_priorities", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "label_id", null: false
+ t.integer "priority", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree
+ add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
+
create_table "labels", force: :cascade do |t|
- t.string "title"
- t.string "color"
- t.integer "project_id"
+ t.string "title"
+ t.string "color"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "template", default: false
- t.string "description"
- t.integer "priority"
+ t.boolean "template", default: false
+ t.string "description"
+ t.text "description_html"
+ t.string "type"
+ t.integer "group_id"
end
- 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
+ add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
+ add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
- t.string "oid", null: false
- t.integer "size", limit: 8, null: false
+ t.string "oid", null: false
+ t.integer "size", limit: 8, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "file"
+ t.string "file"
end
add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
create_table "lfs_objects_projects", force: :cascade do |t|
- t.integer "lfs_object_id", null: false
- t.integer "project_id", null: false
+ t.integer "lfs_object_id", null: false
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -543,12 +566,12 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
create_table "lists", force: :cascade do |t|
- t.integer "board_id", null: false
- t.integer "label_id"
- t.integer "list_type", default: 1, null: false
- t.integer "position"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "board_id", null: false
+ t.integer "label_id"
+ t.integer "list_type", default: 1, null: false
+ t.integer "position"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree
@@ -556,20 +579,20 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree
create_table "members", force: :cascade do |t|
- t.integer "access_level", null: false
- t.integer "source_id", null: false
- t.string "source_type", null: false
- t.integer "user_id"
- t.integer "notification_level", null: false
- t.string "type"
+ t.integer "access_level", null: false
+ t.integer "source_id", null: false
+ t.string "source_type", null: false
+ t.integer "user_id"
+ t.integer "notification_level", null: false
+ t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "created_by_id"
- t.string "invite_email"
- t.string "invite_token"
+ t.integer "created_by_id"
+ t.string "invite_email"
+ t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
- t.date "expires_at"
+ t.date "expires_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
@@ -579,59 +602,61 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "merge_request_diffs", force: :cascade do |t|
- t.string "state"
- t.text "st_commits"
- t.text "st_diffs"
- t.integer "merge_request_id", null: false
+ t.string "state"
+ t.text "st_commits"
+ t.text "st_diffs"
+ t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "base_commit_sha"
- t.string "real_size"
- t.string "head_commit_sha"
- t.string "start_commit_sha"
+ t.string "base_commit_sha"
+ t.string "real_size"
+ t.string "head_commit_sha"
+ t.string "start_commit_sha"
end
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.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
+ 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
- t.integer "source_project_id", null: false
- t.integer "author_id"
- t.integer "assignee_id"
- t.string "title"
+ t.string "target_branch", null: false
+ t.string "source_branch", null: false
+ t.integer "source_project_id", null: false
+ t.integer "author_id"
+ t.integer "assignee_id"
+ t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "milestone_id"
- t.string "state"
- t.string "merge_status"
- t.integer "target_project_id", null: false
- t.integer "iid"
- t.text "description"
- t.integer "position", default: 0
+ t.integer "milestone_id"
+ t.string "state"
+ t.string "merge_status"
+ t.integer "target_project_id", null: false
+ t.integer "iid"
+ t.text "description"
+ t.integer "position", default: 0
t.datetime "locked_at"
- t.integer "updated_by_id"
- t.text "merge_error"
- t.text "merge_params"
- t.boolean "merge_when_build_succeeds", default: false, null: false
- t.integer "merge_user_id"
- t.string "merge_commit_sha"
+ t.integer "updated_by_id"
+ t.text "merge_error"
+ t.text "merge_params"
+ t.boolean "merge_when_build_succeeds", default: false, null: false
+ t.integer "merge_user_id"
+ t.string "merge_commit_sha"
t.datetime "deleted_at"
- t.string "in_progress_merge_commit_sha"
- t.integer "lock_version"
+ t.string "in_progress_merge_commit_sha"
+ t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -648,24 +673,26 @@ ActiveRecord::Schema.define(version: 20160926145521) do
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
+ 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
- t.text "description"
- t.date "due_date"
+ t.string "title", null: false
+ t.integer "project_id", null: false
+ t.text "description"
+ t.date "due_date"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "state"
- t.integer "iid"
+ t.string "state"
+ t.integer "iid"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -676,19 +703,20 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
- t.integer "owner_id"
+ t.string "name", null: false
+ t.string "path", null: false
+ t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type"
- t.string "description", default: "", null: false
- t.string "avatar"
- t.boolean "share_with_group_lock", default: false
- t.integer "visibility_level", default: 20, null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.string "type"
+ t.string "description", default: "", null: false
+ t.string "avatar"
+ t.boolean "share_with_group_lock", default: false
+ 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"
+ t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -701,26 +729,27 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
- t.text "note"
- t.string "noteable_type"
- t.integer "author_id"
+ t.text "note"
+ t.string "noteable_type"
+ t.integer "author_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "project_id"
- t.string "attachment"
- t.string "line_code"
- t.string "commit_id"
- t.integer "noteable_id"
- t.boolean "system", default: false, null: false
- t.text "st_diff"
- t.integer "updated_by_id"
- t.string "type"
- t.text "position"
- t.text "original_position"
+ t.integer "project_id"
+ t.string "attachment"
+ t.string "line_code"
+ t.string "commit_id"
+ t.integer "noteable_id"
+ t.boolean "system", default: false, null: false
+ t.text "st_diff"
+ t.integer "updated_by_id"
+ t.string "type"
+ t.text "position"
+ t.text "original_position"
t.datetime "resolved_at"
- t.integer "resolved_by_id"
- t.string "discussion_id"
- t.string "original_discussion_id"
+ t.integer "resolved_by_id"
+ t.string "discussion_id"
+ t.string "original_discussion_id"
+ t.text "note_html"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
@@ -736,13 +765,13 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
create_table "notification_settings", force: :cascade do |t|
- t.integer "user_id", null: false
- t.integer "source_id"
- t.string "source_type"
- t.integer "level", default: 0, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.text "events"
+ t.integer "user_id", null: false
+ t.integer "source_id"
+ t.string "source_type"
+ t.integer "level", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "events"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
@@ -750,27 +779,27 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
create_table "oauth_access_grants", force: :cascade do |t|
- t.integer "resource_owner_id", null: false
- t.integer "application_id", null: false
- t.string "token", null: false
- t.integer "expires_in", null: false
- t.text "redirect_uri", null: false
- t.datetime "created_at", null: false
+ t.integer "resource_owner_id", null: false
+ t.integer "application_id", null: false
+ t.string "token", null: false
+ t.integer "expires_in", null: false
+ t.text "redirect_uri", null: false
+ t.datetime "created_at", null: false
t.datetime "revoked_at"
- t.string "scopes"
+ t.string "scopes"
end
add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
create_table "oauth_access_tokens", force: :cascade do |t|
- t.integer "resource_owner_id"
- t.integer "application_id"
- t.string "token", null: false
- t.string "refresh_token"
- t.integer "expires_in"
+ t.integer "resource_owner_id"
+ t.integer "application_id"
+ t.string "token", null: false
+ t.string "refresh_token"
+ t.integer "expires_in"
t.datetime "revoked_at"
- t.datetime "created_at", null: false
- t.string "scopes"
+ t.datetime "created_at", null: false
+ t.string "scopes"
end
add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
@@ -778,100 +807,102 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
create_table "oauth_applications", force: :cascade do |t|
- t.string "name", null: false
- t.string "uid", null: false
- t.string "secret", null: false
- t.text "redirect_uri", null: false
- t.string "scopes", default: "", null: false
+ t.string "name", null: false
+ t.string "uid", null: false
+ t.string "secret", null: false
+ t.text "redirect_uri", null: false
+ t.string "scopes", default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "owner_id"
- t.string "owner_type"
+ t.integer "owner_id"
+ t.string "owner_type"
end
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
create_table "personal_access_tokens", force: :cascade do |t|
- t.integer "user_id", null: false
- t.string "token", null: false
- t.string "name", null: false
- t.boolean "revoked", default: false
+ t.integer "user_id", null: false
+ t.string "token", null: false
+ t.string "name", null: false
+ t.boolean "revoked", default: false
t.datetime "expires_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "project_features", force: :cascade do |t|
- t.integer "project_id"
- t.integer "merge_requests_access_level"
- t.integer "issues_access_level"
- t.integer "wiki_access_level"
- t.integer "snippets_access_level"
- t.integer "builds_access_level"
+ t.integer "project_id"
+ t.integer "merge_requests_access_level"
+ t.integer "issues_access_level"
+ t.integer "wiki_access_level"
+ t.integer "snippets_access_level"
+ t.integer "builds_access_level"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "repository_access_level", default: 20, null: false
end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
create_table "project_group_links", force: :cascade do |t|
- t.integer "project_id", null: false
- t.integer "group_id", null: false
+ t.integer "project_id", null: false
+ t.integer "group_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "group_access", default: 30, null: false
- t.date "expires_at"
+ t.integer "group_access", default: 30, null: false
+ t.date "expires_at"
end
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
- t.text "data"
- t.text "encrypted_credentials"
- t.string "encrypted_credentials_iv"
- t.string "encrypted_credentials_salt"
+ t.text "data"
+ t.text "encrypted_credentials"
+ t.string "encrypted_credentials_iv"
+ t.string "encrypted_credentials_salt"
end
create_table "projects", force: :cascade do |t|
- t.string "name"
- t.string "path"
- t.text "description"
+ t.string "name"
+ t.string "path"
+ t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "creator_id"
- t.integer "namespace_id"
+ t.integer "creator_id"
+ t.integer "namespace_id"
t.datetime "last_activity_at"
- t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
- t.string "avatar"
- t.string "import_status"
- t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
- t.string "import_type"
- t.string "import_source"
- t.integer "commit_count", default: 0
- t.text "import_error"
- t.integer "ci_id"
- t.boolean "shared_runners_enabled", default: true, null: false
- t.string "runners_token"
- t.string "build_coverage_regex"
- t.boolean "build_allow_git_fetch", default: true, null: false
- t.integer "build_timeout", default: 3600, null: false
- t.boolean "pending_delete", default: false
- t.boolean "public_builds", default: true, null: false
- t.boolean "last_repository_check_failed"
+ t.string "import_url"
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
+ t.string "avatar"
+ t.string "import_status"
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
+ t.string "import_type"
+ t.string "import_source"
+ t.integer "commit_count", default: 0
+ t.text "import_error"
+ t.integer "ci_id"
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.string "runners_token"
+ t.string "build_coverage_regex"
+ t.boolean "build_allow_git_fetch", default: true, null: false
+ t.integer "build_timeout", default: 3600, null: false
+ t.boolean "pending_delete", default: false
+ t.boolean "public_builds", default: true, null: false
+ t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
- t.boolean "container_registry_enabled"
- t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
- t.boolean "has_external_issue_tracker"
- t.string "repository_storage", default: "default", null: false
- t.boolean "request_access_enabled", default: true, null: false
- t.boolean "has_external_wiki"
- t.boolean "lfs_enabled"
+ t.boolean "container_registry_enabled"
+ t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
+ t.boolean "has_external_issue_tracker"
+ t.string "repository_storage", default: "default", null: false
+ t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "has_external_wiki"
+ t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -890,26 +921,26 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
create_table "protected_branch_merge_access_levels", force: :cascade do |t|
- t.integer "protected_branch_id", null: false
- t.integer "access_level", default: 40, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "protected_branch_id", null: false
+ t.integer "access_level", default: 40, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
create_table "protected_branch_push_access_levels", force: :cascade do |t|
- t.integer "protected_branch_id", null: false
- t.integer "access_level", default: 40, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "protected_branch_id", null: false
+ t.integer "access_level", default: 40, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
create_table "protected_branches", force: :cascade do |t|
- t.integer "project_id", null: false
- t.string "name", null: false
+ t.integer "project_id", null: false
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -917,11 +948,12 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
create_table "releases", force: :cascade do |t|
- t.string "tag"
- t.text "description"
- t.integer "project_id"
+ t.string "tag"
+ t.text "description"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
+ t.text "description_html"
end
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
@@ -930,52 +962,54 @@ ActiveRecord::Schema.define(version: 20160926145521) do
create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
- t.string "noteable_type"
+ t.string "noteable_type"
t.integer "recipient_id"
- t.string "commit_id"
- t.string "reply_key", null: false
- t.string "line_code"
- t.string "note_type"
- t.text "position"
+ t.string "commit_id"
+ t.string "reply_key", null: false
+ t.string "line_code"
+ t.string "note_type"
+ t.text "position"
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
create_table "services", force: :cascade do |t|
- t.string "type"
- t.string "title"
- t.integer "project_id"
+ t.string "type"
+ t.string "title"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- 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 "confidential_issues_events", default: true, 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 "confidential_issues_events", default: true, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
add_index "services", ["template"], name: "index_services_on_template", using: :btree
create_table "snippets", force: :cascade do |t|
- t.string "title"
- t.text "content"
- t.integer "author_id", null: false
- t.integer "project_id"
+ t.string "title"
+ t.text "content"
+ t.integer "author_id", null: false
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "file_name"
- t.string "type"
- t.integer "visibility_level", default: 0, null: false
+ t.string "file_name"
+ t.string "type"
+ t.integer "visibility_level", default: 0, null: false
+ t.text "title_html"
+ t.text "content_html"
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
@@ -986,23 +1020,23 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
create_table "spam_logs", force: :cascade do |t|
- t.integer "user_id"
- t.string "source_ip"
- t.string "user_agent"
- t.boolean "via_api"
- t.string "noteable_type"
- t.string "title"
- t.text "description"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "submitted_as_ham", default: false, null: false
+ t.integer "user_id"
+ t.string "source_ip"
+ t.string "user_agent"
+ t.boolean "via_api"
+ t.string "noteable_type"
+ t.string "title"
+ t.text "description"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "submitted_as_ham", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
- t.integer "user_id"
- t.integer "subscribable_id"
- t.string "subscribable_type"
- t.boolean "subscribed"
+ t.integer "user_id"
+ t.integer "subscribable_id"
+ t.string "subscribable_type"
+ t.boolean "subscribed"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -1010,12 +1044,12 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
create_table "taggings", force: :cascade do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context"
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context"
t.datetime "created_at"
end
@@ -1023,24 +1057,24 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "tags", force: :cascade do |t|
- t.string "name"
+ t.string "name"
t.integer "taggings_count", default: 0
end
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
create_table "todos", force: :cascade do |t|
- t.integer "user_id", null: false
- t.integer "project_id", null: false
- t.integer "target_id"
- t.string "target_type", null: false
- t.integer "author_id"
- t.integer "action", null: false
- t.string "state", null: false
+ t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.integer "target_id"
+ t.string "target_type", null: false
+ t.integer "author_id"
+ t.integer "action", null: false
+ t.string "state", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "note_id"
- t.string "commit_id"
+ t.integer "note_id"
+ t.string "commit_id"
end
add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree
@@ -1050,89 +1084,95 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
+ create_table "trending_projects", force: :cascade do |t|
+ t.integer "project_id", null: false
+ end
+
+ add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree
+
create_table "u2f_registrations", force: :cascade do |t|
- t.text "certificate"
- t.string "key_handle"
- t.string "public_key"
- t.integer "counter"
- t.integer "user_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "name"
+ t.text "certificate"
+ t.string "key_handle"
+ t.string "public_key"
+ t.integer "counter"
+ t.integer "user_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "name"
end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
create_table "user_agent_details", force: :cascade do |t|
- t.string "user_agent", null: false
- t.string "ip_address", null: false
- t.integer "subject_id", null: false
- t.string "subject_type", null: false
- t.boolean "submitted", default: false, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.string "user_agent", null: false
+ t.string "ip_address", null: false
+ t.integer "subject_id", null: false
+ t.string "subject_type", null: false
+ t.boolean "submitted", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
+ t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0
+ t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
- t.string "last_sign_in_ip"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "name"
- t.integer "projects_limit", default: 10
- t.string "skype", default: "", null: false
- t.string "linkedin", default: "", null: false
- t.string "twitter", default: "", null: false
- t.string "authentication_token"
- t.integer "theme_id", default: 1, null: false
- t.string "bio"
- t.integer "failed_attempts", default: 0
+ t.string "name"
+ t.integer "projects_limit", default: 10
+ t.string "skype", default: "", null: false
+ t.string "linkedin", default: "", null: false
+ t.string "twitter", default: "", null: false
+ t.string "authentication_token"
+ t.integer "theme_id", default: 1, null: false
+ t.string "bio"
+ t.integer "failed_attempts", default: 0
t.datetime "locked_at"
- t.string "username"
- t.boolean "can_create_group", default: true, null: false
- t.boolean "can_create_team", default: true, null: false
- t.string "state"
- t.integer "color_scheme_id", default: 1, null: false
+ t.string "username"
+ t.boolean "can_create_group", default: true, null: false
+ t.boolean "can_create_team", default: true, null: false
+ t.string "state"
+ t.integer "color_scheme_id", default: 1, null: false
t.datetime "password_expires_at"
- t.integer "created_by_id"
+ t.integer "created_by_id"
t.datetime "last_credential_check_at"
- t.string "avatar"
- t.string "confirmation_token"
+ t.string "avatar"
+ t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
- t.string "unconfirmed_email"
- t.boolean "hide_no_ssh_key", default: false
- t.string "website_url", default: "", null: false
- t.string "notification_email"
- t.boolean "hide_no_password", default: false
- t.boolean "password_automatically_set", default: false
- t.string "location"
- t.string "encrypted_otp_secret"
- t.string "encrypted_otp_secret_iv"
- t.string "encrypted_otp_secret_salt"
- t.boolean "otp_required_for_login", default: false, null: false
- t.text "otp_backup_codes"
- t.string "public_email", default: "", null: false
- t.integer "dashboard", default: 0
- t.integer "project_view", default: 0
- t.integer "consumed_timestep"
- t.integer "layout", default: 0
- t.boolean "hide_project_limit", default: false
- t.string "unlock_token"
+ t.string "unconfirmed_email"
+ t.boolean "hide_no_ssh_key", default: false
+ t.string "website_url", default: "", null: false
+ t.string "notification_email"
+ t.boolean "hide_no_password", default: false
+ t.boolean "password_automatically_set", default: false
+ t.string "location"
+ t.string "encrypted_otp_secret"
+ t.string "encrypted_otp_secret_iv"
+ t.string "encrypted_otp_secret_salt"
+ t.boolean "otp_required_for_login", default: false, null: false
+ t.text "otp_backup_codes"
+ t.string "public_email", default: "", null: false
+ t.integer "dashboard", default: 0
+ t.integer "project_view", default: 0
+ t.integer "consumed_timestep"
+ t.integer "layout", default: 0
+ t.boolean "hide_project_limit", default: false
+ t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
- t.boolean "ldap_email", default: false, null: false
- t.boolean "external", default: false
- t.integer "role_type", default: 0, null: false
- t.string "organization"
+ t.boolean "ldap_email", default: false, null: false
+ t.boolean "external", default: false
+ t.string "organization"
+ t.integer "role_type", default: 0, null: false
end
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
@@ -1149,8 +1189,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
create_table "users_star_projects", force: :cascade do |t|
- t.integer "project_id", null: false
- t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.integer "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -1160,29 +1200,32 @@ ActiveRecord::Schema.define(version: 20160926145521) 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.integer "project_id"
+ t.string "url", limit: 2000
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- 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.string "token"
- t.boolean "pipeline_events", default: false, null: false
- t.boolean "confidential_issues_events", default: false, null: false
+ 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.string "token"
+ 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 "label_priorities", "labels", on_delete: :cascade
+ add_foreign_key "label_priorities", "projects", on_delete: :cascade
+ add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
@@ -1191,5 +1234,6 @@ ActiveRecord::Schema.define(version: 20160926145521) do
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"
+ add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index 4ff1a0582c8..c30bf328003 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -6,8 +6,8 @@
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
-- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
-- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
+- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
@@ -20,6 +20,7 @@
- [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.
+- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
## Administrator documentation
@@ -35,7 +36,7 @@
- [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.
+- [Operations](administration/operations.md) Keeping GitLab up and running.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
- [Repository storages](administration/repository_storages.md) Manage the paths used to store repositories.
@@ -43,12 +44,13 @@
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
-- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
+- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
-- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint.
+- [GitLab Performance Monitoring](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
+- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index c5611e2a121..d7cfb464f74 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -1,42 +1,32 @@
-# GitLab Container Registry Administration
+# GitLab Container Registry administration
> [Introduced][ce-4040] in GitLab 8.8.
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
---
-<!-- 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)*
+> **Notes:**
+- Container Registry manifest `v1` support was added in GitLab 8.9 to support
+ Docker versions earlier than 1.10.
+- This document is about the admin guide. To learn how to use GitLab Container
+ Registry [user documentation](../user/project/container_registry.md).
-- [Enable the Container Registry](#enable-the-container-registry)
-- [Container Registry domain configuration](#container-registry-domain-configuration)
- - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain)
- - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain)
-- [Disable Container Registry site-wide](#disable-container-registry-site-wide)
-- [Disable Container Registry per project](#disable-container-registry-per-project)
-- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide)
-- [Container Registry storage path](#container-registry-storage-path)
-- [Container Registry storage driver](#container-registry-storage-driver)
-- [Storage limitations](#storage-limitations)
-- [Changelog](#changelog)
+With the Container Registry integrated into GitLab, every project can have its
+own space to store its Docker images.
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+You can read more about the Container Registry at
+https://docs.docker.com/registry/introduction/.
## Enable the Container Registry
**Omnibus GitLab installations**
All you have to do is configure the domain name under which the Container
-Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration)
+Registry will listen to. Read
+[#container-registry-domain-configuration](#container-registry-domain-configuration)
and pick one of the two options that fits your case.
>**Note:**
-The container Registry works under HTTPS by default. Using HTTP is possible
+The container registry works under HTTPS by default. Using HTTP is possible
but not recommended and out of the scope of this document.
Read the [insecure Registry documentation][docker-insecure] if you want to
implement this.
@@ -47,7 +37,7 @@ implement this.
If you have installed GitLab from source:
-1. You will have to [install Docker Registry][registry-deploy] by yourself.
+1. You will have to [install Registry][registry-deploy] by yourself.
1. After the installation is complete, you will have to configure the Registry's
settings in `gitlab.yml` in order to enable it.
1. Use the sample NGINX configuration file that is found under
@@ -80,11 +70,13 @@ where:
| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. |
>**Note:**
-GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab]
-will not restart the Registry should you modify its settings. Read the upstream
-documentation on how to achieve that.
+A Registry init file is not shipped with GitLab if you install it from source.
+Hence, [restarting GitLab][restart gitlab] will not restart the Registry should
+you modify its settings. Read the upstream documentation on how to achieve that.
-The Docker Registry configuration will need `container_registry` as the service and `https://gitlab.example.com/jwt/auth` as the realm:
+At the absolute minimum, make sure your [Registry configuration][registry-auth]
+has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
+as the realm:
```
auth:
@@ -275,12 +267,6 @@ Registry application itself.
1. Save the file and [restart GitLab][] for the changes to take effect.
-## Disable Container Registry per project
-
-If Registry is enabled in your GitLab instance, but you don't need it for your
-project, you can disable it from your project's settings. Read the user guide
-on how to achieve that.
-
## Disable Container Registry for new projects site-wide
If the Container Registry is enabled, then it will be available on all new
@@ -436,6 +422,46 @@ storage:
enabled: true
```
+## Change the registry's internal port
+
+> **Note:**
+This is not to be confused with the port that GitLab itself uses to expose
+the Registry to the world.
+
+The Registry server listens on localhost at port `5000` by default,
+which is the address for which the Registry server should accept connections.
+In the examples below we set the Registry's port to `5001`.
+
+**Omnibus GitLab**
+
+1. Open `/etc/gitlab/gitlab.rb` and set `registry['registry_http_addr']`:
+
+ ```ruby
+ registry['registry_http_addr'] = "localhost:5001"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+1. Open the configuration file of your Registry server and edit the
+ [`http:addr`][registry-http-config] value:
+
+ ```
+ http
+ addr: localhost:5001
+ ```
+
+1. Save the file and restart the Registry server.
+
+## Disable Container Registry per project
+
+If Registry is enabled in your GitLab instance, but you don't need it for your
+project, you can disable it from your project's settings. Read the user guide
+on how to achieve that.
+
## Storage limitations
Currently, there is no storage limitation, which means a user can upload an
@@ -455,6 +481,8 @@ configurable in future releases.
[docker-insecure]: https://docs.docker.com/registry/insecure/
[registry-deploy]: https://docs.docker.com/registry/deploying/
[storage-config]: https://docs.docker.com/registry/configuration/#storage
+[registry-http-config]: https://docs.docker.com/registry/configuration/#http
+[registry-auth]: https://docs.docker.com/registry/configuration/#auth
[token-config]: https://docs.docker.com/registry/configuration/#token
[8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md
[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 7f53915a4d7..b4a953d1ccc 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -13,15 +13,17 @@ override certain values.
Variable | Type | Description
-------- | ---- | -----------
-`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
-`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
-`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
-`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
-`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
-`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
+`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
+`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
+`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
+`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
+`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
## Complete database variables
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 8a881ce8863..137fed35a73 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -101,9 +101,9 @@ need some additional configuration.
```ruby
gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860'
- gitlab_rails['secret_token'] = 'b719fe119132c7810908bba18315259ed12888d4f5ee5430c42a776d840a396799b0a5ef0a801348c8a357f07aa72bbd58e25a84b8f247a25c72f539c7a6c5fa'
- gitlab_ci['secret_key_base'] = '6e657410d57c71b4fc3ed0d694e7842b1895a8b401d812c17fe61caf95b48a6d703cb53c112bc01ebd197a85da81b18e29682040e99b4f26594772a4a2c98c6d'
- gitlab_ci['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964'
+ gitlab_rails['otp_key_base'] = 'b719fe119132c7810908bba18315259ed12888d4f5ee5430c42a776d840a396799b0a5ef0a801348c8a357f07aa72bbd58e25a84b8f247a25c72f539c7a6c5fa'
+ gitlab_rails['secret_key_base'] = '6e657410d57c71b4fc3ed0d694e7842b1895a8b401d812c17fe61caf95b48a6d703cb53c112bc01ebd197a85da81b18e29682040e99b4f26594772a4a2c98c6d'
+ gitlab_rails['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964'
```
1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index 34b4f1faa94..ad1fa98b63b 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -12,7 +12,7 @@ revisions (to reduce disk space and increase performance) and removing
unreachable objects which may have been created from prior invocations of
`git add`.
-You can find this option under your **[Project] > Settings**.
+You can find this option under your **[Project] > Edit Project**.
---
diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png
index f72ad9a45d5..6ebc6205635 100644
--- a/doc/administration/img/housekeeping_settings.png
+++ b/doc/administration/img/housekeeping_settings.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/gitlab_configuration.md b/doc/administration/monitoring/performance/gitlab_configuration.md
new file mode 100644
index 00000000000..771584268d9
--- /dev/null
+++ b/doc/administration/monitoring/performance/gitlab_configuration.md
@@ -0,0 +1,40 @@
+# GitLab Configuration
+
+GitLab Performance Monitoring is disabled by default. To enable it and change any of its
+settings, navigate to the Admin area in **Settings > Metrics**
+(`/admin/application_settings`).
+
+The minimum required settings you need to set are the InfluxDB host and port.
+Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
+changes.
+
+---
+
+![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
+
+---
+
+Finally, a restart of all GitLab processes is required for the changes to take
+effect:
+
+```bash
+# For Omnibus installations
+sudo gitlab-ctl restart
+
+# For installations from source
+sudo service gitlab restart
+```
+
+## Pending Migrations
+
+When any migrations are pending, the metrics are disabled until the migrations
+have been performed.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
new file mode 100644
index 00000000000..7947b0fedc4
--- /dev/null
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -0,0 +1,111 @@
+# Grafana Configuration
+
+[Grafana](http://grafana.org/) is a tool that allows you to visualize time
+series metrics through graphs and dashboards. It supports several backend
+data stores, including InfluxDB. GitLab writes performance data to InfluxDB
+and Grafana will allow you to query InfluxDB to display useful graphs.
+
+For the easiest installation and configuration, install Grafana on the same
+server as InfluxDB. For larger installations, you may want to split out these
+services.
+
+## Installation
+
+Grafana supplies package repositories (Yum/Apt) for easy installation.
+See [Grafana installation documentation](http://docs.grafana.org/installation/)
+for detailed steps.
+
+> **Note**: Before starting Grafana for the first time, set the admin user
+and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
+will be `admin`.
+
+## Configuration
+
+Login as the admin user. Expand the menu by clicking the Grafana logo in the
+top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
+in the top bar.
+
+![Grafana empty data source page](img/grafana_data_source_empty.png)
+
+Fill in the configuration details for the InfluxDB data source. Save and
+Test Connection to ensure the configuration is correct.
+
+- **Name**: InfluxDB
+- **Default**: Checked
+- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
+- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
+on a separate server)
+- **Access**: proxy
+- **Database**: gitlab
+- **User**: admin (Or the username configured when setting up InfluxDB)
+- **Password**: The password configured when you set up InfluxDB
+
+![Grafana data source configurations](img/grafana_data_source_configuration.png)
+
+## Apply retention policies and create continuous queries
+
+If you intend to import the GitLab provided Grafana dashboards, you will need to
+set up the right retention policies and continuous queries. The easiest way of
+doing this is by using the [influxdb-management](https://gitlab.com/gitlab-org/influxdb-management)
+repository.
+
+To use this repository you must first clone it:
+
+```
+git clone https://gitlab.com/gitlab-org/influxdb-management.git
+cd influxdb-management
+```
+
+Next you must install the required dependencies:
+
+```
+gem install bundler
+bundle install
+```
+
+Now you must configure the repository by first copying `.env.example` to `.env`
+and then editing the `.env` file to contain the correct InfluxDB settings. Once
+configured you can simply run `bundle exec rake` and the InfluxDB database will
+be configured for you.
+
+For more information see the [influxdb-management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
+
+## Import Dashboards
+
+You can now import a set of default dashboards that will give you a good
+start on displaying useful information. GitLab has published a set of default
+[Grafana dashboards][grafana-dashboards] to get you started. Clone the
+repository or download a zip/tarball, then follow these steps to import each
+JSON file.
+
+Open the dashboard dropdown menu and click 'Import'
+
+![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png)
+
+Click 'Choose file' and browse to the location where you downloaded or cloned
+the dashboard repository. Pick one of the JSON files to import.
+
+![Grafana dashboard import](img/grafana_dashboard_import.png)
+
+Once the dashboard is imported, be sure to click save icon in the top bar. If
+you do not save the dashboard after importing it will be removed when you
+navigate away.
+
+![Grafana save icon](img/grafana_save_icon.png)
+
+Repeat this process for each dashboard you wish to import.
+
+Alternatively you can automatically import all the dashboards into your Grafana
+instance. See the README of the [Grafana dashboards][grafana-dashboards]
+repository for more information on this process.
+
+[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Installation/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
new file mode 100644
index 00000000000..7e34fad71ce
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_import.png b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
new file mode 100644
index 00000000000..f97624365c7
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
new file mode 100644
index 00000000000..7d50e4c88c2
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_empty.png b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
new file mode 100644
index 00000000000..aa39a53acae
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_save_icon.png b/doc/administration/monitoring/performance/img/grafana_save_icon.png
new file mode 100644
index 00000000000..c740e33cd1c
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_save_icon.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
new file mode 100644
index 00000000000..db396423e30
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/request_profile_result.png b/doc/administration/monitoring/performance/img/request_profile_result.png
new file mode 100644
index 00000000000..73e2fdcab67
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/request_profile_result.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/request_profiling_token.png b/doc/administration/monitoring/performance/img/request_profiling_token.png
new file mode 100644
index 00000000000..04d87567816
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/request_profiling_token.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/influxdb_configuration.md b/doc/administration/monitoring/performance/influxdb_configuration.md
new file mode 100644
index 00000000000..c30cd2950d8
--- /dev/null
+++ b/doc/administration/monitoring/performance/influxdb_configuration.md
@@ -0,0 +1,193 @@
+# InfluxDB Configuration
+
+The default settings provided by [InfluxDB] are not sufficient for a high traffic
+GitLab environment. The settings discussed in this document are based on the
+settings GitLab uses for GitLab.com, depending on your own needs you may need to
+further adjust them.
+
+If you are intending to run InfluxDB on the same server as GitLab, make sure
+you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
+
+Unless you are going with a budget setup, it's advised to run it separately.
+
+## Requirements
+
+- InfluxDB 0.9.5 or newer
+- A fairly modern version of Linux
+- At least 4GB of RAM
+- At least 10GB of storage for InfluxDB data
+
+Note that the RAM and storage requirements can differ greatly depending on the
+amount of data received/stored. To limit the amount of stored data users can
+look into [InfluxDB Retention Policies][influxdb-retention].
+
+## Installation
+
+Installing InfluxDB is out of the scope of this document. Please refer to the
+[InfluxDB documentation].
+
+## InfluxDB Server Settings
+
+Since InfluxDB has many settings that users may wish to customize themselves
+(e.g. what port to run InfluxDB on), we'll only cover the essentials.
+
+The configuration file in question is usually located at
+`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
+InfluxDB needs to be restarted.
+
+### Storage Engine
+
+InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
+storage engine is available, called [TSM Tree]. All users **must** use the new
+`tsm1` storage engine as this [will be the default engine][tsm1-commit] in
+upcoming InfluxDB releases.
+
+Make sure you have the following in your configuration file:
+
+```
+[data]
+ dir = "/var/lib/influxdb/data"
+ engine = "tsm1"
+```
+
+### Admin Panel
+
+Production environments should have the InfluxDB admin panel **disabled**. This
+feature can be disabled by adding the following to your InfluxDB configuration
+file:
+
+```
+[admin]
+ enabled = false
+```
+
+### HTTP
+
+HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana,
+thus it should be enabled. When enabling make sure to _also_ enable
+authentication:
+
+```
+[http]
+ enabled = true
+ auth-enabled = true
+```
+
+_**Note:** Before you enable authentication, you might want to [create an
+admin user](#create-a-new-admin-user)._
+
+### UDP
+
+GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
+UDP can be done using the following settings:
+
+```
+[[udp]]
+ enabled = true
+ bind-address = ":8089"
+ database = "gitlab"
+ batch-size = 1000
+ batch-pending = 5
+ batch-timeout = "1s"
+ read-buffer = 209715200
+```
+
+This does the following:
+
+1. Enable UDP and bind it to port 8089 for all addresses.
+2. Store any data received in the "gitlab" database.
+3. Define a batch of points to be 1000 points in size and allow a maximum of
+ 5 batches _or_ flush them automatically after 1 second.
+4. Define a UDP read buffer size of 200 MB.
+
+One of the most important settings here is the UDP read buffer size as if this
+value is set too low, packets will be dropped. You must also make sure the OS
+buffer size is set to the same value, the default value is almost never enough.
+
+To set the OS buffer size to 200 MB, on Linux you can run the following command:
+
+```bash
+sysctl -w net.core.rmem_max=209715200
+```
+
+To make this permanent, add the following to `/etc/sysctl.conf` and restart the
+server:
+
+```bash
+net.core.rmem_max=209715200
+```
+
+It is **very important** to make sure the buffer sizes are large enough to
+handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
+buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
+traffic, users may be able to use a smaller buffer size, but we highly recommend
+using _at least_ 100 MB.
+
+When enabling UDP, users should take care to not expose the port to the public,
+as doing so will allow anybody to write data into your InfluxDB database (as
+[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either
+whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
+allowing traffic from members of said VLAN.
+
+## Create a new admin user
+
+If you want to [enable authentication](#http), you might want to [create an
+admin user][influx-admin]:
+
+```
+influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
+```
+
+## Create the `gitlab` database
+
+Once you get InfluxDB up and running, you need to create a database for GitLab.
+Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
+before creating a database.
+
+_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
+[HTTP authentication](#http), remember to append the username (`-username <username>`)
+and password (`-password <password>`) you set earlier to the commands below._
+
+Run the following command to create a database named `gitlab`:
+
+```bash
+influx -execute 'CREATE DATABASE gitlab'
+```
+
+The name **must** be `gitlab`, do not use any other name.
+
+Next, make sure that the database was successfully created:
+
+```bash
+influx -execute 'SHOW DATABASES'
+```
+
+The output should be similar to:
+
+```
+name: databases
+---------------
+name
+_internal
+gitlab
+```
+
+That's it! Now your GitLab instance should send data to InfluxDB.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
+
+[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
+[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
+[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/
+[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/
+[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d
+[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user
diff --git a/doc/administration/monitoring/performance/influxdb_schema.md b/doc/administration/monitoring/performance/influxdb_schema.md
new file mode 100644
index 00000000000..eff0e29f58d
--- /dev/null
+++ b/doc/administration/monitoring/performance/influxdb_schema.md
@@ -0,0 +1,97 @@
+# InfluxDB Schema
+
+The following measurements are currently stored in InfluxDB:
+
+- `PROCESS_file_descriptors`
+- `PROCESS_gc_statistics`
+- `PROCESS_memory_usage`
+- `PROCESS_method_calls`
+- `PROCESS_object_counts`
+- `PROCESS_transactions`
+- `PROCESS_views`
+- `events`
+
+Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
+process type. In all series, any form of duration is stored in milliseconds.
+
+## PROCESS_file_descriptors
+
+This measurement contains the number of open file descriptors over time. The
+value field `value` contains the number of descriptors.
+
+## PROCESS_gc_statistics
+
+This measurement contains Ruby garbage collection statistics such as the amount
+of minor/major GC runs (relative to the last sampling interval), the time spent
+in garbage collection cycles, and all fields/values returned by `GC.stat`.
+
+## PROCESS_memory_usage
+
+This measurement contains the process' memory usage (in bytes) over time. The
+value field `value` contains the number of bytes.
+
+## PROCESS_method_calls
+
+This measurement contains the methods called during a transaction along with
+their duration, and a name of the transaction action that invoked the method (if
+available). The method call duration is stored in the value field `duration`,
+while the method name is stored in the tag `method`. The tag `action` contains
+the full name of the transaction action. Both the `method` and `action` fields
+are in the following format:
+
+```
+ClassName#method_name
+```
+
+For example, a method called by the `show` method in the `UsersController` class
+would have `action` set to `UsersController#show`.
+
+## PROCESS_object_counts
+
+This measurement is used to store retained Ruby objects (per class) and the
+amount of retained objects. The number of objects is stored in the `count` value
+field while the class name is stored in the `type` tag.
+
+## PROCESS_transactions
+
+This measurement is used to store basic transaction details such as the time it
+took to complete a transaction, how much time was spent in SQL queries, etc. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration` | The total duration of the transaction |
+| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
+| `method_duration` | The total time spent in method calls |
+| `sql_duration` | The total time spent in SQL queries |
+| `view_duration` | The total time spent in views |
+
+## PROCESS_views
+
+This measurement is used to store view rendering timings for a transaction. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration` | The rendering time of the view |
+| `view` | The path of the view, relative to the application's root directory |
+
+The `action` tag contains the action name of the transaction that rendered the
+view.
+
+## events
+
+This measurement is used to store generic events such as the number of Git
+pushes, Emails sent, etc. Each point in this measurement has a single value
+field called `count`. The value of this field is simply set to `1`. Each point
+also has at least one tag: `event`. This tag's value is set to the event name.
+Depending on the event type additional tags may be available as well.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
new file mode 100644
index 00000000000..79904916b7e
--- /dev/null
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -0,0 +1,65 @@
+# GitLab Performance Monitoring
+
+GitLab comes with its own application performance measuring system as of GitLab
+8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
+Community and Enterprise editions.
+
+Apart from this introduction, you are advised to read through the following
+documents in order to understand and properly configure GitLab Performance Monitoring:
+
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Install/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
+
+## Introduction to GitLab Performance Monitoring
+
+GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
+including (but not limited to):
+
+- The time it took to complete a transaction (a web request or Sidekiq job).
+- The time spent in running SQL queries and rendering HAML views.
+- The time spent executing (instrumented) Ruby methods.
+- Ruby object allocations, and retained objects in particular.
+- System statistics such as the process' memory usage and open file descriptors.
+- Ruby garbage collection statistics.
+
+Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
+data can be visualized using [Grafana][grafana] or any other application that
+supports reading data from InfluxDB. Alternatively data can be queried using the
+InfluxDB CLI.
+
+## Metric Types
+
+Two types of metrics are collected:
+
+1. Transaction specific metrics.
+1. Sampled metrics, collected at a certain interval in a separate thread.
+
+### Transaction Metrics
+
+Transaction metrics are metrics that can be associated with a single
+transaction. This includes statistics such as the transaction duration, timings
+of any executed SQL queries, time spent rendering HAML views, etc. These metrics
+are collected for every Rack request and Sidekiq job processed.
+
+### Sampled Metrics
+
+Sampled metrics are metrics that can't be associated with a single transaction.
+Examples include garbage collection statistics and retained Ruby objects. These
+metrics are collected at a regular interval. This interval is made up out of two
+parts:
+
+1. A user defined interval.
+1. A randomly generated offset added on top of the interval, the same offset
+ can't be used twice in a row.
+
+The actual interval can be anywhere between a half of the defined interval and a
+half above the interval. For example, for a user defined interval of 15 seconds
+the actual interval can be anywhere between 7.5 and 22.5. The interval is
+re-generated for every sampling run instead of being generated once and re-used
+for the duration of the process' lifetime.
+
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[grafana]: http://grafana.org/
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
new file mode 100644
index 00000000000..c358dfbead2
--- /dev/null
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -0,0 +1,16 @@
+# Request Profiling
+
+## Procedure
+1. Grab the profiling token from `Monitoring > Requests Profiles` admin page
+(highlighted in a blue in the image below).
+![Profile token](img/request_profiling_token.png)
+1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use any of these tools
+ * [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension
+ * [Modify Headers](https://addons.mozilla.org/en-US/firefox/addon/modify-headers/) Firefox extension
+ * `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project`
+1. Once request is finished (which will take a little longer than usual), you can
+view the profiling output from `Monitoring > Requests Profiles` admin page.
+![Profiling output](img/request_profile_result.png)
+
+## Cleaning up
+Profiling output will be cleared out every day via a Sidekiq worker.
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
new file mode 100644
index 00000000000..4b582d16b64
--- /dev/null
+++ b/doc/administration/operations.md
@@ -0,0 +1,6 @@
+# GitLab operations
+
+- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
+- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
+- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
+- [Moving repositories to a new location](operations/moving_repositories.md)
diff --git a/doc/administration/operations/cleaning_up_redis_sessions.md b/doc/administration/operations/cleaning_up_redis_sessions.md
new file mode 100644
index 00000000000..93521e976d5
--- /dev/null
+++ b/doc/administration/operations/cleaning_up_redis_sessions.md
@@ -0,0 +1,52 @@
+# Cleaning up stale Redis sessions
+
+Since version 6.2, GitLab stores web user sessions as key-value pairs in Redis.
+Prior to GitLab 7.3, user sessions did not automatically expire from Redis. If
+you have been running a large GitLab server (thousands of users) since before
+GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
+database after you upgrade to GitLab 7.3. You can also perform a cleanup while
+still running GitLab 7.2 or older, but in that case new stale sessions will
+start building up again after you clean up.
+
+In GitLab versions prior to 7.3.0, the session keys in Redis are 16-byte
+hexadecimal values such as '976aa289e2189b17d7ef525a6702ace9'. Starting with
+GitLab 7.3.0, the keys are
+prefixed with 'session:gitlab:', so they would look like
+'session:gitlab:976aa289e2189b17d7ef525a6702ace9'. Below we describe how to
+remove the keys in the old format.
+
+First we define a shell function with the proper Redis connection details.
+
+```
+rcli() {
+ # This example works for Omnibus installations of GitLab 7.3 or newer. For an
+ # installation from source you will have to change the socket path and the
+ # path to redis-cli.
+ sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@"
+}
+
+# test the new shell function; the response should be PONG
+rcli ping
+```
+
+Now we do a search to see if there are any session keys in the old format for
+us to clean up.
+
+```
+# returns the number of old-format session keys in Redis
+rcli keys '*' | grep '^[a-f0-9]\{32\}$' | wc -l
+```
+
+If the number is larger than zero, you can proceed to expire the keys from
+Redis. If the number is zero there is nothing to clean up.
+
+```
+# Tell Redis to expire each matched key after 600 seconds.
+rcli keys '*' | grep '^[a-f0-9]\{32\}$' | awk '{ print "expire", $0, 600 }' | rcli
+# This will print '(integer) 1' for each key that gets expired.
+```
+
+Over the next 15 minutes (10 minutes expiry time plus 5 minutes Redis
+background save interval) your Redis database will be compacted. If you are
+still using GitLab 7.2, users who are not clicking around in GitLab during the
+10 minute expiry window will be signed out of GitLab.
diff --git a/doc/administration/operations/moving_repositories.md b/doc/administration/operations/moving_repositories.md
new file mode 100644
index 00000000000..54adb99386a
--- /dev/null
+++ b/doc/administration/operations/moving_repositories.md
@@ -0,0 +1,180 @@
+# Moving repositories managed by GitLab
+
+Sometimes you need to move all repositories managed by GitLab to
+another filesystem or another server. In this document we will look
+at some of the ways you can copy all your repositories from
+`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`.
+
+We will look at three scenarios: the target directory is empty, the
+target directory contains an outdated copy of the repositories, and
+how to deal with thousands of repositories.
+
+**Each of the approaches we list can/will overwrite data in the
+target directory `/mnt/gitlab/repositories`. Do not mix up the
+source and the target.**
+
+## Target directory is empty: use a tar pipe
+
+If the target directory `/mnt/gitlab/repositories` is empty the
+simplest thing to do is to use a tar pipe. This method has low
+overhead and tar is almost always already installed on your system.
+However, it is not possible to resume an interrupted tar pipe: if
+that happens then all data must be copied again.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+ tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to see progress, replace `-xf` with `-xvf`.
+
+### Tar pipe to another server
+
+You can also use a tar pipe to copy data to another server. If your
+'git' user has SSH access to the newserver as 'git@newserver', you
+can pipe the data through SSH.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+ ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to compress the data before it goes over the network
+(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`.
+
+## The target directory contains an outdated copy of the repositories: use rsync
+
+If the target directory already contains a partial / outdated copy
+of the repositories it may be wasteful to copy all the data again
+with tar. In this scenario it is better to use rsync. This utility
+is either already installed on your system or easily installable
+via apt, yum etc.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+ /mnt/gitlab/repositories
+```
+
+The `/.` in the command above is very important, without it you can
+easily get the wrong directory structure in the target directory.
+If you want to see progress, replace `-a` with `-av`.
+
+### Single rsync to another server
+
+If the 'git' user on your source system has SSH access to the target
+server you can send the repositories over the network with rsync.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+ git@newserver:/mnt/gitlab/repositories
+```
+
+## Thousands of Git repositories: use one rsync per repository
+
+Every time you start an rsync job it has to inspect all files in
+the source directory, all files in the target directory, and then
+decide what files to copy or not. If the source or target directory
+has many contents this startup phase of rsync can become a burden
+for your GitLab server. In cases like this you can make rsync's
+life easier by dividing its work in smaller pieces, and sync one
+repository at a time.
+
+In addition to rsync we will use [GNU
+Parallel](http://www.gnu.org/software/parallel/). This utility is
+not included in GitLab so you need to install it yourself with apt
+or yum. Also note that the GitLab scripts we used below were added
+in GitLab 8.1.
+
+** This process does not clean up repositories at the target location that no
+longer exist at the source. ** If you start using your GitLab instance with
+`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos`
+after switching to the new repository storage directory.
+
+### Parallel rsync for all repositories known to GitLab
+
+This will sync repositories with 10 rsync processes at a time. We keep
+track of progress so that the transfer can be restarted if necessary.
+
+First we create a new directory, owned by 'git', to hold transfer
+logs. We assume the directory is empty before we start the transfer
+procedure, and that we are the only ones writing files in it.
+
+```
+# Omnibus
+sudo mkdir /var/opt/gitlab/transfer-logs
+sudo chown git:git /var/opt/gitlab/transfer-logs
+
+# Source
+sudo -u git -H mkdir /home/git/transfer-logs
+```
+
+We seed the process with a list of the directories we want to copy.
+
+```
+# Omnibus
+sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt'
+```
+
+Now we can start the transfer. The command below is idempotent, and
+the number of jobs done by GNU Parallel should converge to zero. If it
+does not some repositories listed in all-repos-1234.txt may have been
+deleted/renamed before they could be copied.
+
+```
+# Omnibus
+sudo -u git sh -c '
+cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\
+ /usr/bin/env JOBS=10 \
+ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
+ /var/opt/gitlab/transfer-logs/success-$(date +%s).log \
+ /var/opt/gitlab/git-data/repositories \
+ /mnt/gitlab/repositories
+'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H sh -c '
+cat /home/git/transfer-logs/* | sort | uniq -u |\
+ /usr/bin/env JOBS=10 \
+ bin/parallel-rsync-repos \
+ /home/git/transfer-logs/success-$(date +%s).log \
+ /home/git/repositories \
+ /mnt/gitlab/repositories
+`
+```
+
+### Parallel rsync only for repositories with recent activity
+
+Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
+Then you might only want to sync repositories that were changed via GitLab
+_after_ that time. You can use the 'SINCE' variable to tell 'rake
+gitlab:list_repos' to only print repositories with recent activity.
+
+```
+# Omnibus
+sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
+ sudo -u git \
+ /usr/bin/env JOBS=10 \
+ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
+ success-$(date +%s).log \
+ /var/opt/gitlab/git-data/repositories \
+ /mnt/gitlab/repositories
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
+ sudo -u git -H \
+ /usr/bin/env JOBS=10 \
+ bin/parallel-rsync-repos \
+ success-$(date +%s).log \
+ /home/git/repositories \
+ /mnt/gitlab/repositories
+```
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
new file mode 100644
index 00000000000..b5e78348989
--- /dev/null
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -0,0 +1,40 @@
+# Sidekiq MemoryKiller
+
+The GitLab Rails application code suffers from memory leaks. For web requests
+this problem is made manageable using
+[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which
+restarts Unicorn worker processes in between requests when needed. The Sidekiq
+MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
+to process background jobs.
+
+Unlike unicorn-worker-killer, which is enabled by default for all GitLab
+installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default
+_only_ for Omnibus packages. The reason for this is that the MemoryKiller
+relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab
+installations from source do not all use Runit or an equivalent.
+
+With the default settings, the MemoryKiller will cause a Sidekiq restart no
+more often than once every 15 minutes, with the restart causing about one
+minute of delay for incoming background jobs.
+
+## Configuring the MemoryKiller
+
+The MemoryKiller is controlled using environment variables.
+
+- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is
+ greater than 0, then after each Sidekiq job, the MemoryKiller will check the
+ RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq
+ process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a
+ delayed shutdown is triggered. The default value for Omnibus packages is set
+ [in the omnibus-gitlab
+ repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
+- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
+ a shutdown is triggered, the Sidekiq process will keep working normally for
+ another 15 minutes.
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
+ time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs.
+ Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
+ Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
+ restart Sidekiq.
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
+ the final signal sent to the Sidekiq process when we want it to shut down.
diff --git a/doc/administration/operations/unicorn.md b/doc/administration/operations/unicorn.md
new file mode 100644
index 00000000000..bad61151bda
--- /dev/null
+++ b/doc/administration/operations/unicorn.md
@@ -0,0 +1,86 @@
+# Understanding Unicorn and unicorn-worker-killer
+
+## Unicorn
+
+GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web
+server, to handle web requests (web browsers and Git HTTP clients). Unicorn is
+a daemon written in Ruby and C that can load and run a Ruby on Rails
+application; in our case the Rails application is GitLab Community Edition or
+GitLab Enterprise Edition.
+
+Unicorn has a multi-process architecture to make better use of available CPU
+cores (processes can run on different cores) and to have stronger fault
+tolerance (most failures stay isolated in only one process and cannot take down
+GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby
+environment with the GitLab application code, and then spawns 'workers' which
+inherit this clean initial environment. The 'master' never handles any
+requests, that is left to the workers. The operating system network stack
+queues incoming requests and distributes them among the workers.
+
+In a perfect world, the master would spawn its pool of workers once, and then
+the workers handle incoming web requests one after another until the end of
+time. In reality, worker processes can crash or time out: if the master notices
+that a worker takes too long to handle a request it will terminate the worker
+process with SIGKILL ('kill -9'). No matter how the worker process ended, the
+master process will replace it with a new 'clean' process again. Unicorn is
+designed to be able to replace 'crashed' workers without dropping user
+requests.
+
+This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The
+master process has PID 56227 below.
+
+```
+[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing
+[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10
+[2015-06-05T10:58:08.708141 #62538] INFO -- : worker=10 spawned pid=62538
+[2015-06-05T10:58:08.708824 #62538] INFO -- : worker=10 ready
+```
+
+### Tunables
+
+The main tunables for Unicorn are the number of worker processes and the
+request timeout after which the Unicorn master terminates a worker process.
+See the [omnibus-gitlab Unicorn settings
+documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md)
+if you want to adjust these settings.
+
+## unicorn-worker-killer
+
+GitLab has memory leaks. These memory leaks manifest themselves in long-running
+processes, such as Unicorn workers. (The Unicorn master process is not known to
+leak memory, probably because it does not handle user requests.)
+
+To make these memory leaks manageable, GitLab comes with the
+[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
+gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
+workers to do a memory self-check after every 16 requests. If the memory of the
+Unicorn worker exceeds a pre-set limit then the worker process exits. The
+Unicorn master then automatically replaces the worker process.
+
+This is a robust way to handle memory leaks: Unicorn is designed to handle
+workers that 'crash' so no user requests will be dropped. The
+unicorn-worker-killer gem is designed to only terminate a worker process _in
+between requests_, so no user requests are affected.
+
+This is what a Unicorn worker memory restart looks like in unicorn_stderr.log.
+You see that worker 4 (PID 125918) is inspecting itself and decides to exit.
+The threshold memory value was 254802235 bytes, about 250MB. With GitLab this
+threshold is a random value between 200 and 250 MB. The master process (PID
+117565) then reaps the worker process and spawns a new 'worker 4' with PID
+127549.
+
+```
+[2015-06-05T12:07:41.828374 #125918] WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes)
+[2015-06-05T12:07:41.828472 #125918] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1)
+[2015-06-05T12:07:42.025916 #117565] INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4
+[2015-06-05T12:07:42.034527 #127549] INFO -- : worker=4 spawned pid=127549
+[2015-06-05T12:07:42.035217 #127549] INFO -- : worker=4 ready
+```
+
+One other thing that stands out in the log snippet above, taken from
+GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
+is a normal value for our current GitLab.com setup and traffic.
+
+The high frequency of Unicorn memory restarts on some GitLab sites can be a
+source of confusion for administrators. Usually they are a [red
+herring](https://en.wikipedia.org/wiki/Red_herring).
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
new file mode 100644
index 00000000000..5a9a1582877
--- /dev/null
+++ b/doc/administration/reply_by_email.md
@@ -0,0 +1,302 @@
+# Reply by email
+
+GitLab can be set up to allow users to comment on issues and merge requests by
+replying to notification emails.
+
+## Requirement
+
+Reply by email requires an IMAP-enabled email account. GitLab allows you to use
+three strategies for this feature:
+- using email sub-addressing
+- using a dedicated email address
+- using a catch-all mailbox
+
+### Email sub-addressing
+
+**If your provider or server supports email sub-addressing, we recommend using it.**
+
+[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
+a feature where any email to `user+some_arbitrary_tag@example.com` will end up
+in the mailbox for `user@example.com`, and is supported by providers such as
+Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
+mail server which you can run on-premises.
+
+### Dedicated email address
+
+This solution is really simple to set up: you just have to create an email
+address dedicated to receive your users' replies to GitLab notifications.
+
+### Catch-all mailbox
+
+A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
+"catch all" the emails addressed to the domain that do not exist in the mail
+server.
+
+## How it works?
+
+### 1. GitLab sends a notification email
+
+When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
+header is set to the address defined in your GitLab configuration, with the
+`%{key}` placeholder (if present) replaced by a specific "reply key". In
+addition, this "reply key" is also added to the `References` header.
+
+### 2. You reply to the notification email
+
+When you reply to the notification email, your email client will:
+
+- send the email to the `Reply-To` address it got from the notification email
+- set the `In-Reply-To` header to the value of the `Message-ID` header from the
+ notification email
+- set the `References` header to the value of the `Message-ID` plus the value of
+ the notification email's `References` header.
+
+### 3. GitLab receives your reply to the notification email
+
+When GitLab receives your reply, it will look for the "reply key" in the
+following headers, in this order:
+
+1. the `To` header
+1. the `References` header
+
+If it finds a reply key, it will be able to leave your reply as a comment on
+the entity the notification was about (issue, merge request, commit...).
+
+For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
+please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
+
+## Set it up
+
+If you want to use Gmail / Google Apps with Reply by email, make sure you have
+[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
+[these instructions](./postfix.md).
+
+### Omnibus package installations
+
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
+ feature and fill in the details for your specific IMAP server and email account:
+
+ ```ruby
+ # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "incoming"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "gitlab.example.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 143
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = false
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ ```
+
+ ```ruby
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "imap.gmail.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 993
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = true
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ ```
+
+1. Reconfigure GitLab and restart mailroom for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart mailroom
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo gitlab-rake gitlab:incoming_email:check
+ ```
+
+1. Reply by email should now be working.
+
+### Installations from source
+
+1. Go to the GitLab installation directory:
+
+ ```sh
+ cd /home/git/gitlab
+ ```
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
+ and fill in the details for your specific IMAP server and email account:
+
+ ```sh
+ sudo editor config/gitlab.yml
+ ```
+
+ ```yaml
+ # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "incoming+%{key}@gitlab.example.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "incoming"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "gitlab.example.com"
+ # IMAP server port
+ port: 143
+ # Whether the IMAP server uses SSL
+ ssl: false
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ ```
+
+ ```yaml
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ ```
+
+1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
+
+ ```sh
+ sudo mkdir -p /etc/default
+ echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
+ ```
+
+1. Restart GitLab:
+
+ ```sh
+ sudo service gitlab restart
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
+ ```
+
+1. Reply by email should now be working.
+
+### Development
+
+1. Go to the GitLab installation directory.
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
+
+ ```yaml
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ ```
+
+ As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
+
+1. Uncomment the `mail_room` line in your `Procfile`:
+
+ ```yaml
+ mail_room: bundle exec mail_room -q -c config/mail_room.yml
+ ```
+
+1. Restart GitLab:
+
+ ```sh
+ bundle exec foreman start
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
+ ```
+
+1. Reply by email should now be working.
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
new file mode 100644
index 00000000000..22f10489a6c
--- /dev/null
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -0,0 +1,324 @@
+# Set up Postfix for Reply by email
+
+This document will take you through the steps of setting up a basic Postfix mail
+server with IMAP authentication on Ubuntu, to be used with [Reply by email].
+
+The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
+
+## Configure your server firewall
+
+1. Open up port 25 on your server so that people can send email into the server over SMTP.
+2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
+
+## Install packages
+
+1. Install the `postfix` package if it is not installed already:
+
+ ```sh
+ sudo apt-get install postfix
+ ```
+
+ When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
+
+1. Install the `mailutils` package.
+
+ ```sh
+ sudo apt-get install mailutils
+ ```
+
+## Create user
+
+1. Create a user for incoming email.
+
+ ```sh
+ sudo useradd -m -s /bin/bash incoming
+ ```
+
+1. Set a password for this user.
+
+ ```sh
+ sudo passwd incoming
+ ```
+
+ Be sure not to forget this, you'll need it later.
+
+## Test the out-of-the-box setup
+
+1. Connect to the local SMTP server:
+
+ ```sh
+ telnet localhost 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 127.0.0.1...
+ Connected to localhost.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, verify that `postfix` is running:
+
+ ```sh
+ sudo postfix status
+ ```
+
+ If it is not, start it:
+
+ ```sh
+ sudo postfix start
+ ```
+
+1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo localhost
+ mail from: root@localhost
+ rcpt to: incoming@localhost
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ _**Note:** The `.` is a literal period on its own line._
+
+ _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
+ then your Postfix `my_network` configuration is not correct. The error will
+ say 'Temporary lookup failure'. See
+ [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
+
+1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/var/mail/incoming": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Configure Postfix to use Maildir-style mailboxes
+
+Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
+
+1. Configure Postfix to use Maildir-style mailboxes:
+
+ ```sh
+ sudo postconf -e "home_mailbox = Maildir/"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo /etc/init.d/postfix restart
+ ```
+
+1. Test the new setup:
+
+ 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
+ 1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ MAIL=/home/incoming/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/incoming/Maildir": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+ _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
+ version of `mail` doesn't support Maildir style mailboxes. Install
+ `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
+ try the above steps again, substituting `heirloom-mailx` for the `mail`
+ command._
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Install the Courier IMAP server
+
+1. Install the `courier-imap` package:
+
+ ```sh
+ sudo apt-get install courier-imap
+ ```
+
+## Configure Postfix to receive email from the internet
+
+1. Let Postfix know about the domains that it should consider local:
+
+ ```sh
+ sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
+ ```
+
+1. Let Postfix know about the IPs that it should consider part of the LAN:
+
+ We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
+
+ ```sh
+ sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
+ ```
+
+1. Configure Postfix to receive mail on all interfaces, which includes the internet:
+
+ ```sh
+ sudo postconf -e "inet_interfaces = all"
+ ```
+
+1. Configure Postfix to use the `+` delimiter for sub-addressing:
+
+ ```sh
+ sudo postconf -e "recipient_delimiter = +"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo service postfix restart
+ ```
+
+## Test the final setup
+
+1. Test SMTP under the new setup:
+
+ 1. Connect to the SMTP server:
+
+ ```sh
+ telnet gitlab.example.com 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to gitlab.example.com.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
+
+ 1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo gitlab.example.com
+ mail from: root@gitlab.example.com
+ rcpt to: incoming@gitlab.example.com
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ (Note: The `.` is a literal period on its own line)
+
+ 1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ MAIL=/home/incoming/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/incoming/Maildir": 1 message 1 unread
+ >U 1 root@gitlab.example.com 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+ 1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+1. Test IMAP under the new setup:
+
+ 1. Connect to the IMAP server:
+
+ ```sh
+ telnet gitlab.example.com 143
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to mail.example.gitlab.com.
+ Escape character is '^]'.
+ - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information.
+ ```
+
+ 1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt:
+
+ ```
+ a login incoming PASSWORD
+ ```
+
+ Replace PASSWORD with the password you set on the `incoming` user earlier.
+
+ You should see output like this:
+
+ ```
+ a OK LOGIN Ok.
+ ```
+
+ 1. Disconnect from the IMAP server:
+
+ ```sh
+ a logout
+ ```
+
+## Done!
+
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+
+---
+
+_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
+
+[reply by email]: reply_by_email.md
diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md
index 483060395dd..b561c2f82aa 100644
--- a/doc/administration/restart_gitlab.md
+++ b/doc/administration/restart_gitlab.md
@@ -139,7 +139,7 @@ If you are using other init systems, like systemd, you can check the
[omnibus-dl]: https://about.gitlab.com/downloads/ "Download the Omnibus packages"
[install]: ../install/installation.md "Documentation to install GitLab from source"
-[mailroom]: ../incoming_email/README.md "Used for replying by email in GitLab issues and merge requests"
+[mailroom]: reply_by_email.md "Used for replying by email in GitLab issues and merge requests"
[chef]: https://www.chef.io/chef/ "Chef official website"
[src-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab "GitLab init service file"
[gl-recipes]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/init "GitLab Recipes repository"
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index d127d7b85e5..d8dce4388e1 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -144,14 +144,14 @@ separate Rails process to debug the issue:
1. Obtain the private token for your user (Profile Settings -> Account).
1. Bring up the GitLab Rails console. For omnibus users, run:
- ````
+ ```
sudo gitlab-rails console
```
1. At the Rails console, run:
```ruby
- [1] pry(main)> app.get '<URL FROM STEP 1>/private_token?<TOKEN FROM STEP 2>'
+ [1] pry(main)> app.get '<URL FROM STEP 2>/?private_token=<TOKEN FROM STEP 3>'
```
For example:
diff --git a/doc/api/README.md b/doc/api/README.md
index 8e4f7f12b4b..3fbe5197a21 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -17,6 +17,8 @@ following locations:
- [Commits](commits.md)
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
+- [Gitignores templates](templates/gitignores.md)
+- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)
- [Group Access Requests](access_requests.md)
- [Group Members](members.md)
@@ -25,7 +27,7 @@ following locations:
- [Labels](labels.md)
- [Merge Requests](merge_requests.md)
- [Milestones](milestones.md)
-- [Open source license templates](licenses.md)
+- [Open source license templates](templates/licenses.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md)
@@ -46,6 +48,7 @@ following locations:
- [Todos](todos.md)
- [Users](users.md)
- [Validate CI configuration](ci/lint.md)
+- [Version](version.md)
### Internal CI API
@@ -101,7 +104,7 @@ Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header.
-### Session cookie
+### 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
@@ -355,6 +358,19 @@ follows:
}
```
+## Unknown route
+
+When you try to access an API URL that does not exist you will receive 404 Not Found.
+
+```
+HTTP/1.1 404 Not Found
+Content-Type: application/json
+{
+ "error": "404 Not Found"
+}
+```
+
+
## Clients
There are many unofficial GitLab API Clients for most of the popular
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index c464e3f3f71..06111f4ab67 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -43,7 +43,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-15T10:09:34.206Z",
"updated_at": "2016-06-15T10:09:34.206Z",
@@ -59,7 +59,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z",
@@ -103,7 +103,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z",
@@ -146,7 +146,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z",
@@ -190,7 +190,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z",
@@ -238,7 +238,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z",
@@ -279,7 +279,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z",
@@ -319,7 +319,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z",
@@ -362,7 +362,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z",
diff --git a/doc/api/boards.md b/doc/api/boards.md
new file mode 100644
index 00000000000..28681719f43
--- /dev/null
+++ b/doc/api/boards.md
@@ -0,0 +1,251 @@
+# Boards
+
+Every API call to boards must be authenticated.
+
+If a user is not a member of a project and the project is private, a `GET`
+request on that project will result to a `404` status code.
+
+## Project Board
+
+Lists Issue Boards in the given project.
+
+```
+GET /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards
+```
+
+Example response:
+
+```json
+[
+ {
+ "id" : 1,
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+]
+```
+
+## List board lists
+
+Get a list of the board's lists.
+Does not include `backlog` and `done` lists
+
+```
+GET /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists
+```
+
+Example response:
+
+```json
+[
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+]
+```
+
+## Single board list
+
+Get a single board list.
+
+```
+GET /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id`| integer | yes | The ID of a board's list |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## New board list
+
+Creates a new Issue Board list.
+
+If the operation is successful, a status code of `200` and the newly-created
+list is returned. If an error occurs, an error number and a message explaining
+the reason is returned.
+
+```
+POST /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `label_id` | integer | yes | The ID of a label |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists?label_id=5
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## Edit board list
+
+Updates an existing Issue Board list. This call is used to change list position.
+
+If the operation is successful, a code of `200` and the updated board list is
+returned. If an error occurs, an error number and a message explaining the
+reason is returned.
+
+```
+PUT /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+| `position` | integer | yes | The position of the list |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1?position=2
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## Delete a board list
+
+Only for admins and project owners. Soft deletes the board list in question.
+If the operation is successful, a status code `200` is returned. In case you cannot
+destroy this board list, or it is not present, code `404` is given.
+
+```
+DELETE /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+```
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index dce666445d0..0476cac0eda 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -11,10 +11,10 @@ GET /projects/:id/builds
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
-| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/builds?scope%5B0%5D=pending&scope%5B1%5D=running'
```
Example of response
@@ -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",
@@ -58,7 +64,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
- "web_url": "http://gitlab.dev/u/root",
+ "web_url": "http://gitlab.dev/root",
"website_url": ""
}
},
@@ -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",
@@ -96,7 +108,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
- "web_url": "http://gitlab.dev/u/root",
+ "web_url": "http://gitlab.dev/root",
"website_url": ""
}
}
@@ -120,10 +132,10 @@ GET /projects/:id/repository/commits/:sha/builds
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `sha` | string | yes | The SHA id of a commit |
-| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds?scope%5B0%5D=pending&scope%5B1%5D=running'
```
Example of response
@@ -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",
@@ -188,7 +212,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
- "web_url": "http://gitlab.dev/u/root",
+ "web_url": "http://gitlab.dev/root",
"website_url": ""
}
}
@@ -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",
@@ -249,7 +279,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
- "web_url": "http://gitlab.dev/u/root",
+ "web_url": "http://gitlab.dev/root",
"website_url": ""
}
}
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
index ecec53fde03..16028d1f124 100644
--- a/doc/api/ci/runners.md
+++ b/doc/api/ci/runners.md
@@ -12,7 +12,9 @@ communication channel. For the consumer API see the
This API uses two types of authentication:
1. Unique Runner's token, which is the token assigned to the Runner after it
- has been registered.
+ has been registered. This token can be found on the Runner's edit page (go to
+ **Project > Runners**, select one of the Runners listed under **Runners activated for
+ this project**).
2. Using Runners' registration token.
This is a token that can be found in project's settings.
@@ -48,7 +50,7 @@ DELETE /ci/api/v1/runners/delete
| Attribute | Type | Required | Description |
| --------- | ------- | --------- | ----------- |
-| `token` | string | yes | Runner's registration token |
+| `token` | string | yes | Unique Runner's token |
Example request:
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 682151d4b1d..e1ed99d98d3 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -46,6 +46,91 @@ Example response:
]
```
+## Create a commit with multiple files and actions
+
+> [Introduced][ce-6096] in GitLab 8.13.
+
+Create a commit by posting a JSON payload
+
+```
+POST /projects/:id/repository/commits
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME |
+| `branch_name` | string | yes | The name of a branch |
+| `commit_message` | string | yes | Commit message |
+| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
+| `author_email` | string | no | Specify the commit author's email address |
+| `author_name` | string | no | Specify the commit author's name |
+
+
+| `actions[]` Attribute | Type | Required | Description |
+| --------------------- | ---- | -------- | ----------- |
+| `action` | string | yes | The action to perform, `create`, `delete`, `move`, `update` |
+| `file_path` | string | yes | Full path to the file. Ex. `lib/class.rb` |
+| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb` |
+| `content` | string | no | File content, required for all except `delete`. Optional for `move` |
+| `encoding` | string | no | `text` or `base64`. `text` is default. |
+
+```bash
+PAYLOAD=$(cat << 'JSON'
+{
+ "branch_name": "master",
+ "commit_message": "some commit message",
+ "actions": [
+ {
+ "action": "create",
+ "file_path": "foo/bar",
+ "content": "some content"
+ },
+ {
+ "action": "delete",
+ "file_path": "foo/bar2",
+ },
+ {
+ "action": "move",
+ "file_path": "foo/bar3",
+ "previous_path": "foo/bar4",
+ "content": "some content"
+ },
+ {
+ "action": "update",
+ "file_path": "foo/bar5",
+ "content": "new content"
+ }
+ ]
+}
+JSON
+)
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v3/projects/1/repository/commits
+```
+
+Example response:
+```json
+{
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "some commit message",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2016-09-20T09:26:24.000-07:00",
+ "message": "some commit message",
+ "parent_ids": [
+ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+ ],
+ "committed_date": "2016-09-20T09:26:24.000-07:00",
+ "authored_date": "2016-09-20T09:26:24.000-07:00",
+ "stats": {
+ "additions": 2,
+ "deletions": 2,
+ "total": 4
+ },
+ "status": null
+}
+```
+
## Get a single commit
Get a specific commit identified by the commit hash or name of a branch or tag.
@@ -203,7 +288,7 @@ Example response:
```json
{
"author" : {
- "web_url" : "https://gitlab.example.com/u/thedude",
+ "web_url" : "https://gitlab.example.com/thedude",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"username" : "thedude",
"state" : "active",
@@ -234,7 +319,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA
-| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch
+| `ref` | string | no | The name of a repository branch or tag or, if not given, the default branch
| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
| `name` | string | no | Filter by [job name](../ci/yaml/README.md#jobs), e.g., `bundler:audit`
| `all` | boolean | no | Return all statuses, not only the latest ones
@@ -258,7 +343,7 @@ Example response:
"author" : {
"username" : "thedude",
"state" : "active",
- "web_url" : "https://gitlab.example.com/u/thedude",
+ "web_url" : "https://gitlab.example.com/thedude",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"id" : 28,
"name" : "Jeff Lebowski"
@@ -285,7 +370,7 @@ Example response:
"id" : 28,
"name" : "Jeff Lebowski",
"username" : "thedude",
- "web_url" : "https://gitlab.example.com/u/thedude",
+ "web_url" : "https://gitlab.example.com/thedude",
"state" : "active",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png"
},
@@ -323,7 +408,7 @@ Example response:
```json
{
"author" : {
- "web_url" : "https://gitlab.example.com/u/thedude",
+ "web_url" : "https://gitlab.example.com/thedude",
"name" : "Jeff Lebowski",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"username" : "thedude",
@@ -343,3 +428,5 @@ Example response:
"finished_at" : "2016-01-19T09:05:50.365Z"
}
```
+
+[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 417962de82d..3d95c4cde60 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -56,7 +56,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
- "web_url": "http://localhost:3000/u/root",
+ "web_url": "http://localhost:3000/root",
"website_url": ""
}
},
@@ -75,7 +75,7 @@ Example of response
"name": "Administrator",
"state": "active",
"username": "root",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
}
},
{
@@ -114,7 +114,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
- "web_url": "http://localhost:3000/u/root",
+ "web_url": "http://localhost:3000/root",
"website_url": ""
}
},
@@ -133,7 +133,7 @@ Example of response
"name": "Administrator",
"state": "active",
"username": "root",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
}
}
]
@@ -169,7 +169,7 @@ Example of response
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"environment": {
"id": 9,
@@ -193,7 +193,7 @@ Example of response
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/u/root",
+ "web_url": "http://localhost:3000/root",
"created_at": "2016-08-11T07:09:20.351Z",
"is_admin": true,
"bio": null,
diff --git a/doc/api/issues.md b/doc/api/issues.md
index eed0d2fce51..134263d27b4 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -46,7 +46,7 @@ Example response:
"author" : {
"state" : "active",
"id" : 18,
- "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian",
"avatar_url" : null,
"username" : "eileen.lowe"
@@ -67,7 +67,7 @@ Example response:
"state" : "active",
"id" : 1,
"name" : "Administrator",
- "web_url" : "https://gitlab.example.com/u/root",
+ "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root"
},
@@ -134,7 +134,7 @@ Example response:
},
"author" : {
"state" : "active",
- "web_url" : "https://gitlab.example.com/u/root",
+ "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
@@ -145,7 +145,7 @@ Example response:
"iid" : 1,
"assignee" : {
"avatar_url" : null,
- "web_url" : "https://gitlab.example.com/u/lennie",
+ "web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
@@ -215,7 +215,7 @@ Example response:
},
"author" : {
"state" : "active",
- "web_url" : "https://gitlab.example.com/u/root",
+ "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
@@ -226,7 +226,7 @@ Example response:
"iid" : 1,
"assignee" : {
"avatar_url" : null,
- "web_url" : "https://gitlab.example.com/u/lennie",
+ "web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
@@ -281,7 +281,7 @@ Example response:
},
"author" : {
"state" : "active",
- "web_url" : "https://gitlab.example.com/u/root",
+ "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
@@ -292,7 +292,7 @@ Example response:
"iid" : 1,
"assignee" : {
"avatar_url" : null,
- "web_url" : "https://gitlab.example.com/u/lennie",
+ "web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
@@ -357,7 +357,7 @@ Example response:
"name" : "Alexandra Bashirian",
"avatar_url" : null,
"state" : "active",
- "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
"id" : 18,
"username" : "eileen.lowe"
},
@@ -414,7 +414,7 @@ Example response:
"username" : "eileen.lowe",
"id" : 18,
"state" : "active",
- "web_url" : "https://gitlab.example.com/u/eileen.lowe"
+ "web_url" : "https://gitlab.example.com/eileen.lowe"
},
"state" : "closed",
"title" : "Issues with auth",
@@ -500,7 +500,7 @@ Example response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/axel.block"
+ "web_url": "https://gitlab.example.com/axel.block"
},
"author": {
"name": "Kris Steuber",
@@ -508,7 +508,7 @@ Example response:
"id": 10,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/solon.cremin"
+ "web_url": "https://gitlab.example.com/solon.cremin"
},
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
@@ -557,7 +557,7 @@ Example response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/axel.block"
+ "web_url": "https://gitlab.example.com/axel.block"
},
"author": {
"name": "Kris Steuber",
@@ -565,7 +565,7 @@ Example response:
"id": 10,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/solon.cremin"
+ "web_url": "https://gitlab.example.com/solon.cremin"
},
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
@@ -614,7 +614,7 @@ Example response:
"id": 21,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/keyon"
+ "web_url": "https://gitlab.example.com/keyon"
},
"author": {
"name": "Vivian Hermann",
@@ -622,7 +622,7 @@ Example response:
"id": 11,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/orville"
+ "web_url": "https://gitlab.example.com/orville"
},
"subscribed": false,
"due_date": null,
@@ -669,7 +669,7 @@ Example response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"action_name": "marked",
"target_type": "Issue",
@@ -700,7 +700,7 @@ Example response:
"id": 14,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/francisca"
+ "web_url": "https://gitlab.example.com/francisca"
},
"author": {
"name": "Maxie Medhurst",
@@ -708,7 +708,7 @@ Example response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"subscribed": true,
"user_notes_count": 7,
diff --git a/doc/api/keys.md b/doc/api/keys.md
index faa6f212b43..b68f08a007d 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -24,7 +24,7 @@ Parameters:
"id": 25,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
- "web_url": "http://localhost:3000/u/john_smith",
+ "web_url": "http://localhost:3000/john_smith",
"created_at": "2015-09-03T07:24:01.670Z",
"is_admin": false,
"bio": null,
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 3653ccf304a..656232cc940 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -148,7 +148,7 @@ PUT /projects/:id/labels
| --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer | yes | The ID of the project |
| `name` | string | yes | The name of the existing label |
-| `new_name` | string | yes if `color` if not provided | The new name of the label |
+| `new_name` | string | yes if `color` is not provided | The new name of the label |
| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign |
| `description` | string | no | The new description of the label |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 494040a1ce8..f4167403c2c 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -621,7 +621,7 @@ Example response when the GitLab issue tracker is used:
"author" : {
"state" : "active",
"id" : 18,
- "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian",
"avatar_url" : null,
"username" : "eileen.lowe"
@@ -642,7 +642,7 @@ Example response when the GitLab issue tracker is used:
"state" : "active",
"id" : 1,
"name" : "Administrator",
- "web_url" : "https://gitlab.example.com/u/root",
+ "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root"
},
@@ -711,7 +711,7 @@ Example response:
"id": 19,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/leila"
+ "web_url": "https://gitlab.example.com/leila"
},
"assignee": {
"name": "Celine Wehner",
@@ -719,7 +719,7 @@ Example response:
"id": 16,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/carli"
+ "web_url": "https://gitlab.example.com/carli"
},
"source_project_id": 5,
"target_project_id": 5,
@@ -787,7 +787,7 @@ Example response:
"id": 19,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/leila"
+ "web_url": "https://gitlab.example.com/leila"
},
"assignee": {
"name": "Celine Wehner",
@@ -795,7 +795,7 @@ Example response:
"id": 16,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/carli"
+ "web_url": "https://gitlab.example.com/carli"
},
"source_project_id": 5,
"target_project_id": 5,
@@ -858,7 +858,7 @@ Example response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"action_name": "marked",
"target_type": "MergeRequest",
@@ -881,7 +881,7 @@ Example response:
"id": 14,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/francisca"
+ "web_url": "https://gitlab.example.com/francisca"
},
"assignee": {
"name": "Dr. Gabrielle Strosin",
@@ -889,7 +889,7 @@ Example response:
"id": 4,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/barrett.krajcik"
+ "web_url": "https://gitlab.example.com/barrett.krajcik"
},
"source_project_id": 3,
"target_project_id": 3,
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 572844b8b3f..58d40eecf3e 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -143,7 +143,7 @@ Example Response:
"state": "active",
"created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/pipin"
+ "web_url": "https://gitlab.example.com/pipin"
},
"created_at": "2016-04-05T22:10:44.164Z",
"system": false,
@@ -268,7 +268,7 @@ Example Response:
"state": "active",
"created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/pipin"
+ "web_url": "https://gitlab.example.com/pipin"
},
"created_at": "2016-04-06T16:51:53.239Z",
"system": false,
@@ -398,7 +398,7 @@ Example Response:
"state": "active",
"created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/pipin"
+ "web_url": "https://gitlab.example.com/pipin"
},
"created_at": "2016-04-05T22:11:59.923Z",
"system": false,
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index b34e8075775..5ef5e3f5744 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -1,4 +1,4 @@
-# GitLab as an OAuth2 client
+# GitLab as an OAuth2 provider
This document covers using the OAuth2 protocol to access GitLab.
@@ -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/pipelines.md b/doc/api/pipelines.md
index 847408a7f61..a29b3eb6f44 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -34,7 +34,7 @@ Example of response
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"created_at": "2016-08-16T10:23:19.007Z",
"updated_at": "2016-08-16T10:23:19.216Z",
@@ -57,7 +57,7 @@ Example of response
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"created_at": "2016-08-16T10:23:21.184Z",
"updated_at": "2016-08-16T10:23:21.314Z",
@@ -103,7 +103,7 @@ Example of response
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
@@ -148,7 +148,7 @@ Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
@@ -193,7 +193,7 @@ Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 750ce1508df..b69db90e70d 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -20,7 +20,7 @@ Constants for project visibility levels are next:
## List projects
-Get a list of projects accessible by the authenticated user.
+Get a list of projects for which the authenticated user is a member.
```
GET /projects
@@ -28,11 +28,14 @@ GET /projects
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
```json
[
@@ -153,6 +156,138 @@ Parameters:
]
```
+Get a list of projects which the authenticated user can see.
+
+```
+GET /projects/visible
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+
+```json
+[
+ {
+ "id": 4,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
+ "web_url": "http://example.com/diaspora/diaspora-client",
+ "tag_list": [
+ "example",
+ "disapora client"
+ ],
+ "owner": {
+ "id": 3,
+ "name": "Diaspora",
+ "created_at": "2013-09-30T13:46:02Z"
+ },
+ "name": "Diaspora Client",
+ "name_with_namespace": "Diaspora / Diaspora Client",
+ "path": "diaspora-client",
+ "path_with_namespace": "diaspora/diaspora-client",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13:46:02Z",
+ "description": "",
+ "id": 3,
+ "name": "Diaspora",
+ "owner_id": 1,
+ "path": "diaspora",
+ "updated_at": "2013-09-30T13:46:02Z"
+ },
+ "archived": false,
+ "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true,
+ "shared_with_groups": []
+ },
+ {
+ "id": 6,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
+ "http_url_to_repo": "http://example.com/brightbox/puppet.git",
+ "web_url": "http://example.com/brightbox/puppet",
+ "tag_list": [
+ "example",
+ "puppet"
+ ],
+ "owner": {
+ "id": 4,
+ "name": "Brightbox",
+ "created_at": "2013-09-30T13:46:02Z"
+ },
+ "name": "Puppet",
+ "name_with_namespace": "Brightbox / Puppet",
+ "path": "puppet",
+ "path_with_namespace": "brightbox/puppet",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13:46:02Z",
+ "description": "",
+ "id": 4,
+ "name": "Brightbox",
+ "owner_id": 1,
+ "path": "brightbox",
+ "updated_at": "2013-09-30T13:46:02Z"
+ },
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
+ "archived": false,
+ "avatar_url": null,
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+]
+```
+
### List owned projects
Get a list of projects which are owned by the authenticated user.
@@ -163,11 +298,13 @@ GET /projects/owned
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### List starred projects
@@ -179,11 +316,13 @@ GET /projects/starred
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### List ALL projects
@@ -195,11 +334,13 @@ GET /projects/all
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### Get single project
@@ -212,7 +353,9 @@ GET /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```json
{
@@ -293,7 +436,7 @@ Parameters:
### Get project events
Get the events for the specified project.
-Sorted from newest to latest
+Sorted from newest to oldest
```
GET /projects/:id/events
@@ -301,7 +444,9 @@ GET /projects/:id/events
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```json
[
@@ -320,7 +465,7 @@ Parameters:
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
@@ -337,7 +482,7 @@ Parameters:
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"author_username": "john",
"data": {
@@ -383,7 +528,7 @@ Parameters:
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
@@ -407,7 +552,7 @@ Parameters:
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
@@ -422,7 +567,7 @@ Parameters:
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/u/root"
+ "web_url": "http://localhost:3000/root"
},
"author_username": "root"
}
@@ -439,24 +584,26 @@ POST /projects
Parameters:
-- `name` (required) - new project name
-- `path` (optional) - custom repository name for new project. By default generated based on name
-- `namespace_id` (optional) - namespace for the new project (defaults to user)
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `name` | string | yes | The name of the new project |
+| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
### Create project for user
@@ -468,23 +615,27 @@ POST /projects/user/:user_id
Parameters:
-- `user_id` (required) - user_id of owner
-- `name` (required) - new project name
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The user ID of the project owner |
+| `name` | string | yes | The name of the new project |
+| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
### Edit project
@@ -496,24 +647,26 @@ PUT /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `name` (optional) - project name
-- `path` (optional) - repository name for project
-- `description` (optional) - short project description
-- `default_branch` (optional)
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `name` | string | yes | The name of the project |
+| `path` | string | no | Custom repository name for the project. By default generated based on name |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -528,8 +681,10 @@ 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
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
### Star a project
@@ -540,9 +695,11 @@ Stars a given project. Returns status code `201` and the project on success and
POST /projects/:id/star
```
+Parameters:
+
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -610,7 +767,7 @@ DELETE /projects/:id/star
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -682,7 +839,7 @@ POST /projects/:id/archive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
@@ -770,7 +927,7 @@ POST /projects/:id/unarchive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
@@ -853,7 +1010,9 @@ DELETE /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
## Uploads
@@ -867,8 +1026,10 @@ POST /projects/:id/uploads
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `file` (required) - The file to be uploaded
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `file` | string | yes | The file to be uploaded |
```json
{
@@ -896,9 +1057,12 @@ POST /projects/:id/share
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
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `group_id` | integer | yes | The ID of the group to share with |
+| `group_access` | integer | yes | The permissions level to grant the group |
+| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
## Hooks
@@ -915,7 +1079,9 @@ GET /projects/:id/hooks
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
### Get project hook
@@ -927,8 +1093,10 @@ GET /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of a project hook |
```json
{
@@ -958,17 +1126,19 @@ POST /projects/:id/hooks
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `build_events` - Trigger hook on build events
-- `pipeline_events` - Trigger hook on pipeline events
-- `wiki_page_events` - Trigger hook on wiki page events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on issues events |
+| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
+| `tag_push_events` | boolean | no | Trigger hook on tag push events |
+| `note_events` | boolean | no | Trigger hook on note events |
+| `build_events` | boolean | no | Trigger hook on build events |
+| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
+| `wiki_events` | boolean | no | Trigger hook on wiki events |
+| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
### Edit project hook
@@ -980,18 +1150,20 @@ PUT /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `build_events` - Trigger hook on build events
-- `pipeline_events` - Trigger hook on pipeline events
-- `wiki_page_events` - Trigger hook on wiki page events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on issues events |
+| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
+| `tag_push_events` | boolean | no | Trigger hook on tag push events |
+| `note_events` | boolean | no | Trigger hook on note events |
+| `build_events` | boolean | no | Trigger hook on build events |
+| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
+| `wiki_events` | boolean | no | Trigger hook on wiki events |
+| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
### Delete project hook
@@ -1004,8 +1176,10 @@ DELETE /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of hook to delete
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
Note the JSON response differs if the hook is available or not. If the project hook
is available before it is returned in the JSON response or an empty response is returned.
@@ -1024,7 +1198,9 @@ GET /projects/:id/repository/branches
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```json
[
@@ -1079,10 +1255,12 @@ GET /projects/:id/repository/branches/:branch
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
-- `developers_can_push` - Flag if developers can push to the branch.
-- `developers_can_merge` - Flag if developers can merge to the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
+| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
+| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
### Protect single branch
@@ -1094,8 +1272,10 @@ PUT /projects/:id/repository/branches/:branch/protect
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
### Unprotect single branch
@@ -1107,8 +1287,10 @@ PUT /projects/:id/repository/branches/:branch/unprotect
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
## Admin fork relation
@@ -1122,8 +1304,10 @@ POST /projects/:id/fork/:forked_from_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `forked_from_id:` (required) - The ID of the project that was forked from
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `forked_from_id` | ID | yes | The ID of the project that was forked from |
### Delete an existing forked from relationship
@@ -1133,7 +1317,9 @@ DELETE /projects/:id/fork
Parameter:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
## Search for projects by name
@@ -1145,8 +1331,8 @@ GET /projects/search/:query
Parameters:
-- `query` (required) - A string contained in the project name
-- `per_page` (optional) - number of projects to return per page
-- `page` (optional) - the page to retrieve
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `query` | string | yes | A string contained in the project name |
+| `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index aaa2c99642b..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
}
```
@@ -72,7 +74,9 @@ PUT /application/settings
| `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/system_hooks.md b/doc/api/system_hooks.md
index 1802fae14fe..073e99b7147 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -98,11 +98,8 @@ Example response:
## Delete system hook
-Deletes a system hook. This is an idempotent API function and returns `200 OK`
-even if the hook is not available.
-
-If the hook is deleted, a JSON object is returned. An error is raised if the
-hook is not found.
+Deletes a system hook. It returns `200 OK` if the hooks is deleted and
+`404 Not Found` if the hook is not found.
---
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
new file mode 100644
index 00000000000..8235be92b12
--- /dev/null
+++ b/doc/api/templates/gitignores.md
@@ -0,0 +1,579 @@
+# Gitignores
+
+## List gitignore templates
+
+Get all gitignore templates.
+
+```
+GET /templates/gitignores
+```
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitignores
+```
+
+Example response:
+
+```json
+[
+ {
+ "name": "AppEngine"
+ },
+ {
+ "name": "Laravel"
+ },
+ {
+ "name": "Elisp"
+ },
+ {
+ "name": "SketchUp"
+ },
+ {
+ "name": "Ada"
+ },
+ {
+ "name": "Ruby"
+ },
+ {
+ "name": "Kohana"
+ },
+ {
+ "name": "Nanoc"
+ },
+ {
+ "name": "Erlang"
+ },
+ {
+ "name": "OCaml"
+ },
+ {
+ "name": "Lithium"
+ },
+ {
+ "name": "Fortran"
+ },
+ {
+ "name": "Scala"
+ },
+ {
+ "name": "Node"
+ },
+ {
+ "name": "Fancy"
+ },
+ {
+ "name": "Perl"
+ },
+ {
+ "name": "Zephir"
+ },
+ {
+ "name": "WordPress"
+ },
+ {
+ "name": "Symfony"
+ },
+ {
+ "name": "FuelPHP"
+ },
+ {
+ "name": "DM"
+ },
+ {
+ "name": "Sdcc"
+ },
+ {
+ "name": "Rust"
+ },
+ {
+ "name": "C"
+ },
+ {
+ "name": "Umbraco"
+ },
+ {
+ "name": "Actionscript"
+ },
+ {
+ "name": "Android"
+ },
+ {
+ "name": "Grails"
+ },
+ {
+ "name": "Composer"
+ },
+ {
+ "name": "ExpressionEngine"
+ },
+ {
+ "name": "Gcov"
+ },
+ {
+ "name": "Qt"
+ },
+ {
+ "name": "Phalcon"
+ },
+ {
+ "name": "ArchLinuxPackages"
+ },
+ {
+ "name": "TeX"
+ },
+ {
+ "name": "SCons"
+ },
+ {
+ "name": "Lilypond"
+ },
+ {
+ "name": "CommonLisp"
+ },
+ {
+ "name": "Rails"
+ },
+ {
+ "name": "Mercury"
+ },
+ {
+ "name": "Magento"
+ },
+ {
+ "name": "ChefCookbook"
+ },
+ {
+ "name": "GitBook"
+ },
+ {
+ "name": "C++"
+ },
+ {
+ "name": "Eagle"
+ },
+ {
+ "name": "Go"
+ },
+ {
+ "name": "OpenCart"
+ },
+ {
+ "name": "Scheme"
+ },
+ {
+ "name": "Typo3"
+ },
+ {
+ "name": "SeamGen"
+ },
+ {
+ "name": "Swift"
+ },
+ {
+ "name": "Elm"
+ },
+ {
+ "name": "Unity"
+ },
+ {
+ "name": "Agda"
+ },
+ {
+ "name": "CUDA"
+ },
+ {
+ "name": "VVVV"
+ },
+ {
+ "name": "Finale"
+ },
+ {
+ "name": "LemonStand"
+ },
+ {
+ "name": "Textpattern"
+ },
+ {
+ "name": "Julia"
+ },
+ {
+ "name": "Packer"
+ },
+ {
+ "name": "Scrivener"
+ },
+ {
+ "name": "Dart"
+ },
+ {
+ "name": "Plone"
+ },
+ {
+ "name": "Jekyll"
+ },
+ {
+ "name": "Xojo"
+ },
+ {
+ "name": "LabVIEW"
+ },
+ {
+ "name": "Autotools"
+ },
+ {
+ "name": "KiCad"
+ },
+ {
+ "name": "Prestashop"
+ },
+ {
+ "name": "ROS"
+ },
+ {
+ "name": "Smalltalk"
+ },
+ {
+ "name": "GWT"
+ },
+ {
+ "name": "OracleForms"
+ },
+ {
+ "name": "SugarCRM"
+ },
+ {
+ "name": "Nim"
+ },
+ {
+ "name": "SymphonyCMS"
+ },
+ {
+ "name": "Maven"
+ },
+ {
+ "name": "CFWheels"
+ },
+ {
+ "name": "Python"
+ },
+ {
+ "name": "ZendFramework"
+ },
+ {
+ "name": "CakePHP"
+ },
+ {
+ "name": "Concrete5"
+ },
+ {
+ "name": "PlayFramework"
+ },
+ {
+ "name": "Terraform"
+ },
+ {
+ "name": "Elixir"
+ },
+ {
+ "name": "CMake"
+ },
+ {
+ "name": "Joomla"
+ },
+ {
+ "name": "Coq"
+ },
+ {
+ "name": "Delphi"
+ },
+ {
+ "name": "Haskell"
+ },
+ {
+ "name": "Yii"
+ },
+ {
+ "name": "Java"
+ },
+ {
+ "name": "UnrealEngine"
+ },
+ {
+ "name": "AppceleratorTitanium"
+ },
+ {
+ "name": "CraftCMS"
+ },
+ {
+ "name": "ForceDotCom"
+ },
+ {
+ "name": "ExtJs"
+ },
+ {
+ "name": "MetaProgrammingSystem"
+ },
+ {
+ "name": "D"
+ },
+ {
+ "name": "Objective-C"
+ },
+ {
+ "name": "RhodesRhomobile"
+ },
+ {
+ "name": "R"
+ },
+ {
+ "name": "EPiServer"
+ },
+ {
+ "name": "Yeoman"
+ },
+ {
+ "name": "VisualStudio"
+ },
+ {
+ "name": "Processing"
+ },
+ {
+ "name": "Leiningen"
+ },
+ {
+ "name": "Stella"
+ },
+ {
+ "name": "Opa"
+ },
+ {
+ "name": "Drupal"
+ },
+ {
+ "name": "TurboGears2"
+ },
+ {
+ "name": "Idris"
+ },
+ {
+ "name": "Jboss"
+ },
+ {
+ "name": "CodeIgniter"
+ },
+ {
+ "name": "Qooxdoo"
+ },
+ {
+ "name": "Waf"
+ },
+ {
+ "name": "Sass"
+ },
+ {
+ "name": "Lua"
+ },
+ {
+ "name": "Clojure"
+ },
+ {
+ "name": "IGORPro"
+ },
+ {
+ "name": "Gradle"
+ },
+ {
+ "name": "Archives"
+ },
+ {
+ "name": "SynopsysVCS"
+ },
+ {
+ "name": "Ninja"
+ },
+ {
+ "name": "Tags"
+ },
+ {
+ "name": "OSX"
+ },
+ {
+ "name": "Dreamweaver"
+ },
+ {
+ "name": "CodeKit"
+ },
+ {
+ "name": "NotepadPP"
+ },
+ {
+ "name": "VisualStudioCode"
+ },
+ {
+ "name": "Mercurial"
+ },
+ {
+ "name": "BricxCC"
+ },
+ {
+ "name": "DartEditor"
+ },
+ {
+ "name": "Eclipse"
+ },
+ {
+ "name": "Cloud9"
+ },
+ {
+ "name": "TortoiseGit"
+ },
+ {
+ "name": "NetBeans"
+ },
+ {
+ "name": "GPG"
+ },
+ {
+ "name": "Espresso"
+ },
+ {
+ "name": "Redcar"
+ },
+ {
+ "name": "Xcode"
+ },
+ {
+ "name": "Matlab"
+ },
+ {
+ "name": "LyX"
+ },
+ {
+ "name": "SlickEdit"
+ },
+ {
+ "name": "Dropbox"
+ },
+ {
+ "name": "CVS"
+ },
+ {
+ "name": "Calabash"
+ },
+ {
+ "name": "JDeveloper"
+ },
+ {
+ "name": "Vagrant"
+ },
+ {
+ "name": "IPythonNotebook"
+ },
+ {
+ "name": "TextMate"
+ },
+ {
+ "name": "Ensime"
+ },
+ {
+ "name": "WebMethods"
+ },
+ {
+ "name": "VirtualEnv"
+ },
+ {
+ "name": "Emacs"
+ },
+ {
+ "name": "Momentics"
+ },
+ {
+ "name": "JetBrains"
+ },
+ {
+ "name": "SublimeText"
+ },
+ {
+ "name": "Kate"
+ },
+ {
+ "name": "ModelSim"
+ },
+ {
+ "name": "Redis"
+ },
+ {
+ "name": "KDevelop4"
+ },
+ {
+ "name": "Bazaar"
+ },
+ {
+ "name": "Linux"
+ },
+ {
+ "name": "Windows"
+ },
+ {
+ "name": "XilinxISE"
+ },
+ {
+ "name": "Lazarus"
+ },
+ {
+ "name": "EiffelStudio"
+ },
+ {
+ "name": "Anjuta"
+ },
+ {
+ "name": "Vim"
+ },
+ {
+ "name": "Otto"
+ },
+ {
+ "name": "MicrosoftOffice"
+ },
+ {
+ "name": "LibreOffice"
+ },
+ {
+ "name": "SBT"
+ },
+ {
+ "name": "MonoDevelop"
+ },
+ {
+ "name": "SVN"
+ },
+ {
+ "name": "FlexBuilder"
+ }
+]
+```
+
+## Single gitignore template
+
+Get a single gitignore template.
+
+```
+GET /templates/gitignores/:key
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key` | string | yes | The key of the gitignore template |
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitignores/Ruby
+```
+
+Example response:
+
+```json
+{
+ "name": "Ruby",
+ "content": "*.gem\n*.rbc\n/.config\n/coverage/\n/InstalledFiles\n/pkg/\n/spec/reports/\n/spec/examples.txt\n/test/tmp/\n/test/version_tmp/\n/tmp/\n\n# Used by dotenv library to load environment variables.\n# .env\n\n## Specific to RubyMotion:\n.dat*\n.repl_history\nbuild/\n*.bridgesupport\nbuild-iPhoneOS/\nbuild-iPhoneSimulator/\n\n## Specific to RubyMotion (use of CocoaPods):\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n#\n# vendor/Pods/\n\n## Documentation cache and generated files:\n/.yardoc/\n/_yardoc/\n/doc/\n/rdoc/\n\n## Environment normalization:\n/.bundle/\n/vendor/bundle\n/lib/bundler/man/\n\n# for a library or gem, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# Gemfile.lock\n# .ruby-version\n# .ruby-gemset\n\n# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:\n.rvmrc\n"
+}
+```
diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md
new file mode 100644
index 00000000000..e120016fbe6
--- /dev/null
+++ b/doc/api/templates/gitlab_ci_ymls.md
@@ -0,0 +1,120 @@
+# GitLab CI YMLs
+
+## List GitLab CI YML templates
+
+Get all GitLab CI YML templates.
+
+```
+GET /templates/gitlab_ci_ymls
+```
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls
+```
+
+Example response:
+
+```json
+[
+ {
+ "name": "C++"
+ },
+ {
+ "name": "Docker"
+ },
+ {
+ "name": "Elixir"
+ },
+ {
+ "name": "LaTeX"
+ },
+ {
+ "name": "Grails"
+ },
+ {
+ "name": "Rust"
+ },
+ {
+ "name": "Nodejs"
+ },
+ {
+ "name": "Ruby"
+ },
+ {
+ "name": "Scala"
+ },
+ {
+ "name": "Maven"
+ },
+ {
+ "name": "Harp"
+ },
+ {
+ "name": "Pelican"
+ },
+ {
+ "name": "Hyde"
+ },
+ {
+ "name": "Nanoc"
+ },
+ {
+ "name": "Octopress"
+ },
+ {
+ "name": "JBake"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "Hugo"
+ },
+ {
+ "name": "Metalsmith"
+ },
+ {
+ "name": "Hexo"
+ },
+ {
+ "name": "Lektor"
+ },
+ {
+ "name": "Doxygen"
+ },
+ {
+ "name": "Brunch"
+ },
+ {
+ "name": "Jekyll"
+ },
+ {
+ "name": "Middleman"
+ }
+]
+```
+
+## Single GitLab CI YML template
+
+Get a single GitLab CI YML template.
+
+```
+GET /templates/gitlab_ci_ymls/:key
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key` | string | yes | The key of the GitLab CI YML template |
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls/Ruby
+```
+
+Example response:
+
+```json
+{
+ "name": "Ruby",
+ "content": "# This file is a template, and might need editing before it works on your project.\n# Official language image. Look for the different tagged releases at:\n# https://hub.docker.com/r/library/ruby/tags/\nimage: \"ruby:2.3\"\n\n# Pick zero or more services to be used on all builds.\n# Only needed when using a docker container to run your tests in.\n# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service\nservices:\n - mysql:latest\n - redis:latest\n - postgres:latest\n\nvariables:\n POSTGRES_DB: database_name\n\n# Cache gems in between builds\ncache:\n paths:\n - vendor/ruby\n\n# This is a basic example for a gem or script which doesn't use\n# services such as redis or postgres\nbefore_script:\n - ruby -v # Print out ruby version for debugging\n # Uncomment next line if your rails app needs a JS runtime:\n # - apt-get update -q && apt-get install nodejs -yqq\n - gem install bundler --no-ri --no-rdoc # Bundler is not installed with the image\n - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby\n\n# Optional - Delete if not using `rubocop`\nrubocop:\n script:\n - rubocop\n\nrspec:\n script:\n - rspec spec\n\nrails:\n variables:\n DATABASE_URL: \"postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB\"\n script:\n - bundle exec rake db:migrate\n - bundle exec rake db:seed\n - bundle exec rake test\n"
+}
+```
diff --git a/doc/api/licenses.md b/doc/api/templates/licenses.md
index ed26d1fb7fb..ae7218cf1bd 100644
--- a/doc/api/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -5,7 +5,7 @@
Get all license templates.
```
-GET /licenses
+GET /templates/licenses
```
| Attribute | Type | Required | Description |
@@ -13,7 +13,7 @@ GET /licenses
| `popular` | boolean | no | If passed, returns only popular licenses |
```bash
-curl https://gitlab.example.com/api/v3/licenses?popular=1
+curl https://gitlab.example.com/api/v3/templates/licenses?popular=1
```
Example response:
@@ -102,7 +102,7 @@ Get a single license template. You can pass parameters to replace the license
placeholder.
```
-GET /licenses/:key
+GET /templates/licenses/:key
```
| Attribute | Type | Required | Description |
@@ -116,7 +116,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of
the authenticated user will be used to replace the copyright holder placeholder.
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/templates/licenses/mit?project=My+Cool+Project
```
Example response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 0cd644dfd2f..a5e81801024 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -44,7 +44,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"action_name": "marked",
"target_type": "MergeRequest",
@@ -67,7 +67,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"assignee": {
"name": "Administrator",
@@ -75,7 +75,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"source_project_id": 2,
"target_project_id": 2,
@@ -117,7 +117,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"action_name": "assigned",
"target_type": "MergeRequest",
@@ -140,7 +140,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"assignee": {
"name": "Administrator",
@@ -148,7 +148,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"source_project_id": 2,
"target_project_id": 2,
@@ -215,7 +215,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"action_name": "marked",
"target_type": "MergeRequest",
@@ -238,7 +238,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"assignee": {
"name": "Administrator",
@@ -246,7 +246,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"source_project_id": 2,
"target_project_id": 2,
diff --git a/doc/api/users.md b/doc/api/users.md
index 2bec20d1aa6..1f923d57e98 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -20,7 +20,7 @@ GET /users
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
- "web_url": "http://localhost:3000/u/john_smith"
+ "web_url": "http://localhost:3000/john_smith"
},
{
"id": 2,
@@ -28,7 +28,7 @@ GET /users
"name": "Jack Smith",
"state": "blocked",
"avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
- "web_url": "http://localhost:3000/u/jack_smith"
+ "web_url": "http://localhost:3000/jack_smith"
}
]
```
@@ -48,7 +48,7 @@ GET /users
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
- "web_url": "http://localhost:3000/u/john_smith",
+ "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"admin": false,
@@ -84,7 +84,7 @@ GET /users
"name": "Jack Smith",
"state": "blocked",
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
- "web_url": "http://localhost:3000/u/jack_smith",
+ "web_url": "http://localhost:3000/jack_smith",
"created_at": "2012-05-23T08:01:01Z",
"is_admin": false,
"admin": false,
@@ -147,7 +147,7 @@ Parameters:
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
- "web_url": "http://localhost:3000/u/john_smith",
+ "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"admin": false,
@@ -181,7 +181,7 @@ Parameters:
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
- "web_url": "http://localhost:3000/u/john_smith",
+ "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"admin": false,
@@ -307,7 +307,7 @@ GET /user
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
- "web_url": "http://localhost:3000/u/john_smith",
+ "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"admin": false,
@@ -644,3 +644,149 @@ Parameters:
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
+
+### Get user contribution events
+
+Get the contribution events for the specified user, sorted from newest to oldest.
+
+```
+GET /users/:id/events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/users/:id/events
+```
+
+Example response:
+
+```json
+[
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 830,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Public project search field",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "opened",
+ "target_id": null,
+ "target_type": null,
+ "author_id": 1,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "john",
+ "data": {
+ "before": "50d4420237a9de7be1304607147aec22e4a14af7",
+ "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "ref": "refs/heads/master",
+ "user_id": 1,
+ "user_name": "Dmitriy Zaporozhets",
+ "repository": {
+ "name": "gitlabhq",
+ "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
+ "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
+ "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
+ },
+ "commits": [
+ {
+ "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "message": "Add simple search to projects in public area",
+ "timestamp": "2013-05-13T18:18:08+00:00",
+ "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 1
+ },
+ "target_title": null
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 840,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Finish & merge Code search PR",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "commented on",
+ "target_id": 1312,
+ "target_type": "Note",
+ "author_id": 1,
+ "data": null,
+ "target_title": null,
+ "created_at": "2015-12-04T10:33:58.089Z",
+ "note": {
+ "id": 1312,
+ "body": "What an awesome day!",
+ "attachment": null,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2015-12-04T10:33:56.698Z",
+ "system": false,
+ "upvote": false,
+ "downvote": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
+ },
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ }
+]
+```
diff --git a/doc/api/version.md b/doc/api/version.md
new file mode 100644
index 00000000000..287d17cf97f
--- /dev/null
+++ b/doc/api/version.md
@@ -0,0 +1,23 @@
+# Version API
+
+>**Note:** This feature was introduced in GitLab 8.13
+
+Retrieve version information for this GitLab instance. Responds `200 OK` for
+authenticated users.
+
+```
+GET /version
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/version
+```
+
+Example response:
+
+```json
+{
+ "version": "8.13.0-pre",
+ "revision": "4e963fe"
+}
+```
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 0f64137a8a9..79bbe8421c6 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -188,7 +188,7 @@ In order to do that, follow the steps:
image = "docker:latest"
privileged = false
disable_cache = false
- volumes = ["/var/run/docker.sock", "/cache"]
+ volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
[runners.cache]
Insecure = false
```
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index a849905ac6b..aba77490915 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -37,7 +37,7 @@ The registered runner will use the `ruby:2.1` docker image and will run two
services, `postgres:latest` and `mysql:latest`, both of which will be
accessible during the build process.
-## What is image
+## What is an image
The `image` keyword is the name of the docker image that is present in the
local Docker Engine (list all images with `docker images`) or any image that
@@ -47,7 +47,7 @@ Hub please read the [Docker Fundamentals][] documentation.
In short, with `image` we refer to the docker image, which will be used to
create a container on which your build will run.
-## What is service
+## What is a service
The `services` keyword defines just another docker image that is run during
your build and is linked to the docker image that the `image` keyword defines.
@@ -61,7 +61,7 @@ time the project is built.
You can see some widely used services examples in the relevant documentation of
[CI services examples](../services/README.md).
-### How is service linked to the build
+### How services are linked to the build
To better understand how the container linking works, read
[Linking containers together][linking-containers].
@@ -221,7 +221,7 @@ time.
*Note: The following commands are run without root privileges. You should be
able to run docker with your regular user account.*
-First start with creating a file named `build script`:
+First start with creating a file named `build_script`:
```bash
cat <<EOF > build_script
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index d85b8a34ced..e070302fb82 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track
Deployments are created when [jobs] deploy versions of code to [environments].
+### Checkout deployments locally
+
+Since 8.13, a reference in the git repository is saved for each deployment. So
+knowing what the state is of your current environments is only a `git fetch`
+away.
+
+In your git config, append the `[remote "<your-remote>"]` block with an extra
+fetch line:
+
+```
+fetch = +refs/environments/*:refs/remotes/origin/environments/*
+```
+
## Defining environments
You can create and delete environments manually in the web interface, but we
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 40f0165deef..ffc310ec8c7 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -11,7 +11,9 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [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)
+- [Test a Phoenix application](test-phoenix-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
+- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [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)
diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md
new file mode 100644
index 00000000000..150698ca04b
--- /dev/null
+++ b/doc/ci/examples/test-phoenix-application.md
@@ -0,0 +1,56 @@
+## Test a Phoenix application
+
+This example demonstrates the integration of Gitlab CI with Phoenix, Elixir and
+Postgres.
+
+### Add `.gitlab-ci.yml` file to project
+
+The following `.gitlab-ci.yml` should be added in the root of your
+repository to trigger CI:
+
+```yaml
+image: elixir:1.3
+
+services:
+ - postgres:9.6
+
+variables:
+ MIX_ENV: "test"
+
+before_script:
+ # Setup phoenix dependencies
+ - apt-get update
+ - apt-get install -y postgresql-client
+ - mix local.hex --force
+ - mix deps.get --only test
+ - mix ecto.reset
+
+test:
+ script:
+ - mix test
+```
+
+The variables will set the Mix environment to "test". The
+`before_script` will install `psql`, some Phoenix dependencies, and will also
+run your migrations.
+
+Finally, the test `script` will run your tests.
+
+### Update the Config Settings
+
+In `config/test.exs`, update the database hostname:
+
+```elixir
+config :my_app, MyApp.Repo,
+ hostname: if(System.get_env("CI"), do: "postgres", else: "localhost"),
+```
+
+### Add the Migrations Folder
+
+If you do not have any migrations yet, you will need to create an empty
+`.gitkeep` file in `priv/repo/migrations`.
+
+### Sources
+
+- https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142
+- https://davejlong.com/ci-with-phoenix-and-gitlab/
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index ca9b986a060..729c1dc8c0d 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -31,6 +31,8 @@ project.
## Seeing build status
Clicking on a pipeline will show the builds that were run for that pipeline.
+Clicking on an individual build will show you its build trace, and allow you to
+cancel the build, retry it, or erase the build trace.
## Badges
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index b78422f6d0e..84048f1d25f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -4,7 +4,7 @@
> **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].
+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/variables/README.md b/doc/ci/variables/README.md
index 22d67bd9964..a4c3a731a20 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -48,6 +48,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **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 |
+| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **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 |
@@ -105,6 +106,39 @@ Variables can be defined at a global level, but also at a job level.
More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
+#### Debug tracing
+
+> **WARNING:** Enabling debug tracing can have severe security implications. The
+ output **will** contain the content of all your secure variables and any other
+ secrets! The output **will** be uploaded to the GitLab server and made visible
+ in build traces!
+
+By default, GitLab Runner hides most of the details of what it is doing when
+processing a job. This behaviour keeps build traces short, and prevents secrets
+from being leaked into the trace unless your script writes them to the screen.
+
+If a job isn't working as expected, this can make the problem difficult to
+investigate; in these cases, you can enable debug tracing in `.gitlab-ci.yml`.
+Available on GitLab Runner v1.7+, this feature enables the shell's execution
+trace, resulting in a verbose build trace listing all commands that were run,
+variables that were set, etc.
+
+Before enabling this, you should ensure builds are visible to
+[team members only](../../../user/permissions.md#project-features). You should
+also [erase](../pipelines.md#seeing-build-traces) all generated build traces
+before making them visible again.
+
+To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
+
+```yaml
+job1:
+ variables:
+ CI_DEBUG_TRACE: "true"
+```
+
+The [example project](https://gitlab.com/gitlab-examples/ci-debug-trace)
+demonstrates a working configuration, including build trace examples.
+
### User-defined variables (Secure Variables)
**This feature requires GitLab Runner 0.4.0 or higher**
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 16868554c1f..5c0e1c44e3f 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -146,20 +146,25 @@ variables:
```
These variables can be later used in all executed commands and scripts.
-
The YAML-defined variables are also set to all created service containers,
-thus allowing to fine tune them.
+thus allowing to fine tune them. Variables can be also defined on a
+[job level](#job-variables).
-Variables can be also defined on [job level](#job-variables).
+Except for the user defined variables, there are also the ones set up by the
+Runner itself. One example would be `CI_BUILD_REF_NAME` which has the value of
+the branch or tag name for which project is built. Apart from the variables
+you can set in `.gitlab-ci.yml`, there are also the so called secret variables
+which can be set in GitLab's UI.
-[Learn more about variables.](../variables/README.md)
+[Learn more about variables.][variables]
### cache
> Introduced in GitLab Runner v0.7.0.
`cache` is used to specify a list of files and directories which should be
-cached between builds.
+cached between builds. You can only use paths that are within the project
+workspace.
**By default the caching is enabled per-job and per-branch.**
@@ -540,20 +545,29 @@ An example usage of manual actions is deployment to production.
> Introduced in GitLab 8.9.
-`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.
+> You can read more about environments and find more examples in the
+[documentation about environments][environment].
+`environment` is used to define that a job deploys to a specific environment.
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 spaces. Common
-names are `qa`, `staging`, and `production`, but you can use whatever name works
-with your workflow.
+The `environment` name can contain:
----
+- letters
+- digits
+- spaces
+- `-`
+- `_`
+- `/`
+- `$`
+- `{`
+- `}`
-**Example configurations**
+Common names are `qa`, `staging`, and `production`, but you can use whatever
+name works with your workflow.
+
+In its simplest form, the `environment` keyword can be defined like:
```
deploy to production:
@@ -562,37 +576,134 @@ deploy to production:
environment: production
```
-The `deploy to production` job will be marked as doing deployment to
-`production` environment.
+In the above example, the `deploy to production` job will be marked as doing a
+deployment to the `production` environment.
+
+#### environment:name
+
+> Introduced in GitLab 8.11.
+
+>**Note:**
+Before GitLab 8.11, the name of an environment could be defined as a string like
+`environment: production`. The recommended way now is to define it under the
+`name` keyword.
+
+Instead of defining the name of the environment right after the `environment`
+keyword, it is also possible to define it as a separate value. For that, use
+the `name` keyword under `environment`:
+
+```
+deploy to production:
+ stage: deploy
+ script: git push production HEAD:master
+ environment:
+ name: production
+```
+
+#### environment:url
+
+> Introduced in GitLab 8.11.
+
+>**Note:**
+Before GitLab 8.11, the URL could be added only in GitLab's UI. The
+recommended way now is to define it in `.gitlab-ci.yml`.
+
+This is an optional value that when set, it exposes buttons in various places
+in GitLab which when clicked take you to the defined URL.
+
+In the example below, if the job finishes successfully, it will create buttons
+in the merge requests and in the environments/deployments pages which will point
+to `https://prod.example.com`.
+
+```
+deploy to production:
+ stage: deploy
+ script: git push production HEAD:master
+ environment:
+ name: production
+ url: https://prod.example.com
+```
+
+#### environment:on_stop
+
+> [Introduced][ce-6669] in GitLab 8.13.
+
+Closing (stoping) environments can be achieved with the `on_stop` keyword defined under
+`environment`. It declares a different job that runs in order to close
+the environment.
+
+Read the `environment:action` section for an example.
+
+#### environment:action
+
+> [Introduced][ce-6669] in GitLab 8.13.
+
+The `action` keyword is to be used in conjunction with `on_stop` and is defined
+in the job that is called to close the environment.
+
+Take for instance:
+
+```yaml
+review_app:
+ stage: deploy
+ script: make deploy-app
+ environment:
+ name: review
+ on_stop: stop_review_app
+
+stop_review_app:
+ stage: deploy
+ script: make delete-app
+ when: manual
+ environment:
+ name: review
+ action: stop
+```
+
+In the above example we set up the `review_app` job to deploy to the `review`
+environment, and we also defined a new `stop_review_app` job under `on_stop`.
+Once the `review_app` job is successfully finished, it will trigger the
+`stop_review_app` job based on what is defined under `when`. In this case we
+set it up to `manual` so it will need a [manual action](#manual-actions) via
+GitLab's web interface in order to run.
+
+The `stop_review_app` job is **required** to have the following keywords defined:
+
+- `when` - [reference](#when)
+- `environment:name`
+- `environment:action`
#### 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)
+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**
+For example:
```
deploy as review app:
stage: deploy
- script: ...
+ script: make deploy
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.
+create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME`
+is an [environment variable][variables] set by the Runner. If for example the
+`deploy as review app` job was run in a branch named `pow`, this environment
+should be accessible under `https://pow.review.example.com/`.
-This environment should be accessible under `https://branch-name.review.example.com/`.
+This of course implies that the underlying server which hosts the application
+is properly configured.
+
+The common use case is to create dynamic environments for branches and use them
+as Review Apps. You can see a simple example using Review Apps at
+https://gitlab.com/gitlab-examples/review-apps-nginx/.
### artifacts
@@ -604,8 +715,8 @@ This environment should be accessible under `https://branch-name.review.example.
> - Build artifacts are only collected for successful builds by default.
`artifacts` is used to specify a list of files and directories which should be
-attached to the build after success. To pass artifacts between different builds,
-see [dependencies](#dependencies).
+attached to the build after success. You can only use paths that are within the
+project workspace. To pass artifacts between different builds, see [dependencies](#dependencies).
Below are some examples.
@@ -858,27 +969,45 @@ job:
## Git Strategy
-> 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 or be removed
+ completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner
+ v1.7+.
+
+You can set the `GIT_STRATEGY` used for getting recent application code, either
+in the global [`variables`](#variables) section or the [`variables`](#job-variables)
+section for individual jobs. If left unspecified, the default from project
+settings will be used.
-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`
-is faster. `GIT_STRATEGY` can be specified in the global `variables` section or
-in the `variables` section for individual jobs. If it's not specified, then the
-default from project settings will be used.
+There are three possible values: `clone`, `fetch`, and `none`.
+
+`clone` is the slowest option. It clones the repository from scratch for every
+job, ensuring that the project workspace is always pristine.
```
variables:
GIT_STRATEGY: clone
```
-or
+`fetch` is faster as it re-uses the project workspace (falling back to `clone`
+if it doesn't exist). `git clean` is used to undo any changes made by the last
+job, and `git fetch` is used to retrieve commits made since the last job ran.
```
variables:
GIT_STRATEGY: fetch
```
+`none` also re-uses the project workspace, but skips all Git operations
+(including GitLab Runner's pre-clone script, if present). It is mostly useful
+for jobs that operate exclusively on artifacts (e.g., `deploy`). Git repository
+data may be present, but it is certain to be out of date, so you should only
+rely on files brought into the project workspace from cache or artifacts.
+
+```
+variables:
+ GIT_STRATEGY: none
+```
+
## Shallow cloning
> Introduced in GitLab 8.9 as an experimental feature. May change in future
@@ -1084,3 +1213,5 @@ CI with various languages.
[examples]: ../examples/README.md
[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
[environment]: ../environments.md
+[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
+[variables]: ../variables/README.md
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index d7740647a91..fe3e4681ba7 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1,98 +1 @@
-# GitLab Container Registry
-
-> [Introduced][ce-4040] in GitLab 8.8. Docker Registry manifest
-`v1` support was added in GitLab 8.9 to support Docker versions earlier than 1.10.
-
-> **Note:**
-This document is about the user guide. To learn how to enable GitLab Container
-Registry across your GitLab instance, visit the
-[administrator documentation](../administration/container_registry.md).
-
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
----
-
-## Enable the Container Registry for your project
-
-1. First, ask your system administrator to enable GitLab Container Registry
- following the [administration documentation](../administration/container_registry.md).
- If you are using GitLab.com, this is enabled by default so you can start using
- the Registry immediately.
-
-1. Go to your project's settings and enable the **Container Registry** feature
- on your project. For new projects this might be enabled by default. For
- existing projects you will have to explicitly enable it.
-
- ![Enable Container Registry](img/project_feature.png)
-
-## Build and push images
-
-After you save your project's settings, you should see a new link in the
-sidebar called **Container Registry**. Following this link will get you to
-your project's Registry panel where you can see how to login to the Container
-Registry using your GitLab credentials.
-
-For example if the Registry's URL is `registry.example.com`, the you should be
-able to login with:
-
-```
-docker login registry.example.com
-```
-
-Building and publishing images should be a straightforward process. Just make
-sure that you are using the Registry URL with the namespace and project name
-that is hosted on GitLab:
-
-```
-docker build -t registry.example.com/group/project .
-docker push registry.example.com/group/project
-```
-
-## Use images from GitLab Container Registry
-
-To download and run a container from images hosted in GitLab Container Registry,
-use `docker run`:
-
-```
-docker run [options] registry.example.com/group/project [arguments]
-```
-
-For more information on running Docker containers, visit the
-[Docker documentation][docker-docs].
-
-## Control Container Registry from within GitLab
-
-GitLab offers a simple Container Registry management panel. Go to your project
-and click **Container Registry** in the left sidebar.
-
-This view will show you all tags in your project and will easily allow you to
-delete them.
-
-![Container Registry panel](img/container_registry.png)
-
-## Build and push images using GitLab CI
-
-> **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 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
-
-In order to use a container image from your private project as an `image:` in
-your `.gitlab-ci.yml`, you have to follow the
-[Using a private Docker Registry][private-docker]
-documentation. This workflow will be simplified in the future.
-
-## Troubleshooting
-
-See [the GitLab Docker registry troubleshooting guide](troubleshooting.md).
-
-[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
-[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
+This document was moved in [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/container_registry/img/container_registry.png b/doc/container_registry/img/container_registry.png
deleted file mode 100644
index 57d6f9f22c5..00000000000
--- a/doc/container_registry/img/container_registry.png
+++ /dev/null
Binary files differ
diff --git a/doc/container_registry/img/project_feature.png b/doc/container_registry/img/project_feature.png
deleted file mode 100644
index a59b4f82b56..00000000000
--- a/doc/container_registry/img/project_feature.png
+++ /dev/null
Binary files differ
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
index 14c4a7d9a63..2f8cd37b488 100644
--- a/doc/container_registry/troubleshooting.md
+++ b/doc/container_registry/troubleshooting.md
@@ -1,141 +1 @@
-# Troubleshooting the GitLab Container Registry
-
-## Basic Troubleshooting
-
-1. Check to make sure that the system clock on your Docker client and GitLab server have
- been synchronized (e.g. via NTP).
-
-2. If you are using an S3-backed Registry, double check that the IAM
- permissions and the S3 credentials (including region) are correct. See [the
- sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
- for more details.
-
-3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
- for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
- there.
-
-## Advanced Troubleshooting
-
->**NOTE:** The following section is only recommended for experts.
-
-Sometimes it's not obvious what is wrong, and you may need to dive deeper into
-the communication between the Docker client and the Registry to find out
-what's wrong. We will use a concrete example in the past to illustrate how to
-diagnose a problem with the S3 setup.
-
-### Unexpected 403 error during push
-
-A user attempted to enable an S3-backed Registry. The `docker login` step went
-fine. However, when pushing an image, the output showed:
-
-```
-The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
-dc5e59c14160: Pushing [==================================================>] 14.85 kB
-03c20c1a019a: Pushing [==================================================>] 2.048 kB
-a08f14ef632e: Pushing [==================================================>] 2.048 kB
-228950524c88: Pushing 2.048 kB
-6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB
-5f70bf18a086: Pushing 1.024 kB
-737f40e80b7f: Waiting
-82b57dbc5385: Waiting
-19429b698a22: Waiting
-9436069b92a3: Waiting
-error parsing HTTP 403 response body: unexpected end of JSON input: ""
-```
-
-This error is ambiguous, as it's not clear whether the 403 is coming from the
-GitLab Rails application, the Docker Registry, or something else. In this
-case, since we know that since the login succeeded, we probably need to look
-at the communication between the client and the Registry.
-
-The REST API between the Docker client and Registry is [described
-here](https://docs.docker.com/registry/spec/api/). Normally, one would just
-use Wireshark or tcpdump to capture the traffic and see where things went
-wrong. However, since all communication between Docker clients and servers
-are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
-if you know the private key. What can we do instead?
-
-One way would be to disable HTTPS by setting up an [insecure
-Registry](https://docs.docker.com/registry/insecure/). This could introduce a
-security hole and is only recommended for local testing. If you have a
-production system and can't or don't want to do this, there is another way:
-use mitmproxy, which stands for Man-in-the-Middle Proxy.
-
-### mitmproxy
-
-[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
-client and server to inspect all traffic. One wrinkle is that your system
-needs to trust the mitmproxy SSL certificates for this to work.
-
-The following installation instructions assume you are running Ubuntu:
-
-1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
-1. Run `mitmproxy --port 9000` to generate its certificates.
- Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
-1. Install the certificate from `~/.mitmproxy` to your system:
-
- ```sh
- sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
- sudo update-ca-certificates
- ```
-
-If successful, the output should indicate that a certificate was added:
-
-```sh
-Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
-Running hooks in /etc/ca-certificates/update.d....done.
-```
-
-To verify that the certificates are properly installed, run:
-
-```sh
-mitmproxy --port 9000
-```
-
-This will run mitmproxy on port `9000`. In another window, run:
-
-```sh
-curl --proxy http://localhost:9000 https://httpbin.org/status/200
-```
-
-If everything is setup correctly, you will see information on the mitmproxy window and
-no errors from the curl commands.
-
-### Running the Docker daemon with a proxy
-
-For Docker to connect through a proxy, you must start the Docker daemon with the
-proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
-and then run Docker by hand. As root, run:
-
-```sh
-export HTTP_PROXY="http://localhost:9000"
-export HTTPS_PROXY="https://localhost:9000"
-docker daemon --debug
-```
-
-This will launch the Docker daemon and proxy all connections through mitmproxy.
-
-### Running the Docker client
-
-Now that we have mitmproxy and Docker running, we can attempt to login and push
-a container image. You may need to run as root to do this. For example:
-
-```sh
-docker login s3-testing.myregistry.com:4567
-docker push s3-testing.myregistry.com:4567/root/docker-test
-```
-
-In the example above, we see the following trace on the mitmproxy window:
-
-![mitmproxy output from Docker](img/mitmproxy-docker.png)
-
-The above image shows:
-
-* The initial PUT requests went through fine with a 201 status code.
-* The 201 redirected the client to the S3 bucket.
-* The HEAD request to the AWS bucket reported a 403 Unauthorized.
-
-What does this mean? This strongly suggests that the S3 user does not have the right
-[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
-The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
-Once the right permissions were set, the error will go away.
+This document was moved to [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/development/README.md b/doc/development/README.md
index 58c00f618fa..fb6a8a5b095 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -13,7 +13,9 @@
- [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
-- [SQL guidelines](sql.md) for SQL guidelines
+- [Frontend guidelines](frontend.md)
+- [SQL guidelines](sql.md) for working with SQL queries
+- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
## Process
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 40ae55ab905..c5c23b5c0b8 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -34,6 +34,10 @@ request is up to one of our merge request "endbosses", denoted on the
## Having your code reviewed
+Please keep in mind that code review is a process that can take multiple
+iterations, and reviewers may spot things later that they may not have seen the
+first time.
+
- The first reviewer of your code is _you_. Before you perform that first push
of your shiny new branch, read through the entire diff. Does it make sense?
Did you include something unrelated to the overall purpose of the changes? Did
@@ -55,6 +59,7 @@ request is up to one of our merge request "endbosses", denoted on the
Understand why the change is necessary (fixes a bug, improves the user
experience, refactors the existing code). Then:
+- Try to be thorough in your reviews to reduce the number of iterations.
- Communicate which ideas you feel strongly about and those you don't.
- Identify ways to simplify the code while still solving the problem.
- Offer alternative implementations, but assume the author already considered
@@ -64,8 +69,10 @@ experience, refactors the existing code). Then:
someone else would be confused by it as well.
- After a round of line notes, it can be helpful to post a summary note such as
"LGTM :thumbsup:", or "Just a couple things to address."
-- Avoid accepting a merge request before the build succeeds ("Merge when build
- succeeds" is fine).
+- Avoid accepting a merge request before the build succeeds. Of course, "Merge
+ When Build Succeeds" (MWBS) is fine.
+- If you set the MR to "Merge When Build Succeeds", you should take over
+ subsequent revisions for anything that would be spotted after that.
## Credits
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 39b801f761d..f07d2c9af2d 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -95,10 +95,10 @@ merge request.
someone in the Merge Request
- When introducing a new document, be careful for the headings to be
grammatically and syntactically correct. It is advised to mention one or all
- of the following GitLab members for a review: `@axil`, `@rspeicher`,
- `@dblessing`, `@ashleys`. This is to ensure that no document
- with wrong heading is going live without an audit, thus preventing dead links
- and redirection issues when corrected
+ of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`,
+ `@SeanPackham`. This is to ensure that no document with wrong heading is going
+ live without an audit, thus preventing dead links and redirection issues when
+ corrected
- Leave exactly one newline after a heading
## Links
@@ -314,6 +314,29 @@ In this case:
- different highlighting languages are used for each config in the code block
- the [references](#references) guide is used for reconfigure/restart
+## Fake tokens
+
+There may be times where a token is needed to demonstrate an API call using
+cURL or a secret variable used in CI. It is strongly advised not to use real
+tokens in documentation even if the probability of a token being exploited is
+low.
+
+You can use the following fake tokens as examples.
+
+| **Token type** | **Token value** |
+| --------------------- | --------------------------------- |
+| Private user token | `9koXpg98eAheJpvBs5tK` |
+| Personal access token | `n671WNGecHugsdEDPsyo` |
+| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
+| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
+| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
+| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
+| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
+| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
+| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
+| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
+| Request profile token | `7VgpS4Ax5utVD2esNstz` |
+
## API
Here is a list of must-have items. Use them in the exact order that appears
@@ -449,4 +472,4 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
-[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
+[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
new file mode 100644
index 00000000000..56c8516508e
--- /dev/null
+++ b/doc/development/frontend.md
@@ -0,0 +1,236 @@
+# 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
+
+## Common Errors
+
+### Rspec (Capybara/Poltergeist) chokes on general JavaScript errors
+
+If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but
+can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't
+have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're
+working in (`git mv <file>.js> <file.js.es6>`).
+
+
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 8c8c7486fff..05972b33fdb 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -54,6 +54,7 @@ Libraries with the following licenses are acceptable for use:
- [BSD 2-Clause License][BSD-2-Clause]: A permissive (non-copyleft) license as defined by the Open Source Initiative.
- [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative
- [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative.
+- [Creative Commons Zero (CC0)][CC0]: A public domain dedication, recommended as a way to disclaim copyright on your work to the maximum extent possible.
## Unacceptable Licenses
@@ -85,6 +86,7 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause
[BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause
[ISC]: https://opensource.org/licenses/ISC
+[CC0]: https://creativecommons.org/publicdomain/zero/1.0/
[GPL]: http://choosealicense.com/licenses/gpl-3.0/
[GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt
[GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 295eae0a88e..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
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 7ff603e2c4a..8337c2d9cb3 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -34,10 +34,11 @@ graphs/dashboards.
## Tooling
-GitLab provides two built-in tools to aid the process of improving performance:
+GitLab provides built-in tools to aid the process of improving performance:
* [Sherlock](profiling.md#sherlock)
-* [GitLab Performance Monitoring](../monitoring/performance/monitoring.md)
+* [GitLab Performance Monitoring](../administration/monitoring/performance/monitoring.md)
+* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
GitLab employees can use GitLab.com's performance monitoring systems located at
<http://performance.gitlab.net>, this requires you to log in using your
@@ -253,5 +254,5 @@ impact on runtime performance, and as such, using a constant instead of
referencing an object directly may even slow code down.
[#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607
-[yorickpeterse]: https://gitlab.com/u/yorickpeterse
+[yorickpeterse]: https://gitlab.com/yorickpeterse
[anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
new file mode 100644
index 00000000000..e3a20f29a09
--- /dev/null
+++ b/doc/development/sidekiq_style_guide.md
@@ -0,0 +1,38 @@
+# Sidekiq Style Guide
+
+This document outlines various guidelines that should be followed when adding or
+modifying Sidekiq workers.
+
+## Default Queue
+
+Use of the "default" queue is not allowed. Every worker should use a queue that
+matches the worker's purpose the closest. For example, workers that are to be
+executed periodically should use the "cronjob" queue.
+
+A list of all available queues can be found in `config/sidekiq_queues.yml`.
+
+## Dedicated Queues
+
+Most workers should use their own queue. To ease this process a worker can
+include the `DedicatedSidekiqQueue` concern as follows:
+
+```ruby
+class ProcessSomethingWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+end
+```
+
+This will set the queue name based on the class' name, minus the `Worker`
+suffix. In the above example this would lead to the queue being
+`process_something`.
+
+In some cases multiple workers do use the same queue. For example, the various
+workers for updating CI pipelines all use the `pipeline` queue. Adding workers
+to existing queues should be done with care, as adding more workers can lead to
+slow jobs blocking work (even for different jobs) on the shared queue.
+
+## Tests
+
+Each Sidekiq worker must be tested using RSpec, just like any other class. These
+tests should be placed in `spec/workers`.
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 3aa83975ace..d7e3aa35bdd 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,14 +2,14 @@
Step-by-step guides on the basics of working with Git and GitLab.
+- [Command line basics](command-line-commands.md)
- [Start using Git on the command line](start-using-git.md)
- [Create and add your SSH Keys](create-your-ssh-keys.md)
-- [Command Line basics](command-line-commands.md)
- [Create a project](create-project.md)
- [Create a group](create-group.md)
- [Create a branch](create-branch.md)
- [Fork a project](fork-project.md)
- [Add a file](add-file.md)
- [Add an image](add-image.md)
-- [Create a Merge Request](add-merge-request.md)
-- [Create an Issue](create-issue.md)
+- [Create an issue](create-issue.md)
+- [Create a merge request](add-merge-request.md)
diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md
index ff10a98e8f5..e9fbcbc23a9 100644
--- a/doc/gitlab-basics/add-file.md
+++ b/doc/gitlab-basics/add-file.md
@@ -1,27 +1,5 @@
# How to add a file
-You can create a file in your [shell](command-line-commands.md) or in GitLab.
-
-To create a file in GitLab, sign in to GitLab.
-
-Select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-It's a good idea to [create a branch](create-branch.md), but it's not necessary.
-
-Go to the directory where you'd like to add the file and click on the "+" sign next to the name of the project and directory:
-
-![Create a file](basicsimages/create_file.png)
-
-Name your file (you can't add spaces, so you can use hyphens or underscores). Don't forget to include the markup language you'd like to use :
-
-![File name](basicsimages/file_name.png)
-
-Add all the information that you'd like to include in your file:
-
-![Add information](basicsimages/white_space.png)
-
-Add a commit message based on what you just added and then click on "commit changes":
-
-![Commit changes](basicsimages/commit_changes.png)
+You can create a file in your [terminal](command-line-commands.md) and push
+to GitLab or you can use the
+[web interface](../user/project/repository/web_editor.md#create-a-file).
diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md
index 236b4248ea2..bf01fe51dc3 100644
--- a/doc/gitlab-basics/add-merge-request.md
+++ b/doc/gitlab-basics/add-merge-request.md
@@ -1,42 +1,33 @@
# How to create a merge request
-Merge Requests are useful to integrate separate changes that you've made to a project, on different branches.
+Merge requests are useful to integrate separate changes that you've made to a
+project, on different branches. This is a brief guide on how to create a merge
+request. For more information, check the
+[merge requests documentation](../user/project/merge_requests.md).
-To create a new Merge Request, sign in to GitLab.
+---
-Go to the project where you'd like to merge your changes:
+1. Before you start, you should have already [created a branch](create-branch.md)
+ and [pushed your changes](basic-git-commands.md) to GitLab.
-![Select a project](basicsimages/select_project.png)
+1. You can then go to the project where you'd like to merge your changes and
+ click on the **Merge requests** tab.
-Click on "Merge Requests" on the left side of your screen:
+ ![Merge requests](img/project_navbar.png)
-![Merge requests](basicsimages/merge_requests.png)
+1. Click on **New merge request** on the right side of the screen.
-Click on "+ new Merge Request" on the right side of the screen:
+ ![New Merge Request](img/merge_request_new.png)
-![New Merge Request](basicsimages/new_merge_request.png)
+1. Select a source branch and click on the **Compare branches and continue** button.
-Select a source branch or branch:
+ ![Select a branch](img/merge_request_select_branch.png)
-![Select a branch](basicsimages/select_branch.png)
+1. At a minimum, add a title and a description to your merge request. Optionally,
+ select a user to review your merge request and to accept or close it. You may
+ also select a milestone and labels.
-Click on the "compare branches" button:
+ ![New merge request page](img/merge_request_page.png)
-![Compare branches](basicsimages/compare_branches.png)
-
-Add a title and a description to your Merge Request:
-
-![Add a title and description](basicsimages/title_description_mr.png)
-
-Select a user to review your Merge Request and to accept or close it. You may also select milestones and labels (they are optional). Then click on the "submit new Merge Request" button:
-
-![Add a new merge request](basicsimages/add_new_merge_request.png)
-
-Your Merge Request will be ready to be approved and published.
-
-### Note
-
-After you created a new branch, you'll immediately find a "create a Merge Request" button at the top of your screen.
-You may automatically create a Merge Request from your recently created branch when clicking on this button:
-
-![Automatic MR button](basicsimages/button-create-mr.png)
+1. When ready, click on the **Submit merge request** button. Your merge request
+ will be ready to be approved and published.
diff --git a/doc/gitlab-basics/basicsimages/add_new_merge_request.png b/doc/gitlab-basics/basicsimages/add_new_merge_request.png
deleted file mode 100644
index e60992c4c6a..00000000000
--- a/doc/gitlab-basics/basicsimages/add_new_merge_request.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/add_sshkey.png b/doc/gitlab-basics/basicsimages/add_sshkey.png
deleted file mode 100644
index 89c86018629..00000000000
--- a/doc/gitlab-basics/basicsimages/add_sshkey.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_info.png b/doc/gitlab-basics/basicsimages/branch_info.png
deleted file mode 100644
index 2264f3c5bf2..00000000000
--- a/doc/gitlab-basics/basicsimages/branch_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_name.png b/doc/gitlab-basics/basicsimages/branch_name.png
deleted file mode 100644
index 75fe8313611..00000000000
--- a/doc/gitlab-basics/basicsimages/branch_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branches.png b/doc/gitlab-basics/basicsimages/branches.png
deleted file mode 100644
index 8621bc05776..00000000000
--- a/doc/gitlab-basics/basicsimages/branches.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png
deleted file mode 100644
index b52ab148839..00000000000
--- a/doc/gitlab-basics/basicsimages/button-create-mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/click-on-new-group.png b/doc/gitlab-basics/basicsimages/click-on-new-group.png
deleted file mode 100644
index 6450deec6fc..00000000000
--- a/doc/gitlab-basics/basicsimages/click-on-new-group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_changes.png b/doc/gitlab-basics/basicsimages/commit_changes.png
deleted file mode 100644
index a88809c5a3f..00000000000
--- a/doc/gitlab-basics/basicsimages/commit_changes.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_message.png b/doc/gitlab-basics/basicsimages/commit_message.png
deleted file mode 100644
index 4abe4517f98..00000000000
--- a/doc/gitlab-basics/basicsimages/commit_message.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commits.png b/doc/gitlab-basics/basicsimages/commits.png
deleted file mode 100644
index 2bfcaf75f01..00000000000
--- a/doc/gitlab-basics/basicsimages/commits.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/compare_branches.png b/doc/gitlab-basics/basicsimages/compare_branches.png
deleted file mode 100644
index 8a18453dd05..00000000000
--- a/doc/gitlab-basics/basicsimages/compare_branches.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_file.png b/doc/gitlab-basics/basicsimages/create_file.png
deleted file mode 100644
index 5ebe1b227dd..00000000000
--- a/doc/gitlab-basics/basicsimages/create_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_group.png b/doc/gitlab-basics/basicsimages/create_group.png
deleted file mode 100644
index 7ecc3baa990..00000000000
--- a/doc/gitlab-basics/basicsimages/create_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/edit_file.png b/doc/gitlab-basics/basicsimages/edit_file.png
deleted file mode 100644
index 9d3e817d036..00000000000
--- a/doc/gitlab-basics/basicsimages/edit_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_located.png b/doc/gitlab-basics/basicsimages/file_located.png
deleted file mode 100644
index e357cb5c6ab..00000000000
--- a/doc/gitlab-basics/basicsimages/file_located.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_name.png b/doc/gitlab-basics/basicsimages/file_name.png
deleted file mode 100644
index 01639c77d0d..00000000000
--- a/doc/gitlab-basics/basicsimages/file_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_file.png b/doc/gitlab-basics/basicsimages/find_file.png
deleted file mode 100644
index 6f26d26ae18..00000000000
--- a/doc/gitlab-basics/basicsimages/find_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_group.png b/doc/gitlab-basics/basicsimages/find_group.png
deleted file mode 100644
index 1211510aae9..00000000000
--- a/doc/gitlab-basics/basicsimages/find_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/fork.png b/doc/gitlab-basics/basicsimages/fork.png
deleted file mode 100644
index 13ff8345616..00000000000
--- a/doc/gitlab-basics/basicsimages/fork.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/group_info.png b/doc/gitlab-basics/basicsimages/group_info.png
deleted file mode 100644
index 2507d6c295b..00000000000
--- a/doc/gitlab-basics/basicsimages/group_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/groups.png b/doc/gitlab-basics/basicsimages/groups.png
deleted file mode 100644
index ef3dca60cc8..00000000000
--- a/doc/gitlab-basics/basicsimages/groups.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/https.png b/doc/gitlab-basics/basicsimages/https.png
deleted file mode 100644
index e74dbc13f9a..00000000000
--- a/doc/gitlab-basics/basicsimages/https.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/image_file.png b/doc/gitlab-basics/basicsimages/image_file.png
deleted file mode 100644
index 7f304b8e1f2..00000000000
--- a/doc/gitlab-basics/basicsimages/image_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issue_title.png b/doc/gitlab-basics/basicsimages/issue_title.png
deleted file mode 100644
index 60a6f7973be..00000000000
--- a/doc/gitlab-basics/basicsimages/issue_title.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issues.png b/doc/gitlab-basics/basicsimages/issues.png
deleted file mode 100644
index 14e9cdb64e1..00000000000
--- a/doc/gitlab-basics/basicsimages/issues.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/key.png b/doc/gitlab-basics/basicsimages/key.png
deleted file mode 100644
index 04400173ce8..00000000000
--- a/doc/gitlab-basics/basicsimages/key.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/merge_requests.png b/doc/gitlab-basics/basicsimages/merge_requests.png
deleted file mode 100644
index 570164df18b..00000000000
--- a/doc/gitlab-basics/basicsimages/merge_requests.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_issue.png b/doc/gitlab-basics/basicsimages/new_issue.png
deleted file mode 100644
index 94e7503dd8b..00000000000
--- a/doc/gitlab-basics/basicsimages/new_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_merge_request.png b/doc/gitlab-basics/basicsimages/new_merge_request.png
deleted file mode 100644
index 842f5ebed74..00000000000
--- a/doc/gitlab-basics/basicsimages/new_merge_request.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_project.png b/doc/gitlab-basics/basicsimages/new_project.png
deleted file mode 100644
index 421e8bc247b..00000000000
--- a/doc/gitlab-basics/basicsimages/new_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/newbranch.png b/doc/gitlab-basics/basicsimages/newbranch.png
deleted file mode 100644
index d5fcf33c4ea..00000000000
--- a/doc/gitlab-basics/basicsimages/newbranch.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/paste_sshkey.png b/doc/gitlab-basics/basicsimages/paste_sshkey.png
deleted file mode 100644
index 578ebee4440..00000000000
--- a/doc/gitlab-basics/basicsimages/paste_sshkey.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/profile_settings.png b/doc/gitlab-basics/basicsimages/profile_settings.png
deleted file mode 100644
index cb3f79f1879..00000000000
--- a/doc/gitlab-basics/basicsimages/profile_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/project_info.png b/doc/gitlab-basics/basicsimages/project_info.png
deleted file mode 100644
index e1adb8d48c2..00000000000
--- a/doc/gitlab-basics/basicsimages/project_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group.png b/doc/gitlab-basics/basicsimages/select-group.png
deleted file mode 100644
index 33b978dd899..00000000000
--- a/doc/gitlab-basics/basicsimages/select-group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group2.png b/doc/gitlab-basics/basicsimages/select-group2.png
deleted file mode 100644
index aee22c638db..00000000000
--- a/doc/gitlab-basics/basicsimages/select-group2.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_branch.png b/doc/gitlab-basics/basicsimages/select_branch.png
deleted file mode 100644
index f72a3ffb57f..00000000000
--- a/doc/gitlab-basics/basicsimages/select_branch.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_project.png b/doc/gitlab-basics/basicsimages/select_project.png
deleted file mode 100644
index 3bb832ea8d0..00000000000
--- a/doc/gitlab-basics/basicsimages/select_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/settings.png b/doc/gitlab-basics/basicsimages/settings.png
deleted file mode 100644
index 78637013d9b..00000000000
--- a/doc/gitlab-basics/basicsimages/settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/shh_keys.png b/doc/gitlab-basics/basicsimages/shh_keys.png
deleted file mode 100644
index c87f11a9d3d..00000000000
--- a/doc/gitlab-basics/basicsimages/shh_keys.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/submit_new_issue.png b/doc/gitlab-basics/basicsimages/submit_new_issue.png
deleted file mode 100644
index 78b854c8903..00000000000
--- a/doc/gitlab-basics/basicsimages/submit_new_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/title_description_mr.png b/doc/gitlab-basics/basicsimages/title_description_mr.png
deleted file mode 100644
index c31d61ec336..00000000000
--- a/doc/gitlab-basics/basicsimages/title_description_mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/white_space.png b/doc/gitlab-basics/basicsimages/white_space.png
deleted file mode 100644
index eaa969bdcf4..00000000000
--- a/doc/gitlab-basics/basicsimages/white_space.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index addd3b6b6eb..3b075ff5fc0 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -4,18 +4,21 @@
In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to GitLab.
-When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen.
+When you are on your Dashboard, click on the project that you'd like to clone.
+To work in the project, you can copy a link to the Git repository through a SSH
+or a HTTPS protocol. SSH is easier to use after it's been
+[setup](create-your-ssh-keys.md). While you are at the **Project** tab, select
+HTTPS or SSH from the dropdown menu and copy the link using the 'Copy to clipboard'
+button (you'll have to paste it on your shell in the next step).
-![Select a project](basicsimages/select_project.png)
-
-To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been [setup](create-your-ssh-keys.md). When you're in the project, click on the HTTPS or SSH button at the right side of your screen. Then copy the link (you'll have to paste it on your shell in the next step).
-
-![Copy the HTTPS or SSH](basicsimages/https.png)
+![Copy the HTTPS or SSH](img/project_clone_url.png)
## On the command line
### Clone your project
+
Go to your computer's shell and type the following command:
+
```
git clone PASTE HTTPS OR SSH HERE
```
@@ -23,26 +26,31 @@ git clone PASTE HTTPS OR SSH HERE
A clone of the project will be created in your computer.
### Go into a project, directory or file to work in it
+
```
cd NAME-OF-PROJECT-OR-FILE
```
### Go back one directory or file
+
```
cd ../
```
### View what’s in the directory that you are in
+
```
ls
```
### Create a directory
+
```
mkdir NAME-OF-YOUR-DIRECTORY
```
### Create a README.md or file in directory
+
```
touch README.md
nano README.md
@@ -53,27 +61,33 @@ nano README.md
```
### Remove a file
+
```
rm NAME-OF-FILE
```
### Remove a directory and all of its contents
+
```
rm -rf NAME-OF-DIRECTORY
```
### View history in the command line
+
```
history
```
### Carry out commands for which the account you are using lacks authority
+
You will be asked for an administrator’s password.
+
```
sudo
```
### Tell where you are
+
```
pwd
```
diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md
index 7556b0f663e..ad94f0dad29 100644
--- a/doc/gitlab-basics/create-branch.md
+++ b/doc/gitlab-basics/create-branch.md
@@ -2,38 +2,11 @@
A branch is an independent line of development.
-New commits are recorded in the history for the current branch, which results in taking the source from someone’s repository (the place where the history of your work is stored) at certain point in time, and apply your own changes to it in the history of the project.
-
-To add changes to your GitLab project, you should create a branch. You can do it in your [shell](basic-git-commands.md) or in GitLab.
-
-To create a new branch in GitLab, sign in and then select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-Click on "commits" on the menu on the left side of your screen:
-
-![Commits](basicsimages/commits.png)
-
-Click on the "branches" tab:
-
-![Branches](basicsimages/branches.png)
-
-Click on the "new branch" button on the right side of the screen:
-
-![New branch](basicsimages/newbranch.png)
-
-Fill out the information required:
-
-1. Add a name for your new branch (you can't add spaces, so you can use hyphens or underscores)
-
-1. On the "create from" space, add the the name of the branch you want to branch off from
-
-1. Click on the button "create branch"
-
-![Branch info](basicsimages/branch_info.png)
-
-### Note:
-
-You will be able to find and select the name of your branch in the white box next to a project's name:
-
-![Branch name](basicsimages/branch_name.png)
+New commits are recorded in the history for the current branch, which results
+in taking the source from someone’s repository (the place where the history of
+your work is stored) at certain point in time, and apply your own changes to it
+in the history of the project.
+
+To add changes to your GitLab project, you should create a branch. You can do
+it in your [terminal](basic-git-commands.md) or by
+[using the web interface](../user/project/repository/web_editor.md#create-a-new-branch).
diff --git a/doc/gitlab-basics/create-group.md b/doc/gitlab-basics/create-group.md
index f80ae62e442..64274ccd5eb 100644
--- a/doc/gitlab-basics/create-group.md
+++ b/doc/gitlab-basics/create-group.md
@@ -1,43 +1,48 @@
# How to create a group in GitLab
-## Create a group
-
Your projects in GitLab can be organized in 2 different ways:
-under your own namespace for single projects, such as ´your-name/project-1'; or under groups.
-If you organize your projects under a group, it works like a folder. You can manage your group members' permissions and access to the projects.
-
-To create a group, follow the instructions below:
+under your own namespace for single projects, such as `your-name/project-1` or
+under groups.
-Sign in to [GitLab.com](https://gitlab.com).
+If you organize your projects under a group, it works like a folder. You can
+manage your group members' permissions and access to the projects.
-When you are on your Dashboard, click on "Groups" on the left menu of your screen:
+---
-![Go to groups](basicsimages/select-group2.png)
+To create a group:
-Click on "New group" on the top right side of your screen:
+1. Expand the left sidebar by clicking the three bars at the upper left corner
+ and then navigate to **Groups**.
-![New group](basicsimages/click-on-new-group.png)
+ ![Go to groups](img/create_new_group_sidebar.png)
-Fill out the information required:
+1. Once in your groups dashboard, click on **New group**.
-1. Add a group path or group name (you can't add spaces, so you can use hyphens or underscores)
+ ![Create new group information](img/create_new_group_info.png)
-1. Add details or a group description
+1. Fill out the needed information:
-1. You can choose a group avatar if you'd like
+ 1. Set the "Group path" which will be the namespace under which your projects
+ will be hosted (path can contain only letters, digits, underscores, dashes
+ and dots; it cannot start with dashes or end in dot).
+ 1. Optionally, you can add a description so that others can briefly understand
+ what this group is about.
+ 1. Optionally, choose and avatar for your project.
+ 1. Choose the [visibility level](../public_access/public_access.md).
-1. Click on "create group"
+1. Finally, click the **Create group** button.
-![Group information](basicsimages/group_info.png)
-
-## Add a project to a group
+## Add a new project to a group
There are 2 different ways to add a new project to a group:
-* Select a group and then click on "New project" on the right side of your screen. Then you can [create a project](create-project.md)
+- Select a group and then click on the **New project** button.
+
+ ![New project](img/create_new_project_from_group.png)
-![New project](basicsimages/new_project.png)
+ You can then continue on [creating a project](create-project.md).
-* When you are [creating a project](create-project.md), click on "create a group" on the bottom right side of your screen
+- While you are [creating a project](create-project.md), select a group namespace
+ you've already created from the dropdown menu.
-![Create a group](basicsimages/create_group.png)
+ ![Select group](img/select_group_dropdown.png)
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index da9a165b8f5..13e5a738c89 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,27 +1,30 @@
# 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.
+---
-Go to the project where you'd like to create the Issue:
+1. Go to the project where you'd like to create the issue and navigate to the
+ **Issues** tab on top.
-![Select a project](basicsimages/select_project.png)
+ ![Issues](img/project_navbar.png)
-Click on "Issues" on the left side of your screen:
+1. Click on the **New issue** button on the right side of your screen.
-![Issues](basicsimages/issues.png)
+ ![New issue](img/new_issue_button.png)
-Click on the "+ new issue" button on the right side of your screen:
+1. At the very minimum, add a title and a description to your issue.
+ You may assign it to a user, add a milestone or add labels (all optional).
-![New issue](basicsimages/new_issue.png)
+ ![Issue title and description](img/new_issue_page.png)
-Add a title and a description to your issue:
+1. When ready, click on **Submit issue**.
-![Issue title and description](basicsimages/issue_title.png)
+---
-You may assign the Issue to a user, add a milestone and add labels (they are all optional). Then click on "submit new issue":
-
-![Submit new issue](basicsimages/submit_new_issue.png)
-
-Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md).
+Your Issue will now be added to the issue tracker of the project you opened it
+at 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/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index f737dffc024..3f45a631b3a 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -1,21 +1,24 @@
# How to create a project in GitLab
-To create a new project, sign in to GitLab.
+There are two ways to create a new project in GitLab.
-Go to your Dashboard and click on "new project" on the right side of your screen.
+1. While in your dashboard, you can create a new project using the **New project**
+ green button or you can use the cross icon in the upper right corner next to
+ your avatar which is always visible.
-![Create a project](basicsimages/new_project.png)
+ ![Create a project](img/create_new_project_button.png)
-Fill out the required information:
+1. From there you can see several options.
-1. Project path or the name of your project (you can't add spaces, so you can use hyphens or underscores)
+ ![Project information](img/create_new_project_info.png)
-1. Your project's description
+1. Fill out the information:
-1. Select a [visibility level](https://gitlab.com/help/public_access/public_access)
+ 1. "Project name" is the name of your project (you can't use spaces, but you
+ can use hyphens or underscores).
+ 1. The "Project description" is optional and will be shown in your project's
+ dashboard so others can briefly understand what your project is about.
+ 1. Select a [visibility level](../public_access/public_access.md).
+ 1. You can also [import your existing projects](../workflow/importing/README.md).
-1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html)
-
-1. Click on "create project"
-
-!![Project information](basicsimages/project_info.png)
+1. Finally, click **Create project**.
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index f31c353f2cf..b6ebe374de3 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -1,33 +1,37 @@
# How to create your SSH Keys
-You need to connect your computer to your GitLab account through SSH Keys. They are unique for every computer that you link your GitLab account with.
+1. The first thing you need to do is go to your [command line](start-using-git.md)
+ and follow the [instructions](../ssh/README.md) to generate your SSH key pair.
-## Generate your SSH Key
+1. Once you do that, login to GitLab with your credentials.
+1. On the upper right corner, click on your avatar and go to your **Profile settings**.
-Create an account on GitLab. Sign up and check your email for your confirmation link.
+ ![Profile settings dropdown](img/profile_settings.png)
-After you confirm, go to GitLab and sign in to your account.
+1. Navigate to the **SSH keys** tab.
-## Add your SSH Key
+ ![SSH Keys](img/profile_settings_ssh_keys.png)
-On the left side menu, click on "profile settings" and then click on "SSH Keys":
+3. Paste your **public** key that you generated in the first step in the 'Key'
+ box.
-![SSH Keys](basicsimages/shh_keys.png)
+ ![Paste SSH public key](img/profile_settings_ssh_keys_paste_pub.png)
-Then click on the green button "Add SSH Key":
+1. Optionally, give it a descriptive title so that you can recognize it in the
+ event you add multiple keys.
-![Add SSH Key](basicsimages/add_sshkey.png)
+ ![SSH key title](img/profile_settings_ssh_keys_title.png)
-There, you should paste the SSH Key that your command line will generate for you. Below you'll find the steps to generate it:
+1. Finally, click on **Add key** to add it to GitLab. You will be able to see
+ its fingerprint, its title and creation date.
-![Paste SSH Key](basicsimages/paste_sshkey.png)
+ ![SSH key single page](img/profile_settings_ssh_keys_single_key.png)
-## To generate an SSH Key on your command line
-Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate it.
+>**Note:**
+Once you add a key, you cannot edit it, only remove it. In case the paste
+didn't work, you will have to remove the offending key and re-add it.
-Copy the SSH Key that your command line created and paste it on the "Key" box on the GitLab page. The title will be added automatically.
+---
-![Paste SSH Key](basicsimages/key.png)
-
-Now, you'll be able to use Git over SSH, instead of Git over HTTP.
+Congratulations! You are now ready to use Git over SSH, instead of Git over HTTP!
diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md
index 5f8b81ea919..6c232fe6086 100644
--- a/doc/gitlab-basics/fork-project.md
+++ b/doc/gitlab-basics/fork-project.md
@@ -1,19 +1,20 @@
# How to fork a project
-A fork is a copy of an original repository that you can put somewhere else
-or where you can experiment and apply changes that you can later decide if
+A fork is a copy of an original repository that you can put in another namespace
+where you can experiment and apply changes that you can later decide if
publishing or not, without affecting your original project.
It takes just a few steps to fork a project in GitLab.
-Sign in to GitLab.
+1. Go to a project's dashboard under the **Project** tab and click on the
+ **Fork** button.
-Select a project on the right side of your screen:
+ ![Click on Fork button](img/fork_new.png)
-![Select a project](basicsimages/select_project.png)
+1. You will be asked where to fork the repository. Click on the user or group
+ to where you'd like to add the forked project.
-Click on the "fork" button on the right side of your screen:
+ ![Choose namespace](img/fork_choose_namespace.png)
-![Fork](basicsimages/fork.png)
-
-Click on the user or group to where you'd like to add the forked project.
+1. After a few moments, depending on the repository's size, the forking will
+ complete.
diff --git a/doc/gitlab-basics/img/create_new_group_info.png b/doc/gitlab-basics/img/create_new_group_info.png
new file mode 100644
index 00000000000..c8eddfd1bbb
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_group_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_group_sidebar.png b/doc/gitlab-basics/img/create_new_group_sidebar.png
new file mode 100644
index 00000000000..28017ee02e0
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_group_sidebar.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
new file mode 100644
index 00000000000..e7c794d943f
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_from_group.png b/doc/gitlab-basics/img/create_new_project_from_group.png
new file mode 100644
index 00000000000..6d41d17f9ca
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_from_group.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
new file mode 100644
index 00000000000..16d56f0707f
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_choose_namespace.png b/doc/gitlab-basics/img/fork_choose_namespace.png
new file mode 100644
index 00000000000..82c9c3bd39e
--- /dev/null
+++ b/doc/gitlab-basics/img/fork_choose_namespace.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_new.png b/doc/gitlab-basics/img/fork_new.png
new file mode 100644
index 00000000000..41885223286
--- /dev/null
+++ b/doc/gitlab-basics/img/fork_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png
new file mode 100644
index 00000000000..0aba5743f01
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_page.png b/doc/gitlab-basics/img/merge_request_page.png
new file mode 100644
index 00000000000..68c3bbf9444
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png
new file mode 100644
index 00000000000..516436ff6cc
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_select_branch.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png
new file mode 100644
index 00000000000..46b626bed65
--- /dev/null
+++ b/doc/gitlab-basics/img/new_issue_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png
new file mode 100644
index 00000000000..843504130b7
--- /dev/null
+++ b/doc/gitlab-basics/img/new_issue_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
new file mode 100644
index 00000000000..f0abd478849
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys.png b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
new file mode 100644
index 00000000000..2c9a42fe10c
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
new file mode 100644
index 00000000000..cd7add6937f
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
new file mode 100644
index 00000000000..095beb02be8
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
new file mode 100644
index 00000000000..4b998a7f948
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_clone_url.png b/doc/gitlab-basics/img/project_clone_url.png
new file mode 100644
index 00000000000..eed430e1036
--- /dev/null
+++ b/doc/gitlab-basics/img/project_clone_url.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png
new file mode 100644
index 00000000000..97cf3cd9702
--- /dev/null
+++ b/doc/gitlab-basics/img/project_navbar.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/public_file_link.png b/doc/gitlab-basics/img/public_file_link.png
index f60df6807f4..f60df6807f4 100644
--- a/doc/gitlab-basics/basicsimages/public_file_link.png
+++ b/doc/gitlab-basics/img/public_file_link.png
Binary files differ
diff --git a/doc/gitlab-basics/img/select_group_dropdown.png b/doc/gitlab-basics/img/select_group_dropdown.png
new file mode 100644
index 00000000000..7d8b89c2df9
--- /dev/null
+++ b/doc/gitlab-basics/img/select_group_dropdown.png
Binary files differ
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index b61f436c1a4..42cd8bb3e48 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -1,11 +1,10 @@
# Start using Git on the command line
-If you want to start using a Git and GitLab, make sure that you have created an
-account on GitLab.
+If you want to start using Git and GitLab, make sure that you have created and/or signed into an account on GitLab.
## Open a shell
-Depending on your operating system, find the shell of your preference. Here are some suggestions.
+Depending on your operating system, you will need to use a shell of your preference. Here are some suggestions:
- [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on Mac OSX
@@ -22,19 +21,19 @@ Type the following command and then press enter:
git --version
```
-You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
+You should receive a message that will tell you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window.
-After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed.
+After you are finished installing, open a new shell and type "git --version" again to verify that it was correctly installed.
## Add your Git username and set your email
-It is important because every Git commit that you create will use this information.
+It is important to configure your Git username and email address as every Git commit will use this information to identify you as the author.
On your shell, type the following command to add your username:
```
-git config --global user.name ADD YOUR USERNAME
+git config --global user.name "YOUR_USERNAME"
```
Then verify that you have the correct username:
@@ -44,7 +43,7 @@ git config --global user.name
To set your email address, type the following command:
```
-git config --global user.email ADD YOUR EMAIL
+git config --global user.email "your_email_address@example.com"
```
To verify that you entered your email correctly, type:
@@ -52,7 +51,7 @@ To verify that you entered your email correctly, type:
git config --global user.email
```
-You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project.
+You'll need to do this only once as you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project.
## Check your information
@@ -76,7 +75,7 @@ git pull REMOTE NAME-OF-BRANCH -u
(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
### Create a branch
-Spaces won't be recognized, so you need to use a hyphen or underscore.
+Spaces won't be recognized, so you will need to use a hyphen or underscore.
```
git checkout -b NAME-OF-BRANCH
```
@@ -127,4 +126,3 @@ You need to be in the master branch.
git checkout master
git merge NAME-OF-BRANCH
```
-
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 5a9a1582877..db0f03f2c98 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -1,302 +1 @@
-# Reply by email
-
-GitLab can be set up to allow users to comment on issues and merge requests by
-replying to notification emails.
-
-## Requirement
-
-Reply by email requires an IMAP-enabled email account. GitLab allows you to use
-three strategies for this feature:
-- using email sub-addressing
-- using a dedicated email address
-- using a catch-all mailbox
-
-### Email sub-addressing
-
-**If your provider or server supports email sub-addressing, we recommend using it.**
-
-[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
-a feature where any email to `user+some_arbitrary_tag@example.com` will end up
-in the mailbox for `user@example.com`, and is supported by providers such as
-Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
-mail server which you can run on-premises.
-
-### Dedicated email address
-
-This solution is really simple to set up: you just have to create an email
-address dedicated to receive your users' replies to GitLab notifications.
-
-### Catch-all mailbox
-
-A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
-"catch all" the emails addressed to the domain that do not exist in the mail
-server.
-
-## How it works?
-
-### 1. GitLab sends a notification email
-
-When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
-header is set to the address defined in your GitLab configuration, with the
-`%{key}` placeholder (if present) replaced by a specific "reply key". In
-addition, this "reply key" is also added to the `References` header.
-
-### 2. You reply to the notification email
-
-When you reply to the notification email, your email client will:
-
-- send the email to the `Reply-To` address it got from the notification email
-- set the `In-Reply-To` header to the value of the `Message-ID` header from the
- notification email
-- set the `References` header to the value of the `Message-ID` plus the value of
- the notification email's `References` header.
-
-### 3. GitLab receives your reply to the notification email
-
-When GitLab receives your reply, it will look for the "reply key" in the
-following headers, in this order:
-
-1. the `To` header
-1. the `References` header
-
-If it finds a reply key, it will be able to leave your reply as a comment on
-the entity the notification was about (issue, merge request, commit...).
-
-For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
-please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
-
-## Set it up
-
-If you want to use Gmail / Google Apps with Reply by email, make sure you have
-[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
-and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
-
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
-[these instructions](./postfix.md).
-
-### Omnibus package installations
-
-1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
- feature and fill in the details for your specific IMAP server and email account:
-
- ```ruby
- # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
- gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- gitlab_rails['incoming_email_email'] = "incoming"
- # Email account password
- gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
- # IMAP server host
- gitlab_rails['incoming_email_host'] = "gitlab.example.com"
- # IMAP server port
- gitlab_rails['incoming_email_port'] = 143
- # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_ssl'] = false
- # Whether the IMAP server uses StartTLS
- gitlab_rails['incoming_email_start_tls'] = false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- gitlab_rails['incoming_email_mailbox_name'] = "inbox"
- ```
-
- ```ruby
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
- # Email account password
- gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
- # IMAP server host
- gitlab_rails['incoming_email_host'] = "imap.gmail.com"
- # IMAP server port
- gitlab_rails['incoming_email_port'] = 993
- # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_ssl'] = true
- # Whether the IMAP server uses StartTLS
- gitlab_rails['incoming_email_start_tls'] = false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- gitlab_rails['incoming_email_mailbox_name'] = "inbox"
- ```
-
-1. Reconfigure GitLab and restart mailroom for the changes to take effect:
-
- ```sh
- sudo gitlab-ctl reconfigure
- sudo gitlab-ctl restart mailroom
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- sudo gitlab-rake gitlab:incoming_email:check
- ```
-
-1. Reply by email should now be working.
-
-### Installations from source
-
-1. Go to the GitLab installation directory:
-
- ```sh
- cd /home/git/gitlab
- ```
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
- and fill in the details for your specific IMAP server and email account:
-
- ```sh
- sudo editor config/gitlab.yml
- ```
-
- ```yaml
- # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "incoming+%{key}@gitlab.example.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "incoming"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "gitlab.example.com"
- # IMAP server port
- port: 143
- # Whether the IMAP server uses SSL
- ssl: false
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- ```
-
- ```yaml
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "gitlab-incoming@gmail.com"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "imap.gmail.com"
- # IMAP server port
- port: 993
- # Whether the IMAP server uses SSL
- ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- ```
-
-1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
-
- ```sh
- sudo mkdir -p /etc/default
- echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
- ```
-
-1. Restart GitLab:
-
- ```sh
- sudo service gitlab restart
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
- ```
-
-1. Reply by email should now be working.
-
-### Development
-
-1. Go to the GitLab installation directory.
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
-
- ```yaml
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "gitlab-incoming@gmail.com"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "imap.gmail.com"
- # IMAP server port
- port: 993
- # Whether the IMAP server uses SSL
- ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- ```
-
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
-
-1. Uncomment the `mail_room` line in your `Procfile`:
-
- ```yaml
- mail_room: bundle exec mail_room -q -c config/mail_room.yml
- ```
-
-1. Restart GitLab:
-
- ```sh
- bundle exec foreman start
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
- ```
-
-1. Reply by email should now be working.
+This document was moved to [administration/reply_by_email](../administration/reply_by_email.md).
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
index 787d21f7f8f..90833238ac5 100644
--- a/doc/incoming_email/postfix.md
+++ b/doc/incoming_email/postfix.md
@@ -1,321 +1 @@
-# Set up Postfix for Reply by email
-
-This document will take you through the steps of setting up a basic Postfix mail server with IMAP authentication on Ubuntu, to be used with Reply by email.
-
-The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
-
-## Configure your server firewall
-
-1. Open up port 25 on your server so that people can send email into the server over SMTP.
-2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
-
-## Install packages
-
-1. Install the `postfix` package if it is not installed already:
-
- ```sh
- sudo apt-get install postfix
- ```
-
- When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
-
-1. Install the `mailutils` package.
-
- ```sh
- sudo apt-get install mailutils
- ```
-
-## Create user
-
-1. Create a user for incoming email.
-
- ```sh
- sudo useradd -m -s /bin/bash incoming
- ```
-
-1. Set a password for this user.
-
- ```sh
- sudo passwd incoming
- ```
-
- Be sure not to forget this, you'll need it later.
-
-## Test the out-of-the-box setup
-
-1. Connect to the local SMTP server:
-
- ```sh
- telnet localhost 25
- ```
-
- You should see a prompt like this:
-
- ```sh
- Trying 127.0.0.1...
- Connected to localhost.
- Escape character is '^]'.
- 220 gitlab.example.com ESMTP Postfix (Ubuntu)
- ```
-
- If you get a `Connection refused` error instead, verify that `postfix` is running:
-
- ```sh
- sudo postfix status
- ```
-
- If it is not, start it:
-
- ```sh
- sudo postfix start
- ```
-
-1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
-
- ```
- ehlo localhost
- mail from: root@localhost
- rcpt to: incoming@localhost
- data
- Subject: Re: Some issue
-
- Sounds good!
- .
- quit
- ```
-
- _**Note:** The `.` is a literal period on its own line._
-
- _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
- then your Postfix `my_network` configuration is not correct. The error will
- say 'Temporary lookup failure'. See
- [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
-
-1. Check if the `incoming` user received the email:
-
- ```sh
- su - incoming
- mail
- ```
-
- You should see output like this:
-
- ```
- "/var/mail/incoming": 1 message 1 unread
- >U 1 root@localhost 59/2842 Re: Some issue
- ```
-
- Quit the mail app:
-
- ```sh
- q
- ```
-
-1. Log out of the `incoming` account and go back to being `root`:
-
- ```sh
- logout
- ```
-
-## Configure Postfix to use Maildir-style mailboxes
-
-Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
-
-1. Configure Postfix to use Maildir-style mailboxes:
-
- ```sh
- sudo postconf -e "home_mailbox = Maildir/"
- ```
-
-1. Restart Postfix:
-
- ```sh
- sudo /etc/init.d/postfix restart
- ```
-
-1. Test the new setup:
-
- 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
- 1. Check if the `incoming` user received the email:
-
- ```sh
- su - incoming
- MAIL=/home/incoming/Maildir
- mail
- ```
-
- You should see output like this:
-
- ```
- "/home/incoming/Maildir": 1 message 1 unread
- >U 1 root@localhost 59/2842 Re: Some issue
- ```
-
- Quit the mail app:
-
- ```sh
- q
- ```
-
- _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
- version of `mail` doesn't support Maildir style mailboxes. Install
- `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
- try the above steps again, substituting `heirloom-mailx` for the `mail`
- command._
-
-1. Log out of the `incoming` account and go back to being `root`:
-
- ```sh
- logout
- ```
-
-## Install the Courier IMAP server
-
-1. Install the `courier-imap` package:
-
- ```sh
- sudo apt-get install courier-imap
- ```
-
-## Configure Postfix to receive email from the internet
-
-1. Let Postfix know about the domains that it should consider local:
-
- ```sh
- sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
- ```
-
-1. Let Postfix know about the IPs that it should consider part of the LAN:
-
- We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
-
- ```sh
- sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
- ```
-
-1. Configure Postfix to receive mail on all interfaces, which includes the internet:
-
- ```sh
- sudo postconf -e "inet_interfaces = all"
- ```
-
-1. Configure Postfix to use the `+` delimiter for sub-addressing:
-
- ```sh
- sudo postconf -e "recipient_delimiter = +"
- ```
-
-1. Restart Postfix:
-
- ```sh
- sudo service postfix restart
- ```
-
-## Test the final setup
-
-1. Test SMTP under the new setup:
-
- 1. Connect to the SMTP server:
-
- ```sh
- telnet gitlab.example.com 25
- ```
-
- You should see a prompt like this:
-
- ```sh
- Trying 123.123.123.123...
- Connected to gitlab.example.com.
- Escape character is '^]'.
- 220 gitlab.example.com ESMTP Postfix (Ubuntu)
- ```
-
- If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
-
- 1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
-
- ```
- ehlo gitlab.example.com
- mail from: root@gitlab.example.com
- rcpt to: incoming@gitlab.example.com
- data
- Subject: Re: Some issue
-
- Sounds good!
- .
- quit
- ```
-
- (Note: The `.` is a literal period on its own line)
-
- 1. Check if the `incoming` user received the email:
-
- ```sh
- su - incoming
- MAIL=/home/incoming/Maildir
- mail
- ```
-
- You should see output like this:
-
- ```
- "/home/incoming/Maildir": 1 message 1 unread
- >U 1 root@gitlab.example.com 59/2842 Re: Some issue
- ```
-
- Quit the mail app:
-
- ```sh
- q
- ```
-
- 1. Log out of the `incoming` account and go back to being `root`:
-
- ```sh
- logout
- ```
-
-1. Test IMAP under the new setup:
-
- 1. Connect to the IMAP server:
-
- ```sh
- telnet gitlab.example.com 143
- ```
-
- You should see a prompt like this:
-
- ```sh
- Trying 123.123.123.123...
- Connected to mail.example.gitlab.com.
- Escape character is '^]'.
- - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information.
- ```
-
- 1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt:
-
- ```
- a login incoming PASSWORD
- ```
-
- Replace PASSWORD with the password you set on the `incoming` user earlier.
-
- You should see output like this:
-
- ```
- a OK LOGIN Ok.
- ```
-
- 1. Disconnect from the IMAP server:
-
- ```sh
- a logout
- ```
-
-## Done!
-
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
-
----------
-
-_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
+This document was moved to [administration/reply_by_email_postfix_setup](../administration/reply_by_email_postfix_setup.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index cb4c1f4a091..c9acc9cdfb0 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
@@ -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.8.2
+ sudo -u git -H git checkout v0.8.5
sudo -u git -H make
### Initialize Database and Activate Advanced Features
@@ -563,7 +563,7 @@ Using a self-signed certificate is discouraged but if you must use it follow the
### Enable Reply by email
-See the ["Reply by email" documentation](../incoming_email/README.md) for more information on how to set this up.
+See the ["Reply by email" documentation](../administration/reply_by_email.md) for more information on how to set this up.
### LDAP Authentication
diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md
index eac57bc3de4..6cf93c33ec2 100644
--- a/doc/monitoring/health_check.md
+++ b/doc/monitoring/health_check.md
@@ -1,66 +1 @@
-# Health Check
-
-> [Introduced][ce-3888] in GitLab 8.8.
-
-GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
-endpoint. The health check reports on the overall system status based on the status of
-the database connection, the state of the database migrations, and the ability to write
-and access the cache. This endpoint can be provided to uptime monitoring services like
-[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
-
-## Access Token
-
-An access token needs to be provided while accessing the health check endpoint. The current
-accepted token can be found on the `admin/health_check` page of your GitLab instance.
-
-![access token](img/health_check_token.png)
-
-The access token can be passed as a URL parameter:
-
-```
-https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
-```
-
-or as an HTTP header:
-
-```bash
-curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
-```
-
-## Using the Endpoint
-
-Once you have the access token, health information can be retrieved as plain text, JSON,
-or XML using the `health_check` endpoint:
-
-- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN`
-
-You can also ask for the status of specific services:
-
-- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN`
-
-For example, the JSON output of the following health check:
-
-```bash
-curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
-```
-
-would be like:
-
-```
-{"healthy":true,"message":"success"}
-```
-
-## Status
-
-On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
-will return a valid successful HTTP status code, and a `success` message. Ideally your
-uptime monitoring should look for the success message.
-
-[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
-[pingdom]: https://www.pingdom.com
-[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
-[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
+This document was moved to [user/admin_area/monitoring/health_check](../user/admin_area/monitoring/health_check.md).
diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md
index 771584268d9..19d46135930 100644
--- a/doc/monitoring/performance/gitlab_configuration.md
+++ b/doc/monitoring/performance/gitlab_configuration.md
@@ -1,40 +1 @@
-# GitLab Configuration
-
-GitLab Performance Monitoring is disabled by default. To enable it and change any of its
-settings, navigate to the Admin area in **Settings > Metrics**
-(`/admin/application_settings`).
-
-The minimum required settings you need to set are the InfluxDB host and port.
-Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
-changes.
-
----
-
-![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
-
----
-
-Finally, a restart of all GitLab processes is required for the changes to take
-effect:
-
-```bash
-# For Omnibus installations
-sudo gitlab-ctl restart
-
-# For installations from source
-sudo service gitlab restart
-```
-
-## Pending Migrations
-
-When any migrations are pending, the metrics are disabled until the migrations
-have been performed.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+This document was moved to [administration/monitoring/performance/gitlab_configuration](../../administration/monitoring/performance/gitlab_configuration.md).
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
index 7947b0fedc4..0d4be02ff5f 100644
--- a/doc/monitoring/performance/grafana_configuration.md
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -1,111 +1 @@
-# Grafana Configuration
-
-[Grafana](http://grafana.org/) is a tool that allows you to visualize time
-series metrics through graphs and dashboards. It supports several backend
-data stores, including InfluxDB. GitLab writes performance data to InfluxDB
-and Grafana will allow you to query InfluxDB to display useful graphs.
-
-For the easiest installation and configuration, install Grafana on the same
-server as InfluxDB. For larger installations, you may want to split out these
-services.
-
-## Installation
-
-Grafana supplies package repositories (Yum/Apt) for easy installation.
-See [Grafana installation documentation](http://docs.grafana.org/installation/)
-for detailed steps.
-
-> **Note**: Before starting Grafana for the first time, set the admin user
-and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
-will be `admin`.
-
-## Configuration
-
-Login as the admin user. Expand the menu by clicking the Grafana logo in the
-top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
-in the top bar.
-
-![Grafana empty data source page](img/grafana_data_source_empty.png)
-
-Fill in the configuration details for the InfluxDB data source. Save and
-Test Connection to ensure the configuration is correct.
-
-- **Name**: InfluxDB
-- **Default**: Checked
-- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
-- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
-on a separate server)
-- **Access**: proxy
-- **Database**: gitlab
-- **User**: admin (Or the username configured when setting up InfluxDB)
-- **Password**: The password configured when you set up InfluxDB
-
-![Grafana data source configurations](img/grafana_data_source_configuration.png)
-
-## Apply retention policies and create continuous queries
-
-If you intend to import the GitLab provided Grafana dashboards, you will need to
-set up the right retention policies and continuous queries. The easiest way of
-doing this is by using the [influxdb-management](https://gitlab.com/gitlab-org/influxdb-management)
-repository.
-
-To use this repository you must first clone it:
-
-```
-git clone https://gitlab.com/gitlab-org/influxdb-management.git
-cd influxdb-management
-```
-
-Next you must install the required dependencies:
-
-```
-gem install bundler
-bundle install
-```
-
-Now you must configure the repository by first copying `.env.example` to `.env`
-and then editing the `.env` file to contain the correct InfluxDB settings. Once
-configured you can simply run `bundle exec rake` and the InfluxDB database will
-be configured for you.
-
-For more information see the [influxdb-management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
-
-## Import Dashboards
-
-You can now import a set of default dashboards that will give you a good
-start on displaying useful information. GitLab has published a set of default
-[Grafana dashboards][grafana-dashboards] to get you started. Clone the
-repository or download a zip/tarball, then follow these steps to import each
-JSON file.
-
-Open the dashboard dropdown menu and click 'Import'
-
-![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png)
-
-Click 'Choose file' and browse to the location where you downloaded or cloned
-the dashboard repository. Pick one of the JSON files to import.
-
-![Grafana dashboard import](img/grafana_dashboard_import.png)
-
-Once the dashboard is imported, be sure to click save icon in the top bar. If
-you do not save the dashboard after importing it will be removed when you
-navigate away.
-
-![Grafana save icon](img/grafana_save_icon.png)
-
-Repeat this process for each dashboard you wish to import.
-
-Alternatively you can automatically import all the dashboards into your Grafana
-instance. See the README of the [Grafana dashboards][grafana-dashboards]
-repository for more information on this process.
-
-[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Installation/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
+This document was moved to [administration/monitoring/performance/grafana_configuration](../../administration/monitoring/performance/grafana_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md
index c30cd2950d8..15fd275e916 100644
--- a/doc/monitoring/performance/influxdb_configuration.md
+++ b/doc/monitoring/performance/influxdb_configuration.md
@@ -1,193 +1 @@
-# InfluxDB Configuration
-
-The default settings provided by [InfluxDB] are not sufficient for a high traffic
-GitLab environment. The settings discussed in this document are based on the
-settings GitLab uses for GitLab.com, depending on your own needs you may need to
-further adjust them.
-
-If you are intending to run InfluxDB on the same server as GitLab, make sure
-you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
-
-Unless you are going with a budget setup, it's advised to run it separately.
-
-## Requirements
-
-- InfluxDB 0.9.5 or newer
-- A fairly modern version of Linux
-- At least 4GB of RAM
-- At least 10GB of storage for InfluxDB data
-
-Note that the RAM and storage requirements can differ greatly depending on the
-amount of data received/stored. To limit the amount of stored data users can
-look into [InfluxDB Retention Policies][influxdb-retention].
-
-## Installation
-
-Installing InfluxDB is out of the scope of this document. Please refer to the
-[InfluxDB documentation].
-
-## InfluxDB Server Settings
-
-Since InfluxDB has many settings that users may wish to customize themselves
-(e.g. what port to run InfluxDB on), we'll only cover the essentials.
-
-The configuration file in question is usually located at
-`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
-InfluxDB needs to be restarted.
-
-### Storage Engine
-
-InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
-storage engine is available, called [TSM Tree]. All users **must** use the new
-`tsm1` storage engine as this [will be the default engine][tsm1-commit] in
-upcoming InfluxDB releases.
-
-Make sure you have the following in your configuration file:
-
-```
-[data]
- dir = "/var/lib/influxdb/data"
- engine = "tsm1"
-```
-
-### Admin Panel
-
-Production environments should have the InfluxDB admin panel **disabled**. This
-feature can be disabled by adding the following to your InfluxDB configuration
-file:
-
-```
-[admin]
- enabled = false
-```
-
-### HTTP
-
-HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana,
-thus it should be enabled. When enabling make sure to _also_ enable
-authentication:
-
-```
-[http]
- enabled = true
- auth-enabled = true
-```
-
-_**Note:** Before you enable authentication, you might want to [create an
-admin user](#create-a-new-admin-user)._
-
-### UDP
-
-GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
-UDP can be done using the following settings:
-
-```
-[[udp]]
- enabled = true
- bind-address = ":8089"
- database = "gitlab"
- batch-size = 1000
- batch-pending = 5
- batch-timeout = "1s"
- read-buffer = 209715200
-```
-
-This does the following:
-
-1. Enable UDP and bind it to port 8089 for all addresses.
-2. Store any data received in the "gitlab" database.
-3. Define a batch of points to be 1000 points in size and allow a maximum of
- 5 batches _or_ flush them automatically after 1 second.
-4. Define a UDP read buffer size of 200 MB.
-
-One of the most important settings here is the UDP read buffer size as if this
-value is set too low, packets will be dropped. You must also make sure the OS
-buffer size is set to the same value, the default value is almost never enough.
-
-To set the OS buffer size to 200 MB, on Linux you can run the following command:
-
-```bash
-sysctl -w net.core.rmem_max=209715200
-```
-
-To make this permanent, add the following to `/etc/sysctl.conf` and restart the
-server:
-
-```bash
-net.core.rmem_max=209715200
-```
-
-It is **very important** to make sure the buffer sizes are large enough to
-handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
-buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
-traffic, users may be able to use a smaller buffer size, but we highly recommend
-using _at least_ 100 MB.
-
-When enabling UDP, users should take care to not expose the port to the public,
-as doing so will allow anybody to write data into your InfluxDB database (as
-[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either
-whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
-allowing traffic from members of said VLAN.
-
-## Create a new admin user
-
-If you want to [enable authentication](#http), you might want to [create an
-admin user][influx-admin]:
-
-```
-influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
-```
-
-## Create the `gitlab` database
-
-Once you get InfluxDB up and running, you need to create a database for GitLab.
-Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
-before creating a database.
-
-_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
-[HTTP authentication](#http), remember to append the username (`-username <username>`)
-and password (`-password <password>`) you set earlier to the commands below._
-
-Run the following command to create a database named `gitlab`:
-
-```bash
-influx -execute 'CREATE DATABASE gitlab'
-```
-
-The name **must** be `gitlab`, do not use any other name.
-
-Next, make sure that the database was successfully created:
-
-```bash
-influx -execute 'SHOW DATABASES'
-```
-
-The output should be similar to:
-
-```
-name: databases
----------------
-name
-_internal
-gitlab
-```
-
-That's it! Now your GitLab instance should send data to InfluxDB.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
-
-[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
-[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
-[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/
-[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
-[influxdb]: https://influxdata.com/time-series-platform/influxdb/
-[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/
-[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d
-[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user
+This document was moved to [administration/monitoring/performance/influxdb_configuration](../../administration/monitoring/performance/influxdb_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index eff0e29f58d..e53f9701dc3 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -1,97 +1 @@
-# InfluxDB Schema
-
-The following measurements are currently stored in InfluxDB:
-
-- `PROCESS_file_descriptors`
-- `PROCESS_gc_statistics`
-- `PROCESS_memory_usage`
-- `PROCESS_method_calls`
-- `PROCESS_object_counts`
-- `PROCESS_transactions`
-- `PROCESS_views`
-- `events`
-
-Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
-process type. In all series, any form of duration is stored in milliseconds.
-
-## PROCESS_file_descriptors
-
-This measurement contains the number of open file descriptors over time. The
-value field `value` contains the number of descriptors.
-
-## PROCESS_gc_statistics
-
-This measurement contains Ruby garbage collection statistics such as the amount
-of minor/major GC runs (relative to the last sampling interval), the time spent
-in garbage collection cycles, and all fields/values returned by `GC.stat`.
-
-## PROCESS_memory_usage
-
-This measurement contains the process' memory usage (in bytes) over time. The
-value field `value` contains the number of bytes.
-
-## PROCESS_method_calls
-
-This measurement contains the methods called during a transaction along with
-their duration, and a name of the transaction action that invoked the method (if
-available). The method call duration is stored in the value field `duration`,
-while the method name is stored in the tag `method`. The tag `action` contains
-the full name of the transaction action. Both the `method` and `action` fields
-are in the following format:
-
-```
-ClassName#method_name
-```
-
-For example, a method called by the `show` method in the `UsersController` class
-would have `action` set to `UsersController#show`.
-
-## PROCESS_object_counts
-
-This measurement is used to store retained Ruby objects (per class) and the
-amount of retained objects. The number of objects is stored in the `count` value
-field while the class name is stored in the `type` tag.
-
-## PROCESS_transactions
-
-This measurement is used to store basic transaction details such as the time it
-took to complete a transaction, how much time was spent in SQL queries, etc. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration` | The total duration of the transaction |
-| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
-| `method_duration` | The total time spent in method calls |
-| `sql_duration` | The total time spent in SQL queries |
-| `view_duration` | The total time spent in views |
-
-## PROCESS_views
-
-This measurement is used to store view rendering timings for a transaction. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration` | The rendering time of the view |
-| `view` | The path of the view, relative to the application's root directory |
-
-The `action` tag contains the action name of the transaction that rendered the
-view.
-
-## events
-
-This measurement is used to store generic events such as the number of Git
-pushes, Emails sent, etc. Each point in this measurement has a single value
-field called `count`. The value of this field is simply set to `1`. Each point
-also has at least one tag: `event`. This tag's value is set to the event name.
-Depending on the event type additional tags may be available as well.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+This document was moved to [administration/monitoring/performance/influxdb_schema](../../administration/monitoring/performance/influxdb_schema.md).
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index 79904916b7e..ae88baa0c14 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -1,65 +1 @@
-# GitLab Performance Monitoring
-
-GitLab comes with its own application performance measuring system as of GitLab
-8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
-Community and Enterprise editions.
-
-Apart from this introduction, you are advised to read through the following
-documents in order to understand and properly configure GitLab Performance Monitoring:
-
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Install/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
-
-## Introduction to GitLab Performance Monitoring
-
-GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
-including (but not limited to):
-
-- The time it took to complete a transaction (a web request or Sidekiq job).
-- The time spent in running SQL queries and rendering HAML views.
-- The time spent executing (instrumented) Ruby methods.
-- Ruby object allocations, and retained objects in particular.
-- System statistics such as the process' memory usage and open file descriptors.
-- Ruby garbage collection statistics.
-
-Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
-data can be visualized using [Grafana][grafana] or any other application that
-supports reading data from InfluxDB. Alternatively data can be queried using the
-InfluxDB CLI.
-
-## Metric Types
-
-Two types of metrics are collected:
-
-1. Transaction specific metrics.
-1. Sampled metrics, collected at a certain interval in a separate thread.
-
-### Transaction Metrics
-
-Transaction metrics are metrics that can be associated with a single
-transaction. This includes statistics such as the transaction duration, timings
-of any executed SQL queries, time spent rendering HAML views, etc. These metrics
-are collected for every Rack request and Sidekiq job processed.
-
-### Sampled Metrics
-
-Sampled metrics are metrics that can't be associated with a single transaction.
-Examples include garbage collection statistics and retained Ruby objects. These
-metrics are collected at a regular interval. This interval is made up out of two
-parts:
-
-1. A user defined interval.
-1. A randomly generated offset added on top of the interval, the same offset
- can't be used twice in a row.
-
-The actual interval can be anywhere between a half of the defined interval and a
-half above the interval. For example, for a user defined interval of 15 seconds
-the actual interval can be anywhere between 7.5 and 22.5. The interval is
-re-generated for every sampling run instead of being generated once and re-used
-for the duration of the process' lifetime.
-
-[influxdb]: https://influxdata.com/time-series-platform/influxdb/
-[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
-[grafana]: http://grafana.org/
+This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/introduction.md).
diff --git a/doc/operations/README.md b/doc/operations/README.md
index 6a35dab7b6c..58f16aff7bd 100644
--- a/doc/operations/README.md
+++ b/doc/operations/README.md
@@ -1,5 +1 @@
-# GitLab operations
-
-- [Sidekiq MemoryKiller](sidekiq_memory_killer.md)
-- [Cleaning up Redis sessions](cleaning_up_redis_sessions.md)
-- [Understanding Unicorn and unicorn-worker-killer](unicorn.md)
+This document was moved to [administration/operations](../administration/operations.md).
diff --git a/doc/operations/cleaning_up_redis_sessions.md b/doc/operations/cleaning_up_redis_sessions.md
index 93521e976d5..2a1d0a8c8eb 100644
--- a/doc/operations/cleaning_up_redis_sessions.md
+++ b/doc/operations/cleaning_up_redis_sessions.md
@@ -1,52 +1 @@
-# Cleaning up stale Redis sessions
-
-Since version 6.2, GitLab stores web user sessions as key-value pairs in Redis.
-Prior to GitLab 7.3, user sessions did not automatically expire from Redis. If
-you have been running a large GitLab server (thousands of users) since before
-GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
-database after you upgrade to GitLab 7.3. You can also perform a cleanup while
-still running GitLab 7.2 or older, but in that case new stale sessions will
-start building up again after you clean up.
-
-In GitLab versions prior to 7.3.0, the session keys in Redis are 16-byte
-hexadecimal values such as '976aa289e2189b17d7ef525a6702ace9'. Starting with
-GitLab 7.3.0, the keys are
-prefixed with 'session:gitlab:', so they would look like
-'session:gitlab:976aa289e2189b17d7ef525a6702ace9'. Below we describe how to
-remove the keys in the old format.
-
-First we define a shell function with the proper Redis connection details.
-
-```
-rcli() {
- # This example works for Omnibus installations of GitLab 7.3 or newer. For an
- # installation from source you will have to change the socket path and the
- # path to redis-cli.
- sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@"
-}
-
-# test the new shell function; the response should be PONG
-rcli ping
-```
-
-Now we do a search to see if there are any session keys in the old format for
-us to clean up.
-
-```
-# returns the number of old-format session keys in Redis
-rcli keys '*' | grep '^[a-f0-9]\{32\}$' | wc -l
-```
-
-If the number is larger than zero, you can proceed to expire the keys from
-Redis. If the number is zero there is nothing to clean up.
-
-```
-# Tell Redis to expire each matched key after 600 seconds.
-rcli keys '*' | grep '^[a-f0-9]\{32\}$' | awk '{ print "expire", $0, 600 }' | rcli
-# This will print '(integer) 1' for each key that gets expired.
-```
-
-Over the next 15 minutes (10 minutes expiry time plus 5 minutes Redis
-background save interval) your Redis database will be compacted. If you are
-still using GitLab 7.2, users who are not clicking around in GitLab during the
-10 minute expiry window will be signed out of GitLab.
+This document was moved to [administration/operations/cleaning_up_redis_sessions](../administration/operations/cleaning_up_redis_sessions.md).
diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md
index 54adb99386a..c54bca324a5 100644
--- a/doc/operations/moving_repositories.md
+++ b/doc/operations/moving_repositories.md
@@ -1,180 +1 @@
-# Moving repositories managed by GitLab
-
-Sometimes you need to move all repositories managed by GitLab to
-another filesystem or another server. In this document we will look
-at some of the ways you can copy all your repositories from
-`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`.
-
-We will look at three scenarios: the target directory is empty, the
-target directory contains an outdated copy of the repositories, and
-how to deal with thousands of repositories.
-
-**Each of the approaches we list can/will overwrite data in the
-target directory `/mnt/gitlab/repositories`. Do not mix up the
-source and the target.**
-
-## Target directory is empty: use a tar pipe
-
-If the target directory `/mnt/gitlab/repositories` is empty the
-simplest thing to do is to use a tar pipe. This method has low
-overhead and tar is almost always already installed on your system.
-However, it is not possible to resume an interrupted tar pipe: if
-that happens then all data must be copied again.
-
-```
-# As the git user
-tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
- tar -C /mnt/gitlab/repositories -xf -
-```
-
-If you want to see progress, replace `-xf` with `-xvf`.
-
-### Tar pipe to another server
-
-You can also use a tar pipe to copy data to another server. If your
-'git' user has SSH access to the newserver as 'git@newserver', you
-can pipe the data through SSH.
-
-```
-# As the git user
-tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
- ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
-```
-
-If you want to compress the data before it goes over the network
-(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`.
-
-## The target directory contains an outdated copy of the repositories: use rsync
-
-If the target directory already contains a partial / outdated copy
-of the repositories it may be wasteful to copy all the data again
-with tar. In this scenario it is better to use rsync. This utility
-is either already installed on your system or easily installable
-via apt, yum etc.
-
-```
-# As the 'git' user
-rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
- /mnt/gitlab/repositories
-```
-
-The `/.` in the command above is very important, without it you can
-easily get the wrong directory structure in the target directory.
-If you want to see progress, replace `-a` with `-av`.
-
-### Single rsync to another server
-
-If the 'git' user on your source system has SSH access to the target
-server you can send the repositories over the network with rsync.
-
-```
-# As the 'git' user
-rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
- git@newserver:/mnt/gitlab/repositories
-```
-
-## Thousands of Git repositories: use one rsync per repository
-
-Every time you start an rsync job it has to inspect all files in
-the source directory, all files in the target directory, and then
-decide what files to copy or not. If the source or target directory
-has many contents this startup phase of rsync can become a burden
-for your GitLab server. In cases like this you can make rsync's
-life easier by dividing its work in smaller pieces, and sync one
-repository at a time.
-
-In addition to rsync we will use [GNU
-Parallel](http://www.gnu.org/software/parallel/). This utility is
-not included in GitLab so you need to install it yourself with apt
-or yum. Also note that the GitLab scripts we used below were added
-in GitLab 8.1.
-
-** This process does not clean up repositories at the target location that no
-longer exist at the source. ** If you start using your GitLab instance with
-`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos`
-after switching to the new repository storage directory.
-
-### Parallel rsync for all repositories known to GitLab
-
-This will sync repositories with 10 rsync processes at a time. We keep
-track of progress so that the transfer can be restarted if necessary.
-
-First we create a new directory, owned by 'git', to hold transfer
-logs. We assume the directory is empty before we start the transfer
-procedure, and that we are the only ones writing files in it.
-
-```
-# Omnibus
-sudo mkdir /var/opt/gitlab/transfer-logs
-sudo chown git:git /var/opt/gitlab/transfer-logs
-
-# Source
-sudo -u git -H mkdir /home/git/transfer-logs
-```
-
-We seed the process with a list of the directories we want to copy.
-
-```
-# Omnibus
-sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt'
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt'
-```
-
-Now we can start the transfer. The command below is idempotent, and
-the number of jobs done by GNU Parallel should converge to zero. If it
-does not some repositories listed in all-repos-1234.txt may have been
-deleted/renamed before they could be copied.
-
-```
-# Omnibus
-sudo -u git sh -c '
-cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\
- /usr/bin/env JOBS=10 \
- /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
- /var/opt/gitlab/transfer-logs/success-$(date +%s).log \
- /var/opt/gitlab/git-data/repositories \
- /mnt/gitlab/repositories
-'
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H sh -c '
-cat /home/git/transfer-logs/* | sort | uniq -u |\
- /usr/bin/env JOBS=10 \
- bin/parallel-rsync-repos \
- /home/git/transfer-logs/success-$(date +%s).log \
- /home/git/repositories \
- /mnt/gitlab/repositories
-`
-```
-
-### Parallel rsync only for repositories with recent activity
-
-Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
-Then you might only want to sync repositories that were changed via GitLab
-_after_ that time. You can use the 'SINCE' variable to tell 'rake
-gitlab:list_repos' to only print repositories with recent activity.
-
-```
-# Omnibus
-sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
- sudo -u git \
- /usr/bin/env JOBS=10 \
- /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
- success-$(date +%s).log \
- /var/opt/gitlab/git-data/repositories \
- /mnt/gitlab/repositories
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
- sudo -u git -H \
- /usr/bin/env JOBS=10 \
- bin/parallel-rsync-repos \
- success-$(date +%s).log \
- /home/git/repositories \
- /mnt/gitlab/repositories
-```
+This document was moved to [administration/operations/moving_repositories](../administration/operations/moving_repositories.md).
diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md
index b5e78348989..cf7c3b2e2ed 100644
--- a/doc/operations/sidekiq_memory_killer.md
+++ b/doc/operations/sidekiq_memory_killer.md
@@ -1,40 +1 @@
-# Sidekiq MemoryKiller
-
-The GitLab Rails application code suffers from memory leaks. For web requests
-this problem is made manageable using
-[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which
-restarts Unicorn worker processes in between requests when needed. The Sidekiq
-MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
-to process background jobs.
-
-Unlike unicorn-worker-killer, which is enabled by default for all GitLab
-installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default
-_only_ for Omnibus packages. The reason for this is that the MemoryKiller
-relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab
-installations from source do not all use Runit or an equivalent.
-
-With the default settings, the MemoryKiller will cause a Sidekiq restart no
-more often than once every 15 minutes, with the restart causing about one
-minute of delay for incoming background jobs.
-
-## Configuring the MemoryKiller
-
-The MemoryKiller is controlled using environment variables.
-
-- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is
- greater than 0, then after each Sidekiq job, the MemoryKiller will check the
- RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq
- process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a
- delayed shutdown is triggered. The default value for Omnibus packages is set
- [in the omnibus-gitlab
- repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
- a shutdown is triggered, the Sidekiq process will keep working normally for
- another 15 minutes.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
- time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs.
- Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
- Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
- restart Sidekiq.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
- the final signal sent to the Sidekiq process when we want it to shut down.
+This document was moved to [administration/operations/sidekiq_memory_killer](../administration/operations/sidekiq_memory_killer.md).
diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md
index bad61151bda..fbc9697b755 100644
--- a/doc/operations/unicorn.md
+++ b/doc/operations/unicorn.md
@@ -1,86 +1 @@
-# Understanding Unicorn and unicorn-worker-killer
-
-## Unicorn
-
-GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web
-server, to handle web requests (web browsers and Git HTTP clients). Unicorn is
-a daemon written in Ruby and C that can load and run a Ruby on Rails
-application; in our case the Rails application is GitLab Community Edition or
-GitLab Enterprise Edition.
-
-Unicorn has a multi-process architecture to make better use of available CPU
-cores (processes can run on different cores) and to have stronger fault
-tolerance (most failures stay isolated in only one process and cannot take down
-GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby
-environment with the GitLab application code, and then spawns 'workers' which
-inherit this clean initial environment. The 'master' never handles any
-requests, that is left to the workers. The operating system network stack
-queues incoming requests and distributes them among the workers.
-
-In a perfect world, the master would spawn its pool of workers once, and then
-the workers handle incoming web requests one after another until the end of
-time. In reality, worker processes can crash or time out: if the master notices
-that a worker takes too long to handle a request it will terminate the worker
-process with SIGKILL ('kill -9'). No matter how the worker process ended, the
-master process will replace it with a new 'clean' process again. Unicorn is
-designed to be able to replace 'crashed' workers without dropping user
-requests.
-
-This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The
-master process has PID 56227 below.
-
-```
-[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing
-[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10
-[2015-06-05T10:58:08.708141 #62538] INFO -- : worker=10 spawned pid=62538
-[2015-06-05T10:58:08.708824 #62538] INFO -- : worker=10 ready
-```
-
-### Tunables
-
-The main tunables for Unicorn are the number of worker processes and the
-request timeout after which the Unicorn master terminates a worker process.
-See the [omnibus-gitlab Unicorn settings
-documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md)
-if you want to adjust these settings.
-
-## unicorn-worker-killer
-
-GitLab has memory leaks. These memory leaks manifest themselves in long-running
-processes, such as Unicorn workers. (The Unicorn master process is not known to
-leak memory, probably because it does not handle user requests.)
-
-To make these memory leaks manageable, GitLab comes with the
-[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
-gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
-workers to do a memory self-check after every 16 requests. If the memory of the
-Unicorn worker exceeds a pre-set limit then the worker process exits. The
-Unicorn master then automatically replaces the worker process.
-
-This is a robust way to handle memory leaks: Unicorn is designed to handle
-workers that 'crash' so no user requests will be dropped. The
-unicorn-worker-killer gem is designed to only terminate a worker process _in
-between requests_, so no user requests are affected.
-
-This is what a Unicorn worker memory restart looks like in unicorn_stderr.log.
-You see that worker 4 (PID 125918) is inspecting itself and decides to exit.
-The threshold memory value was 254802235 bytes, about 250MB. With GitLab this
-threshold is a random value between 200 and 250 MB. The master process (PID
-117565) then reaps the worker process and spawns a new 'worker 4' with PID
-127549.
-
-```
-[2015-06-05T12:07:41.828374 #125918] WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes)
-[2015-06-05T12:07:41.828472 #125918] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1)
-[2015-06-05T12:07:42.025916 #117565] INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4
-[2015-06-05T12:07:42.034527 #127549] INFO -- : worker=4 spawned pid=127549
-[2015-06-05T12:07:42.035217 #127549] INFO -- : worker=4 ready
-```
-
-One other thing that stands out in the log snippet above, taken from
-GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
-is a normal value for our current GitLab.com setup and traffic.
-
-The high frequency of Unicorn memory restarts on some GitLab sites can be a
-source of confusion for administrators. Usually they are a [red
-herring](https://en.wikipedia.org/wiki/Red_herring).
+This document was moved to [administration/operations/unicorn](../administration/operations/unicorn.md).
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
index 88943dc410e..440728795be 100644
--- a/doc/project_services/img/builds_emails_service.png
+++ b/doc/project_services/img/builds_emails_service.png
Binary files differ
diff --git a/doc/raketasks/backup_hrz.png b/doc/raketasks/backup_hrz.png
index 42084717ebe..287587609a1 100644
--- a/doc/raketasks/backup_hrz.png
+++ b/doc/raketasks/backup_hrz.png
Binary files differ
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 3f4056dc440..fc0cd1b8af2 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -2,34 +2,51 @@
![backup banner](backup_hrz.png)
-## Create a backup of the GitLab system
-
-A backup creates an archive file that contains the database, all repositories and all attachments.
-This archive will be saved in backup_path (see `config/gitlab.yml`).
-The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup.
-You can only restore a backup to exactly the same version of GitLab that you created it
-on, for example 7.2.1. The best way to migrate your repositories from one server to
+An application data backup creates an archive file that contains the database,
+all repositories and all attachments.
+This archive will be saved in `backup_path`, which is specified in the
+`config/gitlab.yml` file.
+The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
+identifies the time at which each backup was created.
+
+You can only restore a backup to exactly the same version of GitLab on which it
+was created. The best way to migrate your repositories from one server to
another is through backup restore.
-You need to keep separate copies of `/etc/gitlab/gitlab-secrets.json` and
-`/etc/gitlab/gitlab.rb` (for omnibus packages) or
-`/home/git/gitlab/config/secrets.yml` (for installations from source). This file
-contains the database encryption keys used for two-factor authentication and CI
-secret variables, among other things. If you restore a GitLab backup without
-restoring the database encryption key, users who have two-factor authentication
-enabled will lose access to your GitLab server.
+To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
+(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
+from source). This file contains the database encryption key and CI secret
+variables used for two-factor authentication. If you fail to restore this
+encryption key file along with the application data backup, users with two-factor
+authentication enabled will lose access to your GitLab server.
+
+## Create a backup of the GitLab system
+Use this command if you've installed GitLab with the Omnibus package:
```
-# use this command if you've installed GitLab with the Omnibus package
sudo gitlab-rake gitlab:backup:create
-
-# if you've installed GitLab from source
+```
+Use this if you've installed GitLab from source:
+```
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
+If you are running GitLab within a Docker container, you can run the backup from the host:
+```
+docker -t exec <container name> gitlab-rake gitlab:backup:create
+```
+
+You can specify that portions of the application data be skipped using the
+environment variable `SKIP`. You can skip:
+
+- `db` (database)
+- `uploads` (attachments)
+- `repositories` (Git repositories data)
+- `builds` (CI build output logs)
+- `artifacts` (CI build artifacts)
+- `lfs` (LFS objects)
+- `registry` (Container Registry images)
-Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
-uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts), lfs (LFS objects).
-Use a comma to specify several options at the same time.
+Separate multiple data types to skip using a comma. For example:
```
sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
@@ -69,7 +86,7 @@ Deleting old backups... [SKIPPING]
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage.
-But Fog also lets you use [other storage providers](http://fog.io/storage/).
+Fog also supports [other storage providers](http://fog.io/storage/).
For omnibus packages:
@@ -161,7 +178,7 @@ with the name of your bucket:
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
-using the [`Local`](https://github.com/fog/fog-local#usage) storage provider.
+using the Fog [`Local`](https://github.com/fog/fog-local#usage) storage provider.
The directory pointed to by the `local_root` key **must** be owned by the `git`
user **when mounted** (mounting with the `uid=` of the `git` user for `CIFS` and
`SMB`) or the user that you are executing the backup tasks under (for omnibus
@@ -228,7 +245,7 @@ of using encryption in the first place!
If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration).
If you have a cookbook installation there should be a copy of your configuration in Chef.
-If you have an installation from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+If you installed from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 8a5e2d6e16b..044b104f5c2 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -70,3 +70,18 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
# installation from source
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
+
+## Clear authentication tokens for all users. Important! Data loss!
+
+Clear authentication tokens for all users in the GitLab database. This
+task is useful if your users' authentication tokens might have been exposed in
+any way. All the existing tokens will become invalid, and new tokens are
+automatically generated upon sign-in or user modification.
+
+```
+# omnibus-gitlab
+sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
+
+# installation from source
+bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
+```
diff --git a/doc/university/README.md b/doc/university/README.md
index 6ca1c20c9b2..f5a0dab39fe 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -1,139 +1,216 @@
+# GitLab University
-## What is GitLab University
+GitLab University is the best place to learn about **Version Control with Git and GitLab**.
-_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.
+It doesn't replace, but accompanies our great [Documentation](http://docs.gitlab.com)
+and [Blog Articles](https://about.gitlab.com/blog/).
-_University materials don't replace our [Documentation](http://docs.gitlab.com) or [Blog Articles](https://about.gitlab.com/blog/)._
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information.
----
+## Gitlab University Curriculum
+
+The curriculum is composed of GitLab videos, screencasts, presentations, projects and external GitLab content hosted on other services and has been organized into the following sections.
-### 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
+1. [GitLab Beginner](#beginner)
+1. [GitLab Intermediate](#intermediate)
+1. [GitLab Advanced](#advanced)
+1. [External Articles](#external)
+1. [Resources for GitLab Team Members](#team)
---
-+ [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)
+### 1. <a name="beginner"></a> GitLab Beginner
----
+#### 1.1. Version Control and 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)
+1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
+1. [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
+1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
----
+#### 1.2. GitLab Basics
-+ [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)
+1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4)
+1. [Why Use Git and GitLab - Slides](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/edit?usp=drive_web)
+1. [GitLab Basics - Article](http://doc.gitlab.com/ce/gitlab-basics/README.html)
+1. [Git and GitLab Basics - Video](https://www.youtube.com/watch?v=03wb9FvO4Ak&index=5&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-23370/material/)
+1. [Comparison of GitLab Versions](https://about.gitlab.com/features/#compare)
----
+#### 1.3. Your GitLab Account
-+ [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)
+1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
+1. [Create and Add your SSH key to GitLab - Video](https://www.youtube.com/watch?v=54mxyLo3Mqk)
----
+#### 1.4. GitLab Projects
-+ [GLF1] [GitLab Flow](https://www.youtube.com/watch?v=UGotqAUACZA)
+1. [Repositories, Projects and Groups - Video](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Creating a Project in GitLab - Video](https://www.youtube.com/watch?v=7p0hrpNaJ14)
+1. [How to Create Files and Directories](https://about.gitlab.com/2016/02/10/feature-highlight-create-files-and-directories-from-files-page/)
+1. [GitLab Todos](https://about.gitlab.com/2016/03/02/gitlab-todos-feature-highlight/)
+1. [GitLab's Work in Progress (WIP) Flag](https://about.gitlab.com/2016/01/08/feature-highlight-wip/)
----
+#### 1.5. Migrating from other Source Control
-+ [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)
+1. [Migrating from BitBucket/Stash](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
+1. [Migrating from GitHub](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
+1. [Migrating from SVN](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
+1. [Migrating from Fogbugz](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
----
+#### 1.6. GitLab Inc.
-+ [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/)
+1. [About GitLab](https://about.gitlab.com/about/)
+1. [GitLab Direction](https://about.gitlab.com/direction/)
+1. [GitLab Master Plan](https://about.gitlab.com/2016/09/13/gitlab-master-plan/)
+1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter
+1. [Using Innersourcing to Improve Collaboration](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
+1. [The Software Development Market and GitLab - Video](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6) - [Slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit)
+1. [The GitLab Book Club](bookclub/index.md)
----
+#### 1.7 Community and Support
-+ [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/)
+1. [Getting Help](https://about.gitlab.com/getting-help/)
+ - Proposing Features and Reporting and Tracking bugs for GitLab
+ - The GitLab IRC channel, Gitter Chat Room, Community Forum and Mailing List
+ - Getting Technical Support
+ - Being part of our Great Community and Contributing to GitLab
+1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
+1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
+1. [GitLab Training Workshops](https://about.gitlab.com/training)
----
+#### 1.8 GitLab Training Material
-+ [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/)
+1. [Git and GitLab Terminology](glossary/README.md)
+1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web)
+1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user)
---
-+ [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)
+### 2. <a name="intermediate"></a> GitLab Intermediate
+
+#### 2.1 GitLab Pages
+
+1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+1. [GitLab Pages Documentation](http://doc.gitlab.com/ee/pages/README.html)
+
+#### 2.2. GitLab Issues
+
+1. [Markdown in GitLab](http://doc.gitlab.com/ce/markdown/markdown.html)
+1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
+1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
+1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
+1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
+1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
+1. [An Overview of GitLab Issue Board](https://about.gitlab.com/2016/08/22/announcing-the-gitlab-issue-board/)
+1. [Designing GitLab Issue Board](https://about.gitlab.com/2016/08/31/designing-issue-boards/)
+1. [From Idea to Production with GitLab - Video](https://www.youtube.com/watch?v=25pHyknRgEo&index=14&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+
+#### 2.3. Continuous Integration
+
+1. [Operating Systems, Servers, VMs, Containers and Unix - Video](https://www.youtube.com/watch?v=V61kL6IC-zY&index=8&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [GitLab CI - Product Page](https://about.gitlab.com/gitlab-ci/)
+1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
+1. [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
+1. [GitLab and Docker - Video](https://www.youtube.com/watch?v=ugOrCcbdHko&index=12&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [How we scale GitLab with built in Docker](https://about.gitlab.com/2016/06/21/how-we-scale-gitlab-by-having-docker-built-in/)
+1. [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+1. [Deployments and Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+1. [Sequential, Parallel or Custom Pipelines](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+1. [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+1. [Setting up GitLab Runner on DigitalOcean](https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/)
+1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
+1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
+1. See **[Integrations](#integrations)** for integrations with other CI services.
+
+#### 2.4. Workflow
+
+1. [GitLab Flow - Video](https://youtu.be/enMumwvLAug?list=PLFGfElNsQthZnwMUFi6rqkyUZkI00OxIV)
+1. [GitLab Flow vs Forking in GitLab - Video](https://www.youtube.com/watch?v=UGotqAUACZA)
+1. [GitLab Flow Overview](https://about.gitlab.com/2014/09/29/gitlab-flow/)
+1. [Always Start with an Issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)
+1. [GitLab Flow Documentation](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
+
+#### 2.5. GitLab Comparisons
+
+1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
+1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
+1. [GitLab Compared to Atlassian (Recording 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
+1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
+1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
---
-+ [SLS1] [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
-+ [SLS2] [GitLab Direction](https://about.gitlab.com/direction/)
+### 3. <a name="advanced"></a> GitLab Advanced
+
+#### 3.1. Dev Ops
+
+1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
+1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
+1. [Puppet Labs: State of Dev Ops 2015 - Book](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf)
+
+#### 3.2. Installing GitLab with Omnibus
+
+1. [What is Omnibus - Video](https://www.youtube.com/watch?v=XTmpKudd-Oo)
+1. [How to Install GitLab with Omnibus - Video](https://www.youtube.com/watch?v=Q69YaOjqNhg)
+1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-3/material/)
+1. [Using a Non-Packaged PostgreSQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
+1. [Using a MySQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-mysql-database-management-server-enterprise-edition-only)
+1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
+1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
+
+#### 3.3. Permissions
+
+1. [How to Manage Permissions in GitLab EE - Video](https://www.youtube.com/watch?v=DjUoIrkiNuM)
+
+#### 3.4. Large Files
+
+1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
+
+#### 3.5. LDAP and Active Directory
+
+1. [How to Manage LDAP, Active Directory in GitLab - Video](https://www.youtube.com/watch?v=HPMjM-14qa8)
+
+#### 3.6 Custom Languages
+
+1. [How to add Syntax Highlighting Support for Custom Langauges to GitLab - Video](how to add support for your favorite language to GitLab)
+
+#### 3.7. Scalability and High Availability
+
+1. [Scalability and High Availability - Video](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
+1. [High Availability - Video](https://www.youtube.com/watch?v=36KS808u6bE&index=15&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [High Availability Documentation](https://about.gitlab.com/high-availability/)
+
+#### 3.8 Cycle Analytics
+
+1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/)
+
+#### 3.9. <a name="integrations"></a> Integrations
+
+1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
+1. [How to Integrate Jira with GitLab](http://doc.gitlab.com/ee/integration/jira.html)
+1. [How to Integrate Jenkins with GitLab](http://doc.gitlab.com/ee/integration/jenkins.html)
+1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
+1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
+1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
---
-+ [TRA1] [End User Training](training/end-user/README.md)
+## 4. <a name="external"></a> External Articles
+
+1. [2011 WSJ article by Mark Andreeson - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
+1. [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/)
---
-### 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)
+## 5. <a name="team"></a> Resources for GitLab Team Members
+
+*Some content can only be accessed by GitLab team members*
+
+1. [Support Path](support/README.md)
+1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
+1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
+1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
diff --git a/doc/university/bookclub/booklist.md b/doc/university/bookclub/booklist.md
new file mode 100644
index 00000000000..c4229832e9f
--- /dev/null
+++ b/doc/university/bookclub/booklist.md
@@ -0,0 +1,113 @@
+# Books
+
+List of books and resources, that may be worth reading.
+
+## Papers
+
+1. **The Humble Programmer**
+
+ Edsger W. Dijkstra, 1972 ([paper](http://dl.acm.org/citation.cfm?id=361591))
+
+## Programming
+
+1. **Design Patterns: Elements of Reusable Object-Oriented Software**
+
+ Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994 ([amazon](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612))
+
+1. **Clean Code: A Handbook of Agile Software Craftsmanship**
+
+ Robert C. "Uncle Bob" Martin, 2008 ([amazon](http://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882))
+
+1. **Code Complete: A Practical Handbook of Software Construction**, 2nd Edition
+
+ Steve McConnell, 2004 ([amazon](http://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670))
+
+1. **The Pragmatic Programmer: From Journeyman to Master**
+
+ Andrew Hunt, David Thomas, 1999 ([amazon](http://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X))
+
+1. **Working Effectively with Legacy Code**
+
+ Michael Feathers, 2004 ([amazon](http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052))
+
+1. **Eloquent Ruby**
+
+ Russ Olsen, 2011 ([amazon](http://www.amazon.com/Eloquent-Ruby-Addison-Wesley-Professional/dp/0321584104))
+
+1. **Domain-Driven Design: Tackling Complexity in the Heart of Software**
+
+ Eric Evans, 2003 ([amazon](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215))
+
+1. **How to Solve It: A New Aspect of Mathematical Method**
+
+ Polya G. 1957 ([amazon](http://www.amazon.com/How-Solve-Mathematical-Princeton-Science/dp/069116407X))
+
+1. **Software Creativity 2.0**
+
+ Robert L. Glass, 2006 ([amazon](http://www.amazon.com/Software-Creativity-2-0-Robert-Glass/dp/0977213315))
+
+1. **Object-Oriented Software Construction**
+
+ Bertrand Meyer, 1997 ([amazon](http://www.amazon.com/Object-Oriented-Software-Construction-Book-CD-ROM/dp/0136291554))
+
+1. **Refactoring: Improving the Design of Existing Code**
+
+ Martin Fowler, Kent Beck, 1999 ([amazon](http://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672))
+
+1. **Test Driven Development: By Example**
+
+ Kent Beck, 2002 ([amazon](http://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530))
+
+1. **Algorithms in C++: Fundamentals, Data Structure, Sorting, Searching**
+
+ Robert Sedgewick, 1990 ([amazon](http://www.amazon.com/Algorithms-Parts-1-4-Fundamentals-Structure/dp/0201350882))
+
+1. **Effective C++**
+
+ Scott Mayers, 1996 ([amazon](http://www.amazon.com/Effective-Specific-Improve-Programs-Designs/dp/0321334876))
+
+1. **Extreme Programming Explained: Embrace Change**
+
+ Kent Beck, 1999 ([amazon](http://www.amazon.com/Extreme-Programming-Explained-Embrace-Change/dp/0321278658))
+
+1. **The Art of Computer Programming**
+
+ Donald E. Knuth, 1997 ([amazon](http://www.amazon.com/Computer-Programming-Volumes-1-4A-Boxed/dp/0321751043))
+
+1. **Writing Efficient Programs**
+
+ Jon Louis Bentley, 1982 ([amazon](http://www.amazon.com/Writing-Efficient-Programs-Prentice-Hall-Software/dp/013970244X))
+
+1. **The Mythical Man-Month: Essays on Software Engineering**
+
+ Frederick Phillips Brooks, 1975 ([amazon](http://www.amazon.com/Mythical-Man-Month-Essays-Software-Engineering/dp/0201006502))
+
+1. **Peopleware: Productive Projects and Teams** 3rd Edition
+
+ Tom DeMarco, Tim Lister, 2013 ([amazon](http://www.amazon.com/Peopleware-Productive-Projects-Teams-3rd/dp/0321934113))
+
+1. **Principles Of Software Engineering Management**
+
+ Tom Gilb, 1988 ([amazon](http://www.amazon.com/Principles-Software-Engineering-Management-Gilb/dp/0201192462))
+
+## Other
+
+1. **Thinking, Fast and Slow**
+
+ Daniel Kahneman, 2013 ([amazon](http://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555))
+
+1. **The Social Animal** 11th Edition
+
+ Elliot Aronson, 2011 ([amazon](http://www.amazon.com/Social-Animal-Elliot-Aronson/dp/1429233419))
+
+1. **Influence: Science and Practice** 5th Edition
+
+ Robert B. Cialdini, 2008 ([amazon](http://www.amazon.com/Influence-Practice-Robert-B-Cialdini/dp/0205609996))
+
+1. **Getting to Yes: Negotiating Agreement Without Giving In**
+
+ Roger Fisher, William L. Ury, Bruce Patton, 2011 ([amazon](http://www.amazon.com/Getting-Yes-Negotiating-Agreement-Without/dp/0143118757))
+
+1. **How to Win Friends & Influence People**
+
+ Dale Carnegie, 1981 ([amazon](http://www.amazon.com/How-Win-Friends-Influence-People/dp/0671027034))
diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md
new file mode 100644
index 00000000000..022a61f4429
--- /dev/null
+++ b/doc/university/bookclub/index.md
@@ -0,0 +1,19 @@
+# The GitLab Book Club
+
+The Book Club is a casual meet-up to read and discuss books we like.
+We'll find a time that suits most, if not all.
+
+See the [book list](booklist.md) for additional recommendations.
+
+## Currently reading : Books about remote work
+
+1. **Remote: Office not required**
+
+ David Heinemeier Hansson and Jason Fried, 2013
+ ([amazon](http://www.amazon.co.uk/Remote-Required-David-Heinemeier-Hansson/dp/0091954673))
+
+1. **The Year Without Pants**
+
+ Scott Berkun, 2013 ([ScottBerkun.com](http://scottberkun.com/yearwithoutpants/))
+
+Any other books you'd like to suggest? Edit this page and add them to the queue.
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index a86ff165f2e..cf836667fac 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -6,83 +6,87 @@ 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.
+User authentication by combination of 2 different steps during login. This allows for [more security](https://about.gitlab.com/handbook/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)
+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
+A Microsoft-based [directory service](https://msdn.microsoft.com/en-us/library/bb742424.aspx) 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
+Building and [delivering software](http://agilemethodology.org/) in phases/parts rather than trying to build everything at once then delivering to the user/client. The latter is known as the WaterFall model.
### Application Lifecycle Management (ALM)
-Entire product lifecycle management process for an application. From requirements management, development and testing until deployment.
+The entire product lifecycle management process for an application, from requirements management, development, and testing until deployment. GitLab has [advantages](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit#slide=id.g72f2e4906_2_288) over both legacy and modern ALM tools.
### Artifactory
-Version control for binaries.
+A version control [system](https://www.jfrog.com/open-source/#os-arti) for non-text files.
### Artifacts
-objects (usually binary and large) created by a build process
+Objects (usually binary and large) created by a build process. These can include use cases, class diagrams, requirements and design documents.
### Atlassian
-A company that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo. See [Atlassian] (https://www.atlassian.com)
+A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo.
### Audit Log
-*** Needs definition here
+Also called an [audit trail](https://en.wikipedia.org/wiki/Audit_trail), an audit log is a document that records an event in an IT system.
### 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...
+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
+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/)
+Entry level [subscription](https://about.gitlab.com/pricing/) for GitLab EE currently available in packs of 10.
### Bitbucket
-Atlassian's web hosting service for Git and Mercurial Projects i.e. GitLab.com competitor
+Atlassian's web hosting service for Git and Mercurial Projects. Read about [migrating](https://docs.gitlab.com/ce/workflow/importing/import_projects_from_bitbucket.html) from BitBucket to a GitLab instance.
### 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".
+A branch is a parallel version of a repository. This allows you to work on the repository without affecting the "master" branch, and without affecting the current "live" version. When you have made all your changes to your branch you can then merge to the master. When your merge request is accepted your changes will be "live."
### Branded Login
-Having your own logo on your GitLab instance login page instead of the GitLab logo.
+Having your own logo on [your GitLab instance login page](https://docs.gitlab.com/ee/customization/branded_login_page.html) instead of the GitLab logo.
+
+### Build triggers
+These protect your code base against breaks, for instance when a team is working on the same project. Learn about [setting up](https://docs.gitlab.com/ce/ci/triggers/README.html) build triggers.
### CEPH
-is a distributed object store and file system designed to provide excellent performance, reliability and scalability.
+ A distributed object store and file [system](http://ceph.com/) designed to provide excellent performance, reliability and scalability.
+
+### ChatOps
+
+The ability to [initiate an action](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/1412) from chat. ChatBots run in your chat application and give you the ability to do "anything" from chat.
### 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.
+A [copy](https://git-scm.com/docs/git-clone) 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.
+Examination of a progam's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab.
### Code Snippet
-A small amount of code. Usually for the purpose of showing other developers how
-to do something specific or reproduce a problem.
+A small amount of code, usually selected for the purpose of showing other developers how to do something specific or reproduce a problem.
### Collaborator
@@ -90,31 +94,39 @@ Person with read and write access to a repository who has been invited by reposi
### 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.
+A [change](https://git-scm.com/docs/git-commit) (revision) to a file that also creates an ID, allowing you to see revision history and the author of the changes.
### Community
-Everyone who is using GitLab
+[Everyone](https://about.gitlab.com/community/) who uses GitLab.
### Confluence
-Atlassian's product for collaboration of documents and projects.
+Atlassian's product for collaboration on documents and projects.
-### Continuous Deivery
+### Continuous Delivery
-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.
+A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually.
### Continuous Deployment
-Continuous deployment is the next step of continuous delivery: Every change that passes the automated tests is deployed to production automatically.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all.
### 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.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day.
### Contributor
-Term used to a person contributing to an Open Source Project.
+Term used for a person contributing to an open source project.
+
+### Conversational Development (ConvDev)
+
+A [natural evolution](https://about.gitlab.com/2016/09/14/gitlab-live-event-recap/) of software development that carries a conversation across functional groups throughout the development process, enabling developers to track the full path of development in a cohesive and intuitive way. ConvDev accelerates the development lifecycle by fostering collaboration and knowledge sharing from idea to production.
+
+### Cycle Time
+
+The time it takes to move from [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab).
### Data Centre
@@ -122,41 +134,59 @@ 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.
+A [SSH key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html)stored on 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.
+For us at GitLab, this means a software developer, or someone who makes software. It is also one of the levels of access in our multi-level approval system.
+
+### DevOps
+
+The intersection of software engineering, quality assurance, and technology operations. Explore more DevOps topics in the [glossary by XebiaLabs](https://xebialabs.com/glossary/)
### Diff
-Is the difference between two commits, or saved changes. This will also be shown visually after the changes.
+The difference between two commits, or saved changes. This will also be shown visually after the changes.
-### Docker
+#### Directory
-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.
+A folder used for storing multiple files.
+
+### Docker Container Registry
+
+A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of GitLab projects. 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.
+
+### Dynamic Environment
+
+### ElasticSearch
+
+Elasticsearch is a flexible, scalable and powerful search service. When [enabled](https://gitlab.com/help/integration/elasticsearch.md), it helps keep GitLab's search fast when dealing with a huge amount of data.
+
+### Emacs
### Fork
-Your own copy of a repository that allows you to make changes to the repository without affecting the original.
+Your [own copy](https://docs.gitlab.com/ce/workflow/forking_workflow.html) 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.
+A code review [tool](https://www.gerritcodereview.com/) built on top of Git.
+
+### Git Attributes
+
+A [git attributes file](https://git-scm.com/docs/gitattributes) is a simple text file that gives attributes to pathnames.
### Git Hooks
-Are scripts you can use to trigger actions at certain points.
+[Scripts](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) 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.
+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.
+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. It offers free public repos, private repos and enterprise services are paid. Read about [importing a project](https://docs.gitlab.com/ce/workflow/importing/import_projects_from_github.html) from GitHub to GitLab.
### GitLab CE
@@ -164,51 +194,78 @@ Our free on Premise solution with >100,000 users
### GitLab CI
-Our own Continuos Integration feature that is shipped with each instance
+Our own Continuos Integration [feature](https://about.gitlab.com/gitlab-ci/) 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.
+Our premium on premise [solution](https://about.gitlab.com/features/#enterprise) that currently has Basic, Standard and Plus subscription packages with additional features and support.
### GitLab.com
Our free SaaS for public and private repositories.
+### GitLab Geo
+
+Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/gitlab-geo/README.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
+
+### GitLab Pages
+These allow you to [create websites](https://gitlab.com/help/pages/README.md) for your GitLab projects, groups, or user account.
+
### 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.
+An [access layer](https://git-scm.com/book/en/v1/Git-on-the-Server-Gitolite) that sits on top of Git. Users are granted access to repos via a simple config file. As an admin, you only need the users' public SSH key and a username.
### 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/)
+A web-based hosting service for projects using Git. It was acquired by GitLab and we discontinued the service. Read the[Gitorious Acquisition Blog Post](https://about.gitlab.com/2015/03/03/gitlab-acquires-gitorious/).
+
+### Go
+
+An open source programming [language](https://golang.org/).
-### HADR
+### GUI/ Git GUI
-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.
+A portable [graphical interface](https://git-scm.com/docs/git-gui) to Git that allows users to make changes to their repository by making new commits, amending existing ones, creating branches, performing local merges, and fetching/pushing to remote repositories.
+
+### High Availability for Disaster Recovery (HADR)
+
+Sometimes written HA/DR, this usually refers to a strategy for 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.
+Atlassian's real time chat application for teams, Hip Chat is a 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."
+Refers to a [system or component](https://about.gitlab.com/high-availability/) that is continuously operational for a desirably long length of time. Availability can be measured relative to "100% operational" or "never failing."
+
+### Inner-sourcing
+
+The [use of](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/) open source development techniques within the corporation.
+
+### Internet Relay Chat (IRC)
+
+An [application layer protocol](http://www.irchelp.org/) that facilitates communication in the form of text.
### Issue Tracker
-A tool used to manage, organize, and maintain a list of issues, making it easier for an organization to manage.
+A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) 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/)
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular.
### Jira
-Atlassian's project management software. i.e. a complex issue tracker. See[Jira](https://www.atlassian.com/software/jira)
+Atlassian's [project management software](https://www.atlassian.com/software/jira), i.e. a complex issue tracker. GitLab [can be configured](https://docs.gitlab.com/ee/project_services/jira.html) to interact with JIRA Core either using an on-premise instance or the SaaS solution that Atlassian offers.
+
+### JUnit
+
+A testing framework for the Java programming language, [JUnit](http://junit.org/junit4/) has been important in the evolution of test-driven development.
### Kerberos
-A network authentication protocol that uses secret-key cryptography for security.
+A network authentication [protocol](http://web.mit.edu/kerberos/) that uses secret-key cryptography for security.
### Kubernetes
@@ -216,23 +273,27 @@ An open source container cluster manager originally designed by Google. It's bas
### Labels
-An identifier to describe a group of one or more specific file revisions
+An [identifier](https://docs.gitlab.com/ce/user/project/labels.html) to describe a group of one or more specific file revisions.
-### LDAP
+### Lightweight Directory Access Protocol (LDAP)
-Lightweight Directory Access Protocol - basically its a directory (electronic address book) with user information e.g. name, phone_number etc
+ 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.
+GitLab [integrates](https://docs.gitlab.com/ce/administration/auth/ldap.html) with LDAP to support user authentication. This enables GitLab to sign in people from an LDAP server (i.e., allowing 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
+### Load Balancer
-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.
+A [device](https://en.wikipedia.org/wiki/Load_balancing_(computing)) that distributes network or application traffic across multiple servers.
+
+### Git Large File Storage (LFS)
+
+A way [to enable](https://about.gitlab.com/2015/11/23/announcing-git-lfs-support-in-gitlab/) git to handle large binary files by using reference pointers within small text files to point to the large files. Large files such as high resolution images and videos, audio files, and assets can be called from a remote server.
### Linux
@@ -240,8 +301,7 @@ An operating system like Windows or OS X. It is mostly used by software develope
### 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.
+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. Checkout GitLab's [Markdown guide](https://gitlab.com/help/user/markdown.md).
### Maria DB
@@ -249,193 +309,215 @@ A community developed fork/variation of MySQL. MySQL is owned by Oracle.
### Master
-Name of the default branch in every git repository.
+Name of the [default branch](https://git-scm.com/book/en/v1/Git-Branching-What-a-Branch-Is) in every git repository.
+
+### Mattermost
+
+An open source, self-hosted messaging alternative to Slack. View GitLab's Mattermost [feature](https://gitlab.com/gitlab-org/gitlab-mattermost).
### Mercurial
-A free distributed version control system like Git. Think of it as a competitor to Git.
+A free distributed version control system similar to and a competitor with Git.
### Merge
-Takes changes from one branch, and applies them into another branch.
+Takes changes from one branch, and [applies them](https://git-scm.com/docs/git-merge) into another branch.
+
+### Merge Conflict
+
+[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file.
### Meteor
-A hip platform for building javascript apps.[Meteor] (https://www.meteor.com)
+A [platform](https://www.meteor.com) for building javascript apps.
### Milestones
-Allows you to track the progress on issues, and merge requests, which allows you to get a snapshot of the progress made.
+Allow you to [organize issues](https://docs.gitlab.com/ce/workflow/milestones.html) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
### 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.
+A project that is setup to automatically have its branches, tags, and commits [updated from an upstream repository](https://docs.gitlab.com/ee/workflow/repository_mirroring.html). 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 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
+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](https://docs.gitlab.com/ce/development/licensing.html). This means you can download the code, modify it as you want, and even build a new commercial product using the underlying code and it's not illegal. The only condition is that there is no form of warranty provided by GitLab so whatever happens when you use the code is your own problem.
-*** Needs definition here
+### Mondo Rescue
-### Multi LDAP Server
+A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi).
-*** Needs definition here
+### MySQL
-### My SQL
-
-A relational database. Currently only supported if you are using EE. It is owned by Oracle.
+A relational [database](http://www.mysql.com/) owned by Oracle. Currently only supported if you are using EE.
### 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.
+A set of symbols that are used to organize objects of various kinds so that these objects may be referred to by name. Examples of namespaces in action include file systems that assign names to files; programming languages that organize their variables and subroutines in namespaces; and computer networks and distributed systems that 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.
+A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). 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
+### 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.
+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. GitLab [is](https://docs.gitlab.com/ce/integration/oauth_provider.html) an OAuth2 authentication service provider.
### 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.
+A way to [package different services and tools](https://docs.gitlab.com/omnibus/) required to run GitLab, so that most developers can install it without laborious configuration.
### 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.
+On your own server. In GitLab, this [refers](https://about.gitlab.com/2015/02/12/why-ship-on-premises-in-the-saas-era/) 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 Core
+
+GitLab's [business model](https://about.gitlab.com/2016/07/20/gitlab-is-open-core-github-is-closed-source/). Coined by Andrew Lampitt in 2008, the [open core model](https://en.wikipedia.org/wiki/Open_core) primarily involves offering a "core" or feature-limited version of a software product as free and open-source software, while offering "commercial" versions or add-ons as proprietary software.
### Open Source Software
-Software for which the original source code is freely available and may be redistributed and modified.
+Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
### 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
+The most powerful person on a GitLab project. They have the permissions of all the other users plus the additional permission of being able to destroy (i.e. delete) the project.
-### PaaS
+### Platform as a Service (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
+Typically referred to in regards to application development, PaaS 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
+The company that produces Helix. A commercial, proprietary, centralised VCS well known for its 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.
+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.
+An open source analytics software to help you analyze web traffic. It is similar to Google Analytics, except that the latter is not open source and information is stored by Google. In Piwik, the information is stored on your own server and hence is 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/)
+GitLab Premium EE [subscription](https://about.gitlab.com/pricing/) that includes training and dedicated Account Management and Service Engineer and complete support package.
### PostgreSQL
-A relational database. Touted as the most advanced open source database.
+An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Touted as the most advanced open source database, it is one of two database management systems [supported by](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/database.md) GitLab, the other being MySQL.
### Protected Branches
-A feature that protects branches from unauthorized pushes, force pushing or deletion.
+A [feature](https://docs.gitlab.com/ce/user/project/protected_branches.html) 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.
+Git command to [synchronize](https://git-scm.com/docs/git-pull) 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
+A popular DevOps [automation tool](https://puppet.com/product/how-puppet-works).
### Push
-Git command to send commits from the local repository to the remote repository.
+Git [command](https://git-scm.com/docs/git-push) to send commits from the local repository to the remote repository. Read about [advanced push rules](https://gitlab.com/help/pages/README.md) in GitLab.
### RE Read Only
-Permissions to see a file and it's contents, but not change it
+Permissions to see a file and its contents, but not change it.
### Rebase
-Moves a branch from one commit to another. This allows you to re-write your project's history.
+In addition to the merge, the [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is a main way to integrate changes from one branch into another.
-### Git Repository
+### (Git) Repository
-Storage location of all files which are tracked by git.
+A directory where Git [has been initiatlized](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) to start version controlling your files. The history of your work is stored here. A remote repository is not on your machine, but usually online (like on GitLab.com, for instance). The main remote repository is usually called "Origin."
### Requirements management
-*** Needs definition here
-
-### Revision
-
-*** Needs definition here
+Gives your distributed teams a single shared repository to collaborate and share requirements, understand their relationship to tests, and evaluate linked defects. It includes multiple, preconfigured requirement types.
### 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".
+Also known as version control or source control, this 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.
+An open source chat application for teams, RocketChat is very similar to Slack but it is also open-source.
+
+### Route Table
+
+A route table contains rules (called routes) that determine where network traffic is directed. Each [subnet in a VPC](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html) must be associated with a route table.
### Runners
-Actual build machines/containers that run/execute tests you have specified to be run on GitLab CI
+Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner) you have specified to be run on GitLab CI.
+
+### Sidekiq
+
+The background job processor GitLab [uses](https://docs.gitlab.com/ce/administration/troubleshooting/sidekiq.html) to asynchronously run tasks.
-### SaaS
+### Software as a service (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
+Software that is hosted centrally and accessed on-demand (i.e. whenever you want to). This applies to GitLab.com.
-### SCM
+### Software Configuration Management (SCM)
-Software Configuration Management. Often used by people when they mean Version Control
+This term is 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.
+An Agile [framework](https://www.scrum.org/Resources/What-is-Scrum) designed to typically help complete complex software projects. It's made up of several parts: product requirements backlog, sprint planning, sprint (development), sprint review, and 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.
+### Shell
+
+Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git]() and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+
+### Single-tenant
+
+The tenant purchases their own copy of the software and the software can be customized to meet the specific and needs of that customer. [GitHost.io](https://about.gitlab.com/handbook/positioning-faq/) is our provider of single-tenant 'managed cloud' GitLab instances.
+
### Slack
-Real time messaging app for teams. Used internally by GitLab
+Real time messaging app for teams that is used internally by GitLab team members. GitLab users can enable [Slack integration](https://docs.gitlab.com/ce/project_services/slack.html) to trigger push, issue, and merge request events among others.
### 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.
+Also known as secondary servers, these 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.
+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.
+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](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html) 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 (SSO)
-Single Sign On. An authentication process that allows you enter one username and password to access multiple applications.
+An authentication process that allows you enter one username and password to access multiple applications.
+
+### Staging Area
+
+[Staging occurs](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics) before the commit process in git. The staging area is a file, generally contained in your Git directory, that stores information about what will go into your next commit. It’s sometimes referred to as the “index.""
### Standard Subscription
-Our mid range EE subscription that includes 24/7 support, support for High Availability [Standard Subscription](https://about.gitlab.com/pricing/)
+Our mid range EE subscription that includes 24/7 support and 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.
+Atlassian's Git on-premise solution. Think of it as Atlassian's GitLab EE, now known as BitBucket Server.
+
+### Static Site Generators (SSGs)
+
+A [software](https://wiki.python.org/moin/StaticSiteGenerator) that takes some text and templates as input and produces html files on the output.
### Subversion
@@ -443,40 +525,65 @@ 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'
+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
+### Subversion (SVN)
-Abbreviation for Subversion.
+An open source version control system. Read about [migrating from SVN](https://docs.gitlab.com/ce/workflow/importing/migrating_from_svn.html) to GitLab using SubGit.
### Tag
-Represents a version of a particular branch at a moment in time.
+[Represents](https://docs.gitlab.com/ce/api/tags.html) 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.
+The 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.
+An open source project management and bug tracking web [application](https://trac.edgewall.org/).
+
+### Untracked files
+
+New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously.
### User
Anyone interacting with the software.
-### VCS
+### Version Control Software (VCS)
+
+Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present distributed version control systems like Git, Mercurial, Bazaar, and Darcs.
+
+### Virtual Private Cloud (VPC)
+
+An on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
-Version Control Software
+### Virtual private server (VPS)
+
+A [virtual machine](https://en.wikipedia.org/wiki/Virtual_private_server) sold as a service by an Internet hosting service. A VPS runs its own copy of an operating system, and customers have superuser-level access to that operating system instance, so they can install almost any software that runs on that OS.
+
+### VM Instance
+
+In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any object. An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
### 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
+A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) 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 they specified.
### 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
+A way for for an app to [provide](https://docs.gitlab.com/ce/web_hooks/web_hooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient.
### 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
+A [website/system](http://www.wiki.com/) that allows for collaborative editing of its content by the users. In programming, wikis usually contain documentation of how to use the software.
+
+### Working Tree
+
+[Consists of files](http://stackoverflow.com/questions/3689838/difference-between-head-working-tree-index-in-git) that you are currently working on.
+
+### YAML
+
+A human-readable data serialization [language](http://www.yaml.org/about.html) that takes concepts from programming languages such as C, Perl, and Python, and ideas from XML and the data format of electronic mail.
+
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index d57c0d0674d..bfb83cf79b1 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -99,6 +99,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
# Update init.d script
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 7. Update configuration files
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 46dfa2232b4..7f36ce00e96 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -116,6 +116,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
# Update init.d script
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 7. Update configuration files
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index b058f8e2a03..119c5f475e4 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`.
@@ -156,6 +158,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 076696f565b..cddfa7e3e01 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.6.0
+sudo -u git -H git checkout v3.6.1
```
### 6. Update gitlab-workhorse
@@ -164,6 +166,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
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..8940d14559b
--- /dev/null
+++ b/doc/update/8.12-to-8.13.md
@@ -0,0 +1,205 @@
+# 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.3
+```
+
+### 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.5
+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
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
+
+### 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/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 9f5c6c4dc84..dd3fdafd8d1 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -158,6 +158,10 @@ it where the 'public' directory of GitLab is.
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Use Redis v2.8.0+
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 9f6517d9487..e62d894609a 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -98,6 +98,10 @@ We updated the init script for GitLab in order to set a specific PATH for gitlab
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0cb137a03cc..678cc69d773 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -119,6 +119,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 6267f14eba4..a76346516b9 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -138,6 +138,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index cb66ef920bb..05ef4e61759 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 32906650f6f..8ce434e5f78 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index f078a2bece5..aa077316bbe 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index a057a423e61..bb2c79fbb84 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
new file mode 100644
index 00000000000..eac57bc3de4
--- /dev/null
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -0,0 +1,66 @@
+# Health Check
+
+> [Introduced][ce-3888] in GitLab 8.8.
+
+GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
+endpoint. The health check reports on the overall system status based on the status of
+the database connection, the state of the database migrations, and the ability to write
+and access the cache. This endpoint can be provided to uptime monitoring services like
+[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
+
+## Access Token
+
+An access token needs to be provided while accessing the health check endpoint. The current
+accepted token can be found on the `admin/health_check` page of your GitLab instance.
+
+![access token](img/health_check_token.png)
+
+The access token can be passed as a URL parameter:
+
+```
+https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
+```
+
+or as an HTTP header:
+
+```bash
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+## Using the Endpoint
+
+Once you have the access token, health information can be retrieved as plain text, JSON,
+or XML using the `health_check` endpoint:
+
+- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN`
+
+You can also ask for the status of specific services:
+
+- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN`
+
+For example, the JSON output of the following health check:
+
+```bash
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+would be like:
+
+```
+{"healthy":true,"message":"success"}
+```
+
+## Status
+
+On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
+will return a valid successful HTTP status code, and a `success` message. Ideally your
+uptime monitoring should look for the success message.
+
+[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
+[pingdom]: https://www.pingdom.com
+[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
+[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
diff --git a/doc/monitoring/img/health_check_token.png b/doc/user/admin_area/monitoring/img/health_check_token.png
index 2d7c82a65a8..2d7c82a65a8 100644
--- a/doc/monitoring/img/health_check_token.png
+++ b/doc/user/admin_area/monitoring/img/health_check_token.png
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 56e5b802a52..7a7a0b864bd 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -501,6 +501,10 @@ There are two ways to create links, inline-style and reference-style.
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[I'm a relative reference to a repository file](LICENSE)
+
+ [I am an absolute reference within the repository](/doc/user/markdown.md)
+
+ [I link to the Milestones page](/../milestones)
[You can use numbers for reference-style link definitions][1]
@@ -518,6 +522,10 @@ There are two ways to create links, inline-style and reference-style.
[I'm a relative reference to a repository file](LICENSE)[^1]
+[I am an absolute reference within the repository](/doc/user/markdown.md)
+
+[I link to the Milestones page](/../milestones)
+
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself][]
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 21fd450ec72..51a7fc9eaf7 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -26,6 +26,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 | | ✓ | ✓ | ✓ | ✓ |
@@ -34,6 +35,7 @@ The following table depicts the various user permission levels in a project.
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
+| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
new file mode 100644
index 00000000000..b205fea2c40
--- /dev/null
+++ b/doc/user/project/container_registry.md
@@ -0,0 +1,253 @@
+# GitLab Container Registry
+
+> [Introduced][ce-4040] in GitLab 8.8.
+
+---
+
+> **Note**
+Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
+versions earlier than 1.10.
+>
+This document is about the user guide. To learn how to enable GitLab Container
+Registry across your GitLab instance, visit the
+[administrator documentation](../../administration/container_registry.md).
+
+With the Docker Container Registry integrated into GitLab, every project can
+have its own space to store its Docker images.
+
+You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
+
+---
+
+## Enable the Container Registry for your project
+
+1. First, ask your system administrator to enable GitLab Container Registry
+ following the [administration documentation](../../administration/container_registry.md).
+ If you are using GitLab.com, this is enabled by default so you can start using
+ the Registry immediately.
+
+1. Go to your project's settings and enable the **Container Registry** feature
+ on your project. For new projects this might be enabled by default. For
+ existing projects (prior GitLab 8.8), you will have to explicitly enable it.
+
+ ![Enable Container Registry](img/container_registry_enable.png)
+
+1. Hit **Save changes** for the changes to take effect. You should now be able
+ to see the **Registry** link in the project menu.
+
+ ![Container Registry tab](img/container_registry_tab.png)
+
+## Build and push images
+
+If you visit the **Registry** link under your project's menu, you can see the
+explicit instructions to login to the Container Registry using your GitLab
+credentials.
+
+For example if the Registry's URL is `registry.example.com`, the you should be
+able to login with:
+
+```
+docker login registry.example.com
+```
+
+Building and publishing images should be a straightforward process. Just make
+sure that you are using the Registry URL with the namespace and project name
+that is hosted on GitLab:
+
+```
+docker build -t registry.example.com/group/project .
+docker push registry.example.com/group/project
+```
+
+Your image will be named after the following scheme:
+
+```
+<registry URL>/<namespace>/<project>
+```
+
+As such, the name of the image is unique, but you can differentiate the images
+using tags.
+
+## Use images from GitLab Container Registry
+
+To download and run a container from images hosted in GitLab Container Registry,
+use `docker run`:
+
+```
+docker run [options] registry.example.com/group/project [arguments]
+```
+
+For more information on running Docker containers, visit the
+[Docker documentation][docker-docs].
+
+## Control Container Registry from within GitLab
+
+GitLab offers a simple Container Registry management panel. Go to your project
+and click **Registry** in the project menu.
+
+This view will show you all tags in your project and will easily allow you to
+delete them.
+
+![Container Registry panel](img/container_registry_panel.png)
+
+## Build and push images using GitLab CI
+
+> **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 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
+
+In order to use a container image from your private project as an `image:` in
+your `.gitlab-ci.yml`, you have to follow the
+[Using a private Docker Registry][private-docker]
+documentation. This workflow will be simplified in the future.
+
+## Troubleshooting the GitLab Container Registry
+
+### Basic Troubleshooting
+
+1. Check to make sure that the system clock on your Docker client and GitLab server have
+ been synchronized (e.g. via NTP).
+
+2. If you are using an S3-backed Registry, double check that the IAM
+ permissions and the S3 credentials (including region) are correct. See [the
+ sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
+ for more details.
+
+3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
+ for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
+ there.
+
+### Advanced Troubleshooting
+
+>**NOTE:** The following section is only recommended for experts.
+
+Sometimes it's not obvious what is wrong, and you may need to dive deeper into
+the communication between the Docker client and the Registry to find out
+what's wrong. We will use a concrete example in the past to illustrate how to
+diagnose a problem with the S3 setup.
+
+#### Unexpected 403 error during push
+
+A user attempted to enable an S3-backed Registry. The `docker login` step went
+fine. However, when pushing an image, the output showed:
+
+```
+The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
+dc5e59c14160: Pushing [==================================================>] 14.85 kB
+03c20c1a019a: Pushing [==================================================>] 2.048 kB
+a08f14ef632e: Pushing [==================================================>] 2.048 kB
+228950524c88: Pushing 2.048 kB
+6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB
+5f70bf18a086: Pushing 1.024 kB
+737f40e80b7f: Waiting
+82b57dbc5385: Waiting
+19429b698a22: Waiting
+9436069b92a3: Waiting
+error parsing HTTP 403 response body: unexpected end of JSON input: ""
+```
+
+This error is ambiguous, as it's not clear whether the 403 is coming from the
+GitLab Rails application, the Docker Registry, or something else. In this
+case, since we know that since the login succeeded, we probably need to look
+at the communication between the client and the Registry.
+
+The REST API between the Docker client and Registry is [described
+here](https://docs.docker.com/registry/spec/api/). Normally, one would just
+use Wireshark or tcpdump to capture the traffic and see where things went
+wrong. However, since all communication between Docker clients and servers
+are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
+if you know the private key. What can we do instead?
+
+One way would be to disable HTTPS by setting up an [insecure
+Registry](https://docs.docker.com/registry/insecure/). This could introduce a
+security hole and is only recommended for local testing. If you have a
+production system and can't or don't want to do this, there is another way:
+use mitmproxy, which stands for Man-in-the-Middle Proxy.
+
+#### mitmproxy
+
+[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
+client and server to inspect all traffic. One wrinkle is that your system
+needs to trust the mitmproxy SSL certificates for this to work.
+
+The following installation instructions assume you are running Ubuntu:
+
+1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
+1. Run `mitmproxy --port 9000` to generate its certificates.
+ Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
+1. Install the certificate from `~/.mitmproxy` to your system:
+
+ ```sh
+ sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
+ sudo update-ca-certificates
+ ```
+
+If successful, the output should indicate that a certificate was added:
+
+```sh
+Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
+Running hooks in /etc/ca-certificates/update.d....done.
+```
+
+To verify that the certificates are properly installed, run:
+
+```sh
+mitmproxy --port 9000
+```
+
+This will run mitmproxy on port `9000`. In another window, run:
+
+```sh
+curl --proxy http://localhost:9000 https://httpbin.org/status/200
+```
+
+If everything is setup correctly, you will see information on the mitmproxy window and
+no errors from the curl commands.
+
+#### Running the Docker daemon with a proxy
+
+For Docker to connect through a proxy, you must start the Docker daemon with the
+proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
+and then run Docker by hand. As root, run:
+
+```sh
+export HTTP_PROXY="http://localhost:9000"
+export HTTPS_PROXY="https://localhost:9000"
+docker daemon --debug
+```
+
+This will launch the Docker daemon and proxy all connections through mitmproxy.
+
+#### Running the Docker client
+
+Now that we have mitmproxy and Docker running, we can attempt to login and push
+a container image. You may need to run as root to do this. For example:
+
+```sh
+docker login s3-testing.myregistry.com:4567
+docker push s3-testing.myregistry.com:4567/root/docker-test
+```
+
+In the example above, we see the following trace on the mitmproxy window:
+
+![mitmproxy output from Docker](img/mitmproxy-docker.png)
+
+The above image shows:
+
+* The initial PUT requests went through fine with a 201 status code.
+* The 201 redirected the client to the S3 bucket.
+* The HEAD request to the AWS bucket reported a 403 Unauthorized.
+
+What does this mean? This strongly suggests that the S3 user does not have the right
+[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
+The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
+Once the right permissions were set, the error will go away.
+
+[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
+[docker-docs]: https://docs.docker.com/engine/userguide/intro/
+[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index abef80e7914..1892ccabb70 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -3,10 +3,10 @@
> [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].
+There are more changes coming to Cycle Analytics, you can follow the following
+issue to track the changes to this feature: [#20975][ce-20975].
-Cycle Analytics measures the time it takes to go from [an idea to production] for
+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.
@@ -28,9 +28,10 @@ You can see that there are seven stages in total:
(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
+ first commit to the branch
- **Code** (IDE)
- - Median time from the first commit until the merge request is created
+ - Median time from the first commit to the branch until the merge request is
+ created
- **Test** (CI)
- Median total test time for all commits/merges
- **Review** (Merge Request/MR)
@@ -40,42 +41,42 @@ You can see that there are seven stages in total:
- 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
+ - Sum of all the above stages' times excluding the Test (CI) time. To clarify,
+ it's not so much that CI time is "excluded", but rather CI time is already
+ counted in the review stage since CI is done automatically. Most of the
+ other stages are purely sequential, but **Test** is not.
## 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.
+Cycle Analytics records cycle time and data based on the project issues with the
+exception of the staging and production stages, where only data deployed to
+production are measured.
Specifically, if your CI is not set up and you have not defined a `production`
-[environment], then you will not have any data.
+[environment], then you will not have any data for those stages.
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. |
+| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the branch. The very first commit of the branch is the one that triggers the separation between **Plan** and **Code**, and at least one of the commits in the branch needs to contain the related issue number (e.g., `#42`). If none of the commits in the branch mention the related issue number, 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 (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
| 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. |
+| 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` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Production| The sum of all time (medians) 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.
-
+ `<issue, merge request>` pair, the merge request has the [issue closing pattern]
+ for the corresponding issue. All other issues and merge requests are **not**
+ considered.
+1. Then the <issue, merge request> pairs are filtered out by last XX days (specified
+ by the UI - default is 90 days). So it 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.
@@ -83,8 +84,62 @@ Here's a little explanation of how this works behind the scenes:
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.
+`production` environment (for staging and production stages), the Cycle Analytics
+dashboard won't present any data at all.
+
+## Example workflow
+
+Below is a simple fictional workflow of a single cycle that happens in a
+single day passing through all seven stages. Note that if a stage does not have
+a start/stop mark, it is not measured and hence not calculated in the median
+time. It is assumed that milestones are created and CI for testing and setting
+environments is configured.
+
+1. Issue is created at 09:00 (start of **Issue** stage).
+1. Issue is added to a milestone at 11:00 (stop of **Issue** stage / start of
+ **Plan** stage).
+1. Start working on the issue, create a branch locally and make one commit at
+ 12:00.
+1. Make a second commit to the branch which mentions the issue number at 12.30
+ (stop of **Plan** stage / start of **Code** stage).
+1. Push branch and create a merge request that contains the [issue closing pattern]
+ in its description at 14:00 (stop of **Code** stage / start of **Test** and
+ **Review** stages).
+1. The CI starts running your scripts defined in [`.gitlab-ci.yml`][yml] and
+ takes 5min (stop of **Test** stage).
+1. Review merge request, ensure that everything is OK and merge the merge
+ request at 19:00. (stop of **Review** stage / start of **Staging** stage).
+1. Now that the merge request is merged, a deployment to the `production`
+ environment starts and finishes at 19:30 (stop of **Staging** stage).
+1. The cycle completes and the sum of the median times of the previous stages
+ is recorded to the **Production** stage. That is the time between creating an
+ issue and deploying its relevant merge request to production.
+
+From the above example you can conclude the time it took each stage to complete
+as long as their total time:
+
+- **Issue**: 2h (11:00 - 09:00)
+- **Plan**: 1h (12:00 - 11:00)
+- **Code**: 2h (14:00 - 12:00)
+- **Test**: 5min
+- **Review**: 5h (19:00 - 14:00)
+- **Staging**: 30min (19:30 - 19:00)
+- **Production**: Since this stage measures the sum of median time off all
+ previous stages, we cannot calculate it if we don't know the status of the
+ stages before. In case this is the very first cycle that is run in the project,
+ then the **Production** time is 10h 30min (19:30 - 09:00)
+
+A few notes:
+
+- In the above example we demonstrated that it doesn't matter if your first
+ commit doesn't mention the issue number, you can do this later in any commit
+ of the branch you are working on.
+- You can see that the **Test** stage is not calculated to the overall time of
+ the cycle since it is included in the **Review** process (every MR should be
+ tested).
+- The example above was just **one cycle** of the seven stages. Add multiple
+ cycles, calculate their median time and the result is what the dashboard of
+ Cycle Analytics is showing.
## Permissions
@@ -104,11 +159,12 @@ Learn more about Cycle Analytics in the following resources:
- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+[board]: issue_board.md#creating-a-new-list
[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
+[GitLab flow]: ../../workflow/gitlab_flow.md
[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
+[permissions]: ../permissions.md
+[yml]: ../../ci/yaml/README.md
diff --git a/doc/user/project/git_attributes.md b/doc/user/project/git_attributes.md
new file mode 100644
index 00000000000..21ef94e61f7
--- /dev/null
+++ b/doc/user/project/git_attributes.md
@@ -0,0 +1,22 @@
+# Git Attributes
+
+GitLab supports defining custom [Git attributes][gitattributes] such as what
+files to treat as binary, and what language to use for syntax highlighting
+diffs.
+
+To define these attributes, create a file called `.gitattributes` in the root
+directory of your repository and push it to the default branch of your project.
+
+## Encoding Requirements
+
+The `.gitattributes` file _must_ be encoded in UTF-8 and _must not_ contain a
+Byte Order Mark. If a different encoding is used, the file's contents will be
+ignored.
+
+## Syntax Highlighting
+
+The `.gitattributes` file can be used to define which language to use when
+syntax highlighting files and diffs. See ["Syntax
+Highlighting"](highlighting.md) for more information.
+
+[gitattributes]: https://git-scm.com/docs/gitattributes
diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png
new file mode 100644
index 00000000000..6fffa2a91d8
--- /dev/null
+++ b/doc/user/project/img/container_registry_enable.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png
new file mode 100644
index 00000000000..60fd76192b7
--- /dev/null
+++ b/doc/user/project/img/container_registry_panel.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png
new file mode 100644
index 00000000000..36b883aaa97
--- /dev/null
+++ b/doc/user/project/img/container_registry_tab.png
Binary files differ
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
index 4fa42c87395..b212134d5ed 100644
--- a/doc/user/project/img/cycle_analytics_landing_page.png
+++ b/doc/user/project/img/cycle_analytics_landing_page.png
Binary files differ
diff --git a/doc/container_registry/img/mitmproxy-docker.png b/doc/user/project/img/mitmproxy-docker.png
index 4e3e37b413d..4e3e37b413d 100644
--- a/doc/container_registry/img/mitmproxy-docker.png
+++ b/doc/user/project/img/mitmproxy-docker.png
Binary files differ
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index cac926b3e28..4a6c0d88241 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -31,9 +31,10 @@ Below is a table of the definitions used for GitLab's Issue Board.
There are three types of lists, the ones you create based on your labels, and
two default:
-- **Backlog** (default): shows all opened issues that do not fall in one of the other lists. Always appears on the very left.
-- **Done** (default): shows all closed issues that do not fall in one of the other lists. Always appears on the very right.
-- Label list: a list based on a label. It shows all opened or closed issues with that label.
+- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
+- **Done** (default): shows all closed issues. Always appears on the very right.
+Label list: a list based on a label. It shows all issues with that label.
+- Label list: a list based on a label. It shows all opened issues with that label.
![GitLab Issue Board](img/issue_board.png)
diff --git a/doc/user/project/merge_requests/img/versions-compare.png b/doc/user/project/merge_requests/img/versions_compare.png
index 890cae7768c..890cae7768c 100644
--- a/doc/user/project/merge_requests/img/versions-compare.png
+++ 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
index 9bab9304e14..9bab9304e14 100644
--- a/doc/user/project/merge_requests/img/versions-dropdown.png
+++ b/doc/user/project/merge_requests/img/versions_dropdown.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions_system_note.png b/doc/user/project/merge_requests/img/versions_system_note.png
new file mode 100644
index 00000000000..7c9d7715745
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions_system_note.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
index 011f9cbc381..c138061fd40 100644
--- a/doc/user/project/merge_requests/merge_when_build_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md
@@ -1,16 +1,16 @@
# 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.
+more CI builds running, you can set it to be merged automatically when the
+builds pipeline 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.
+for the pipeline 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
@@ -18,9 +18,9 @@ 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 pipeline succeeds, the merge request will automatically be merged.
+When the pipeline 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
@@ -40,7 +40,7 @@ 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.
+From now on, every time the pipelinefails you will not be able to merge the
+merge request from the UI, until you make all relevant builds 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/versions.md b/doc/user/project/merge_requests/versions.md
index 2805fdf635c..77eab7ba5e3 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -7,26 +7,36 @@ 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)
+![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)
+![Merge request versions dropdown](img/versions_dropdown.png)
+
+---
-You can also compare the merge request version with older one to see what is
+You can also compare the merge request version with an older one to see what has
changed since then.
-![Merge Request Versions](img/versions-compare.png)
+![Merge request versions compare](img/versions_compare.png)
+
+---
+
+Every time you push new changes to the branch, a link to compare the last
+changes appears as a system note.
-Please note that comments are disabled while viewing outdated merge versions
-or comparing to versions other than base.
+![Merge request versions system note](img/versions_system_note.png)
---
->**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.
+>**Notes:**
+- Comments are disabled while viewing outdated merge versions or comparing to
+ versions other than base.
+- 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/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index e73f60023b5..8827b501901 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -87,17 +87,6 @@ your Runners in the most possible secure way, by avoiding the following:
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.
@@ -149,17 +138,46 @@ 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
+With the new build permissions 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
+Below you can see the prerequisites needed to make use of the new permissions
+model and how that works with Git submodules and private Docker images hosted on
the container registry.
-## Git submodules
+### Prerequisites to use the new permissions model
+
+With the new permissions 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.
+
+In short here's what you need to do should you encounter any issues.
+
+As an administrator:
+
+- **500 errors**: You will need to update [GitLab Workhorse][workhorse] to at
+ least 0.8.2. This is done automatically for Omnibus installations, you need to
+ [check manually][update-docs] for installations from source.
+- **500 errors**: Check if you have another web proxy sitting in front of NGINX (HAProxy,
+ Apache, etc.). It might be a good idea to let GitLab use the internal NGINX
+ web server and not disable it completely. See [this comment][comment] for an
+ example.
+- **403 errors**: You need to make sure that your installation has [HTTP(S)
+ cloning enabled][https]. HTTP(S) support is now a **requirement** by GitLab CI
+ to clone all sources.
+
+As a user:
+
+- Make sure you are a member of the group or project you're trying to have
+ access to. As an Administrator, you can verify that by impersonating the user
+ and retry the failing build in order to verify that everything is correct.
+
+### Git submodules
>
It often happens that while working on one project, you need to use another
@@ -283,7 +301,11 @@ test:
- 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
+[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
[ext]: ../permissions.md#external-users
+[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+[https]: ../admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols
[triggers]: ../../ci/triggers/README.md
+[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
+[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
diff --git a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
new file mode 100644
index 00000000000..b0a63ddf0ab
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
Binary files differ
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 993c6bfb7e9..675e89e4247 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -97,11 +97,11 @@ There are multiple ways to create a branch from GitLab's web interface.
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
+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)
+![New Branch Button](img/web_editor_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
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 445c0ee8333..dfc762fe1d3 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -7,7 +7,8 @@
> 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.
+> Ask your administrator if you don't see the **GitLab export** button when
+> creating a new project.
> - You can find some useful raketasks if you are an administrator in the
> [import_export](../../../administration/raketasks/project_import_export.md)
> raketask.
@@ -21,7 +22,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| -------- | -------- |
-| 8.12.0 to current | 0.1.4 |
+| 8.13.0 to current | 0.1.5 |
+| 8.12.0 | 0.1.4 |
| 8.10.3 | 0.1.3 |
| 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 |
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index 1792a0c501d..5f6a6c6503e 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -27,4 +27,5 @@ do.
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
-| `/remove_due_date` | Remove due date |
+| `/remove_due_date` | Remove due date |
+| `/wip` | Toggle the Work In Progress status |
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 7c0eb90d540..2215f37b81a 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -228,7 +228,7 @@ We'll discuss the three reasons to merge in master: leveraging code, merge confl
If you need to leverage some code that was introduced in master after you created the feature branch you can sometimes solve this by just cherry-picking a commit.
If your feature branch has a merge conflict, creating a merge commit is a normal way of solving this.
You can prevent some merge conflicts by using [gitattributes](http://git-scm.com/docs/gitattributes) for files that can be in a random order.
-For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG merge=union` so that there are fewer merge conflicts in it.
+For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG.md merge=union` so that there are fewer merge conflicts in it.
The last reason for creating merge commits is having long lived branches that you want to keep up to date with the latest state of the project.
Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI).
At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit.
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 4828bb5dce6..423b095e69e 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -4,6 +4,112 @@ Subversion (SVN) is a central version control system (VCS) while
Git is a distributed version control system. There are some major differences
between the two, for more information consult your favorite search engine.
+## Overview
+
+There are two approaches to SVN to Git migration:
+
+1. [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which:
+ - Makes the GitLab repository to mirror the SVN project.
+ - Git and SVN repositories are kept in sync; you can use either one.
+ - Smoothens the migration process and allows to manage migration risks.
+
+1. [Cut over migration](#cut-over-migration-with-svn2git) which:
+ - Translates and imports the existing data and history from SVN to Git.
+ - Is a fire and forget approach, good for smaller teams.
+
+## Smooth migration with a Git/SVN mirror using SubGit
+
+[SubGit](https://subgit.com) is a tool for a smooth, stress-free SVN to Git
+migration. It creates a writable Git mirror of a local or remote Subversion
+repository and that way you can use both Subversion and Git as long as you like.
+It requires access to your GitLab server as it talks with the Git repositories
+directly in a filesystem level.
+
+### SubGit prerequisites
+
+1. Install Oracle JRE 1.8 or newer. On Debian-based Linux distributions you can
+ follow [this article](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html).
+1. Download SubGit from https://subgit.com/download/.
+1. Unpack the downloaded SubGit zip archive to the `/opt` directory. The `subgit`
+ command will be available at `/opt/subgit-VERSION/bin/subgit`.
+
+### SubGit configuration
+
+The first step to mirror you SVN repository in GitLab is to create a new empty
+project which will be used as a mirror. For Omnibus installations the path to
+the repository will be located at
+`/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For
+installations from source, the default repository directory will be
+`/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a
+variable:
+
+```
+GIT_REPO_PATH=/var/opt/gitlab/git-data/repositories/USER/REPOS.git
+```
+
+SubGit will keep this repository in sync with a remote SVN project. For
+convenience, assign your remote SVN project URL to a variable:
+
+```
+SVN_PROJECT_URL=http://svn.company.com/repos/project
+```
+
+Next you need to run SubGit to set up a Git/SVN mirror. Make sure the following
+`subgit` command is ran on behalf of the same user that keeps ownership of
+GitLab Git repositories (by default `git`):
+
+```
+subgit configure --layout auto $SVN_PROJECT_URL $GIT_REPO_PATH
+```
+
+Adjust authors and branches mappings, if necessary. Open with your favorite
+text editor:
+
+```
+edit $GIT_REPO_PATH/subgit/authors.txt
+edit $GIT_REPO_PATH/subgit/config
+```
+
+For more information regarding the SubGit configuration options, refer to
+[SubGit's documentation](https://subgit.com/documentation.html) website.
+
+### Initial translation
+
+Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
+initial translation of existing SVN revisions into the Git repository:
+
+```
+subgit install $GIT_REPOS_PATH
+```
+
+After the initial translation is completed, the Git repository and the SVN
+project will be kept in sync by `subgit` - new Git commits will be translated to
+SVN revisions and new SVN revisions will be translated to Git commits. Mirror
+works transparently and does not require any special commands.
+
+If you would prefer to perform one-time cut over migration with `subgit`, use
+the `import` command instead of `install`:
+
+```
+subgit import $GIT_REPO_PATH
+```
+
+### SubGit licensing
+
+Running SubGit in a mirror mode requires a
+[registration](https://subgit.com/pricing.html). Registration is free for open
+source, academic and startup projects.
+
+We're currently working on deeper GitLab/SubGit integration. You may track our
+progress at [this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/990).
+
+### SubGit support
+
+For any questions related to SVN to GitLab migration with SubGit, you can
+contact the SubGit team directly at [support@subgit.com](mailto:support@subgit.com).
+
+## Cut over migration with svn2git
+
If you are currently using an SVN repository, you can migrate the repository
to Git and GitLab. We recommend a hard cut over - run the migration command once
and then have all developers start using the new GitLab repository immediately.
@@ -75,5 +181,3 @@ git push --tags origin
## Contribute to this guide
We welcome all contributions that would expand this guide with instructions on
how to migrate from SVN and other version control systems.
-
-
diff --git a/docker/README.md b/docker/README.md
index ee1f32adc26..f9e12c5733b 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -2,6 +2,6 @@
* The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/).
* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/).
-* The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/)
+* The complete usage guide can be found in [Using GitLab Docker images](https://docs.gitlab.com/omnibus/docker/)
* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
-* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#build-docker-image)
+* Check the guide for [creating Omnibus-based Docker Image](https://docs.gitlab.com/omnibus/build/README.html#build-docker-image)
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index 1f4c9020731..b1d5e4a7acb 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -32,19 +32,6 @@ Feature: Dashboard
Then I see prefilled new Merge Request page
@javascript
- Scenario: I should see User joined Project event
- Given user with name "John Doe" joined project "Shop"
- When I visit dashboard activity page
- Then I should see "John Doe joined project Shop" event
-
- @javascript
- Scenario: I should see User left Project event
- Given user with name "John Doe" joined project "Shop"
- And user with name "John Doe" left project "Shop"
- When I visit dashboard activity page
- Then I should see "John Doe left project Shop" event
-
- @javascript
Scenario: Sorting Issues
Given I visit dashboard issues page
And I sort the list by "Oldest updated"
diff --git a/features/explore/projects.feature b/features/explore/projects.feature
index 092e18d1b86..4e0f4486ab7 100644
--- a/features/explore/projects.feature
+++ b/features/explore/projects.feature
@@ -128,6 +128,7 @@ Feature: Explore Projects
And project "Archive" has comments
And I sign in as a user
And project "Community" has comments
+ And trending projects are refreshed
When I visit the explore trending projects
Then I should see project "Community"
And I should not see project "Internal"
diff --git a/features/groups.feature b/features/groups.feature
index 49e939807b5..4044bd9be79 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -39,11 +39,6 @@ Feature: Groups
When I visit group "Owned" merge requests page
Then I should not see merge requests from the archived project
- Scenario: I should see edit group "Owned" page
- When I visit group "Owned" settings page
- And I change group "Owned" name to "new-name"
- Then I should see new group "Owned" name
-
Scenario: I edit group "Owned" avatar
When I visit group "Owned" settings page
And I change group "Owned" avatar
diff --git a/features/profile/ssh_keys.feature b/features/profile/ssh_keys.feature
deleted file mode 100644
index b0d5b748916..00000000000
--- a/features/profile/ssh_keys.feature
+++ /dev/null
@@ -1,20 +0,0 @@
-@profile
-Feature: Profile SSH Keys
- Background:
- Given I sign in as a user
- And I have ssh key "ssh-rsa Work"
- And I visit profile keys page
-
- Scenario: I should see ssh keys
- Then I should see my ssh keys
-
- Scenario: Add new ssh key
- Given I should see new ssh key form
- And I submit new ssh key "Laptop"
- Then I should see new ssh key "Laptop"
-
- Scenario: Remove ssh key
- Given I click link "Work"
- And I click link "Remove"
- Then I visit profile keys page
- And I should not see "Work" ssh key
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 8b0cb90765e..1776c07e60e 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -37,6 +37,11 @@ Feature: Project Commits
Then I see commit info
And I see side-by-side diff button
+ Scenario: I browse commit from list and create a new tag
+ Given I click on commit link
+ And I click on tag link
+ Then I see commit SHA pre-filled
+
Scenario: I browse commit with ci from list
Given commit has ci status
And repository contains ".gitlab-ci.yml" file
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 358e622b736..80670063ea0 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -37,6 +37,7 @@ Feature: Project Issues
And I submit new issue "500 error on profile"
Then I should see issue "500 error on profile"
+ @javascript
Scenario: I submit new unassigned issue with labels
Given project "Shop" has labels: "bug", "feature", "enhancement"
And I click link "New Issue"
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index fdffd71de85..d4b91fec6e8 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -71,6 +71,7 @@ Feature: Project Source Browse Files
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
+ When I click on "Changes" tab
And I should see its new content
@javascript
@@ -80,9 +81,10 @@ Feature: Project Source Browse Files
And I fill the upload file commit message
And I fill the new branch name
And I click on "Upload file"
- Then I can see the new text file
+ Then I can see the new commit message
And I am redirected to the new merge request page
- And I can see the new commit message
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: I can upload file and commit when I don't have write access
@@ -93,9 +95,10 @@ Feature: Project Source Browse Files
And I upload a new text file
And I fill the upload file commit message
And I click on "Upload file"
- Then I can see the new text file
+ Then I can see the new commit message
And I am redirected to the fork's new merge request page
- And I can see the new commit message
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: I can replace file and commit
@@ -119,9 +122,10 @@ Feature: Project Source Browse Files
And I replace it with a text file
And I fill the replace file commit message
And I click on "Replace file"
- Then I can see the new text file
- And I am redirected to the fork's new merge request page
And I can see the replacement commit message
+ And I am redirected to the fork's new merge request page
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: If I enter an illegal file name I see an error message
@@ -191,6 +195,7 @@ Feature: Project Source Browse Files
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
+ Then I click on "Changes" tab
And I should see its new content
@javascript @wip
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index 0c89a3db9ad..9396a76f0a2 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -105,7 +105,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
select "Developer", from: "access_level"
end
- click_button "Add users to group"
+ click_button "Add to group"
end
step 'I should see current user as "Developer"' do
diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb
index d77945a6b9c..2b8cd030ace 100644
--- a/features/steps/admin/projects.rb
+++ b/features/steps/admin/projects.rb
@@ -70,7 +70,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
select "Developer", from: "access_level"
end
- click_button "Add users to project"
+ click_button "Add to project"
end
step 'I should see current user as "Developer"' do
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index a7d61bc28e0..b2bec369e0f 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -33,33 +33,6 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
expect(find("input#merge_request_target_branch").value).to eq "master"
end
- step 'user with name "John Doe" joined project "Shop"' do
- user = create(:user, { name: "John Doe" })
- project.team << [user, :master]
- Event.create(
- project: project,
- author_id: user.id,
- action: Event::JOINED
- )
- end
-
- step 'I should see "John Doe joined project Shop" event' do
- expect(page).to have_content "John Doe joined project #{project.name_with_namespace}"
- end
-
- step 'user with name "John Doe" left project "Shop"' do
- user = User.find_by(name: "John Doe")
- Event.create(
- project: project,
- author_id: user.id,
- action: Event::LEFT
- )
- end
-
- step 'I should see "John Doe left project Shop" event' do
- expect(page).to have_content "John Doe left project #{project.name_with_namespace}"
- end
-
step 'I have group with projects' do
@group = create(:group)
@project = create(:project, namespace: @group)
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index 2f0941e4113..cb36d6ae1a9 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -20,6 +20,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('GitLab.com')
expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL')
+ expect(page).to have_link('GitLab export')
end
step 'I click on "Import project from GitHub"' do
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index e9b45823c67..cefc55d07ab 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -1,4 +1,5 @@
class Spinach::Features::GroupMembers < Spinach::FeatureSteps
+ include WaitForAjax
include SharedAuthentication
include SharedPaths
include SharedGroup
@@ -13,7 +14,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Reporter", from: "access_level"
end
- click_button "Add users to group"
+ click_button "Add to group"
end
step 'I select "Mike" as "Master"' do
@@ -24,7 +25,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Master", from: "access_level"
end
- click_button "Add users to group"
+ click_button "Add to group"
end
step 'I should see "Mike" in team list as "Reporter"' do
@@ -47,7 +48,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Reporter", from: "access_level"
end
- click_button "Add users to group"
+ click_button "Add to group"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
@@ -66,7 +67,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
select "Reporter", from: "access_level"
end
- click_button "Add users to group"
+ click_button "Add to group"
end
step 'I should see user "John Doe" in team list' do
@@ -108,7 +109,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
step 'I search for \'Mary\' member' do
page.within '.member-search-form' do
fill_in 'search', with: 'Mary'
- click_button 'Search'
+ find('.member-search-btn').click
end
end
@@ -116,9 +117,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- click_button 'Edit'
select 'Developer', from: "member_access_level_#{member.id}"
- click_on 'Save'
+ wait_for_ajax
end
end
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 4fa7d7c6567..0e81e99120b 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -73,18 +73,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
author: current_user
end
- step 'I change group "Owned" name to "new-name"' do
- fill_in 'group_name', with: 'new-name'
- fill_in 'group_path', with: 'new-name'
- click_button "Save group"
- end
-
- step 'I should see new group "Owned" name' do
- page.within ".navbar-gitlab" do
- expect(page).to have_content "new-name"
- end
- end
-
step 'I change group "Owned" avatar' do
attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Save group"
diff --git a/features/steps/profile/ssh_keys.rb b/features/steps/profile/ssh_keys.rb
deleted file mode 100644
index a400488a532..00000000000
--- a/features/steps/profile/ssh_keys.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps
- include SharedAuthentication
-
- step 'I should see my ssh keys' do
- @user.keys.each do |key|
- expect(page).to have_content(key.title)
- end
- end
-
- step 'I should see new ssh key form' do
- expect(page).to have_content("Add an SSH key")
- end
-
- step 'I submit new ssh key "Laptop"' do
- fill_in "key_title", with: "Laptop"
- fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
- click_button "Add key"
- end
-
- step 'I should see new ssh key "Laptop"' do
- key = Key.find_by(title: "Laptop")
- expect(page).to have_content(key.title)
- expect(page).to have_content(key.key)
- expect(current_path).to eq profile_key_path(key)
- end
-
- step 'I click link "Work"' do
- click_link "Work"
- end
-
- step 'I click link "Remove"' do
- click_link "Remove"
- end
-
- step 'I visit profile keys page' do
- visit profile_keys_path
- end
-
- step 'I should not see "Work" ssh key' do
- expect(page).not_to have_content "Work"
- end
-
- step 'I have ssh key "ssh-rsa Work"' do
- create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work")
- end
-end
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 80043463188..58225032859 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -54,7 +54,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Branches" tab' do
- page.within '.content' do
+ page.within '.sub-nav' do
click_link('Branches')
end
end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index bea9f9d198b..244306e8464 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -21,7 +21,15 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(response_headers['Content-Type']).to have_content("application/atom+xml")
expect(body).to have_selector("title", text: "#{@project.name}:master commits")
expect(body).to have_selector("author email", text: commit.author_email)
- expect(body).to have_selector("entry summary", text: commit.description[0..10])
+ expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r"))
+ end
+
+ step 'I click on tag link' do
+ click_link "Tag"
+ end
+
+ step 'I see commit SHA pre-filled' do
+ expect(page).to have_selector("input[value='#{sample_commit.id}']")
end
step 'I click on commit link' do
@@ -34,15 +42,16 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I fill compare fields with branches' do
- fill_in 'from', with: 'feature'
- fill_in 'to', with: 'master'
+ select_using_dropdown('from', 'feature')
+ select_using_dropdown('to', 'master')
click_button 'Compare'
end
step 'I fill compare fields with refs' do
- fill_in "from", with: sample_commit.parent_id
- fill_in "to", with: sample_commit.id
+ select_using_dropdown('from', sample_commit.parent_id, true)
+ select_using_dropdown('to', sample_commit.id, true)
+
click_button "Compare"
end
@@ -89,8 +98,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I fill compare fields with branches' do
- fill_in 'from', with: 'master'
- fill_in 'to', with: 'feature'
+ select_using_dropdown('from', 'master')
+ select_using_dropdown('to', 'feature')
click_button 'Compare'
end
@@ -174,4 +183,15 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "More submodules"
expect(page).not_to have_content "Change some files"
end
+
+ def select_using_dropdown(dropdown_type, selection, is_commit = false)
+ dropdown = find(".js-compare-#{dropdown_type}-dropdown")
+ dropdown.find(".compare-dropdown-toggle").click
+ dropdown.fill_in("Filter by Git revision", with: selection)
+ if is_commit
+ dropdown.find('input[type="search"]').send_keys(:return)
+ else
+ find_link(selection, visible: true).click
+ end
+ end
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 8abeb5ee242..70dbd030003 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike')
+ @project.team << [user, :reporter]
@forked_project = Projects::ForkService.new(@project, user).execute
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index dacab6c7977..6c14d835004 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -138,19 +138,19 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I click "Assign to" dropdown"' do
- first('.ajax-users-select').click
+ click_button 'Assignee'
end
step 'I should see the target project ID in the input selector' do
- expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]")
+ expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}"
end
step 'I should see the users from the target project ID' do
- expect(page).to have_selector('.user-result', visible: true, count: 3)
- users = page.all('.user-name')
- expect(users[0].text).to eq 'Unassigned'
- expect(users[1].text).to eq current_user.name
- expect(users[2].text).to eq @project.users.first.name
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content 'Unassigned'
+ expect(page).to have_content current_user.name
+ expect(page).to have_content @project.users.first.name
+ end
end
# Verify a link is generated against the correct project
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index b09ec86e5df..7490d2bc6e7 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -19,8 +19,8 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
end
step 'page should have languages graphs' do
- expect(page).to have_content "Ruby 66.63 %"
- expect(page).to have_content "JavaScript 22.96 %"
+ expect(page).to have_content /Ruby 66.* %/
+ expect(page).to have_content /JavaScript 22.* %/
end
step 'page should have commits graphs' do
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index ed7241679ee..b50f5238e80 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -84,7 +84,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I submit new issue "500 error on profile" with label \'bug\'' do
fill_in "issue_title", with: "500 error on profile"
- select 'bug', from: "Labels"
+ click_button "Label"
+ click_link "bug"
click_button "Submit issue"
end
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index 2937d5d7ca8..f74a9b5df47 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
end
step 'I remove label \'bug\'' do
- page.within "#label_#{bug_label.id}" do
+ page.within "#project_label_#{bug_label.id}" do
first(:link, 'Delete').click
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 4a67cf06fba..2ccab4334eb 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -7,6 +7,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
include SharedMarkdown
include SharedDiffNote
include SharedUser
+ include WaitForAjax
+
+ after do
+ wait_for_ajax if javascript_test?
+ end
step 'I click link "New Merge Request"' do
click_link "New Merge Request"
@@ -90,6 +95,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click button "Unsubscribe"' do
click_on "Unsubscribe"
+ wait_for_ajax
end
step 'I click link "Close"' do
@@ -114,7 +120,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
source_project: project,
target_project: project,
source_branch: 'fix',
- target_branch: 'master',
+ target_branch: 'merge-test',
author: project.users.first,
description: "# Description header"
)
@@ -137,7 +143,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
title: "Bug NS-05",
source_project: project,
target_project: project,
- author: project.users.first)
+ author: project.users.first,
+ source_branch: 'merge-test')
end
step 'project "Shop" have "Feature NS-05" merged merge request' do
@@ -508,7 +515,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see new target branch changes' do
expect(page).to have_content 'Request to merge fix into feature'
- expect(page).to have_content 'Target branch changed from master to feature'
+ expect(page).to have_content 'Target branch changed from merge-test to feature'
+ wait_for_ajax
end
step 'I click on "Email Patches"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index bb79424ee08..1cc9e37b075 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -105,6 +105,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_button 'Commit Changes'
end
+ step 'I click on "Changes" tab' do
+ click_link 'Changes'
+ end
+
step 'I click on "Create directory"' do
click_button 'Create directory'
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index e920f5a706b..b21d0849ad1 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
- click_button "Add users to project"
+ click_button "Add to project"
end
step 'I should see "Mike" in team list as "Reporter"' do
@@ -36,10 +36,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I select "sjobs@apple.com" as "Reporter"' do
page.within ".users-project-form" do
- select2("sjobs@apple.com", from: "#user_ids", multiple: true)
+ find('#user_ids', visible: false).set('sjobs@apple.com')
select "Reporter", from: "access_level"
end
- click_button "Add users to project"
+ click_button "Add to project"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
@@ -65,9 +65,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- click_button 'Edit'
select "Reporter", from: "member_access_level_#{project_member.id}"
- click_button "Save"
end
end
@@ -112,7 +110,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I click link "Import team from another project"' do
- click_link "Import members from another project"
+ click_link "Import"
end
When 'I submit "Website" project for import team' do
@@ -144,8 +142,9 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "Opensource" group user listing' do
- expect(page).to have_content("Shared with OpenSource group, members with Master role (2)")
- expect(page).to have_content(@os_user1.name)
- expect(page).to have_content(@os_user2.name)
+ page.within '.project-members-groups' do
+ expect(page).to have_content('OpenSource')
+ expect(find('select').value).to eq('40')
+ end
end
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 3d7c6ef9d2d..9dc1fc41b3b 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -1,5 +1,6 @@
module SharedNote
include Spinach::DSL
+ include WaitForAjax
step 'I delete a comment' do
page.within('.main-notes-list') do
@@ -116,8 +117,9 @@ module SharedNote
page.within(".js-main-target-form") do
fill_in "note[note]", with: "# Comment with a header"
click_button "Comment"
- sleep 0.05
end
+
+ wait_for_ajax
end
step 'The comment with the header should not have an ID' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index afbd8ef1233..cab85a48396 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -218,6 +218,10 @@ module SharedProject
2.times { create(:note_on_issue, project: project) }
end
+ step 'trending projects are refreshed' do
+ TrendingProject.refresh!
+ end
+
step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
project = Project.find_by(name: "Shop")
create(:label, project: project, title: 'bug')
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index fe9e39cf509..dae0d0f918c 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -20,5 +20,5 @@ unless ENV['CI'] || ENV['CI_SERVER']
end
Spinach.hooks.before_run do
- TestEnv.warm_asset_cache
+ TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
end
diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb
index 1ab308cfa55..8294bb1445f 100644
--- a/features/support/db_cleaner.rb
+++ b/features/support/db_cleaner.rb
@@ -1,6 +1,6 @@
require 'database_cleaner'
-DatabaseCleaner.strategy = :truncation
+DatabaseCleaner[:active_record].strategy = :truncation
Spinach.hooks.before_scenario do
DatabaseCleaner.start
diff --git a/features/support/env.rb b/features/support/env.rb
index 569fd444e86..8dbe3624410 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -15,7 +15,7 @@ if ENV['CI']
Knapsack::Adapters::SpinachAdapter.bind
end
-%w(select2_helper test_env repo_helpers).each do |f|
+%w(select2_helper test_env repo_helpers wait_for_ajax).each do |f|
require Rails.root.join('spec', 'support', f)
end
diff --git a/features/support/rerun.rb b/features/support/rerun.rb
index 8b176c5be89..60b78f9d050 100644
--- a/features/support/rerun.rb
+++ b/features/support/rerun.rb
@@ -1,5 +1,7 @@
# The spinach-rerun-reporter doesn't define the on_undefined_step
# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb
+require 'spinach-rerun-reporter'
+
module Spinach
class Reporter
class Rerun
diff --git a/features/support/wait_for_ajax.rb b/features/support/wait_for_ajax.rb
deleted file mode 100644
index b90fc112671..00000000000
--- a/features/support/wait_for_ajax.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module WaitForAjax
- def wait_for_ajax
- Timeout.timeout(Capybara.default_max_wait_time) do
- loop until finished_all_ajax_requests?
- end
- end
-
- def finished_all_ajax_requests?
- page.evaluate_script('jQuery.active').zero?
- end
-end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 9d1d9058996..87915b19480 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -5,32 +5,27 @@ module API
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
resource source_type.pluralize do
- # Get a list of group/project access requests viewable by the authenticated user.
- #
- # Parameters:
- # id (required) - The group/project ID
- #
- # Example Request:
- # GET /groups/:id/access_requests
- # GET /projects/:id/access_requests
+ desc "Gets a list of access requests for a #{source_type}." do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::AccessRequester
+ end
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, source: source
end
- # Request access to the group/project
- #
- # Parameters:
- # id (required) - The group/project ID
- #
- # Example Request:
- # POST /groups/:id/access_requests
- # POST /projects/:id/access_requests
+ desc "Requests access for the authenticated user to a #{source_type}." do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::AccessRequester
+ end
post ":id/access_requests" do
source = find_source(source_type, params[:id])
access_requester = source.request_access(current_user)
@@ -42,42 +37,34 @@ module API
end
end
- # Approve a group/project access request
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the access requester
- # access_level (optional) - Access level
- #
- # Example Request:
- # PUT /groups/:id/access_requests/:user_id/approve
- # PUT /projects/:id/access_requests/:user_id/approve
+ desc 'Approves an access request for the given user.' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+ optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ end
put ':id/access_requests/:user_id/approve' do
- required_attributes! [:user_id]
source = find_source(source_type, params[:id])
- member = ::Members::ApproveAccessRequestService.new(source, current_user, params).execute
+ member = ::Members::ApproveAccessRequestService.new(source, current_user, declared(params)).execute
status :created
present member.user, with: Entities::Member, member: member
end
- # Deny a group/project access request
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the access requester
- #
- # Example Request:
- # DELETE /groups/:id/access_requests/:user_id
- # DELETE /projects/:id/access_requests/:user_id
+ desc 'Denies an access request for the given user.' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+ end
delete ":id/access_requests/:user_id" do
- required_attributes! [:user_id]
source = find_source(source_type, params[:id])
- access_requester = source.requesters.find_by!(user_id: params[:user_id])
-
- ::Members::DestroyService.new(access_requester, current_user).execute
+ ::Members::DestroyService.new(source, current_user, params).
+ execute(:requesters)
end
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index cb47ec8f33f..67109ceeef9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -31,11 +31,12 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::AwardEmoji
+ mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
mount ::API::Builds
- mount ::API::CommitStatuses
mount ::API::Commits
+ mount ::API::CommitStatuses
mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments
@@ -45,19 +46,18 @@ module API
mount ::API::Issues
mount ::API::Keys
mount ::API::Labels
- mount ::API::LicenseTemplates
mount ::API::Lint
mount ::API::Members
- mount ::API::MergeRequests
mount ::API::MergeRequestDiffs
+ mount ::API::MergeRequests
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
mount ::API::Pipelines
mount ::API::ProjectHooks
- mount ::API::ProjectSnippets
mount ::API::Projects
+ mount ::API::ProjectSnippets
mount ::API::Repositories
mount ::API::Runners
mount ::API::Services
@@ -72,5 +72,10 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
+ mount ::API::Version
+
+ route :any, '*path' do
+ error!('404 Not Found', 404)
+ end
end
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 2461a783ea8..e9ccba3b465 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -8,16 +8,19 @@ module API
awardable_string = awardable_type.pluralize
awardable_id_string = "#{awardable_type}_id"
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
+ end
+
[ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint|
- # Get a list of project +awardable+ award emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or MR
- # Example Request:
- # GET /projects/:id/issues/:awardable_id/award_emoji
+
+ desc 'Get a list of project +awardable+ award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
get endpoint do
if can_read_awardable?
awards = paginate(awardable.award_emoji)
@@ -27,14 +30,13 @@ module API
end
end
- # Get a specific award emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or MR
- # award_id (required) - The ID of the award
- # Example Request:
- # GET /projects/:id/issues/:awardable_id/award_emoji/:award_id
+ desc 'Get a specific award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :award_id, type: Integer, desc: 'The ID of the award'
+ end
get "#{endpoint}/:award_id" do
if can_read_awardable?
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
@@ -43,17 +45,14 @@ module API
end
end
- # Award a new Emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or mr
- # name (required) - The name of a award_emoji (without colons)
- # Example Request:
- # POST /projects/:id/issues/:awardable_id/award_emoji
+ desc 'Award a new Emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
+ end
post endpoint do
- required_attributes! [:name]
-
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user)
@@ -65,14 +64,13 @@ module API
end
end
- # Delete a +awardables+ award emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or MR
- # award_emoji_id (required) - The ID of an award emoji
- # Example Request:
- # DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+ desc 'Delete a +awardables+ award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :award_id, type: Integer, desc: 'The ID of an award emoji'
+ end
delete "#{endpoint}/:award_id" do
award = awardable.award_emoji.find(params[:award_id])
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
new file mode 100644
index 00000000000..4ac491edc1b
--- /dev/null
+++ b/lib/api/boards.rb
@@ -0,0 +1,132 @@
+module API
+ # Boards API
+ class Boards < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get all project boards' do
+ detail 'This feature was introduced in 8.13'
+ success Entities::Board
+ end
+ get ':id/boards' do
+ authorize!(:read_board, user_project)
+ present user_project.boards, with: Entities::Board
+ end
+
+ params do
+ requires :board_id, type: Integer, desc: 'The ID of a board'
+ end
+ segment ':id/boards/:board_id' do
+ helpers do
+ def project_board
+ board = user_project.boards.first
+
+ if params[:board_id] == board.id
+ board
+ else
+ not_found!('Board')
+ end
+ end
+
+ def board_lists
+ project_board.lists.destroyable
+ end
+ end
+
+ desc 'Get the lists of a project board' do
+ detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13'
+ success Entities::List
+ end
+ get '/lists' do
+ authorize!(:read_board, user_project)
+ present board_lists, with: Entities::List
+ end
+
+ desc 'Get a list of a project board' do
+ detail 'This feature was introduced in 8.13'
+ success Entities::List
+ end
+ params do
+ requires :list_id, type: Integer, desc: 'The ID of a list'
+ end
+ get '/lists/:list_id' do
+ authorize!(:read_board, user_project)
+ present board_lists.find(params[:list_id]), with: Entities::List
+ end
+
+ desc 'Create a new board list' do
+ detail 'This feature was introduced in 8.13'
+ success Entities::List
+ end
+ params do
+ requires :label_id, type: Integer, desc: 'The ID of an existing label'
+ end
+ post '/lists' do
+ unless available_labels.exists?(params[:label_id])
+ render_api_error!({ error: 'Label not found!' }, 400)
+ end
+
+ authorize!(:admin_list, user_project)
+
+ service = ::Boards::Lists::CreateService.new(user_project, current_user,
+ { label_id: params[:label_id] })
+
+ list = service.execute(project_board)
+
+ if list.valid?
+ present list, with: Entities::List
+ else
+ render_validation_error!(list)
+ end
+ end
+
+ desc 'Moves a board list to a new position' do
+ detail 'This feature was introduced in 8.13'
+ success Entities::List
+ end
+ params do
+ requires :list_id, type: Integer, desc: 'The ID of a list'
+ requires :position, type: Integer, desc: 'The position of the list'
+ end
+ put '/lists/:list_id' do
+ list = project_board.lists.movable.find(params[:list_id])
+
+ authorize!(:admin_list, user_project)
+
+ service = ::Boards::Lists::MoveService.new(user_project, current_user,
+ { position: params[:position] })
+
+ if service.execute(list)
+ present list, with: Entities::List
+ else
+ render_api_error!({ error: "List could not be moved!" }, 400)
+ end
+ end
+
+ desc 'Delete a board list' do
+ detail 'This feature was introduced in 8.13'
+ success Entities::List
+ end
+ params do
+ requires :list_id, type: Integer, desc: 'The ID of a board list'
+ end
+ delete "/lists/:list_id" do
+ authorize!(:admin_list, user_project)
+
+ list = board_lists.find(params[:list_id])
+
+ service = ::Boards::Lists::DestroyService.new(user_project, current_user)
+
+ if service.execute(list)
+ present list, with: Entities::List
+ else
+ render_api_error!({ error: 'List could not be deleted!' }, 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index b615703df93..6d827448994 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -54,43 +54,25 @@ module API
not_found!('Branch') unless @branch
protected_branch = user_project.protected_branches.find_by(name: @branch.name)
- developers_can_merge = to_boolean(params[:developers_can_merge])
- developers_can_push = to_boolean(params[:developers_can_push])
-
protected_branch_params = {
- name: @branch.name
+ name: @branch.name,
+ developers_can_push: to_boolean(params[:developers_can_push]),
+ developers_can_merge: to_boolean(params[:developers_can_merge])
}
- # If `developers_can_merge` is switched off, _all_ `DEVELOPER`
- # merge_access_levels need to be deleted.
- if developers_can_merge == false
- protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
- end
+ service_args = [user_project, current_user, protected_branch_params]
- # If `developers_can_push` is switched off, _all_ `DEVELOPER`
- # push_access_levels need to be deleted.
- if developers_can_push == false
- protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
- end
+ protected_branch = if protected_branch
+ ProtectedBranches::ApiUpdateService.new(*service_args).execute(protected_branch)
+ else
+ ProtectedBranches::ApiCreateService.new(*service_args).execute
+ end
- protected_branch_params.merge!(
- merge_access_levels_attributes: [{
- access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }],
- push_access_levels_attributes: [{
- access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }]
- )
-
- if protected_branch
- service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
- service.execute(protected_branch)
+ if protected_branch.valid?
+ present @branch, with: Entities::RepoBranch, project: user_project
else
- service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params)
- service.execute
+ render_api_error!(protected_branch.errors.full_messages, 422)
end
-
- present @branch, with: Entities::RepoBranch, project: user_project
end
# Unprotect a single branch
@@ -123,7 +105,7 @@ module API
post ":id/repository/branches" do
authorize_push_project
result = CreateBranchService.new(user_project, current_user).
- execute(params[:branch_name], params[:ref])
+ execute(params[:branch_name], params[:ref])
if result[:status] == :success
present result[:branch],
@@ -142,10 +124,10 @@ module API
# Example Request:
# DELETE /projects/:id/repository/branches/:branch
delete ":id/repository/branches/:branch",
- requirements: { branch: /.+/ } do
+ requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
- execute(params[:branch])
+ execute(params[:branch])
if result[:status] == :success
{
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index 52bdbcae5a8..67adca6605f 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -3,15 +3,32 @@ module API
class Builds < Grape::API
before { authenticate! }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a project builds
- #
- # Parameters:
- # id (required) - The ID of a project
- # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
- # if none provided showing all builds)
- # Example Request:
- # GET /projects/:id/builds
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: ['pending', 'running', 'failed', 'success', 'canceled'],
+ coerce_with: ->(scope) {
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
+ desc 'Get a project builds' do
+ success Entities::Build
+ end
+ params do
+ use :optional_scope
+ end
get ':id/builds' do
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
@@ -20,15 +37,13 @@ module API
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- # Get builds for a specific commit of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The SHA id of a commit
- # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
- # if none provided showing all builds)
- # Example Request:
- # GET /projects/:id/repository/commits/:sha/builds
+ desc 'Get builds for a specific commit of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :sha, type: String, desc: 'The SHA id of a commit'
+ use :optional_scope
+ end
get ':id/repository/commits/:sha/builds' do
authorize_read_builds!
@@ -42,13 +57,12 @@ module API
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- # Get a specific build of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # build_id (required) - The ID of a build
- # Example Request:
- # GET /projects/:id/builds/:build_id
+ desc 'Get a specific build of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
get ':id/builds/:build_id' do
authorize_read_builds!
@@ -58,13 +72,12 @@ module API
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- # Download the artifacts file from build
- #
- # Parameters:
- # id (required) - The ID of a build
- # token (required) - The build authorization token
- # Example Request:
- # GET /projects/:id/builds/:build_id/artifacts
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
get ':id/builds/:build_id/artifacts' do
authorize_read_builds!
@@ -73,14 +86,13 @@ module API
present_artifacts!(build.artifacts_file)
end
- # Download the artifacts file from ref_name and job
- #
- # Parameters:
- # id (required) - The ID of a project
- # ref_name (required) - The ref from repository
- # job (required) - The name for the build
- # Example Request:
- # GET /projects/:id/builds/artifacts/:ref_name/download?job=name
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the build'
+ end
get ':id/builds/artifacts/:ref_name/download',
requirements: { ref_name: /.+/ } do
authorize_read_builds!
@@ -91,17 +103,13 @@ module API
present_artifacts!(latest_build.artifacts_file)
end
- # Get a trace of a specific build of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # build_id (required) - The ID of a build
- # Example Request:
- # GET /projects/:id/build/:build_id/trace
- #
# TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ desc 'Get a trace of a specific build of a project'
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
get ':id/builds/:build_id/trace' do
authorize_read_builds!
@@ -115,13 +123,12 @@ module API
body trace
end
- # Cancel a specific build of a project
- #
- # parameters:
- # id (required) - the id of a project
- # build_id (required) - the id of a build
- # example request:
- # post /projects/:id/build/:build_id/cancel
+ desc 'Cancel a specific build of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
post ':id/builds/:build_id/cancel' do
authorize_update_builds!
@@ -133,13 +140,12 @@ module API
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- # Retry a specific build of a project
- #
- # parameters:
- # id (required) - the id of a project
- # build_id (required) - the id of a build
- # example request:
- # post /projects/:id/build/:build_id/retry
+ desc 'Retry a specific build of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
post ':id/builds/:build_id/retry' do
authorize_update_builds!
@@ -152,13 +158,12 @@ module API
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- # Erase build (remove artifacts and build trace)
- #
- # Parameters:
- # id (required) - the id of a project
- # build_id (required) - the id of a build
- # example Request:
- # post /projects/:id/build/:build_id/erase
+ desc 'Erase build (remove artifacts and build trace)' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
post ':id/builds/:build_id/erase' do
authorize_update_builds!
@@ -170,13 +175,12 @@ module API
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
- # Keep the artifacts to prevent them from being deleted
- #
- # Parameters:
- # id (required) - the id of a project
- # build_id (required) - The ID of a build
- # Example Request:
- # POST /projects/:id/builds/:build_id/artifacts/keep
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
post ':id/builds/:build_id/artifacts/keep' do
authorize_update_builds!
@@ -235,14 +239,6 @@ module API
return builds if scope.nil? || scope.empty?
available_statuses = ::CommitStatus::AVAILABLE_STATUSES
- scope =
- if scope.is_a?(String)
- [scope]
- elsif scope.is_a?(Hashie::Mash)
- scope.values
- else
- ['unknown']
- end
unknown = scope - available_statuses
render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index dfbdd597d29..f54d4f06627 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -6,17 +6,17 @@ module API
resource :projects do
before { authenticate! }
- # Get a commit's statuses
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit hash
- # ref (optional) - The ref
- # stage (optional) - The stage
- # name (optional) - The name
- # all (optional) - Show all statuses, default: false
- # Examples:
- # GET /projects/:id/repository/commits/:sha/statuses
+ desc "Get a commit's statuses" do
+ success Entities::CommitStatus
+ end
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :sha, type: String, desc: 'The commit hash'
+ optional :ref, type: String, desc: 'The ref'
+ optional :stage, type: String, desc: 'The stage'
+ optional :name, type: String, desc: 'The name'
+ optional :all, type: String, desc: 'Show all statuses, default: false'
+ end
get ':id/repository/commits/:sha/statuses' do
authorize!(:read_commit_status, user_project)
@@ -31,22 +31,23 @@ module API
present paginate(statuses), with: Entities::CommitStatus
end
- # Post status to commit
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit hash
- # ref (optional) - The ref
- # state (required) - The state of the status. Can be: pending, running, success, 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"
- # Examples:
- # POST /projects/:id/statuses/:sha
+ desc 'Post status to a commit' do
+ success Entities::CommitStatus
+ end
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :sha, type: String, desc: 'The commit hash'
+ requires :state, type: String, desc: 'The state of the status',
+ values: ['pending', 'running', 'success', 'failed', 'canceled']
+ optional :ref, type: String, desc: 'The ref'
+ optional :target_url, type: String, desc: 'The target URL to associate with this status'
+ optional :description, type: String, desc: 'A short description of the status'
+ optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
+ optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
+ end
post ':id/statuses/:sha' do
authorize! :create_commit_status, user_project
- required_attributes! [:state]
- attrs = attributes_for_keys [:target_url, :description]
+
commit = @project.commit(params[:sha])
not_found! 'Commit' unless commit
@@ -66,9 +67,14 @@ module API
pipeline = @project.ensure_pipeline(ref, commit.sha, current_user)
status = GenericCommitStatus.running_or_pending.find_or_initialize_by(
- project: @project, pipeline: pipeline,
- user: current_user, name: name, ref: ref)
- status.attributes = attrs
+ project: @project,
+ pipeline: pipeline,
+ user: current_user,
+ name: name,
+ ref: ref,
+ target_url: params[:target_url],
+ description: params[:description]
+ )
begin
case params[:state].to_s
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index b4eaf1813d4..2f2cf769481 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -6,102 +6,149 @@ module API
before { authenticate! }
before { authorize! :download_code, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a project repository commits
- #
- # Parameters:
- # id (required) - The ID of a project
- # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used
- # since (optional) - Only commits after or in this date will be returned
- # until (optional) - Only commits before or in this date will be returned
- # Example Request:
- # GET /projects/:id/repository/commits
+ desc 'Get a project repository commits' do
+ success Entities::RepoCommit
+ end
+ params do
+ optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+ optional :since, type: String, desc: 'Only commits after or in this date will be returned'
+ optional :until, type: String, desc: 'Only commits before or in this date will be returned'
+ optional :page, type: Integer, default: 0, desc: 'The page for pagination'
+ optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
+ optional :path, type: String, desc: 'The file path'
+ end
get ":id/repository/commits" do
+ # TODO remove the next line for 9.0, use DateTime type in the params block
datetime_attributes! :since, :until
- page = (params[:page] || 0).to_i
- per_page = (params[:per_page] || 20).to_i
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
- after = params[:since]
- before = params[:until]
+ offset = params[:page] * params[:per_page]
+
+ commits = user_project.repository.commits(ref,
+ path: params[:path],
+ limit: params[:per_page],
+ offset: offset,
+ after: params[:since],
+ before: params[:until])
- commits = user_project.repository.commits(ref, limit: per_page, offset: page * per_page, after: after, before: before)
present commits, with: Entities::RepoCommit
end
- # Get a specific commit of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit hash or name of a repository branch or tag
- # Example Request:
- # GET /projects/:id/repository/commits/:sha
+ desc 'Commit multiple file changes as one commit' do
+ success Entities::RepoCommitDetail
+ detail 'This feature was introduced in GitLab 8.13'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The project ID'
+ requires :branch_name, type: String, desc: 'The name of branch'
+ requires :commit_message, type: String, desc: 'Commit message'
+ requires :actions, type: Array, desc: 'Actions to perform in commit'
+ optional :author_email, type: String, desc: 'Author email for commit'
+ optional :author_name, type: String, desc: 'Author name for commit'
+ end
+ post ":id/repository/commits" do
+ authorize! :push_code, user_project
+
+ attrs = declared(params)
+ attrs[:source_branch] = attrs[:branch_name]
+ attrs[:target_branch] = attrs[:branch_name]
+ attrs[:actions].map! do |action|
+ action[:action] = action[:action].to_sym
+ action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
+ action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
+ action
+ end
+
+ result = ::Files::MultiService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commits(result[:result], limit: 1).first
+ present commit_detail, with: Entities::RepoCommitDetail
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Get a specific commit of a project' do
+ success Entities::RepoCommitDetail
+ failure [[404, 'Not Found']]
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
get ":id/repository/commits/:sha" do
- sha = params[:sha]
- commit = user_project.commit(sha)
+ commit = user_project.commit(params[:sha])
+
not_found! "Commit" unless commit
+
present commit, with: Entities::RepoCommitDetail
end
- # Get the diff for a specific commit of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit or branch name
- # Example Request:
- # GET /projects/:id/repository/commits/:sha/diff
+ desc 'Get the diff for a specific commit of a project' do
+ failure [[404, 'Not Found']]
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
get ":id/repository/commits/:sha/diff" do
- sha = params[:sha]
- commit = user_project.commit(sha)
+ commit = user_project.commit(params[:sha])
+
not_found! "Commit" unless commit
+
commit.raw_diffs.to_a
end
- # Get a commit's comments
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit hash
- # Examples:
- # GET /projects/:id/repository/commits/:sha/comments
+ desc "Get a commit's comments" do
+ success Entities::CommitNote
+ failure [[404, 'Not Found']]
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ optional :per_page, type: Integer, desc: 'The amount of items per page for paginaion'
+ optional :page, type: Integer, desc: 'The page number for pagination'
+ end
get ':id/repository/commits/:sha/comments' do
- sha = params[:sha]
- commit = user_project.commit(sha)
+ commit = user_project.commit(params[:sha])
+
not_found! 'Commit' unless commit
notes = Note.where(commit_id: commit.id).order(:created_at)
+
present paginate(notes), with: Entities::CommitNote
end
- # Post comment to commit
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit hash
- # note (required) - Text of comment
- # path (optional) - The file path
- # line (optional) - The line number
- # line_type (optional) - The type of line (new or old)
- # Examples:
- # POST /projects/:id/repository/commits/:sha/comments
+ desc 'Post comment to commit' do
+ success Entities::CommitNote
+ end
+ params do
+ requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA"
+ requires :note, type: String, desc: 'The text of the comment'
+ optional :path, type: String, desc: 'The file path'
+ given :path do
+ requires :line, type: Integer, desc: 'The line number'
+ requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line'
+ end
+ end
post ':id/repository/commits/:sha/comments' do
- required_attributes! [:note]
-
- sha = params[:sha]
- commit = user_project.commit(sha)
+ commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
+
opts = {
note: params[:note],
noteable_type: 'Commit',
commit_id: commit.id
}
- if params[:path] && params[:line] && params[:line_type]
+ if params[:path]
commit.raw_diffs(all_diffs: true).each do |diff|
next unless diff.new_path == params[:path]
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
lines.each do |line|
- next unless line.new_pos == params[:line].to_i && line.type == params[:line_type]
+ next unless line.new_pos == params[:line] && line.type == params[:line_type]
break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index b081cb8e520..e1995aa6f4b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -344,7 +344,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
@@ -433,8 +433,11 @@ module API
end
end
- class Label < Grape::Entity
+ class LabelBasic < Grape::Entity
expose :name, :color, :description
+ end
+
+ class Label < LabelBasic
expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
expose :subscribed do |label, options|
@@ -442,6 +445,19 @@ module API
end
end
+ class List < Grape::Entity
+ expose :id
+ expose :label, using: Entities::LabelBasic
+ expose :position
+ end
+
+ class Board < Grape::Entity
+ expose :id
+ expose :lists, using: Entities::List do |board|
+ board.lists.destroyable
+ end
+ end
+
class Compare < Grape::Entity
expose :commit, using: Entities::RepoCommit do |compare, options|
Commit.decorate(compare.commits, nil).last
@@ -495,6 +511,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
@@ -546,6 +564,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
@@ -553,6 +575,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
@@ -563,8 +586,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/groups.rb b/lib/api/groups.rb
index e9abd53e62b..8ca4cfa68cd 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -6,6 +6,8 @@ module API
resource :groups do
# Get a groups list
#
+ # Parameters:
+ # skip_groups (optional) - Array of group ids to exclude from list
# Example Request:
# GET /groups
get do
@@ -16,6 +18,7 @@ module API
end
@groups = @groups.search(params[:search]) if params[:search].present?
+ @groups = @groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
@groups = paginate @groups
present @groups, with: Entities::Group
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 9d71e90075a..d05841def89 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -21,8 +21,11 @@ module API
end
# Check the Rails session for valid authentication details
+ #
+ # Until CSRF protection is added to the API, disallow this method for
+ # state-changing endpoints
def find_user_from_warden
- warden ? warden.authenticate : nil
+ warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def find_user_by_private_token
@@ -68,6 +71,10 @@ module API
@project ||= find_project(params[:id])
end
+ def available_labels
+ @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
+ end
+
def find_project(id)
project = Project.find_with_namespace(id) || Project.find_by(id: id)
@@ -115,7 +122,7 @@ module API
end
def find_project_label(id)
- label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id)
+ label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
label || not_found!('Label')
end
@@ -194,16 +201,11 @@ module API
def validate_label_params(params)
errors = {}
- if params[:labels].present?
- params[:labels].split(',').each do |label_name|
- label = user_project.labels.create_with(
- color: Label::DEFAULT_COLOR).find_or_initialize_by(
- title: label_name.strip)
+ params[:labels].to_s.split(',').each do |label_name|
+ label = available_labels.find_or_initialize_by(title: label_name.strip)
+ next if label.valid?
- if label.invalid?
- errors[label.title] = label.errors
- end
- end
+ errors[label.title] = label.errors
end
errors
@@ -430,7 +432,7 @@ module API
end
def secret_token
- File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+ Gitlab::Shell.secret_token
end
def send_git_blob(repository, blob)
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index 2b723b79504..767f27ef334 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -4,10 +4,9 @@ module API
before { authenticate! }
resource :keys do
- # Get single ssh key by id. Only available to admin users.
- #
- # Example Request:
- # GET /keys/:id
+ desc 'Get single ssh key by id. Only available to admin users' do
+ success Entities::SSHKeyWithUser
+ end
get ":id" do
authenticated_as_admin!
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index c806829d69e..326e1e7ae00 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -3,37 +3,32 @@ module API
class Labels < Grape::API
before { authenticate! }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get all labels of the project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/labels
+ desc 'Get all labels of the project' do
+ success Entities::Label
+ end
get ':id/labels' do
- present user_project.labels, with: Entities::Label, current_user: current_user
+ present available_labels, with: Entities::Label, current_user: current_user
end
- # Creates a new label
- #
- # Parameters:
- # id (required) - The ID of a project
- # name (required) - The name of the label to be created
- # color (required) - Color of the label given in 6-digit hex
- # notation with leading '#' sign (e.g. #FFAABB)
- # description (optional) - The description of label to be created
- # Example Request:
- # POST /projects/:id/labels
+ desc 'Create a new label' do
+ success Entities::Label
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be created'
+ requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+ optional :description, type: String, desc: 'The description of label to be created'
+ end
post ':id/labels' do
authorize! :admin_label, user_project
- required_attributes! [:name, :color]
-
- attrs = attributes_for_keys [:name, :color, :description]
- label = user_project.find_label(attrs[:name])
+ label = user_project.find_label(params[:name])
conflict!('Label already exists') if label
- label = user_project.labels.create(attrs)
+ label = user_project.labels.create(declared(params, include_parent_namespaces: false).to_h)
if label.valid?
present label, with: Entities::Label, current_user: current_user
@@ -42,54 +37,44 @@ module API
end
end
- # Deletes an existing label
- #
- # Parameters:
- # id (required) - The ID of a project
- # name (required) - The name of the label to be deleted
- #
- # Example Request:
- # DELETE /projects/:id/labels
+ desc 'Delete an existing label' do
+ success Entities::Label
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be deleted'
+ end
delete ':id/labels' do
authorize! :admin_label, user_project
- required_attributes! [:name]
label = user_project.find_label(params[:name])
not_found!('Label') unless label
- label.destroy
+ present label.destroy, with: Entities::Label, current_user: current_user
end
- # Updates an existing label. At least one optional parameter is required.
- #
- # Parameters:
- # id (required) - The ID of a project
- # name (required) - The name of the label to be deleted
- # new_name (optional) - The new name of the label
- # color (optional) - Color of the label given in 6-digit hex
- # notation with leading '#' sign (e.g. #FFAABB)
- # description (optional) - The description of label to be created
- # Example Request:
- # PUT /projects/:id/labels
+ desc 'Update an existing label. At least one optional parameter is required.' do
+ success Entities::Label
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be updated'
+ optional :new_name, type: String, desc: 'The new name of the label'
+ optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+ optional :description, type: String, desc: 'The new description of label'
+ at_least_one_of :new_name, :color, :description
+ end
put ':id/labels' do
authorize! :admin_label, user_project
- required_attributes! [:name]
label = user_project.find_label(params[:name])
not_found!('Label not found') unless label
- attrs = attributes_for_keys [:new_name, :color, :description]
-
- if attrs.empty?
- render_api_error!('Required parameters "new_name" or "color" ' \
- 'missing',
- 400)
- end
-
+ update_params = declared(params,
+ include_parent_namespaces: false,
+ include_missing: false).to_h
# Rename new name to the actual label attribute name
- attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name)
+ update_params['name'] = update_params.delete('new_name') if update_params.key?('new_name')
- if label.update(attrs)
+ if label.update(update_params)
present label, with: Entities::Label, current_user: current_user
else
render_validation_error!(label)
diff --git a/lib/api/license_templates.rb b/lib/api/license_templates.rb
deleted file mode 100644
index d0552299ed0..00000000000
--- a/lib/api/license_templates.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-module API
- # License Templates API
- class LicenseTemplates < Grape::API
- PROJECT_TEMPLATE_REGEX =
- /[\<\{\[]
- (project|description|
- one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]/xi.freeze
- YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
- FULLNAME_TEMPLATE_REGEX =
- /[\<\{\[]
- (fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]/xi.freeze
-
- # Get the list of the available license templates
- #
- # Parameters:
- # popular - Filter licenses to only the popular ones
- #
- # Example Request:
- # GET /licenses
- # GET /licenses?popular=1
- get 'licenses' do
- options = {
- featured: params[:popular].present? ? true : nil
- }
- present Licensee::License.all(options), with: Entities::RepoLicense
- end
-
- # Get text for specific license
- #
- # Parameters:
- # key (required) - The key of a license
- # project - Copyrighted project name
- # fullname - Full name of copyright holder
- #
- # Example Request:
- # GET /licenses/mit
- #
- get 'licenses/:key', requirements: { key: /[\w\.-]+/ } do
- required_attributes! [:key]
-
- not_found!('License') unless Licensee::License.find(params[:key])
-
- # We create a fresh Licensee::License object since we'll modify its
- # content in place below.
- license = Licensee::License.new(params[:key])
-
- license.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
- license.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
-
- fullname = params[:fullname].presence || current_user.try(:name)
- license.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
-
- present license, with: Entities::RepoLicense
- end
- end
-end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 37f0a6512f4..b80818f0eb6 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -5,16 +5,16 @@ module API
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
resource source_type.pluralize do
- # Get a list of group/project members viewable by the authenticated user.
- #
- # Parameters:
- # id (required) - The group/project ID
- # query - Query string
- #
- # Example Request:
- # GET /groups/:id/members
- # GET /projects/:id/members
+ desc 'Gets a list of group or project members viewable by the authenticated user.' do
+ success Entities::Member
+ end
+ params do
+ optional :query, type: String, desc: 'A query string to search for members'
+ end
get ":id/members" do
source = find_source(source_type, params[:id])
@@ -25,15 +25,12 @@ module API
present users, with: Entities::Member, source: source
end
- # Get a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- #
- # Example Request:
- # GET /groups/:id/members/:user_id
- # GET /projects/:id/members/:user_id
+ desc 'Gets a member of a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
get ":id/members/:user_id" do
source = find_source(source_type, params[:id])
@@ -43,48 +40,34 @@ module API
present member.user, with: Entities::Member, member: member
end
- # Add a new group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the new member
- # access_level (required) - A valid access level
- # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
- #
- # Example Request:
- # POST /groups/:id/members
- # POST /projects/:id/members
+ desc 'Adds a member to a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
post ":id/members" do
source = find_source(source_type, params[:id])
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
- # for both project and group members in 9.0!
+ # We need this explicit check because `source.add_user` doesn't
+ # currently return the member created so it would return 201 even if
+ # the member already existed...
+ # The `source_type == 'group'` check is to ensure back-compatibility
+ # but 409 behavior should be used for both project and group members in 9.0!
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)
@@ -92,21 +75,17 @@ module API
end
end
- # Update a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- # access_level (required) - A valid access level
- # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
- #
- # Example Request:
- # PUT /groups/:id/members/:user_id
- # PUT /projects/:id/members/:user_id
+ desc 'Updates a member of a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
put ":id/members/:user_id" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
- required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
attrs = attributes_for_keys [:access_level, :expires_at]
@@ -121,18 +100,12 @@ module API
end
end
- # Remove a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- #
- # Example Request:
- # DELETE /groups/:id/members/:user_id
- # DELETE /projects/:id/members/:user_id
+ desc 'Removes a user from a group or project.'
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
- required_attributes! [:user_id]
# This is to ensure back-compatibility but find_by! should be used
# in that casse in 9.0!
@@ -147,7 +120,7 @@ module API
if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
- ::Members::DestroyService.new(member, current_user).execute
+ ::Members::DestroyService.new(source, current_user, declared(params)).execute
present member.user, with: Entities::Member, member: member
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 2b685621da9..bf8504e1101 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -86,14 +86,11 @@ module API
render_api_error!({ labels: errors }, 400)
end
+ attrs[:labels] = params[:labels] if params[:labels]
+
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
if merge_request.valid?
- # Find or create labels and attach to issue
- if params[:labels].present?
- merge_request.add_labels_by_names(params[:labels].split(","))
- end
-
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors
@@ -195,15 +192,11 @@ module API
render_api_error!({ labels: errors }, 400)
end
+ attrs[:labels] = params[:labels] if params[:labels]
+
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
if merge_request.valid?
- # Find or create labels and attach to issue
- unless params[:labels].nil?
- merge_request.remove_labels
- merge_request.add_labels_by_names(params[:labels].split(","))
- end
-
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors
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/namespaces.rb b/lib/api/namespaces.rb
index 25dc41a3349..cd398274507 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -4,20 +4,18 @@ module API
before { authenticate! }
resource :namespaces do
- # Get a namespaces list
- #
- # Example Request:
- # GET /namespaces
+ desc 'Get a namespaces list' do
+ success Entities::Namespace
+ end
+ params do
+ optional :search, type: String, desc: "Search query for namespaces"
+ end
get do
- @namespaces = if current_user.admin?
- Namespace.all
- else
- current_user.namespaces
- end
- @namespaces = @namespaces.search(params[:search]) if params[:search].present?
- @namespaces = paginate @namespaces
+ namespaces = current_user.admin? ? Namespace.all : current_user.namespaces
+
+ namespaces = namespaces.search(params[:search]) if params[:search].present?
- present @namespaces, with: Entities::Namespace
+ present paginate(namespaces), with: Entities::Namespace
end
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6d99617b56f..da16e24d7ea 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -22,14 +22,25 @@ module API
# Example Request:
# GET /projects
get do
- @projects = current_user.authorized_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- if params[:simple]
- present @projects, with: Entities::BasicProjectDetails, user: current_user
- else
- present @projects, with: Entities::ProjectWithAccess, user: current_user
- end
+ projects = current_user.authorized_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+ present projects, with: entity, user: current_user
+ end
+
+ # Get a list of visible projects for authenticated user
+ #
+ # Example Request:
+ # GET /projects/visible
+ get '/visible' do
+ projects = ProjectsFinder.new.execute(current_user)
+ projects = filter_projects(projects)
+ projects = paginate projects
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+ present projects, with: entity, user: current_user
end
# Get an owned projects list for authenticated user
@@ -37,10 +48,10 @@ module API
# Example Request:
# GET /projects/owned
get '/owned' do
- @projects = current_user.owned_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::ProjectWithAccess, user: current_user
+ projects = current_user.owned_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::ProjectWithAccess, user: current_user
end
# Gets starred project for the authenticated user
@@ -48,10 +59,10 @@ module API
# Example Request:
# GET /projects/starred
get '/starred' do
- @projects = current_user.viewable_starred_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::Project, user: current_user
+ projects = current_user.viewable_starred_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::Project, user: current_user
end
# Get all projects for admin user
@@ -60,10 +71,10 @@ module API
# GET /projects/all
get '/all' do
authenticated_as_admin!
- @projects = Project.all
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::ProjectWithAccess, user: current_user
+ projects = Project.all
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::ProjectWithAccess, user: current_user
end
# Get a single project
@@ -393,23 +404,30 @@ 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]
+
+ group = Group.find_by_id(attrs[:group_id])
+
+ unless group && can?(current_user, :read_group, group)
+ not_found!('Group')
+ end
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
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 22b8f90dc5c..794e34874f4 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -7,38 +7,36 @@ module API
end
resource :hooks do
- # Get the list of system hooks
- #
- # Example Request:
- # GET /hooks
+ desc 'Get the list of system hooks' do
+ success Entities::Hook
+ end
get do
- @hooks = SystemHook.all
- present @hooks, with: Entities::Hook
+ hooks = SystemHook.all
+ present hooks, with: Entities::Hook
end
- # Create new system hook
- #
- # Parameters:
- # url (required) - url for system hook
- # Example Request
- # POST /hooks
+ desc 'Create a new system hook' do
+ success Entities::Hook
+ end
+ params do
+ requires :url, type: String, desc: 'The URL for the system hook'
+ end
post do
- attrs = attributes_for_keys [:url]
- required_attributes! [:url]
- @hook = SystemHook.new attrs
- if @hook.save
- present @hook, with: Entities::Hook
+ hook = SystemHook.new declared(params).to_h
+
+ if hook.save
+ present hook, with: Entities::Hook
else
not_found!
end
end
- # Test a hook
- #
- # Example Request
- # GET /hooks/:id
+ desc 'Test a hook'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the system hook'
+ end
get ":id" do
- @hook = SystemHook.find(params[:id])
+ hook = SystemHook.find(params[:id])
data = {
event_name: "project_create",
name: "Ruby",
@@ -47,23 +45,21 @@ module API
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
- @hook.execute(data, 'system_hooks')
+ hook.execute(data, 'system_hooks')
data
end
- # Delete a hook. This is an idempotent function.
- #
- # Parameters:
- # id (required) - ID of the hook
- # Example Request:
- # DELETE /hooks/:id
+ desc 'Delete a hook' do
+ success Entities::Hook
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the system hook'
+ end
delete ":id" do
- begin
- @hook = SystemHook.find(params[:id])
- @hook.destroy
- rescue
- # SystemHook raises an Error if no hook with id found
- end
+ hook = SystemHook.find_by(id: params[:id])
+ not_found!('System hook') unless hook
+
+ present hook.destroy, with: Entities::Hook
end
end
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index b9e718147e1..8a53d9c0095 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,39 +1,115 @@
module API
class Templates < Grape::API
GLOBAL_TEMPLATE_TYPES = {
- gitignores: Gitlab::Template::GitignoreTemplate,
- gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
+ gitignores: {
+ klass: Gitlab::Template::GitignoreTemplate,
+ gitlab_version: 8.8
+ },
+ gitlab_ci_ymls: {
+ klass: Gitlab::Template::GitlabCiYmlTemplate,
+ gitlab_version: 8.9
+ }
}.freeze
+ PROJECT_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (project|description|
+ one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+ [\>\}\]]/xi.freeze
+ YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+ FULLNAME_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (fullname|name\sof\s(author|copyright\sowner))
+ [\>\}\]]/xi.freeze
+ DEPRECATION_MESSAGE = ' This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
helpers do
+ def parsed_license_template
+ # We create a fresh Licensee::License object since we'll modify its
+ # content in place below.
+ template = Licensee::License.new(params[:name])
+
+ template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
+ template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
+
+ fullname = params[:fullname].presence || current_user.try(:name)
+ template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
+ template
+ end
+
def render_response(template_type, template)
not_found!(template_type.to_s.singularize) unless template
present template, with: Entities::Template
end
end
- GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
- # Get the list of the available template
- #
- # Example Request:
- # GET /gitignores
- # GET /gitlab_ci_ymls
- get template_type.to_s do
- present klass.all, with: Entities::TemplatesList
- end
-
- # Get the text for a specific template present in local filesystem
- #
- # Parameters:
- # name (required) - The name of a template
- #
- # Example Request:
- # GET /gitignores/Elixir
- # GET /gitlab_ci_ymls/Ruby
- get "#{template_type}/:name" do
- required_attributes! [:name]
- new_template = klass.find(params[:name])
- render_response(template_type, new_template)
+ { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status|
+ desc 'Get the list of the available license template' do
+ detailed_desc = 'This feature was introduced in GitLab 8.7.'
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::RepoLicense
+ end
+ params do
+ optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
+ end
+ get route do
+ options = {
+ featured: declared(params).popular.present? ? true : nil
+ }
+ present Licensee::License.all(options), with: Entities::RepoLicense
+ end
+ end
+
+ { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status|
+ desc 'Get the text for a specific license' do
+ detailed_desc = 'This feature was introduced in GitLab 8.7.'
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::RepoLicense
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get route, requirements: { name: /[\w\.-]+/ } do
+ not_found!('License') unless Licensee::License.find(declared(params).name)
+
+ template = parsed_license_template
+
+ present template, with: Entities::RepoLicense
+ end
+ end
+
+ GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
+ klass = properties[:klass]
+ gitlab_version = properties[:gitlab_version]
+
+ { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status|
+ desc 'Get the list of the available template' do
+ detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::TemplatesList
+ end
+ get route do
+ present klass.all, with: Entities::TemplatesList
+ end
+ end
+
+ { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status|
+ desc 'Get the text for a specific template present in local filesystem' do
+ detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::Template
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get route do
+ new_template = klass.find(declared(params).name)
+
+ render_response(template_type, new_template)
+ end
end
end
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 19df13d8aac..832b04a3bb1 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -8,18 +8,19 @@ module API
'issues' => ->(id) { find_project_issue(id) }
}
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
ISSUABLE_TYPES.each do |type, finder|
type_id_str = "#{type.singularize}_id".to_sym
- # Create a todo on an issuable
- #
- # Parameters:
- # id (required) - The ID of a project
- # issuable_id (required) - The ID of an issuable
- # Example Request:
- # POST /projects/:id/issues/:issuable_id/todo
- # POST /projects/:id/merge_requests/:issuable_id/todo
+ desc 'Create a todo on an issuable' do
+ success Entities::Todo
+ end
+ params do
+ requires type_id_str, type: Integer, desc: 'The ID of an issuable'
+ end
post ":id/#{type}/:#{type_id_str}/todo" do
issuable = instance_exec(params[type_id_str], &finder)
todo = TodoService.new.mark_todo(issuable, current_user).first
@@ -40,25 +41,21 @@ module API
end
end
- # Get a todo list
- #
- # Example Request:
- # GET /todos
- #
+ desc 'Get a todo list' do
+ success Entities::Todo
+ end
get do
todos = find_todos
present paginate(todos), with: Entities::Todo, current_user: current_user
end
- # Mark a todo as done
- #
- # Parameters:
- # id: (required) - The ID of the todo being marked as done
- #
- # Example Request:
- # DELETE /todos/:id
- #
+ desc 'Mark a todo as done' do
+ success Entities::Todo
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
+ end
delete ':id' do
todo = current_user.todos.find(params[:id])
TodoService.new.mark_todos_as_done([todo], current_user)
@@ -66,11 +63,7 @@ module API
present todo.reload, with: Entities::Todo, current_user: current_user
end
- # Mark all todos as done
- #
- # Example Request:
- # DELETE /todos
- #
+ desc 'Mark all todos as done'
delete do
todos = find_todos
TodoService.new.mark_todos_as_done(todos, current_user)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 4924182770b..6ab9786a97a 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -338,6 +338,26 @@ module API
user.activate
end
end
+
+ desc 'Get contribution events of a specified user' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ success Entities::Event
+ end
+ params do
+ requires :id, type: String, desc: 'The user ID'
+ end
+ get ':id/events' do
+ user = User.find_by(id: declared(params).id)
+ not_found!('User') unless user
+
+ events = user.recent_events.
+ merge(ProjectsFinder.new.execute(current_user)).
+ references(:project).
+ with_associations.
+ page(params[:page])
+
+ present paginate(events), with: Entities::Event
+ end
end
resource :user do
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f6495071a11..b9fb3c21dbb 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -4,27 +4,29 @@ module API
before { authenticate! }
before { authorize! :admin_build, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
resource :projects do
- # Get project variables
- #
- # Parameters:
- # id (required) - The ID of a project
- # page (optional) - The page number for pagination
- # per_page (optional) - The value of items per page to show
- # Example Request:
- # GET /projects/:id/variables
+ desc 'Get project variables' do
+ success Entities::Variable
+ end
+ params do
+ optional :page, type: Integer, desc: 'The page number for pagination'
+ optional :per_page, type: Integer, desc: 'The value of items per page to show'
+ end
get ':id/variables' do
variables = user_project.variables
present paginate(variables), with: Entities::Variable
end
- # Get specific variable of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (required) - The `key` of variable
- # Example Request:
- # GET /projects/:id/variables/:key
+ desc 'Get a specific variable from a project' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
get ':id/variables/:key' do
key = params[:key]
variable = user_project.variables.find_by(key: key.to_s)
@@ -34,18 +36,15 @@ module API
present variable, with: Entities::Variable
end
- # Create a new variable in project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (required) - The key of variable
- # value (required) - The value of variable
- # Example Request:
- # POST /projects/:id/variables
+ desc 'Create a new variable in a project' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ requires :value, type: String, desc: 'The value of the variable'
+ end
post ':id/variables' do
- required_attributes! [:key, :value]
-
- variable = user_project.variables.create(key: params[:key], value: params[:value])
+ variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h)
if variable.valid?
present variable, with: Entities::Variable
@@ -54,41 +53,37 @@ module API
end
end
- # Update existing variable of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (optional) - The `key` of variable
- # value (optional) - New value for `value` field of variable
- # Example Request:
- # PUT /projects/:id/variables/:key
+ desc 'Update an existing variable from a project' do
+ success Entities::Variable
+ end
+ params do
+ optional :key, type: String, desc: 'The key of the variable'
+ optional :value, type: String, desc: 'The value of the variable'
+ end
put ':id/variables/:key' do
- variable = user_project.variables.find_by(key: params[:key].to_s)
+ variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable
- attrs = attributes_for_keys [:value]
- if variable.update(attrs)
+ if variable.update(value: params[:value])
present variable, with: Entities::Variable
else
render_validation_error!(variable)
end
end
- # Delete existing variable of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (required) - The ID of a variable
- # Example Request:
- # DELETE /projects/:id/variables/:key
+ desc 'Delete an existing variable from a project' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
delete ':id/variables/:key' do
- variable = user_project.variables.find_by(key: params[:key].to_s)
+ variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable
- variable.destroy
- present variable, with: Entities::Variable
+ present variable.destroy, with: Entities::Variable
end
end
end
diff --git a/lib/api/version.rb b/lib/api/version.rb
new file mode 100644
index 00000000000..9ba576bd828
--- /dev/null
+++ b/lib/api/version.rb
@@ -0,0 +1,12 @@
+module API
+ class Version < Grape::API
+ before { authenticate! }
+
+ desc 'Get the version information of the GitLab instance.' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ end
+ get '/version' do
+ { version: Gitlab::VERSION, revision: Gitlab::REVISION }
+ end
+ end
+end
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 9ebe379f454..35ca234c1ba 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -3,6 +3,10 @@ module Banzai
Renderer.render(text, context)
end
+ def self.render_field(object, field)
+ Renderer.render_field(object, field)
+ end
+
def self.cache_collection_render(texts_and_contexts)
Renderer.cache_collection_render(texts_and_contexts)
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index affe34394c2..cb213a76a05 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -208,8 +208,12 @@ module Banzai
@references_per_project ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
- regex = Regexp.union(object_class.reference_pattern,
- object_class.link_reference_pattern)
+ regex =
+ if uses_reference_pattern?
+ Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
+ else
+ object_class.link_reference_pattern
+ end
nodes.each do |node|
node.to_html.scan(regex) do
@@ -295,6 +299,14 @@ module Banzai
value
end
end
+
+ # There might be special cases like filters
+ # that should ignore reference pattern
+ # eg: IssueReferenceFilter when using a external issues tracker
+ # In those cases this method should be overridden on the filter subclass
+ def uses_reference_pattern?
+ true
+ end
end
end
end
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 2492b5213ac..a8c1ca0c60a 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,6 +1,6 @@
module Banzai
module Filter
- # HTML filter that replaces :emoji: with images.
+ # HTML filter that replaces :emoji: and unicode with images.
#
# Based on HTML::Pipeline::EmojiFilter
#
@@ -13,16 +13,17 @@ module Banzai
def call
search_text_nodes(doc).each do |node|
content = node.to_html
- next unless content.include?(':')
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
- html = emoji_image_filter(content)
+ next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
+
+ html = emoji_name_image_filter(content)
+ html = emoji_unicode_image_filter(html)
next if html == content
node.replace(html)
end
-
doc
end
@@ -31,18 +32,38 @@ module Banzai
# text - String text to replace :emoji: in.
#
# Returns a String with :emoji: replaced with images.
- def emoji_image_filter(text)
+ def emoji_name_image_filter(text)
text.gsub(emoji_pattern) do |match|
name = $1
- "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{emoji_url(name)}' height='20' width='20' align='absmiddle' />"
+ emoji_image_tag(name, emoji_url(name))
end
end
+ # Replace unicode emoji with corresponding images if they exist.
+ #
+ # text - String text to replace unicode emoji in.
+ #
+ # Returns a String with unicode emoji replaced with images.
+ def emoji_unicode_image_filter(text)
+ text.gsub(emoji_unicode_pattern) do |moji|
+ emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
+ end
+ end
+
+ def emoji_image_tag(emoji_name, emoji_url)
+ "<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
+ end
+
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
end
+ # Build a regexp that matches all valid unicode emojis names.
+ def self.emoji_unicode_pattern
+ @emoji_unicode_pattern ||= /(#{Gitlab::Emoji.emojis_unicodes.map { |moji| Regexp.escape(moji) }.join('|')})/
+ end
+
private
def emoji_url(name)
@@ -60,6 +81,18 @@ module Banzai
end
end
+ def emoji_unicode_url(moji)
+ emoji_unicode_path = emoji_unicode_filename(moji)
+
+ if context[:asset_host]
+ url_to_image(emoji_unicode_path)
+ elsif context[:asset_root]
+ File.join(context[:asset_root], url_to_image(emoji_unicode_path))
+ else
+ url_to_image(emoji_unicode_path)
+ end
+ end
+
def url_to_image(image)
ActionController::Base.helpers.url_to_image(image)
end
@@ -71,6 +104,14 @@ module Banzai
def emoji_filename(name)
"#{Gitlab::Emoji.emoji_filename(name)}.png"
end
+
+ def emoji_unicode_pattern
+ self.class.emoji_unicode_pattern
+ end
+
+ def emoji_unicode_filename(name)
+ "#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
+ end
end
end
end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index eaa702952cc..0d20be557a0 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -8,7 +8,7 @@ module Banzai
# Public: Find `JIRA-123` issue references in text
#
- # ExternalIssueReferenceFilter.references_in(text) do |match, issue|
+ # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
# "<a href=...>##{issue}</a>"
# end
#
@@ -17,8 +17,8 @@ module Banzai
# Yields the String match and the String issue reference.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(ExternalIssue.reference_pattern) do |match|
+ def self.references_in(text, pattern)
+ text.gsub(pattern) do |match|
yield match, $~[:issue]
end
end
@@ -27,7 +27,7 @@ module Banzai
# Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker?
- ref_pattern = ExternalIssue.reference_pattern
+ ref_pattern = issue_reference_pattern
ref_start_pattern = /\A#{ref_pattern}\z/
each_node do |node|
@@ -60,7 +60,7 @@ module Banzai
def issue_link_filter(text, link_text: nil)
project = context[:project]
- self.class.references_in(text) do |match, id|
+ self.class.references_in(text, issue_reference_pattern) do |match, id|
ExternalIssue.new(id, project)
url = url_for_issue(id, project, only_path: context[:only_path])
@@ -82,18 +82,21 @@ module Banzai
end
def default_issues_tracker?
- if RequestStore.active?
- default_issues_tracker_cache[project.id] ||=
- project.default_issues_tracker?
- else
- project.default_issues_tracker?
- end
+ external_issues_cached(:default_issues_tracker?)
+ end
+
+ def issue_reference_pattern
+ external_issues_cached(:issue_reference_pattern)
end
private
- def default_issues_tracker_cache
- RequestStore[:banzai_default_issues_tracker_cache] ||= {}
+ def external_issues_cached(attribute)
+ return project.public_send(attribute) unless RequestStore.active?
+
+ cached_attributes = RequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
+ cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil?
+ cached_attributes[project.id][attribute]
end
end
end
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 0a29c547a4d..2f19b59e725 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -3,10 +3,17 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
- # Skip non-HTTP(S) links and internal links
- doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node|
- node.set_attribute('rel', 'nofollow noreferrer')
- node.set_attribute('target', '_blank')
+ links.each do |node|
+ href = href_to_lowercase_scheme(node["href"].to_s)
+
+ unless node["href"].to_s == href
+ node.set_attribute('href', href)
+ end
+
+ if href =~ /\Ahttp(s)?:\/\// && external_url?(href)
+ node.set_attribute('rel', 'nofollow noreferrer')
+ node.set_attribute('target', '_blank')
+ end
end
doc
@@ -14,6 +21,25 @@ module Banzai
private
+ def links
+ query = 'descendant-or-self::a[@href and not(@href = "")]'
+ doc.xpath(query)
+ end
+
+ def href_to_lowercase_scheme(href)
+ scheme_match = href.match(/\A(\w+):\/\//)
+
+ if scheme_match
+ scheme_match.to_s.downcase + scheme_match.post_match
+ else
+ href
+ end
+ end
+
+ def external_url?(url)
+ !url.start_with?(internal_url)
+ end
+
def internal_url
@internal_url ||= Gitlab.config.gitlab.url
end
diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb
new file mode 100644
index 00000000000..f3bd587c28b
--- /dev/null
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -0,0 +1,12 @@
+require 'erb'
+
+module Banzai
+ module Filter
+ # Text filter that escapes these HTML entities: & " < >
+ class HtmlEntityFilter < HTML::Pipeline::TextFilter
+ def call
+ ERB::Util.html_escape_once(text)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 54c5f9a71a4..4d1bc687696 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -4,6 +4,10 @@ module Banzai
# issues that do not exist are ignored.
#
# This filter supports cross-project references.
+ #
+ # When external issues tracker like Jira is activated we should not
+ # use issue reference pattern, but we should still be able
+ # to reference issues from other GitLab projects.
class IssueReferenceFilter < AbstractReferenceFilter
self.reference_type = :issue
@@ -11,6 +15,10 @@ module Banzai
Issue
end
+ def uses_reference_pattern?
+ context[:project].default_issues_tracker?
+ end
+
def find_object(project, iid)
issues_per_project[project][iid]
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 8f262ef3d8d..c24831e68ee 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -9,7 +9,7 @@ module Banzai
end
def find_object(project, id)
- project.labels.find(id)
+ find_labels(project).find(id)
end
def self.references_in(text, pattern = Label.reference_pattern)
@@ -35,7 +35,11 @@ module Banzai
return unless project
label_params = label_params(label_id, label_name)
- project.labels.find_by(label_params)
+ find_labels(project).find_by(label_params)
+ end
+
+ def find_labels(project)
+ LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false)
end
# Parameters to pass to `Label.find_by` based on the given arguments
@@ -60,13 +64,50 @@ module Banzai
end
def object_link_text(object, matches)
- if context[:project] == object.project
- LabelsHelper.render_colored_label(object)
+ if same_group?(object) && namespace_match?(matches)
+ render_same_project_label(object)
+ elsif same_project?(object)
+ render_same_project_label(object)
else
- LabelsHelper.render_colored_cross_project_label(object)
+ render_cross_project_label(object, matches)
end
end
+ def same_group?(object)
+ object.is_a?(GroupLabel) && object.group == project.group
+ end
+
+ def namespace_match?(matches)
+ matches[:project].blank? || matches[:project] == project.path_with_namespace
+ end
+
+ def same_project?(object)
+ object.is_a?(ProjectLabel) && object.project == project
+ end
+
+ def user
+ context[:current_user] || context[:author]
+ end
+
+ def project
+ context[:project]
+ end
+
+ def render_same_project_label(object)
+ LabelsHelper.render_colored_label(object)
+ end
+
+ def render_cross_project_label(object, matches)
+ source_project =
+ if matches[:project]
+ Project.find_with_namespace(matches[:project])
+ else
+ object.project
+ end
+
+ LabelsHelper.render_colored_cross_project_label(object, source_project)
+ end
+
def unescape_html_entities(text)
CGI.unescapeHTML(text.to_s)
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 2470362e019..af1e575fc89 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -25,7 +25,7 @@ module Banzai
return if customized?(whitelist[:transformers])
# Allow code highlighting
- whitelist[:attributes]['pre'] = %w(class)
+ whitelist[:attributes]['pre'] = %w(class v-pre)
whitelist[:attributes]['span'] = %w(class)
# Allow table alignment
diff --git a/lib/banzai/filter/set_direction_filter.rb b/lib/banzai/filter/set_direction_filter.rb
new file mode 100644
index 00000000000..c2976aeb7c6
--- /dev/null
+++ b/lib/banzai/filter/set_direction_filter.rb
@@ -0,0 +1,15 @@
+module Banzai
+ module Filter
+ # HTML filter that sets dir="auto" for RTL languages support
+ class SetDirectionFilter < HTML::Pipeline::Filter
+ def call
+ # select these elements just on top level of the document
+ doc.xpath('p|h1|h2|h3|h4|h5|h6|ol|ul[not(@class="section-nav")]|blockquote|table').each do |el|
+ el['dir'] = 'auto'
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index fcdb496aed2..026b81ac175 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -30,7 +30,7 @@ module Banzai
# users can still access an issue/comment/etc.
end
- highlighted = %(<pre class="#{css_classes}"><code>#{code}</code></pre>)
+ highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>)
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index 66608c9859c..9fa5f589f3e 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -2,27 +2,7 @@ require 'task_list/filter'
module Banzai
module Filter
- # Work around a bug in the default TaskList::Filter that adds a `task-list`
- # class to every list element, regardless of whether or not it contains a
- # task list.
- #
- # This is a (hopefully) temporary fix, pending a new release of the
- # 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)
- 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')
- end
-
- add_css_class_without_fix(node, *new_class_names)
- end
-
- alias_method_chain :add_css_class, :fix
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index e1ca7f4d24b..c6302b586d3 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -106,13 +106,17 @@ module Banzai
project = context[:project]
author = context[:author]
- url = urls.namespace_project_url(project.namespace, project,
- only_path: context[:only_path])
+ if author && !project.team.member?(author)
+ link_text
+ else
+ url = urls.namespace_project_url(project.namespace, project,
+ only_path: context[:only_path])
- data = data_attribute(project: project.id, author: author.try(:id))
- text = link_text || User.reference_prefix + 'all'
+ data = data_attribute(project: project.id, author: author.try(:id))
+ text = link_text || User.reference_prefix + 'all'
- link_tag(url, data, text, 'All Project and Group Members')
+ link_tag(url, data, text, 'All Project and Group Members')
+ end
end
def link_to_namespace(namespace, link_text: nil)
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
index bab6a9934d1..2b7c10f1a0e 100644
--- a/lib/banzai/note_renderer.rb
+++ b/lib/banzai/note_renderer.rb
@@ -3,7 +3,7 @@ module Banzai
# Renders a collection of Note instances.
#
# notes - The notes to render.
- # project - The project to use for rendering/redacting.
+ # project - The project to use for redacting.
# user - The user viewing the notes.
# path - The request path.
# wiki - The project's wiki.
@@ -13,8 +13,7 @@ module Banzai
user,
requested_path: path,
project_wiki: wiki,
- ref: git_ref,
- pipeline: :note)
+ ref: git_ref)
renderer.render(notes, :note)
end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index 9aef807c152..9f8eb0931b8 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -1,28 +1,32 @@
module Banzai
- # Class for rendering multiple objects (e.g. Note instances) in a single pass.
+ # Class for rendering multiple objects (e.g. Note instances) in a single pass,
+ # using +render_field+ to benefit from caching in the database. Rendering and
+ # redaction are both performed.
#
- # Rendered Markdown is stored in an attribute in every object based on the
- # name of the attribute containing the Markdown. For example, when the
- # attribute `note` is rendered the HTML is stored in `note_html`.
+ # The unredacted HTML is generated according to the usual +render_field+
+ # policy, so specify the pipeline and any other context options on the model.
+ #
+ # The *redacted* (i.e., suitable for use) HTML is placed in an attribute
+ # named "redacted_<foo>", where <foo> is the name of the cache field for the
+ # chosen attribute.
+ #
+ # As an example, rendering the attribute `note` would place the unredacted
+ # HTML into `note_html` and the redacted HTML into `redacted_note_html`.
class ObjectRenderer
attr_reader :project, :user
- # Make sure to set the appropriate pipeline in the `raw_context` attribute
- # (e.g. `:note` for Note instances).
- #
- # project - A Project to use for rendering and redacting Markdown.
+ # project - A Project to use for redacting Markdown.
# user - The user viewing the Markdown/HTML documents, if any.
- # context - A Hash containing extra attributes to use in the rendering
- # pipeline.
- def initialize(project, user = nil, raw_context = {})
+ # context - A Hash containing extra attributes to use during redaction
+ def initialize(project, user = nil, redaction_context = {})
@project = project
@user = user
- @raw_context = raw_context
+ @redaction_context = redaction_context
end
# Renders and redacts an Array of objects.
#
- # objects - The objects to render
+ # objects - The objects to render.
# attribute - The attribute containing the raw Markdown to render.
#
# Returns the same input objects.
@@ -32,7 +36,7 @@ module Banzai
objects.each_with_index do |object, index|
redacted_data = redacted[index]
- object.__send__("#{attribute}_html=", redacted_data[:document].to_html.html_safe)
+ object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe)
object.user_visible_reference_count = redacted_data[:visible_reference_count]
end
end
@@ -53,12 +57,8 @@ module Banzai
# Returns a Banzai context for the given object and attribute.
def context_for(object, attribute)
- context = base_context.merge(cache_key: [object, attribute])
-
- if object.respond_to?(:author)
- context[:author] = object.author
- end
-
+ context = base_context.dup
+ context = context.merge(object.banzai_render_context(attribute))
context
end
@@ -66,21 +66,16 @@ module Banzai
#
# Returns an Array of `Nokogiri::HTML::Document`.
def render_attributes(objects, attribute)
- strings_and_contexts = objects.map do |object|
+ objects.map do |object|
+ string = Banzai.render_field(object, attribute)
context = context_for(object, attribute)
- string = object.__send__(attribute)
-
- { text: string, context: context }
- end
-
- Banzai.cache_collection_render(strings_and_contexts).each_with_index.map do |html, index|
- Banzai::Pipeline[:relative_link].to_document(html, strings_and_contexts[index][:context])
+ Banzai::Pipeline[:relative_link].to_document(string, context)
end
end
def base_context
- @base_context ||= @raw_context.merge(current_user: user, project: project)
+ @base_context ||= @redaction_context.merge(current_user: user, project: project)
end
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 8d94b199c66..5da2d0b008c 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -25,7 +25,9 @@ module Banzai
Filter::MilestoneReferenceFilter,
Filter::TaskListFilter,
- Filter::InlineDiffFilter
+ Filter::InlineDiffFilter,
+
+ Filter::SetDirectionFilter
]
end
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index ba2555df98d..1929099931b 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -3,6 +3,7 @@ module Banzai
class SingleLinePipeline < GfmPipeline
def self.filters
@filters ||= FilterArray[
+ Filter::HtmlEntityFilter,
Filter::SanitizationFilter,
Filter::EmojiFilter,
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index a4ae27eefd8..ce048a36fa0 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -1,6 +1,6 @@
module Banzai
module Renderer
- extend self
+ module_function
# Convert a Markdown String into an HTML-safe String of HTML
#
@@ -31,6 +31,34 @@ module Banzai
end
end
+ # Convert a Markdown-containing field on an object into an HTML-safe String
+ # of HTML. This method is analogous to calling render(object.field), but it
+ # can cache the rendered HTML in the object, rather than Redis.
+ #
+ # The context to use is learned from the passed-in object by calling
+ # #banzai_render_context(field), and cannot be changed. Use #render, passing
+ # it the field text, if a custom rendering is needed. The generated context
+ # is returned along with the HTML.
+ def render_field(object, field)
+ html_field = object.markdown_cache_field_for(field)
+
+ html = object.__send__(html_field)
+ return html if html.present?
+
+ html = cacheless_render_field(object, field)
+ object.update_column(html_field, html) unless object.new_record? || object.destroyed?
+
+ html
+ end
+
+ # Same as +render_field+, but without consulting or updating the cache field
+ def cacheless_render_field(object, field)
+ text = object.__send__(field)
+ context = object.banzai_render_context(field)
+
+ cacheless_render(text, context)
+ end
+
# Perform multiple render from an Array of Markdown String into an
# Array of HTML-safe String of HTML.
#
@@ -113,8 +141,6 @@ module Banzai
end.html_safe
end
- private
-
def cacheless_render(text, context = {})
Gitlab::Metrics.measure(:banzai_cacheless_render) do
result = render_result(text, context)
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 59f85416ee5..ed87a2603e8 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -12,10 +12,9 @@ module Ci
# POST /builds/register
post "register" do
authenticate_runner!
- update_runner_last_contact(save: false)
- update_runner_info
required_attributes! [:token]
not_found! unless current_runner.active?
+ update_runner_info
build = Ci::RegisterBuildService.new.execute(current_runner)
@@ -41,10 +40,11 @@ module Ci
# PUT /builds/:id
put ":id" do
authenticate_runner!
- update_runner_last_contact
build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
forbidden!('Build has been erased!') if build.erased?
+ update_runner_info
+
build.update_attributes(trace: params[:trace]) if params[:trace]
Gitlab::Metrics.add_event(:update_build,
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 23353c62885..e608f5f6cad 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 = 40 * 60
+ UPDATE_RUNNER_EVERY = 10 * 60
def authenticate_runners!
forbidden! unless runner_registration_token_valid?
@@ -30,14 +30,22 @@ module Ci
token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
end
- def update_runner_last_contact(save: true)
- # Use a random threshold to prevent beating DB updates
- # it generates a distribution between: [40m, 80m]
+ def update_runner_info
+ return unless update_runner?
+
+ current_runner.contacted_at = Time.now
+ current_runner.assign_attributes(get_runner_version_from_params)
+ current_runner.save if current_runner.changed?
+ end
+
+ def update_runner?
+ # 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.contacted_at = Time.now
- current_runner.save if current_runner.changed? && save
- end
+
+ current_runner.contacted_at.nil? ||
+ (Time.now - current_runner.contacted_at) >= contacted_at_max_age
end
def build_not_found!
@@ -57,11 +65,6 @@ module Ci
attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
end
- def update_runner_info
- current_runner.assign_attributes(get_runner_version_from_params)
- current_runner.save if current_runner.changed?
- end
-
def max_artifacts_size
current_application_settings.max_artifacts_size.megabytes.to_i
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 0369e80312a..3e33c9399e2 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)
@@ -109,6 +109,7 @@ module Ci
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
+ validate_job_environment!(name, job)
end
end
@@ -150,6 +151,35 @@ module Ci
end
end
+ def validate_job_environment!(name, job)
+ return unless job[:environment]
+ return unless job[:environment].is_a?(Hash)
+
+ environment = job[:environment]
+ validate_on_stop_job!(name, environment, environment[:on_stop])
+ end
+
+ def validate_on_stop_job!(name, environment, on_stop)
+ return unless on_stop
+
+ on_stop_job = @jobs[on_stop.to_sym]
+ unless on_stop_job
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
+ end
+
+ unless on_stop_job[:environment]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
+ end
+
+ unless on_stop_job[:environment][:name] == environment[:name]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
+ end
+
+ unless on_stop_job[:environment][:action] == 'stop'
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
+ end
+ end
+
def process?(only_params, except_params, ref, tag, trigger_request)
if only_params.present?
return false unless matching?(only_params, ref, tag, trigger_request)
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/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
new file mode 100644
index 00000000000..ca39b1961ae
--- /dev/null
+++ b/lib/constraints/group_url_constrainer.rb
@@ -0,0 +1,7 @@
+require 'constraints/namespace_url_constrainer'
+
+class GroupUrlConstrainer < NamespaceUrlConstrainer
+ def find_resource(id)
+ Group.find_by_path(id)
+ end
+end
diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb
new file mode 100644
index 00000000000..91b70143f11
--- /dev/null
+++ b/lib/constraints/namespace_url_constrainer.rb
@@ -0,0 +1,24 @@
+class NamespaceUrlConstrainer
+ def matches?(request)
+ id = request.path
+ id = id.sub(/\A#{relative_url_root}/, '') if relative_url_root
+ id = id.sub(/\A\/+/, '').split('/').first
+ id = id.sub(/.atom\z/, '') if id
+
+ if id =~ Gitlab::Regex.namespace_regex
+ find_resource(id)
+ end
+ end
+
+ def find_resource(id)
+ Namespace.find_by_path(id)
+ end
+
+ private
+
+ def relative_url_root
+ if defined?(Gitlab::Application.config.relative_url_root)
+ Gitlab::Application.config.relative_url_root
+ end
+ end
+end
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
new file mode 100644
index 00000000000..504a0f5d93e
--- /dev/null
+++ b/lib/constraints/user_url_constrainer.rb
@@ -0,0 +1,7 @@
+require 'constraints/namespace_url_constrainer'
+
+class UserUrlConstrainer < NamespaceUrlConstrainer
+ def find_resource(id)
+ User.find_by('lower(username) = ?', id.downcase)
+ end
+end
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 668d2fa41b3..21f6a9a762b 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -2,8 +2,8 @@ class EventFilter
attr_accessor :params
class << self
- def default_filter
- %w{ push issues merge_requests team}
+ def all
+ 'all'
end
def push
@@ -35,18 +35,28 @@ class EventFilter
return events unless params.present?
filter = params.dup
-
actions = []
- actions << Event::PUSHED if filter.include? 'push'
- actions << Event::MERGED if filter.include? 'merged'
- if filter.include? 'team'
- actions << Event::JOINED
- actions << Event::LEFT
+ case filter
+ when EventFilter.push
+ actions = [Event::PUSHED]
+ when EventFilter.merged
+ actions = [Event::MERGED]
+ when EventFilter.comments
+ actions = [Event::COMMENTED]
+ when EventFilter.team
+ actions = [Event::JOINED, Event::LEFT, Event::EXPIRED]
+ when EventFilter.all
+ actions = [
+ Event::PUSHED,
+ Event::MERGED,
+ Event::COMMENTED,
+ Event::JOINED,
+ Event::LEFT,
+ Event::EXPIRED
+ ]
end
- actions << Event::COMMENTED if filter.include? 'comments'
-
events.where(action: actions)
end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index a4558d157c0..9b74364849e 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -52,8 +52,7 @@ module ExtractsPath
# Append a trailing slash if we only get a ref and no file path
id += '/' unless id.ends_with?('/')
- valid_refs = @project.repository.ref_names
- valid_refs.select! { |v| id.start_with?("#{v}/") }
+ valid_refs = ref_names.select { |v| id.start_with?("#{v}/") }
if valid_refs.length == 0
# No exact ref match, so just try our best
@@ -74,6 +73,19 @@ module ExtractsPath
pair
end
+ # If we have an ID of 'foo.atom', and the controller provides Atom and HTML
+ # formats, then we have to check if the request was for the Atom version of
+ # the ID without the '.atom' suffix, or the HTML version of the ID including
+ # the suffix. We only check this if the version including the suffix doesn't
+ # match, so it is possible to create a branch which has an unroutable Atom
+ # feed.
+ def extract_ref_without_atom(id)
+ id_without_atom = id.sub(/\.atom$/, '')
+ valid_refs = ref_names.select { |v| "#{id_without_atom}/".start_with?("#{v}/") }
+
+ valid_refs.max_by(&:length)
+ end
+
# Assigns common instance variables for views working with Git tree-ish objects
#
# Assignments are:
@@ -86,6 +98,10 @@ module ExtractsPath
# If the :id parameter appears to be requesting a specific response format,
# that will be handled as well.
#
+ # If there is no path and the ref doesn't exist in the repo, try to resolve
+ # the ref without an '.atom' suffix. If _that_ ref is found, set the request's
+ # format to Atom manually.
+ #
# Automatically renders `not_found!` if a valid tree path could not be
# resolved (e.g., when a user inserts an invalid path or ref).
def assign_ref_vars
@@ -97,10 +113,18 @@ module ExtractsPath
@id = get_id
@ref, @path = extract_ref(@id)
@repo = @project.repository
- if @options[:extended_sha1].blank?
- @commit = @repo.commit(@ref)
- else
+
+ if @options[:extended_sha1].present?
@commit = @repo.commit(@options[:extended_sha1])
+ else
+ @commit = @repo.commit(@ref)
+
+ if @path.empty? && !@commit && @id.ends_with?('.atom')
+ @id = @ref = extract_ref_without_atom(@id)
+ @commit = @repo.commit(@ref)
+
+ request.format = :atom if @commit
+ end
end
raise InvalidPathError unless @commit
@@ -125,4 +149,10 @@ module ExtractsPath
id += "/" + params[:path] unless params[:path].blank?
id
end
+
+ def ref_names
+ return [] unless @project
+
+ @ref_names ||= @project.repository.ref_names
+ 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/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 79eac66b364..9cec71a3222 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -17,6 +17,18 @@ module Gitlab
end
class << self
+ def secret_token
+ @secret_token ||= begin
+ File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+ end
+ end
+
+ def ensure_secret_token!
+ return if File.exist?(File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret'))
+
+ generate_and_link_secret_token
+ end
+
def version_required
@version_required ||= File.read(Rails.root.
join('GITLAB_SHELL_VERSION')).strip
@@ -25,6 +37,25 @@ module Gitlab
def strip_key(key)
key.split(/ /)[0, 2].join(' ')
end
+
+ private
+
+ # Create (if necessary) and link the secret token file
+ def generate_and_link_secret_token
+ secret_file = Gitlab.config.gitlab_shell.secret_file
+ shell_path = Gitlab.config.gitlab_shell.path
+
+ unless File.size?(secret_file)
+ # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
+ @secret_token = SecureRandom.hex(16)
+ File.write(secret_file, @secret_token)
+ end
+
+ link_path = File.join(shell_path, '.gitlab_shell_secret')
+ if File.exist?(shell_path) && !File.exist?(link_path)
+ FileUtils.symlink(secret_file, link_path)
+ end
+ end
end
# Init new repository
@@ -201,21 +232,6 @@ module Gitlab
File.exist?(full_path(storage, dir_name))
end
- # 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.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)
- end
-
- link_path = File.join(gitlab_shell_path, '.gitlab_shell_secret')
- if File.exist?(gitlab_shell_path) && !File.exist?(link_path)
- FileUtils.symlink(secret_file, link_path)
- end
- end
-
protected
def gitlab_shell_path
diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb
index d388ab6b879..9a95ef43628 100644
--- a/lib/gitlab/ci/config/node/environment.rb
+++ b/lib/gitlab/ci/config/node/environment.rb
@@ -8,7 +8,7 @@ module Gitlab
class Environment < Entry
include Validatable
- ALLOWED_KEYS = %i[name url]
+ ALLOWED_KEYS = %i[name url action on_stop]
validations do
validate do
@@ -35,6 +35,12 @@ module Gitlab
length: { maximum: 255 },
addressable_url: true,
allow_nil: true
+
+ validates :action,
+ inclusion: { in: %w[start stop], message: 'should be start or stop' },
+ allow_nil: true
+
+ validates :on_stop, type: String, allow_nil: true
end
end
@@ -54,9 +60,17 @@ module Gitlab
value[:url]
end
+ def action
+ value[:action] || 'start'
+ end
+
+ def on_stop
+ value[:on_stop]
+ end
+
def value
case @config
- when String then { name: @config }
+ when String then { name: @config, action: 'start' }
when Hash then @config
else {}
end
diff --git a/lib/gitlab/ci/trace_reader.rb b/lib/gitlab/ci/trace_reader.rb
new file mode 100644
index 00000000000..37e51536e8f
--- /dev/null
+++ b/lib/gitlab/ci/trace_reader.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Ci
+ # This was inspired from: http://stackoverflow.com/a/10219411/1520132
+ class TraceReader
+ BUFFER_SIZE = 4096
+
+ attr_accessor :path, :buffer_size
+
+ def initialize(new_path, buffer_size: BUFFER_SIZE)
+ self.path = new_path
+ self.buffer_size = Integer(buffer_size)
+ end
+
+ def read(last_lines: nil)
+ if last_lines
+ read_last_lines(last_lines)
+ else
+ File.read(path)
+ end
+ end
+
+ def read_last_lines(max_lines)
+ File.open(path) do |file|
+ chunks = []
+ pos = lines = 0
+ max = file.size
+
+ # We want an extra line to make sure fist line has full contents
+ while lines <= max_lines && pos < max
+ pos += buffer_size
+
+ buf = if pos <= max
+ file.seek(-pos, IO::SEEK_END)
+ file.read(buffer_size)
+ else # Reached the head, read only left
+ file.seek(0)
+ file.read(buffer_size - (pos - max))
+ end
+
+ lines += buf.count("\n")
+ chunks.unshift(buf)
+ end
+
+ chunks.join.lines.last(max_lines).join
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index dff9e29c6a5..c843315782d 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -4,7 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers
include IconsHelper
- class MissingResolution < StandardError
+ class MissingResolution < ResolutionError
end
CONTEXT_LINES = 3
@@ -21,12 +21,34 @@ module Gitlab
@match_line_headers = {}
end
+ def content
+ merge_file_result[:data]
+ end
+
+ def our_blob
+ @our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path)
+ end
+
+ def type
+ lines unless @type
+
+ @type.inquiry
+ end
+
# Array of Gitlab::Diff::Line objects
def lines
- @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data],
+ return @lines if defined?(@lines)
+
+ begin
+ @type = 'text'
+ @lines = Gitlab::Conflict::Parser.new.parse(content,
our_path: our_path,
their_path: their_path,
parent_file: self)
+ rescue Gitlab::Conflict::Parser::ParserError
+ @type = 'text-editor'
+ @lines = nil
+ end
end
def resolve_lines(resolution)
@@ -53,6 +75,14 @@ module Gitlab
end.compact
end
+ def resolve_content(resolution)
+ if resolution == content
+ raise MissingResolution, "Resolved content has no changes for file #{our_path}"
+ end
+
+ resolution
+ end
+
def highlight_lines!
their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
@@ -170,21 +200,39 @@ module Gitlab
match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
end
- def as_json(opts = nil)
- {
+ def as_json(opts = {})
+ json_hash = {
old_path: their_path,
new_path: our_path,
blob_icon: file_type_icon_class('file', our_mode, our_path),
blob_path: namespace_project_blob_path(merge_request.project.namespace,
merge_request.project,
- ::File.join(merge_request.diff_refs.head_sha, our_path)),
- sections: sections
+ ::File.join(merge_request.diff_refs.head_sha, our_path))
}
+
+ json_hash.tap do |json_hash|
+ if opts[:full_content]
+ json_hash[:content] = content
+ json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode)
+ else
+ json_hash[:sections] = sections if type.text?
+ json_hash[:type] = type
+ json_hash[:content_path] = content_path
+ end
+ end
+ end
+
+ def content_path
+ conflict_for_path_namespace_project_merge_request_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request,
+ old_path: their_path,
+ new_path: our_path)
end
# Don't try to print merge_request or repository.
def inspect
- instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable|
+ instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable|
value = instance_variable_get("@#{instance_variable}")
"#{instance_variable}=\"#{value}\""
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index bbd0427a2c8..fa5bd4649d4 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -30,6 +30,10 @@ module Gitlab
end
end
+ def file_for_path(old_path, new_path)
+ files.find { |file| file.their_path == old_path && file.our_path == new_path }
+ end
+
def as_json(opts = nil)
{
target_branch: merge_request.target_branch,
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index 98e842cded3..ddd657903fb 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -1,19 +1,24 @@
module Gitlab
module Conflict
class Parser
- class ParserError < StandardError
+ class UnresolvableError < StandardError
end
- class UnexpectedDelimiter < ParserError
+ class UnmergeableFile < UnresolvableError
end
- class MissingEndDelimiter < ParserError
+ class UnsupportedEncoding < UnresolvableError
+ end
+
+ # Recoverable errors - the conflict can be resolved in an editor, but not with
+ # sections.
+ class ParserError < StandardError
end
- class UnmergeableFile < ParserError
+ class UnexpectedDelimiter < ParserError
end
- class UnsupportedEncoding < ParserError
+ class MissingEndDelimiter < ParserError
end
def parse(text, our_path:, their_path:, parent_file: nil)
diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb
new file mode 100644
index 00000000000..a0f2006bc24
--- /dev/null
+++ b/lib/gitlab/conflict/resolution_error.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module Conflict
+ class ResolutionError < StandardError
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index e47df508ca2..ce85e5e0123 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -125,6 +125,10 @@ module Gitlab
repository.blob_at(commit.id, file_path)
end
+
+ def cache_key
+ "#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
+ end
end
end
end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 36348b33943..dc4d47c878b 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -35,16 +35,16 @@ module Gitlab
# for the highlighted ones, so we just skip their execution.
# If the highlighted diff files lines are not cached we calculate and cache them.
#
- # The content of the cache is a Hash where the key correspond to the file_path and the values are Arrays of
+ # The content of the cache is a Hash where the key identifies the file and the values are Arrays of
# hashes that represent serialized diff lines.
#
def cache_highlight!(diff_file)
- file_path = diff_file.file_path
+ item_key = diff_file.cache_key
- if highlight_cache[file_path]
- highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path])
+ if highlight_cache[item_key]
+ highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
else
- highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash)
+ highlight_cache[item_key] = diff_file.highlighted_diff_lines.map(&:to_hash)
end
end
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 06dae31cc27..447c7a6a6b9 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -46,7 +46,9 @@ module Gitlab
noteable_type: sent_notification.noteable_type,
noteable_id: sent_notification.noteable_id,
commit_id: sent_notification.commit_id,
- line_code: sent_notification.line_code
+ line_code: sent_notification.line_code,
+ position: sent_notification.position,
+ type: sent_notification.note_type
).execute
end
end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index b63213ae208..bbbca8acc40 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -10,12 +10,20 @@ module Gitlab
Gemojione.index.instance_variable_get(:@emoji_by_moji)
end
+ def emojis_unicodes
+ emojis_by_moji.keys
+ end
+
def emojis_names
- emojis.keys.sort
+ emojis.keys
end
def emoji_filename(name)
emojis[name]["unicode"]
end
+
+ def emoji_unicode_filename(moji)
+ emojis_by_moji[moji]["unicode"]
+ end
end
end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 501d5a95547..65ee85ca5a9 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -74,8 +74,8 @@ module Gitlab
end
def create_label(name)
- color = nice_label_color(name)
- Label.create!(project_id: project.id, title: name, color: color)
+ params = { title: name, color: nice_label_color(name) }
+ ::Labels::FindOrCreateService.new(project.owner, project, params).execute
end
def user_info(person_id)
@@ -122,25 +122,21 @@ module Gitlab
author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
issue = Issue.create!(
- project_id: project.id,
- title: bug['sTitle'],
- description: body,
- author_id: author_id,
- assignee_id: assignee_id,
- state: bug['fOpen'] == 'true' ? 'opened' : 'closed'
+ iid: bug['ixBug'],
+ project_id: project.id,
+ title: bug['sTitle'],
+ description: body,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ state: bug['fOpen'] == 'true' ? 'opened' : 'closed',
+ created_at: date,
+ updated_at: DateTime.parse(bug['dtLastUpdated'])
)
- issue.add_labels_by_names(labels)
- if issue.iid != bug['ixBug']
- issue.update_attribute(:iid, bug['ixBug'])
- end
+ issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
+ issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
-
- issue.update_attribute(:created_at, date)
-
- last_update = DateTime.parse(bug['dtLastUpdated'])
- issue.update_attribute(:updated_at, last_update)
end
end
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 78d7a4f27cf..a7c596dced0 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -58,7 +58,7 @@ module Gitlab
referable = find_referable(reference)
return reference unless referable
- cross_reference = referable.to_reference(target_project)
+ cross_reference = build_cross_reference(referable, target_project)
return reference if reference == cross_reference
new_text = before + cross_reference + after
@@ -72,6 +72,14 @@ module Gitlab
extractor.all.first
end
+ def build_cross_reference(referable, target_project)
+ if referable.respond_to?(:project)
+ referable.to_reference(target_project)
+ else
+ referable.to_reference(@source_project, target_project)
+ end
+ end
+
def substitution_valid?(substituted)
@original_html == markdown(substituted)
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index e33ac61f5ae..7f424b74efb 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -102,9 +102,19 @@ module Gitlab
def request(method, *args, &block)
sleep rate_limit_sleep_time if rate_limit_exceed?
- data = api.send(method, *args, &block)
- yield data
+ data = api.send(method, *args)
+ return data unless data.is_a?(Array)
+ if block_given?
+ yield data
+ each_response_page(&block)
+ else
+ each_response_page { |page| data.concat(page) }
+ data
+ end
+ end
+
+ def each_response_page
last_response = api.last_response
while last_response.rels[:next]
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index b8321244473..4b70f33a851 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -170,10 +170,9 @@ module Gitlab
end
def import_wiki
- unless project.wiki_enabled?
+ unless project.wiki.repository_exists?
wiki = WikiFormatter.new(project)
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
- project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
end
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
index 2cad7fca88e..942dfb3312b 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/github_import/label_formatter.rb
@@ -14,9 +14,13 @@ module Gitlab
end
def create!
- project.labels.find_or_create_by!(title: title) do |label|
- label.color = color
- end
+ params = attributes.except(:project)
+ service = ::Labels::FindOrCreateService.new(project.owner, project, params)
+ label = service.execute
+
+ raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
+
+ label
end
private
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 605abfabdab..a2410068845 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,7 +1,7 @@
module Gitlab
module GithubImport
class ProjectCreator
- attr_reader :repo, :namespace, :current_user, :session_data
+ attr_reader :repo, :name, :namespace, :current_user, :session_data
def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
@@ -12,24 +12,37 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(
+ ::Projects::CreateService.new(
current_user,
- name: @name,
- path: @name,
+ name: name,
+ path: name,
description: repo.description,
namespace_id: namespace.id,
- visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
+ visibility_level: visibility_level,
import_type: "github",
import_source: repo.full_name,
- import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
+ import_url: import_url,
+ skip_wiki: skip_wiki
).execute
+ end
+
+ private
- # If repo has wiki we'll import it later
- if repo.has_wiki? && project
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
- end
+ def import_url
+ repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@")
+ end
+
+ def visibility_level
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
+ end
- project
+ #
+ # If the GitHub project repository has wiki, we should not create the
+ # default wiki. Otherwise the GitHub importer will fail because the wiki
+ # repository already exist.
+ #
+ def skip_wiki
+ repo.has_wiki?
end
end
end
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 62da327931f..6a68e786b4f 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -92,19 +92,17 @@ module Gitlab
end
issue = Issue.create!(
- project_id: project.id,
- title: raw_issue["title"],
- description: body,
- author_id: project.creator_id,
- assignee_id: assignee_id,
- state: raw_issue["state"] == "closed" ? "closed" : "opened"
+ iid: raw_issue['id'],
+ project_id: project.id,
+ title: raw_issue['title'],
+ description: body,
+ author_id: project.creator_id,
+ assignee_id: assignee_id,
+ state: raw_issue['state'] == 'closed' ? 'closed' : 'opened'
)
- issue.add_labels_by_names(labels)
-
- if issue.iid != raw_issue["id"]
- issue.update_attribute(:iid, raw_issue["id"])
- end
+ issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
+ issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
end
@@ -236,8 +234,8 @@ module Gitlab
end
def create_label(name)
- color = nice_label_color(name)
- Label.create!(project_id: project.id, name: name, color: color)
+ params = { name: name, color: nice_label_color(name) }
+ ::Labels::FindOrCreateService.new(project.owner, project, params).execute
end
def format_content(raw_content)
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 3e5d728f3bc..f8809db21aa 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -5,19 +5,61 @@ module Gitlab
def identify(identifier, project, newrev)
if identifier.blank?
# Local push from gitlab
- email = project.commit(newrev).author_email rescue nil
- User.find_by(email: email) if email
-
+ identify_using_commit(project, newrev)
elsif identifier =~ /\Auser-\d+\Z/
# git push over http
- user_id = identifier.gsub("user-", "")
- User.find_by(id: user_id)
-
+ identify_using_user(identifier)
elsif identifier =~ /\Akey-\d+\Z/
# git push over ssh
- key_id = identifier.gsub("key-", "")
- Key.find_by(id: key_id).try(:user)
+ identify_using_ssh_key(identifier)
+ end
+ end
+
+ # Tries to identify a user based on a commit SHA.
+ def identify_using_commit(project, ref)
+ commit = project.commit(ref)
+
+ return if !commit || !commit.author_email
+
+ email = commit.author_email
+
+ identify_with_cache(:email, email) do
+ User.find_by(email: email)
end
end
+
+ # Tries to identify a user based on a user identifier (e.g. "user-123").
+ def identify_using_user(identifier)
+ user_id = identifier.gsub("user-", "")
+
+ identify_with_cache(:user, user_id) do
+ User.find_by(id: user_id)
+ end
+ end
+
+ # Tries to identify a user based on an SSH key identifier (e.g. "key-123").
+ def identify_using_ssh_key(identifier)
+ key_id = identifier.gsub("key-", "")
+
+ identify_with_cache(:ssh_key, key_id) do
+ User.find_by_ssh_key_id(key_id)
+ end
+ end
+
+ def identify_with_cache(category, key)
+ if identification_cache[category].key?(key)
+ identification_cache[category][key]
+ else
+ identification_cache[category][key] = yield
+ end
+ end
+
+ def identification_cache
+ @identification_cache ||= {
+ email: {},
+ user: {},
+ ssh_key: {}
+ }
+ end
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 181e288a014..eb667a85b78 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.1.4'
+ VERSION = '0.1.5'
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
new file mode 100644
index 00000000000..f755a404693
--- /dev/null
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module ImportExport
+ class AttributeCleaner
+ ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
+
+ def self.clean!(relation_hash:)
+ relation_hash.reject! do |key, _value|
+ key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index e522a0fc8f6..f00c7460e82 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -1,6 +1,8 @@
module Gitlab
module ImportExport
module CommandLineUtil
+ DEFAULT_MODE = 0700
+
def tar_czf(archive:, dir:)
tar_with_options(archive: archive, dir: dir, options: 'czf')
end
@@ -21,6 +23,11 @@ module Gitlab
execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
end
+ def mkdir_p(path)
+ FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
+ FileUtils.chmod(DEFAULT_MODE, path)
+ end
+
private
def tar_with_options(archive:, dir:, options:)
@@ -45,7 +52,7 @@ module Gitlab
# if we are copying files, create the destination folder
destination_folder = File.file?(source) ? File.dirname(destination) : destination
- FileUtils.mkdir_p(destination_folder)
+ mkdir_p(destination_folder)
FileUtils.copy_entry(source, destination)
true
end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index eca6e5b6d51..113895ba22c 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def import
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
wait_for_archived_file do
decompress_archive
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index bb9d1080330..e6ecd118609 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -1,6 +1,7 @@
# Model relationships to be included in the project import/export
project_tree:
- - :labels
+ - labels:
+ :priorities
- milestones:
- :events
- issues:
@@ -9,7 +10,8 @@ project_tree:
- :author
- :events
- label_links:
- - :label
+ - label:
+ :priorities
- milestone:
- :events
- snippets:
@@ -26,7 +28,8 @@ project_tree:
- :merge_request_diff
- :events
- label_links:
- - :label
+ - label:
+ :priorities
- milestone:
- :events
- pipelines:
@@ -71,6 +74,10 @@ excluded_attributes:
- :awardable_id
methods:
+ labels:
+ - :type
+ label:
+ - :type
statuses:
- :type
services:
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
index 0cc10f40087..48c09dafcb6 100644
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -65,11 +65,17 @@ module Gitlab
# +value+ existing model to be included in the hash
# +parsed_hash+ the original hash
def parse_hash(value)
+ return nil if already_contains_methods?(value)
+
@attributes_finder.parse(value) do |hash|
{ include: hash_or_merge(value, hash) }
end
end
+ def already_contains_methods?(value)
+ value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
+ end
+
# Adds new model configuration to an existing hash with key +current_key+
# It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
#
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 35ff134ea19..7cdba880a93 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -110,13 +110,18 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: relation_hash.merge('project_id' => restored_project.id),
+ relation_hash: parsed_relation_hash(relation_hash),
members_mapper: members_mapper,
- user: @user)
+ user: @user,
+ project_id: restored_project.id)
end
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
+
+ def parsed_relation_hash(relation_hash)
+ relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 9153088e966..2fbf437ec26 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -1,6 +1,8 @@
module Gitlab
module ImportExport
class ProjectTreeSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
attr_reader :full_path
def initialize(project:, shared:)
@@ -10,7 +12,7 @@ module Gitlab
end
def save
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
File.write(full_path, project_json_tree)
true
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 0c2cd208bf9..ce31c923feb 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -9,25 +9,28 @@ module Gitlab
builds: 'Ci::Build',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
- push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze
+ push_access_levels: 'ProtectedBranch::PushAccessLevel',
+ labels: :project_labels,
+ priorities: :label_priorities,
+ label: :project_label }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
+ PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+
BUILD_MODELS = %w[Ci::Build commit_status].freeze
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
-
- FINDER_ATTRIBUTES = %w[title project_id].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
def self.create(*args)
new(*args).create
end
- def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
+ def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('id', 'noteable_id')
+ @relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id)
@members_mapper = members_mapper
@user = user
@imported_object_retries = 0
@@ -54,6 +57,8 @@ module Gitlab
update_user_references
update_project_references
+
+ handle_group_label if group_label?
reset_ci_tokens if @relation_name == 'Ci::Trigger'
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff
@@ -121,6 +126,20 @@ module Gitlab
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
+ def group_label?
+ @relation_hash['type'] == 'GroupLabel'
+ end
+
+ def handle_group_label
+ # If there's no group, move the label to a project label
+ if @relation_hash['group_id']
+ @relation_hash['project_id'] = nil
+ @relation_name = :group_label
+ else
+ @relation_hash['type'] = 'ProjectLabel'
+ end
+ end
+
def reset_ci_tokens
return unless Gitlab::ImportExport.reset_tokens?
@@ -153,7 +172,11 @@ module Gitlab
end
def parsed_relation_hash
- @parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ @parsed_relation_hash ||= begin
+ Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash)
+
+ @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ end
end
def set_st_diffs
@@ -165,11 +188,9 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
- events = parsed_relation_hash.delete('events')
+ attribute_hash = attribute_hash_for(['events', 'priorities'])
- unless events.blank?
- existing_object.assign_attributes(events: events)
- end
+ existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
existing_object
else
@@ -178,14 +199,22 @@ module Gitlab
end
end
+ def attribute_hash_for(attributes)
+ attributes.inject({}) do |hash, value|
+ hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value]
+ hash
+ end
+ end
+
def existing_object
@existing_object ||=
begin
- finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES)
+ finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ 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.update!(parsed_relation_hash)
existing_object
end
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index d1e33ea8678..48a9a6fa5e2 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -12,7 +12,7 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- FileUtils.mkdir_p(path_to_repo)
+ mkdir_p(path_to_repo)
git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
rescue => e
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
index 331e14021e6..a7028a32570 100644
--- a/lib/gitlab/import_export/repo_saver.rb
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -20,7 +20,7 @@ module Gitlab
private
def bundle_to_disk
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
rescue => e
@shared.error(e)
diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb
index 9b642d740b7..7cf88298642 100644
--- a/lib/gitlab/import_export/version_saver.rb
+++ b/lib/gitlab/import_export/version_saver.rb
@@ -1,12 +1,14 @@
module Gitlab
module ImportExport
class VersionSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
def initialize(shared:)
@shared = shared
end
def save
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
rescue => e
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
index 6107420e4dd..1e6722a7bba 100644
--- a/lib/gitlab/import_export/wiki_repo_saver.rb
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def bundle_to_disk(full_path)
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: full_path)
rescue => e
@shared.error(e)
diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb
index 1bec6088292..dbc759367eb 100644
--- a/lib/gitlab/issues_labels.rb
+++ b/lib/gitlab/issues_labels.rb
@@ -18,8 +18,8 @@ module Gitlab
{ title: "enhancement", color: green }
]
- labels.each do |label|
- project.labels.create(label)
+ labels.each do |params|
+ ::Labels::FindOrCreateService.new(project.owner, project, params).execute
end
end
end
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
index 7b3bbcf6a32..5f67e97fa2a 100644
--- a/lib/gitlab/lfs_token.rb
+++ b/lib/gitlab/lfs_token.rb
@@ -20,13 +20,8 @@ module Gitlab
def token
Gitlab::Redis.with do |redis|
token = redis.get(redis_key)
-
- if token
- redis.expire(redis_key, EXPIRY_TIME)
- else
- token = Devise.friendly_token(TOKEN_LENGTH)
- redis.set(redis_key, token, ex: EXPIRY_TIME)
- end
+ token ||= Devise.friendly_token(TOKEN_LENGTH)
+ redis.set(redis_key, token, ex: EXPIRY_TIME)
token
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 5b9cfaeb2f8..24733435a5a 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -73,11 +73,7 @@ module Gitlab
end
def commits
- if project.empty_repo? || query.blank?
- []
- else
- project.repository.find_commits_by_message(query).compact
- end
+ project.repository.find_commits_by_message(query)
end
def project_ids_relation
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 69c4ef721d5..c649da8c426 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -11,13 +11,6 @@ module Gitlab
DEFAULT_REDIS_URL = 'redis://localhost:6379'
CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
- # To be thread-safe we must be careful when writing the class instance
- # variables @_raw_config and @pool. Because @pool depends on @_raw_config we need two
- # mutexes to prevent deadlock.
- RAW_CONFIG_MUTEX = Mutex.new
- POOL_MUTEX = Mutex.new
- private_constant :RAW_CONFIG_MUTEX, :POOL_MUTEX
-
class << self
# Do NOT cache in an instance variable. Result may be mutated by caller.
def params
@@ -31,24 +24,29 @@ module Gitlab
end
def with
- if @pool.nil?
- POOL_MUTEX.synchronize do
- @pool = ConnectionPool.new { ::Redis.new(params) }
- end
- end
+ @pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
@pool.with { |redis| yield redis }
end
+ def pool_size
+ if Sidekiq.server?
+ # the pool will be used in a multi-threaded context
+ Sidekiq.options[:concurrency] + 5
+ else
+ # probably this is a Unicorn process, so single threaded
+ 5
+ end
+ end
+
def _raw_config
return @_raw_config if defined?(@_raw_config)
- RAW_CONFIG_MUTEX.synchronize do
- begin
- @_raw_config = File.read(CONFIG_FILE).freeze
- rescue Errno::ENOENT
- @_raw_config = false
- end
+ begin
+ @_raw_config = File.read(CONFIG_FILE).freeze
+ rescue Errno::ENOENT
+ @_raw_config = false
end
+
@_raw_config
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 776bbcbb5d0..0d30e1bb92e 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,7 +2,7 @@ module Gitlab
module Regex
extend self
- NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze
+ NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
@@ -10,7 +10,7 @@ module Gitlab
def namespace_regex_message
"can contain only letters, digits, '_', '-' and '.'. " \
- "Cannot start with '-' or end in '.'." \
+ "Cannot start with '-' or end in '.', '.git' or '.atom'." \
end
def namespace_name_regex
diff --git a/lib/gitlab/sidekiq_middleware/arguments_logger.rb b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
index 7813091ec7b..82a59a7a87e 100644
--- a/lib/gitlab/sidekiq_middleware/arguments_logger.rb
+++ b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
@@ -2,7 +2,7 @@ module Gitlab
module SidekiqMiddleware
class ArgumentsLogger
def call(worker, job, queue)
- Sidekiq.logger.info "arguments: #{job['args']}"
+ Sidekiq.logger.info "arguments: #{JSON.dump(job['args'])}"
yield
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 5d33f98e89e..594439a5d4b 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -111,7 +111,7 @@ module Gitlab
def write_secret
bytes = SecureRandom.random_bytes(SECRET_LENGTH)
File.open(secret_path, 'w:BINARY', 0600) do |f|
- f.chmod(0600)
+ f.chmod(0600) # If the file already existed, the '0600' passed to 'open' above was a no-op.
f.write(Base64.strict_encode64(bytes))
end
end
diff --git a/lib/tasks/.gitkeep b/lib/tasks/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/lib/tasks/.gitkeep
+++ /dev/null
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 2214f855200..78ae187817a 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,22 +1,33 @@
namespace :cache do
- CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
- REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
+ namespace :clear do
+ REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
+ REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
- desc "GitLab | Clear redis cache"
- task :clear => :environment do
- Gitlab::Redis.with do |redis|
- cursor = REDIS_SCAN_START_STOP
- loop do
- cursor, keys = redis.scan(
- cursor,
- match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
- count: CLEAR_BATCH_SIZE
- )
-
- redis.del(*keys) if keys.any?
-
- break if cursor == REDIS_SCAN_START_STOP
+ desc "GitLab | Clear redis cache"
+ task redis: :environment do
+ Gitlab::Redis.with do |redis|
+ cursor = REDIS_SCAN_START_STOP
+ loop do
+ cursor, keys = redis.scan(
+ cursor,
+ match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
+ count: REDIS_CLEAR_BATCH_SIZE
+ )
+
+ redis.del(*keys) if keys.any?
+
+ break if cursor == REDIS_SCAN_START_STOP
+ end
end
end
+
+ desc "GitLab | Clear database cache (in the background)"
+ task db: :environment do
+ ClearDatabaseCacheWorker.perform_async
+ end
+
+ task all: [:db, :redis]
end
+
+ task clear: 'cache:clear:redis'
end
diff --git a/lib/tasks/ce_to_ee_merge_check.rake b/lib/tasks/ce_to_ee_merge_check.rake
new file mode 100644
index 00000000000..424e7883060
--- /dev/null
+++ b/lib/tasks/ce_to_ee_merge_check.rake
@@ -0,0 +1,4 @@
+desc 'Checks if the branch would apply cleanly to EE'
+task ce_to_ee_merge_check: :environment do
+ Rake::Task['gitlab:dev:ce_to_ee_merge_check'].invoke
+end
diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/lib/tasks/ci/.gitkeep
+++ /dev/null
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/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index b43ee5b3383..a9f1255e8cf 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -51,6 +51,7 @@ namespace :gitlab do
$progress.puts 'done'.color(:green)
Rake::Task['gitlab:backup:db:restore'].invoke
end
+
Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
@@ -58,6 +59,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke
+ Rake::Task['cache:clear'].invoke
backup.cleanup
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 5f4a6bbfa35..2ae48a970ce 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -671,7 +671,7 @@ namespace :gitlab do
"Enable mail_room in the init.d configuration."
)
for_more_information(
- "doc/incoming_email/README.md"
+ "doc/administration/reply_by_email.md"
)
fix_and_rerun
end
@@ -690,7 +690,7 @@ namespace :gitlab do
"Enable mail_room in your Procfile."
)
for_more_information(
- "doc/incoming_email/README.md"
+ "doc/administration/reply_by_email.md"
)
fix_and_rerun
end
@@ -747,7 +747,7 @@ namespace :gitlab do
"Check that the information in config/gitlab.yml is correct"
)
for_more_information(
- "doc/incoming_email/README.md"
+ "doc/administration/reply_by_email.md"
)
fix_and_rerun
end
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
new file mode 100644
index 00000000000..47bdb2d32d2
--- /dev/null
+++ b/lib/tasks/gitlab/dev.rake
@@ -0,0 +1,107 @@
+namespace :gitlab do
+ namespace :dev do
+ desc 'Checks if the branch would apply cleanly to EE'
+ task ce_to_ee_merge_check: :environment do
+ return if defined?(Gitlab::License)
+ return unless ENV['CI']
+
+ ce_repo = ENV['CI_BUILD_REPO']
+ ce_branch = ENV['CI_BUILD_REF_NAME']
+
+ ee_repo = 'https://gitlab.com/gitlab-org/gitlab-ee.git'
+ ee_branch = "#{ce_branch}-ee"
+ ee_dir = 'gitlab-ee-merge-check'
+
+ puts "\n=> Cloning #{ee_repo} into #{ee_dir}\n"
+ `git clone #{ee_repo} #{ee_dir} --depth 1`
+ Dir.chdir(ee_dir) do
+ puts "\n => Fetching #{ce_repo}/#{ce_branch}\n"
+ `git fetch #{ce_repo} #{ce_branch} --depth 1`
+
+ # Try to merge the current tested branch to EE/master...
+ puts "\n => Merging #{ce_repo}/#{ce_branch} into #{ee_repo}/master\n"
+ `git merge FETCH_HEAD`
+
+ exit 0 if $?.success?
+
+ # Check if the <branch>-ee branch exists...
+ puts "\n => Check if #{ee_repo}/#{ee_branch} exists\n"
+ `git rev-parse --verify #{ee_branch}`
+
+ # The <branch>-ee doesn't exist
+ unless $?.success?
+ puts
+ puts <<-MSG.strip_heredoc
+ =================================================================
+ The #{ce_branch} branch cannot be merged without conflicts to the
+ current EE/master, and no #{ee_branch} branch was detected in
+ the EE repository.
+
+ Please create a #{ee_branch} branch that includes changes from
+ #{ce_branch} but also specific changes than can be applied cleanly
+ to EE/master.
+
+ You can create this branch as follows:
+
+ 1. In the EE repo:
+ $ git fetch origin
+ $ git fetch #{ce_repo} #{ce_branch}
+ $ git checkout -b #{ee_branch} FETCH_HEAD
+ $ git rebase origin/master
+ 2. At this point you will likely have conflicts, solve them, and
+ continue/finish the rebase. Note: You can squash the CE commits
+ before rebasing.
+ 3. You can squash all the original #{ce_branch} commits into a
+ single "Port of #{ce_branch} to EE".
+ 4. Push your branch to #{ee_repo}:
+ $ git push origin #{ee_branch}
+ =================================================================\n
+ MSG
+
+ exit 1
+ end
+
+ # Try to merge the <branch>-ee branch to EE/master...
+ puts "\n => Merging #{ee_repo}/#{ee_branch} into #{ee_repo}/master\n"
+ `git merge #{ee_branch} master`
+
+ # The <branch>-ee cannot be merged cleanly to EE/master...
+ unless $?.success?
+ puts
+ puts <<-MSG.strip_heredoc
+ =================================================================
+ The #{ce_branch} branch cannot be merged without conflicts to
+ EE/master, and even though the #{ee_branch} branch exists in the EE
+ repository, it cannot be merged without conflicts to EE/master.
+
+ Please update the #{ee_branch}, push it again to #{ee_repo}, and
+ retry this job.
+ =================================================================\n
+ MSG
+
+ exit 2
+ end
+
+ puts "\n => Merging #{ce_repo}/#{ce_branch} into #{ee_repo}/master\n"
+ `git merge FETCH_HEAD`
+ exit 0 if $?.success?
+
+ # The <branch>-ee can be merged cleanly to EE/master, but <branch> still
+ # cannot be merged cleanly to EE/master...
+ puts
+ puts <<-MSG.strip_heredoc
+ =================================================================
+ The #{ce_branch} branch cannot be merged without conflicts to EE, and
+ even though the #{ee_branch} branch exists in the EE repository and
+ applies cleanly to EE/master, it doesn't prevent conflicts when
+ merging #{ce_branch} into EE.
+
+ We may be in a complex situation here.
+ =================================================================\n
+ MSG
+
+ exit 3
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index bb7eb852f1b..210899882b4 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -78,7 +78,7 @@ namespace :gitlab do
f.puts "PATH=#{ENV['PATH']}"
end
- Gitlab::Shell.new.generate_and_link_secret_token
+ Gitlab::Shell.ensure_secret_token!
end
desc "GitLab | Setup gitlab-shell"
diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake
new file mode 100644
index 00000000000..3a16ace60bd
--- /dev/null
+++ b/lib/tasks/gitlab/users.rake
@@ -0,0 +1,11 @@
+namespace :gitlab do
+ namespace :users do
+ desc "GitLab | Clear the authentication token for all users"
+ task clear_all_authentication_tokens: :environment do |t, args|
+ # Do small batched updates because these updates will be slow and locking
+ User.select(:id).find_in_batches(batch_size: 100) do |batch|
+ User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
+ end
+ end
+ end
+end
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index fb4d8463981..7c4e8276902 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -10,11 +10,11 @@ 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)
+# Ensure that the CHANGELOG.md does not contain duplicate versions
+DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d)
if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
then
- echo '✖ ERROR: Duplicate versions in CHANGELOG:' >&2
+ echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
echo "${DUPLICATE_CHANGELOG_VERSIONS}" >&2
exit 1
fi
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 6e987e7d9c9..1eaafdce389 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -16,21 +16,6 @@ retry() {
}
if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
- mkdir -p vendor/apt
-
- # Install phantomjs package
- pushd vendor/apt
- PHANTOMJS_FILE="phantomjs-$PHANTOMJS_VERSION-linux-x86_64"
- if [ ! -d "$PHANTOMJS_FILE" ]; then
- curl -q -L "https://s3.amazonaws.com/gitlab-build-helpers/$PHANTOMJS_FILE.tar.bz2" | tar jx
- fi
- cp "$PHANTOMJS_FILE/bin/phantomjs" "/usr/bin/"
- popd
-
- # Try to install packages
- retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
- libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
-
cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml
sed -i 's/password:.*/password:/g' config/database.yml
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 92b97bf3d0c..c7db84dd5f9 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,16 +2,10 @@ require 'spec_helper'
describe Groups::GroupMembersController do
let(:user) { create(:user) }
+ let(:group) { create(:group, :public) }
- describe '#index' do
- let(:group) { create(:group) }
-
- before do
- group.add_owner(user)
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
- end
-
- it 'renders index with group members' do
+ describe 'GET index' do
+ it 'renders index with 200 status code' do
get :index, group_id: group
expect(response).to have_http_status(200)
@@ -19,87 +13,109 @@ describe Groups::GroupMembersController do
end
end
- describe '#destroy' do
- let(:group) { create(:group, :public) }
+ describe 'POST create' do
+ let(:group_user) { create(:user) }
+
+ before { sign_in(user) }
+
+ context 'when user does not have enough rights' do
+ before { group.add_developer(user) }
- context 'when member is not found' do
it 'returns 403' do
- delete :destroy, group_id: group,
- id: 42
+ post :create, group_id: group,
+ user_ids: group_user.id,
+ access_level: Gitlab::Access::GUEST
expect(response).to have_http_status(403)
+ expect(group.users).not_to include group_user
end
end
- context 'when member is found' do
- let(:user) { create(:user) }
- let(:group_user) { create(:user) }
- let(:member) do
- group.add_developer(group_user)
- group.members.find_by(user_id: group_user)
+ context 'when user has enough rights' do
+ before { group.add_owner(user) }
+
+ it 'adds user to members' do
+ post :create, group_id: group,
+ user_ids: group_user.id,
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'Users were successfully added.'
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).to include group_user
+ end
+
+ it 'adds no user to members' do
+ post :create, group_id: group,
+ user_ids: '',
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'No users specified.'
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).not_to include group_user
end
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let(:member) { create(:group_member, :developer, group: group) }
+ before { sign_in(user) }
+
+ context 'when member is not found' do
+ it 'returns 403' do
+ delete :destroy, group_id: group, id: 42
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when member is found' do
context 'when user does not have enough rights' do
- before do
- group.add_developer(user)
- sign_in(user)
- end
+ before { group.add_developer(user) }
it 'returns 403' do
- delete :destroy, group_id: group,
- id: member
+ delete :destroy, group_id: group, id: member
expect(response).to have_http_status(403)
- expect(group.users).to include group_user
+ expect(group.members).to include member
end
end
context 'when user has enough rights' do
- before do
- group.add_owner(user)
- sign_in(user)
- end
+ before { group.add_owner(user) }
it '[HTML] removes user from members' do
- delete :destroy, group_id: group,
- id: member
+ delete :destroy, group_id: group, id: member
expect(response).to set_flash.to 'User was successfully removed from group.'
expect(response).to redirect_to(group_group_members_path(group))
- expect(group.users).not_to include group_user
+ expect(group.members).not_to include member
end
it '[JS] removes user from members' do
- xhr :delete, :destroy, group_id: group,
- id: member
+ xhr :delete, :destroy, group_id: group, id: member
expect(response).to be_success
- expect(group.users).not_to include group_user
+ expect(group.members).not_to include member
end
end
end
end
- describe '#leave' do
- let(:group) { create(:group, :public) }
- let(:user) { create(:user) }
+ describe 'DELETE leave' do
+ before { sign_in(user) }
context 'when member is not found' do
- before { sign_in(user) }
-
- it 'returns 403' do
+ it 'returns 404' do
delete :leave, group_id: group
- expect(response).to have_http_status(403)
+ expect(response).to have_http_status(404)
end
end
context 'when member is found' do
context 'and is not an owner' do
- before do
- group.add_developer(user)
- sign_in(user)
- end
+ before { group.add_developer(user) }
it 'removes user from members' do
delete :leave, group_id: group
@@ -111,10 +127,7 @@ describe Groups::GroupMembersController do
end
context 'and is an owner' do
- before do
- group.add_owner(user)
- sign_in(user)
- end
+ before { group.add_owner(user) }
it 'cannot removes himself from the group' do
delete :leave, group_id: group
@@ -124,10 +137,7 @@ describe Groups::GroupMembersController do
end
context 'and is a requester' do
- before do
- group.request_access(user)
- sign_in(user)
- end
+ before { group.request_access(user) }
it 'removes user from members' do
delete :leave, group_id: group
@@ -141,13 +151,8 @@ describe Groups::GroupMembersController do
end
end
- describe '#request_access' do
- let(:group) { create(:group, :public) }
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
+ describe 'POST request_access' do
+ before { sign_in(user) }
it 'creates a new GroupMember that is not a team member' do
post :request_access, group_id: group
@@ -159,53 +164,39 @@ describe Groups::GroupMembersController do
end
end
- describe '#approve_access_request' do
- let(:group) { create(:group, :public) }
+ describe 'POST approve_access_request' do
+ let(:member) { create(:group_member, :access_request, group: group) }
+
+ before { sign_in(user) }
context 'when member is not found' do
it 'returns 403' do
- post :approve_access_request, group_id: group,
- id: 42
+ post :approve_access_request, group_id: group, id: 42
expect(response).to have_http_status(403)
end
end
context 'when member is found' do
- let(:user) { create(:user) }
- let(:group_requester) { create(:user) }
- let(:member) do
- group.request_access(group_requester)
- group.requesters.find_by(user_id: group_requester)
- end
-
context 'when user does not have enough rights' do
- before do
- group.add_developer(user)
- sign_in(user)
- end
+ before { group.add_developer(user) }
it 'returns 403' do
- post :approve_access_request, group_id: group,
- id: member
+ post :approve_access_request, group_id: group, id: member
expect(response).to have_http_status(403)
- expect(group.users).not_to include group_requester
+ expect(group.members).not_to include member
end
end
context 'when user has enough rights' do
- before do
- group.add_owner(user)
- sign_in(user)
- end
+ before { group.add_owner(user) }
it 'adds user to members' do
- post :approve_access_request, group_id: group,
- id: member
+ post :approve_access_request, group_id: group, id: member
expect(response).to redirect_to(group_group_members_path(group))
- expect(group.users).to include group_requester
+ expect(group.members).to include member
end
end
end
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
deleted file mode 100644
index 2b334ed1172..00000000000
--- a/spec/controllers/namespaces_controller_spec.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-require 'spec_helper'
-
-describe NamespacesController do
- let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
-
- describe "GET show" do
- context "when the namespace belongs to a user" do
- let!(:other_user) { create(:user) }
-
- it "redirects to the user's page" do
- get :show, id: other_user.username
-
- expect(response).to redirect_to(user_path(other_user))
- end
- end
-
- context "when the namespace belongs to a group" do
- let!(:group) { create(:group) }
-
- context "when the group is public" do
- context "when not signed in" do
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
- end
-
- context "when the group is private" do
- before do
- group.update_attribute(:visibility_level, Group::PRIVATE)
- end
-
- context "when not signed in" do
- it "redirects to the sign in page" do
- get :show, id: group.path
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- context "when the user has access to the group" do
- before do
- group.add_developer(user)
- end
-
- context "when the user is blocked" do
- before do
- user.block
- end
-
- it "redirects to the sign in page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context "when the user isn't blocked" do
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
- end
-
- context "when the user doesn't have access to the group" do
- it "responds with status 404" do
- get :show, id: group.path
-
- expect(response).to have_http_status(404)
- end
- end
- end
- end
- end
-
- context "when the namespace doesn't exist" do
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- it "responds with status 404" do
- get :show, id: "doesntexist"
-
- expect(response).to have_http_status(404)
- end
- end
-
- context "when not signed in" do
- it "redirects to the sign in page" do
- get :show, id: "doesntexist"
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
- end
-end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 9444a50b1ce..52d13fb6f9e 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -5,7 +5,6 @@ describe Projects::BlobController do
let(:user) { create(:user) }
before do
- user = create(:user)
project.team << [user, :master]
sign_in(user)
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 2896636db5a..da59642f24d 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -1,17 +1,20 @@
require 'spec_helper'
describe Projects::Boards::IssuesController do
- let(:project) { create(:project_with_board) }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
+ let(:guest) { create(:user) }
let(:planning) { create(:label, project: project, name: 'Planning') }
let(:development) { create(:label, project: project, name: 'Development') }
- let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
- let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: development, position: 1) }
before do
project.team << [user, :master]
+ project.team << [guest, :guest]
end
describe 'GET index' do
@@ -22,7 +25,7 @@ describe Projects::Boards::IssuesController do
create(:labeled_issue, project: project, labels: [development])
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
- list_issues user: user, list_id: list2
+ list_issues user: user, board: board, list: list2
parsed_response = JSON.parse(response.body)
@@ -31,9 +34,17 @@ describe Projects::Boards::IssuesController do
end
end
+ context 'with invalid board id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, board: 999, list: list2
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
context 'with invalid list id' do
it 'returns a not found 404 response' do
- list_issues user: user, list_id: 999
+ list_issues user: user, board: board, list: 999
expect(response).to have_http_status(404)
end
@@ -45,19 +56,75 @@ describe Projects::Boards::IssuesController do
allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
end
- it 'returns a successful 403 response' do
- list_issues user: user, list_id: list2
+ it 'returns a forbidden 403 response' do
+ list_issues user: user, board: board, list: list2
expect(response).to have_http_status(403)
end
end
- def list_issues(user:, list_id:)
+ def list_issues(user:, board:, list:)
sign_in(user)
get :index, namespace_id: project.namespace.to_param,
project_id: project.to_param,
- list_id: list_id.to_param
+ board_id: board.to_param,
+ list_id: list.to_param
+ end
+ end
+
+ describe 'POST create' do
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_issue user: user, board: board, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created issue' do
+ create_issue user: user, board: board, list: list1, title: 'New issue'
+
+ expect(response).to match_response_schema('issue')
+ end
+ end
+
+ context 'with invalid params' do
+ context 'when title is nil' do
+ it 'returns an unprocessable entity 422 response' do
+ create_issue user: user, board: board, list: list1, title: nil
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when list does not belongs to project board' do
+ it 'returns a not found 404 response' do
+ list = create(:list)
+
+ create_issue user: user, board: board, list: list, title: 'New issue'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ create_issue user: guest, board: board, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_issue(user:, board:, list:, title:)
+ sign_in(user)
+
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ board_id: board.to_param,
+ list_id: list.to_param,
+ issue: { title: title },
+ format: :json
end
end
@@ -66,13 +133,13 @@ describe Projects::Boards::IssuesController do
context 'with valid params' do
it 'returns a successful 200 response' do
- move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+ move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(200)
end
it 'moves issue to the desired list' do
- move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+ move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(issue.reload.labels).to contain_exactly(development)
end
@@ -80,13 +147,19 @@ describe Projects::Boards::IssuesController do
context 'with invalid params' do
it 'returns a unprocessable entity 422 response for invalid lists' do
- move user: user, issue: issue, from_list_id: nil, to_list_id: nil
+ move user: user, board: board, issue: issue, from_list_id: nil, to_list_id: nil
expect(response).to have_http_status(422)
end
+ it 'returns a not found 404 response for invalid board id' do
+ move user: user, board: 999, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(404)
+ end
+
it 'returns a not found 404 response for invalid issue id' do
- move user: user, issue: 999, from_list_id: list1.id, to_list_id: list2.id
+ move user: user, board: board, issue: 999, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(404)
end
@@ -99,18 +172,19 @@ describe Projects::Boards::IssuesController do
project.team << [guest, :guest]
end
- it 'returns a successful 403 response' do
- move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+ it 'returns a forbidden 403 response' do
+ move user: guest, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(403)
end
end
- def move(user:, issue:, from_list_id:, to_list_id:)
+ def move(user:, board:, issue:, from_list_id:, to_list_id:)
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
id: issue.to_param,
from_list_id: from_list_id,
to_list_id: to_list_id,
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index 709006a3601..34d6119429d 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Projects::Boards::ListsController do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:guest) { create(:user) }
@@ -13,7 +13,7 @@ describe Projects::Boards::ListsController do
describe 'GET index' do
it 'returns a successful 200 response' do
- read_board_list user: user
+ read_board_list user: user, board: board
expect(response).to have_http_status(200)
expect(response.content_type).to eq 'application/json'
@@ -22,7 +22,7 @@ describe Projects::Boards::ListsController do
it 'returns a list of board lists' do
create(:list, board: board)
- read_board_list user: user
+ read_board_list user: user, board: board
parsed_response = JSON.parse(response.body)
@@ -37,17 +37,18 @@ describe Projects::Boards::ListsController do
end
it 'returns a forbidden 403 response' do
- read_board_list user: user
+ read_board_list user: user, board: board
expect(response).to have_http_status(403)
end
end
- def read_board_list(user:)
+ def read_board_list(user:, board:)
sign_in(user)
get :index, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
format: :json
end
end
@@ -57,13 +58,13 @@ describe Projects::Boards::ListsController do
let(:label) { create(:label, project: project, name: 'Development') }
it 'returns a successful 200 response' do
- create_board_list user: user, label_id: label.id
+ create_board_list user: user, board: board, label_id: label.id
expect(response).to have_http_status(200)
end
it 'returns the created list' do
- create_board_list user: user, label_id: label.id
+ create_board_list user: user, board: board, label_id: label.id
expect(response).to match_response_schema('list')
end
@@ -72,7 +73,7 @@ describe Projects::Boards::ListsController do
context 'with invalid params' do
context 'when label is nil' do
it 'returns a not found 404 response' do
- create_board_list user: user, label_id: nil
+ create_board_list user: user, board: board, label_id: nil
expect(response).to have_http_status(404)
end
@@ -82,7 +83,7 @@ describe Projects::Boards::ListsController do
it 'returns a not found 404 response' do
label = create(:label, name: 'Development')
- create_board_list user: user, label_id: label.id
+ create_board_list user: user, board: board, label_id: label.id
expect(response).to have_http_status(404)
end
@@ -93,17 +94,18 @@ describe Projects::Boards::ListsController do
it 'returns a forbidden 403 response' do
label = create(:label, project: project, name: 'Development')
- create_board_list user: guest, label_id: label.id
+ create_board_list user: guest, board: board, label_id: label.id
expect(response).to have_http_status(403)
end
end
- def create_board_list(user:, label_id:)
+ def create_board_list(user:, board:, label_id:)
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
list: { label_id: label_id },
format: :json
end
@@ -115,13 +117,13 @@ describe Projects::Boards::ListsController do
context 'with valid position' do
it 'returns a successful 200 response' do
- move user: user, list: planning, position: 1
+ move user: user, board: board, list: planning, position: 1
expect(response).to have_http_status(200)
end
it 'moves the list to the desired position' do
- move user: user, list: planning, position: 1
+ move user: user, board: board, list: planning, position: 1
expect(planning.reload.position).to eq 1
end
@@ -129,7 +131,7 @@ describe Projects::Boards::ListsController do
context 'with invalid position' do
it 'returns an unprocessable entity 422 response' do
- move user: user, list: planning, position: 6
+ move user: user, board: board, list: planning, position: 6
expect(response).to have_http_status(422)
end
@@ -137,7 +139,7 @@ describe Projects::Boards::ListsController do
context 'with invalid list id' do
it 'returns a not found 404 response' do
- move user: user, list: 999, position: 1
+ move user: user, board: board, list: 999, position: 1
expect(response).to have_http_status(404)
end
@@ -145,17 +147,18 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
- move user: guest, list: planning, position: 6
+ move user: guest, board: board, list: planning, position: 6
expect(response).to have_http_status(403)
end
end
- def move(user:, list:, position:)
+ def move(user:, board:, list:, position:)
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
id: list.to_param,
list: { position: position },
format: :json
@@ -167,19 +170,19 @@ describe Projects::Boards::ListsController do
context 'with valid list id' do
it 'returns a successful 200 response' do
- remove_board_list user: user, list: planning
+ remove_board_list user: user, board: board, list: planning
expect(response).to have_http_status(200)
end
it 'removes list from board' do
- expect { remove_board_list user: user, list: planning }.to change(board.lists, :size).by(-1)
+ expect { remove_board_list user: user, board: board, list: planning }.to change(board.lists, :size).by(-1)
end
end
context 'with invalid list id' do
it 'returns a not found 404 response' do
- remove_board_list user: user, list: 999
+ remove_board_list user: user, board: board, list: 999
expect(response).to have_http_status(404)
end
@@ -187,17 +190,18 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
- remove_board_list user: guest, list: planning
+ remove_board_list user: guest, board: board, list: planning
expect(response).to have_http_status(403)
end
end
- def remove_board_list(user:, list:)
+ def remove_board_list(user:, board:, list:)
sign_in(user)
delete :destroy, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
id: list.to_param,
format: :json
end
@@ -206,13 +210,13 @@ describe Projects::Boards::ListsController do
describe 'POST generate' do
context 'when board lists is empty' do
it 'returns a successful 200 response' do
- generate_default_board_lists user: user
+ generate_default_lists user: user, board: board
expect(response).to have_http_status(200)
end
it 'returns the defaults lists' do
- generate_default_board_lists user: user
+ generate_default_lists user: user, board: board
expect(response).to match_response_schema('lists')
end
@@ -222,7 +226,7 @@ describe Projects::Boards::ListsController do
it 'returns an unprocessable entity 422 response' do
create(:list, board: board)
- generate_default_board_lists user: user
+ generate_default_lists user: user, board: board
expect(response).to have_http_status(422)
end
@@ -230,17 +234,18 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
- generate_default_board_lists user: guest
+ generate_default_lists user: guest, board: board
expect(response).to have_http_status(403)
end
end
- def generate_default_board_lists(user:)
+ def generate_default_lists(user:, board:)
sign_in(user)
post :generate, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
format: :json
end
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 6f6e608e1f3..cc19035740e 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -9,16 +9,71 @@ describe Projects::BoardsController do
sign_in(user)
end
+ describe 'GET index' do
+ it 'creates a new project board when project does not have one' do
+ expect { list_boards }.to change(project.boards, :count).by(1)
+ end
+
+ context 'when format is HTML' do
+ it 'renders template' do
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
+
+ context 'when format is JSON' do
+ it 'returns a list of project boards' do
+ create_list(:board, 2, project: project)
+
+ list_boards format: :json
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('boards')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
+
+ it 'returns a not found 404 response' do
+ list_boards
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ def list_boards(format: :html)
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: format
+ end
+ end
+
describe 'GET show' do
- it 'creates a new board when project does not have one' do
- expect { read_board }.to change(Board, :count).by(1)
+ let!(:board) { create(:board, project: project) }
+
+ context 'when format is HTML' do
+ it 'renders template' do
+ read_board board: board
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
end
- it 'renders HTML template' do
- read_board
+ context 'when format is JSON' do
+ it 'returns project board' do
+ read_board board: board, format: :json
- expect(response).to render_template :show
- expect(response.content_type).to eq 'text/html'
+ expect(response).to match_response_schema('board')
+ end
end
context 'with unauthorized user' do
@@ -27,16 +82,27 @@ describe Projects::BoardsController do
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
- it 'returns a successful 404 response' do
- read_board
+ it 'returns a not found 404 response' do
+ read_board board: board
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when board does not belong to project' do
+ it 'returns a not found 404 response' do
+ another_board = create(:board)
+
+ read_board board: another_board
expect(response).to have_http_status(404)
end
end
- def read_board(format: :html)
+ def read_board(board:, format: :html)
get :show, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ id: board.to_param,
format: format
end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 7e440193d7b..646b097d74e 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -102,15 +102,16 @@ describe Projects::CommitController do
describe "as patch" do
include_examples "export as", :patch
let(:format) { :patch }
+ let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') }
it "is a git email patch" do
- go(id: commit.id, format: format)
+ go(id: commit2.id, format: format)
- expect(response.body).to start_with("From #{commit.id}")
+ expect(response.body).to start_with("From #{commit2.id}")
end
it "contains a git diff" do
- go(id: commit.id, format: format)
+ go(id: commit2.id, format: format)
expect(response.body).to match(/^diff --git/)
end
@@ -135,6 +136,8 @@ describe Projects::CommitController do
describe "GET branches" do
it "contains branch and tags information" do
+ commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+
get(:branches,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -254,16 +257,17 @@ describe Projects::CommitController do
end
let(:existing_path) { '.gitmodules' }
+ let(:commit2) { project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
context 'when the commit exists' do
context 'when the user has access to the project' do
context 'when the path exists in the diff' do
it 'enables diff notes' do
- diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
expect(assigns(:comments_target)).to eq(noteable_type: 'Commit',
- commit_id: commit.id)
+ commit_id: commit2.id)
end
it 'only renders the diffs for the path given' do
@@ -272,7 +276,7 @@ describe Projects::CommitController do
meth.call(diffs)
end
- diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 2518a48e336..1ac7e03a2db 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -10,15 +10,38 @@ describe Projects::CommitsController do
end
describe "GET show" do
- context "as atom feed" do
- it "renders as atom" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: "master",
- format: "atom")
- expect(response).to be_success
- expect(response.content_type).to eq('application/atom+xml')
+ context "when the ref name ends in .atom" do
+ render_views
+
+ context "when the ref does not exist with the suffix" do
+ it "renders as atom" do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: "master.atom")
+
+ expect(response).to be_success
+ expect(response.content_type).to eq('application/atom+xml')
+ end
+ end
+
+ context "when the ref exists with the suffix" do
+ before do
+ commit = project.repository.commit('master')
+
+ allow_any_instance_of(Repository).to receive(:commit).and_call_original
+ allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
+
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: "master.atom")
+ end
+
+ it "renders as HTML" do
+ expect(response).to be_success
+ expect(response.content_type).to eq('text/html')
+ end
end
end
end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
new file mode 100644
index 00000000000..74e6603b0cb
--- /dev/null
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Projects::GraphsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'GET #languages' do
+ let(:linguist_repository) do
+ double(languages: {
+ 'Ruby' => 1000,
+ 'CoffeeScript' => 350,
+ 'PowerShell' => 15
+ })
+ end
+
+ let(:expected_values) do
+ ps_color = "##{Digest::SHA256.hexdigest('PowerShell')[0...6]}"
+ [
+ # colors from Linguist:
+ { label: "Ruby", color: "#701516", highlight: "#701516" },
+ { label: "CoffeeScript", color: "#244776", highlight: "#244776" },
+ # colors from SHA256 fallback:
+ { label: "PowerShell", color: ps_color, highlight: ps_color }
+ ]
+ end
+
+ before do
+ allow(Linguist::Repository).to receive(:new).and_return(linguist_repository)
+ end
+
+ it 'sets the correct colour according to language' do
+ get(:languages, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+
+ expected_values.each do |val|
+ expect(assigns(:languages)).to include(a_hash_including(val))
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index fbe8758dda7..b9d9117c928 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Projects::GroupLinksController do
- let(:project) { create(:project, :private) }
let(:group) { create(:group, :private) }
+ let(:group2) { create(:group, :private) }
+ let(:project) { create(:project, :private, group: group2) }
let(:user) { create(:user) }
before do
@@ -46,5 +47,39 @@ describe Projects::GroupLinksController do
expect(group.shared_projects).not_to include project
end
end
+
+ context 'when project group id equal link group id' do
+ before do
+ post(:create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ link_group_id: group2.id,
+ link_group_access: ProjectGroupLink.default_access)
+ end
+
+ it 'does not share project with selected group' do
+ expect(group2.shared_projects).not_to include project
+ end
+
+ it 'redirects to project group links page' do
+ expect(response).to redirect_to(
+ namespace_project_group_links_path(project.namespace, project)
+ )
+ end
+ end
+
+ context 'when link group id is not present' do
+ before do
+ post(:create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ link_group_access: ProjectGroupLink.default_access)
+ end
+
+ it 'redirects to project group links page' do
+ expect(response).to redirect_to(
+ namespace_project_group_links_path(project.namespace, project)
+ )
+ expect(flash[:alert]).to eq('Please select a group.')
+ end
+ end
end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 3492b6ffbbb..41df63d445a 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -1,52 +1,88 @@
require 'spec_helper'
describe Projects::LabelsController do
- let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
+
sign_in(user)
end
describe 'GET #index' do
- def create_label(attributes)
- create(:label, attributes.merge(project: project))
- end
+ let!(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') }
+ let!(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') }
+ let!(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') }
+ let!(:label_4) { create(:label, project: project, title: 'Label 4') }
+ let!(:label_5) { create(:label, project: project, title: 'Label 5') }
- before do
- 15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") }
- 5.times { |i| create_label(title: "label #{100 - i}") }
+ let!(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') }
+ let!(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
+ let!(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') }
+ let!(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') }
- get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ before do
+ create(:label_priority, project: project, label: group_label_1, priority: 3)
+ create(:label_priority, project: project, label: group_label_2, priority: 1)
end
context '@prioritized_labels' do
- let(:prioritized_labels) { assigns(:prioritized_labels) }
+ before do
+ list_labels
+ end
+
+ it 'does not include labels without priority' do
+ list_labels
- it 'contains only prioritized labels' do
- expect(prioritized_labels).to all(have_attributes(priority: a_value > 0))
+ expect(assigns(:prioritized_labels)).not_to include(group_label_3, group_label_4, label_4, label_5)
end
it 'is sorted by priority, then label title' do
- priorities_and_titles = prioritized_labels.pluck(:priority, :title)
-
- expect(priorities_and_titles.sort).to eq(priorities_and_titles)
+ expect(assigns(:prioritized_labels)).to eq [group_label_2, label_1, label_3, group_label_1, label_2]
end
end
context '@labels' do
- let(:labels) { assigns(:labels) }
+ it 'is sorted by label title' do
+ list_labels
- it 'contains only unprioritized labels' do
- expect(labels).to all(have_attributes(priority: nil))
+ expect(assigns(:labels)).to eq [group_label_3, group_label_4, label_4, label_5]
end
- it 'is sorted by label title' do
- titles = labels.pluck(:title)
+ it 'does not include labels with priority' do
+ list_labels
- expect(titles.sort).to eq(titles)
+ expect(assigns(:labels)).not_to include(group_label_2, label_1, label_3, group_label_1, label_2)
end
+
+ it 'does not include group labels when project does not belong to a group' do
+ project.update(namespace: create(:namespace))
+
+ list_labels
+
+ expect(assigns(:labels)).not_to include(group_label_3, group_label_4)
+ end
+ end
+
+ def list_labels
+ get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ end
+ end
+
+ describe 'POST #generate' do
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'creates labels' do
+ post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(response).to have_http_status(302)
end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 94c9edc91fe..940d54f8686 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -570,7 +570,7 @@ describe Projects::MergeRequestsController do
context 'when the conflicts cannot be resolved in the UI' do
before do
allow_any_instance_of(Gitlab::Conflict::Parser).
- to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
@@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do
format: 'json'
end
+ it 'matches the schema' do
+ expect(response).to match_response_schema('conflicts')
+ end
+
it 'includes meta info about the MR' do
expect(json_response['commit_message']).to include('Merge branch')
expect(json_response['commit_sha']).to match(/\h{40}/)
@@ -644,26 +648,111 @@ describe Projects::MergeRequestsController do
end
end
+ context 'POST remove_wip' do
+ it 'removes the wip status' do
+ merge_request.title = merge_request.wip_title
+ merge_request.save
+
+ post :remove_wip,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project.to_param,
+ id: merge_request.iid
+
+ expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+ end
+ end
+
+ describe 'GET conflict_for_path' do
+ let(:json_response) { JSON.parse(response.body) }
+
+ def conflict_for_path(path)
+ get :conflict_for_path,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ old_path: path,
+ new_path: path,
+ format: 'json'
+ end
+
+ context 'when the conflicts cannot be resolved in the UI' do
+ before do
+ allow_any_instance_of(Gitlab::Conflict::Parser).
+ to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
+
+ conflict_for_path('files/ruby/regex.rb')
+ end
+
+ it 'returns a 404 status code' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when the file does not exist cannot be resolved in the UI' do
+ before { conflict_for_path('files/ruby/regexp.rb') }
+
+ it 'returns a 404 status code' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'with an existing file' do
+ let(:path) { 'files/ruby/regex.rb' }
+
+ before { conflict_for_path(path) }
+
+ it 'returns a 200 status code' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'returns the file in JSON format' do
+ content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content
+
+ expect(json_response).to include('old_path' => path,
+ 'new_path' => path,
+ 'blob_icon' => 'file-text-o',
+ 'blob_path' => a_string_ending_with(path),
+ 'blob_ace_mode' => 'ruby',
+ 'content' => content)
+ end
+ end
+ end
+
context 'POST resolve_conflicts' do
let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
- def resolve_conflicts(sections)
+ def resolve_conflicts(files)
post :resolve_conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
project_id: merge_request_with_conflicts.project.to_param,
id: merge_request_with_conflicts.iid,
format: 'json',
- sections: sections,
+ files: files,
commit_message: 'Commit message'
end
context 'with valid params' do
before do
- resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin')
+ resolved_files = [
+ {
+ 'new_path' => 'files/ruby/popen.rb',
+ 'old_path' => 'files/ruby/popen.rb',
+ 'sections' => {
+ '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
+ }
+ }, {
+ 'new_path' => 'files/ruby/regex.rb',
+ 'old_path' => 'files/ruby/regex.rb',
+ 'sections' => {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ }
+ }
+ ]
+
+ resolve_conflicts(resolved_files)
end
it 'creates a new commit on the branch' do
@@ -678,7 +767,23 @@ describe Projects::MergeRequestsController do
context 'when sections are missing' do
before do
- resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head')
+ resolved_files = [
+ {
+ 'new_path' => 'files/ruby/popen.rb',
+ 'old_path' => 'files/ruby/popen.rb',
+ 'sections' => {
+ '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
+ }
+ }, {
+ 'new_path' => 'files/ruby/regex.rb',
+ 'old_path' => 'files/ruby/regex.rb',
+ 'sections' => {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head'
+ }
+ }
+ ]
+
+ resolve_conflicts(resolved_files)
end
it 'returns a 400 error' do
@@ -686,7 +791,71 @@ describe Projects::MergeRequestsController do
end
it 'has a message with the name of the first missing section' do
- expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9')
+ expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21')
+ end
+
+ it 'does not create a new commit' do
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ end
+ end
+
+ context 'when files are missing' do
+ before do
+ resolved_files = [
+ {
+ 'new_path' => 'files/ruby/regex.rb',
+ 'old_path' => 'files/ruby/regex.rb',
+ 'sections' => {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ }
+ }
+ ]
+
+ resolve_conflicts(resolved_files)
+ end
+
+ it 'returns a 400 error' do
+ expect(response).to have_http_status(:bad_request)
+ end
+
+ it 'has a message with the name of the missing file' do
+ expect(json_response['message']).to include('files/ruby/popen.rb')
+ end
+
+ it 'does not create a new commit' do
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ end
+ end
+
+ context 'when a file has identical content to the conflict' do
+ before do
+ resolved_files = [
+ {
+ 'new_path' => 'files/ruby/popen.rb',
+ 'old_path' => 'files/ruby/popen.rb',
+ 'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content
+ }, {
+ 'new_path' => 'files/ruby/regex.rb',
+ 'old_path' => 'files/ruby/regex.rb',
+ 'sections' => {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ }
+ }
+ ]
+
+ resolve_conflicts(resolved_files)
+ end
+
+ it 'returns a 400 error' do
+ expect(response).to have_http_status(:bad_request)
+ end
+
+ it 'has a message with the path of the problem file' do
+ expect(json_response['message']).to include('files/ruby/popen.rb')
end
it 'does not create a new commit' do
@@ -694,4 +863,82 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'POST assign_related_issues' do
+ let(:issue1) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+
+ def post_assign_issues
+ merge_request.update!(description: "Closes #{issue1.to_reference} and #{issue2.to_reference}",
+ author: user,
+ source_branch: 'feature',
+ target_branch: 'master')
+
+ post :assign_related_issues,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid
+ end
+
+ it 'shows a flash message on success' do
+ post_assign_issues
+
+ expect(flash[:notice]).to eq '2 issues have been assigned to you'
+ end
+
+ it 'correctly pluralizes flash message on success' do
+ issue2.update!(assignee: user)
+
+ post_assign_issues
+
+ expect(flash[:notice]).to eq '1 issue has been assigned to you'
+ end
+
+ it 'calls MergeRequests::AssignIssuesService' do
+ expect(MergeRequests::AssignIssuesService).to receive(:new).
+ with(project, user, merge_request: merge_request).
+ and_return(double(execute: { count: 1 }))
+
+ post_assign_issues
+ end
+
+ it 'is skipped when not signed in' do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ sign_out(:user)
+
+ expect(MergeRequests::AssignIssuesService).not_to receive(:new)
+
+ post_assign_issues
+ end
+ end
+
+ describe 'GET ci_environments_status' do
+ context 'the environment is from a forked project' do
+ let!(:forked) { create(:project) }
+ let!(:environment) { create(:environment, project: forked) }
+ let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
+ let(:json_response) { JSON.parse(response.body) }
+ let(:admin) { create(:admin) }
+
+ let(:merge_request) do
+ create(:forked_project_link, forked_to_project: forked,
+ forked_from_project: project)
+
+ create(:merge_request, source_project: forked, target_project: project)
+ end
+
+ before do
+ forked.team << [user, :master]
+
+ get :ci_environments_status,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project.to_param,
+ id: merge_request.iid, format: 'json'
+ end
+
+ it 'links to the environment on that project' do
+ expect(json_response.first['url']).to match /#{forked.path_with_namespace}/
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 5e2a8cf3849..b4f066d8600 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -1,69 +1,70 @@
require('spec_helper')
describe Projects::ProjectMembersController do
- describe '#apply_import' do
- let(:project) { create(:project) }
- let(:another_project) { create(:project, :private) }
- let(:user) { create(:user) }
- let(:member) { create(:user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
- before do
- project.team << [user, :master]
- another_project.team << [member, :guest]
- sign_in(user)
- end
+ describe 'GET index' do
+ it 'renders index with 200 status code' do
+ get :index, namespace_id: project.namespace, project_id: project
- shared_context 'import applied' do
- before do
- post(:apply_import, namespace_id: project.namespace,
- project_id: project,
- source_project_id: another_project.id)
- end
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:index)
end
+ end
- context 'when user can access source project members' do
- before { another_project.team << [user, :guest] }
- include_context 'import applied'
+ describe 'POST create' do
+ context 'when users are added' do
+ let(:project_user) { create(:user) }
- it 'imports source project members' do
- expect(project.team_members).to include member
- expect(response).to set_flash.to 'Successfully imported'
- expect(response).to redirect_to(
- namespace_project_project_members_path(project.namespace, project)
- )
- end
- end
+ before { sign_in(user) }
- context 'when user is not member of a source project' do
- include_context 'import applied'
+ context 'when user does not have enough rights' do
+ before { project.team << [user, :developer] }
- it 'does not import team members' do
- expect(project.team_members).not_to include member
- end
+ it 'returns 404' do
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: project_user.id,
+ access_level: Gitlab::Access::GUEST
- it 'responds with not found' do
- expect(response.status).to eq 404
+ expect(response).to have_http_status(404)
+ expect(project.users).not_to include project_user
+ end
end
- end
- end
- describe '#index' do
- context 'when user is member' do
- before do
- project = create(:project, :private)
- member = create(:user)
- project.team << [member, :guest]
- sign_in(member)
+ context 'when user has enough rights' do
+ before { project.team << [user, :master] }
- get :index, namespace_id: project.namespace, project_id: project
- end
+ it 'adds user to members' do
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: project_user.id,
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'Users were successfully added.'
+ expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ expect(project.users).to include project_user
+ end
- it { expect(response).to have_http_status(200) }
+ it 'adds no user to members' do
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: '',
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'No users or groups specified.'
+ expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ expect(project.users).not_to include project_user
+ end
+ end
end
end
- describe '#destroy' do
- let(:project) { create(:project, :public) }
+ describe 'DELETE destroy' do
+ let(:member) { create(:project_member, :developer, project: project) }
+
+ before { sign_in(user) }
context 'when member is not found' do
it 'returns 404' do
@@ -76,18 +77,8 @@ describe Projects::ProjectMembersController do
end
context 'when member is found' do
- let(:user) { create(:user) }
- let(:team_user) { create(:user) }
- let(:member) do
- project.team << [team_user, :developer]
- project.members.find_by(user_id: team_user.id)
- end
-
context 'when user does not have enough rights' do
- before do
- project.team << [user, :developer]
- sign_in(user)
- end
+ before { project.team << [user, :developer] }
it 'returns 404' do
delete :destroy, namespace_id: project.namespace,
@@ -95,15 +86,12 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to have_http_status(404)
- expect(project.users).to include team_user
+ expect(project.members).to include member
end
end
context 'when user has enough rights' do
- before do
- project.team << [user, :master]
- sign_in(user)
- end
+ before { project.team << [user, :master] }
it '[HTML] removes user from members' do
delete :destroy, namespace_id: project.namespace,
@@ -113,7 +101,7 @@ describe Projects::ProjectMembersController do
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
)
- expect(project.users).not_to include team_user
+ expect(project.members).not_to include member
end
it '[JS] removes user from members' do
@@ -122,33 +110,27 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to be_success
- expect(project.users).not_to include team_user
+ expect(project.members).not_to include member
end
end
end
end
- describe '#leave' do
- let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
+ describe 'DELETE leave' do
+ before { sign_in(user) }
context 'when member is not found' do
- before { sign_in(user) }
-
- it 'returns 403' do
+ it 'returns 404' do
delete :leave, namespace_id: project.namespace,
project_id: project
- expect(response).to have_http_status(403)
+ expect(response).to have_http_status(404)
end
end
context 'when member is found' do
context 'and is not an owner' do
- before do
- project.team << [user, :developer]
- sign_in(user)
- end
+ before { project.team << [user, :developer] }
it 'removes user from members' do
delete :leave, namespace_id: project.namespace,
@@ -161,11 +143,9 @@ describe Projects::ProjectMembersController do
end
context 'and is an owner' do
- before do
- project.update(namespace_id: user.namespace_id)
- project.team << [user, :master, user]
- sign_in(user)
- end
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ before { project.team << [user, :master] }
it 'cannot remove himself from the project' do
delete :leave, namespace_id: project.namespace,
@@ -176,10 +156,7 @@ describe Projects::ProjectMembersController do
end
context 'and is a requester' do
- before do
- project.request_access(user)
- sign_in(user)
- end
+ before { project.request_access(user) }
it 'removes user from members' do
delete :leave, namespace_id: project.namespace,
@@ -194,13 +171,8 @@ describe Projects::ProjectMembersController do
end
end
- describe '#request_access' do
- let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
+ describe 'POST request_access' do
+ before { sign_in(user) }
it 'creates a new ProjectMember that is not a team member' do
post :request_access, namespace_id: project.namespace,
@@ -215,8 +187,10 @@ describe Projects::ProjectMembersController do
end
end
- describe '#approve' do
- let(:project) { create(:project, :public) }
+ describe 'POST approve' do
+ let(:member) { create(:project_member, :access_request, project: project) }
+
+ before { sign_in(user) }
context 'when member is not found' do
it 'returns 404' do
@@ -229,18 +203,8 @@ describe Projects::ProjectMembersController do
end
context 'when member is found' do
- let(:user) { create(:user) }
- let(:team_requester) { create(:user) }
- let(:member) do
- project.request_access(team_requester)
- project.requesters.find_by(user_id: team_requester.id)
- end
-
context 'when user does not have enough rights' do
- before do
- project.team << [user, :developer]
- sign_in(user)
- end
+ before { project.team << [user, :developer] }
it 'returns 404' do
post :approve_access_request, namespace_id: project.namespace,
@@ -248,15 +212,12 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to have_http_status(404)
- expect(project.users).not_to include team_requester
+ expect(project.members).not_to include member
end
end
context 'when user has enough rights' do
- before do
- project.team << [user, :master]
- sign_in(user)
- end
+ before { project.team << [user, :master] }
it 'adds user to members' do
post :approve_access_request, namespace_id: project.namespace,
@@ -266,9 +227,89 @@ describe Projects::ProjectMembersController do
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
)
- expect(project.users).to include team_requester
+ expect(project.members).to include member
end
end
end
end
+
+ describe 'POST apply_import' do
+ let(:another_project) { create(:project, :private) }
+ let(:member) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ another_project.team << [member, :guest]
+ sign_in(user)
+ end
+
+ shared_context 'import applied' do
+ before do
+ post(:apply_import, namespace_id: project.namespace,
+ project_id: project,
+ source_project_id: another_project.id)
+ end
+ end
+
+ context 'when user can access source project members' do
+ before { another_project.team << [user, :guest] }
+ include_context 'import applied'
+
+ it 'imports source project members' do
+ expect(project.team_members).to include member
+ expect(response).to set_flash.to 'Successfully imported'
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ end
+ end
+
+ context 'when user is not member of a source project' do
+ include_context 'import applied'
+
+ it 'does not import team members' do
+ expect(project.team_members).not_to include member
+ end
+
+ it 'responds with not found' do
+ expect(response.status).to eq 404
+ end
+ end
+ end
+
+ describe 'POST create' do
+ let(:stranger) { create(:user) }
+
+ context 'when creating owner' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it 'does not create a member' do
+ expect do
+ post :create, user_ids: stranger.id,
+ namespace_id: project.namespace,
+ access_level: Member::OWNER,
+ project_id: project
+ end.to change { project.members.count }.by(0)
+ end
+ end
+
+ context 'when create master' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it 'creates a member' do
+ expect do
+ post :create, user_ids: stranger.id,
+ namespace_id: project.namespace,
+ access_level: Member::MASTER,
+ project_id: project
+ end.to change { project.members.count }.by(1)
+ end
+ 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/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index a6995145cc1..5e661c2c41d 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -17,4 +17,18 @@ describe Projects::TagsController do
expect(assigns(:releases)).not_to include(invalid_release)
end
end
+
+ describe 'GET show' do
+ before { get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: id }
+
+ context "valid tag" do
+ let(:id) { 'v1.0.0' }
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "invalid tag" do
+ let(:id) { 'latest' }
+ it { is_expected.to respond_with(:not_found) }
+ 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 b0f740f48f7..8eefa284ba0 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -41,6 +41,46 @@ describe ProjectsController do
end
end
end
+
+ describe "when project repository is disabled" do
+ render_views
+
+ before do
+ project.team << [user, :developer]
+ project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+ end
+
+ it 'shows wiki homepage' do
+ get :show, namespace_id: project.namespace.path, id: project.path
+
+ expect(response).to render_template('projects/_wiki')
+ end
+
+ it 'shows issues list page if wiki is disabled' do
+ project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+
+ get :show, namespace_id: project.namespace.path, id: project.path
+
+ expect(response).to render_template('projects/issues/_issues')
+ end
+
+ it 'shows customize workflow page if wiki and issues are disabled' do
+ project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+
+ get :show, namespace_id: project.namespace.path, id: project.path
+
+ expect(response).to render_template("projects/_customize_workflow")
+ end
+
+ it 'shows activity if enabled by user' do
+ user.update_attribute(:project_view, 'activity')
+
+ get :show, namespace_id: project.namespace.path, id: project.path
+
+ expect(response).to render_template("projects/_activity")
+ end
+ end
end
context "project with empty repo" do
@@ -63,6 +103,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
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 8f27e616c3e..48d69377461 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -109,6 +109,44 @@ describe SessionsController do
end
end
+ context 'when the user is on their last attempt' do
+ before do
+ user.update(failed_attempts: User.maximum_attempts.pred)
+ end
+
+ context 'when OTP is valid' do
+ it 'authenticates correctly' do
+ authenticate_2fa(otp_attempt: user.current_otp)
+
+ expect(subject.current_user).to eq user
+ end
+ end
+
+ context 'when OTP is invalid' do
+ before { authenticate_2fa(otp_attempt: 'invalid') }
+
+ it 'does not authenticate' do
+ expect(subject.current_user).not_to eq user
+ end
+
+ it 'warns about invalid login' do
+ expect(response).to set_flash.now[:alert]
+ .to /Invalid Login or password/
+ end
+
+ it 'locks the user' do
+ expect(user.reload).to be_access_locked
+ end
+
+ it 'keeps the user locked on future login attempts' do
+ post(:create, user: { login: user.username, password: user.password })
+
+ expect(response)
+ .to set_flash.now[:alert].to /Invalid Login or password/
+ end
+ end
+ end
+
context 'when another user does not have 2FA enabled' do
let(:another_user) { create(:user) }
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 54a2d3d9460..19a8b1fe524 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -73,8 +73,8 @@ describe UsersController do
end
context 'forked project' do
- let!(:project) { create(:project) }
- let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let(:project) { create(:project) }
+ let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do
sign_in(user)
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index 35c4a0b6f08..ec46146d9b5 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -1,5 +1,10 @@
FactoryGirl.define do
factory :board do
project factory: :empty_project
+
+ after(:create) do |board|
+ board.lists.create(list_type: :backlog)
+ board.lists.create(list_type: :done)
+ end
end
end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index 795df5dfda9..080b2e75ea1 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -9,5 +9,6 @@ FactoryGirl.define do
trait(:developer) { access_level GroupMember::DEVELOPER }
trait(:master) { access_level GroupMember::MASTER }
trait(:owner) { access_level GroupMember::OWNER }
+ trait(:access_request) { requested_at Time.now }
end
end
diff --git a/spec/factories/label_priorities.rb b/spec/factories/label_priorities.rb
new file mode 100644
index 00000000000..f25939d2d3e
--- /dev/null
+++ b/spec/factories/label_priorities.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :label_priority do
+ project factory: :empty_project
+ label
+ sequence(:priority)
+ end
+end
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index eb489099854..3e8822faf97 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -1,7 +1,23 @@
FactoryGirl.define do
- factory :label do
+ factory :label, class: ProjectLabel do
sequence(:title) { |n| "label#{n}" }
color "#990000"
project
+
+ transient do
+ priority nil
+ end
+
+ after(:create) do |label, evaluator|
+ if evaluator.priority
+ label.priorities.create(project: label.project, priority: evaluator.priority)
+ end
+ end
+ end
+
+ factory :group_label, class: GroupLabel do
+ sequence(:title) { |n| "label#{n}" }
+ color "#990000"
+ group
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index c6a08d78b78..f780e01253c 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -68,5 +68,15 @@ FactoryGirl.define do
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs]
+
+ factory :labeled_merge_request do
+ transient do
+ labels []
+ end
+
+ after(:create) do |merge_request, evaluator|
+ merge_request.update_attributes(labels: evaluator.labels)
+ end
+ end
end
end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index cf3659ba275..c21927640d1 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -4,24 +4,10 @@ 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 }
+ trait(:access_request) { requested_at Time.now }
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index e61b1fd9647..4065e2defbc 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -9,6 +9,9 @@ FactoryGirl.define do
namespace
creator
+ # Behaves differently to nil due to cache_has_external_issue_tracker
+ has_external_issue_tracker false
+
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
end
@@ -27,6 +30,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
@@ -34,6 +45,7 @@ FactoryGirl.define do
snippets_access_level ProjectFeature::ENABLED
issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED
+ repository_access_level ProjectFeature::ENABLED
end
after(:create) do |project, evaluator|
@@ -44,6 +56,7 @@ FactoryGirl.define do
snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level,
merge_requests_access_level: evaluator.merge_requests_access_level,
+ repository_access_level: evaluator.repository_access_level
)
end
end
@@ -56,6 +69,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
@@ -77,6 +97,8 @@ FactoryGirl.define do
end
factory :redmine_project, parent: :project do
+ has_external_issue_tracker true
+
after :create do |project|
project.create_redmine_service(
active: true,
@@ -90,6 +112,8 @@ FactoryGirl.define do
end
factory :jira_project, parent: :project do
+ has_external_issue_tracker true
+
after :create do |project|
project.create_jira_service(
active: true,
@@ -102,12 +126,4 @@ FactoryGirl.define do
)
end
end
-
- 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/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index a8833194421..f8c3ccb416b 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -53,7 +53,7 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in issue descriptions' do
- expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/
+ expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/
end
it 'has XHTML summaries in notes' do
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 26ea06e002b..0fb1608a0a3 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -4,7 +4,8 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
- let(:project) { create(:project_with_board, :public) }
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:user2) { create(:user) }
@@ -17,7 +18,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'no lists' do
before do
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
end
@@ -34,14 +35,14 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'creates default lists' do
- lists = ['Backlog', 'Development', 'Testing', 'Production', 'Ready', 'Done']
+ lists = ['Backlog', 'To Do', 'Doing', 'Done']
page.within(find('.board-blank-state')) do
click_button('Add default lists')
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 6)
+ expect(page).to have_selector('.board', count: 4)
page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i])
@@ -60,8 +61,8 @@ describe 'Issue Boards', feature: true, js: true do
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) }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
let!(:issue1) { create(:issue, project: project, assignee: user) }
@@ -75,7 +76,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
before do
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
@@ -169,7 +170,7 @@ describe 'Issue Boards', feature: true, js: true do
create(:issue, project: project)
end
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
page.within(find('.board', match: :first)) do
@@ -468,7 +469,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'removes filtered labels' do
wait_for_vue_resource
-
+
page.within '.labels-filter' do
click_button('Label')
wait_for_ajax
@@ -603,7 +604,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'keyboard shortcuts' do
before do
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
@@ -616,7 +617,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'signed out user' do
before do
logout
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
@@ -632,7 +633,7 @@ describe 'Issue Boards', feature: true, js: true do
project.team << [user_guest, :guest]
logout
login_as(user_guest)
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
index 7ef68e9eb8d..a5fc766401f 100644
--- a/spec/features/boards/keyboard_shortcut_spec.rb
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -6,9 +6,7 @@ describe 'Issue Boards shortcut', feature: true, js: true do
let(:project) { create(:empty_project) }
before do
- project.create_board
- project.board.lists.create(list_type: :backlog)
- project.board.lists.create(list_type: :done)
+ create(:board, project: project)
login_as :admin
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
new file mode 100644
index 00000000000..67d6da5f39a
--- /dev/null
+++ b/spec/features/boards/new_issue_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+describe 'Issue Boards new issue', feature: true, js: true do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+
+ context 'authorized user' do
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'displays new issue button' do
+ expect(page).to have_selector('.board-issue-count-holder .btn', count: 1)
+ end
+
+ it 'does not display new issue button in done list' do
+ page.within('.board:nth-child(3)') do
+ expect(page).not_to have_selector('.board-issue-count-holder .btn')
+ end
+ end
+
+ it 'shows form when clicking button' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+
+ expect(page).to have_selector('.board-new-issue-form')
+ end
+ end
+
+ it 'hides form when clicking cancel' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+
+ expect(page).to have_selector('.board-new-issue-form')
+
+ click_button 'Cancel'
+
+ expect(page).to have_selector('.board-new-issue-form', visible: false)
+ end
+ end
+
+ it 'creates new issue' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+ end
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('bug')
+ click_button 'Submit issue'
+ end
+
+ wait_for_vue_resource
+
+ page.within(first('.board .board-issue-count')) do
+ expect(page).to have_content('1')
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ before do
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+ end
+
+ it 'does not display new issue button' do
+ expect(page).to have_selector('.board-issue-count-holder .btn', count: 0)
+ end
+ end
+end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index f52682f229c..7fa0c95cae2 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -5,7 +5,9 @@ feature 'Contributions Calendar', js: true, feature: true do
let(:contributed_project) { create(:project, :public) }
- date_format = '%A %b %d, %Y'
+ # Ex/ Sunday Jan 1, 2016
+ date_format = '%A %b %-d, %Y'
+
issue_title = 'Bug in old browser'
issue_params = { title: issue_title }
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
index ca7f73e24cc..43eb4000e58 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 Git revision", with: selection)
+ find_link(selection, visible: true).click
+ end
end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
new file mode 100644
index 00000000000..ba77093a6d4
--- /dev/null
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Project member activity', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public, name: 'x', namespace: user.namespace) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ def visit_activities_and_wait_with_event(event_type)
+ Event.create(project: project, author_id: user.id, action: event_type)
+ visit activity_namespace_project_path(project.namespace.path, project.path)
+ wait_for_ajax
+ end
+
+ subject { page.find(".event-title").text }
+
+ context 'when a user joins the project' do
+ before { visit_activities_and_wait_with_event(Event::JOINED) }
+
+ it { is_expected.to eq("#{user.name} joined project") }
+ end
+
+ context 'when a user leaves the project' do
+ before { visit_activities_and_wait_with_event(Event::LEFT) }
+
+ it { is_expected.to eq("#{user.name} left project") }
+ end
+
+ context 'when a users membership expires for the project' do
+ before { visit_activities_and_wait_with_event(Event::EXPIRED) }
+
+ it "presents the correct message" do
+ message = "#{user.name} removed due to membership expiration from project"
+ is_expected.to eq(message)
+ end
+ 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 4309a726917..b565586ee14 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -19,10 +19,22 @@ feature 'Environments', feature: true do
visit namespace_project_environments_path(project.namespace, project)
end
+ context 'shows two tabs' do
+ scenario 'shows "Available" and "Stopped" tab with links' do
+ expect(page).to have_link('Available')
+ expect(page).to have_link('Stopped')
+ end
+ end
+
context 'without environments' do
scenario 'does show no environments' do
expect(page).to have_content('You don\'t have any environments right now.')
end
+
+ scenario 'does show 0 as counter for environments in both tabs' do
+ expect(page.find('.js-available-environments-count').text).to eq('0')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ end
end
context 'with environments' do
@@ -32,6 +44,11 @@ feature 'Environments', feature: true do
expect(page).to have_link(environment.name)
end
+ scenario 'does show number of available and stopped environments' do
+ expect(page.find('.js-available-environments-count').text).to eq('1')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ end
+
context 'without deployments' do
scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet')
@@ -45,6 +62,10 @@ feature 'Environments', feature: true do
expect(page).to have_link(deployment.short_sha)
end
+ scenario 'does show deployment internal id' do
+ expect(page).to have_content(deployment.iid)
+ end
+
context 'with build and manual actions' do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
@@ -61,6 +82,51 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
+
+ scenario 'does show build name and id' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+
+ scenario 'does not show external link button' do
+ expect(page).not_to have_css('external-url')
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
+
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ scenario 'does show stop button' do
+ expect(page).to have_selector('.stop-env-link')
+ end
+
+ scenario 'starts build when stop button clicked' do
+ first('.stop-env-link').click
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+ end
end
end
end
@@ -109,6 +175,10 @@ feature 'Environments', feature: true do
expect(page).to have_link('Re-deploy')
end
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+
context 'with manual action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
@@ -122,6 +192,39 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
+
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop')
+ end
+
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+ end
+ end
end
end
end
@@ -168,29 +271,4 @@ feature 'Environments', feature: true do
end
end
end
-
- describe 'when deleting existing environment' do
- given(:environment) { create(:environment, project: project) }
-
- before do
- visit namespace_project_environment_path(project.namespace, project, environment)
- end
-
- context 'when logged as master' do
- given(:role) { :master }
-
- scenario 'does delete environment' do
- click_link 'Destroy'
- expect(page).not_to have_link(environment.name)
- end
- end
-
- context 'when logged as developer' do
- given(:role) { :developer }
-
- scenario 'does not have a Destroy link' do
- expect(page).not_to have_link('Destroy')
- end
- end
- end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8863554ee91..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')
@@ -231,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')
@@ -239,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/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
index 10d3713f19f..d811b05b0c3 100644
--- a/spec/features/groups/members/owner_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -41,7 +41,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do
def expect_visible_access_request(group, user)
expect(group.requesters.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "#{group.name} access requests 1"
+ expect(page).to have_content "Users requesting access to #{group.name} 1"
expect(page).to have_content user.name
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 2d8b59472e8..13bfe90302c 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -5,43 +5,105 @@ feature 'Group', feature: true do
login_as(:admin)
end
- describe 'creating a group with space in group path' do
- it 'renders new group form with validation errors' do
- visit new_group_path
- fill_in 'Group path', with: 'space group'
+ matcher :have_namespace_error_message do
+ match do |page|
+ page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.")
+ end
+ end
+
+ describe 'create a group' do
+ before { visit new_group_path }
+
+ describe 'with space in group path' do
+ it 'renders new group form with validation errors' do
+ fill_in 'Group path', with: 'space group'
+ click_button 'Create group'
+
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_namespace_error_message
+ end
+ end
+
+ describe 'with .atom at end of group path' do
+ it 'renders new group form with validation errors' do
+ fill_in 'Group path', with: 'atom_group.atom'
+ click_button 'Create group'
+
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_namespace_error_message
+ end
+ end
+
+ describe 'with .git at end of group path' do
+ it 'renders new group form with validation errors' do
+ fill_in 'Group path', with: 'git_group.git'
+ click_button 'Create group'
+
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_namespace_error_message
+ end
+ end
+ end
+
+ describe 'group edit' do
+ let(:group) { create(:group) }
+ let(:path) { edit_group_path(group) }
+ let(:new_name) { 'new-name' }
+
+ before { visit path }
- click_button 'Create group'
+ it 'saves new settings' do
+ fill_in 'group_name', with: new_name
+ click_button 'Save group'
- expect(current_path).to eq(groups_path)
- expect(page).to have_content("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.'.")
+ expect(page).to have_content 'successfully updated'
+ expect(find('#group_name').value).to eq(new_name)
+
+ page.within ".navbar-gitlab" do
+ expect(page).to have_content new_name
+ end
+ end
+
+ it 'removes group' do
+ click_link 'Remove Group'
+
+ expect(page).to have_content "scheduled for deletion"
end
end
- describe 'description' do
+ describe 'group page with markdown description' do
let(:group) { create(:group) }
let(:path) { group_path(group) }
it 'parses Markdown' do
group.update_attribute(:description, 'This is **my** group')
+
visit path
+
expect(page).to have_css('.description > p > strong')
end
it 'passes through html-pipeline' do
group.update_attribute(:description, 'This group is the :poop:')
+
visit path
+
expect(page).to have_css('.description > p > img')
end
it 'sanitizes unwanted tags' do
group.update_attribute(:description, '# Group Description')
+
visit path
+
expect(page).not_to have_css('.description h1')
end
it 'permits `rel` attribute on links' do
group.update_attribute(:description, 'https://google.com/')
+
visit path
+
expect(page).to have_css('.description a[rel]')
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 79cc50bc18e..ef00f209998 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe 'Awards Emoji', feature: true do
+ include WaitForAjax
+
let!(:project) { create(:project) }
let!(:user) { create(:user) }
@@ -16,20 +18,22 @@ describe 'Awards Emoji', feature: true do
project: project)
end
+ let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
+
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
- sleep 2
+ wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
it 'increments the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
- sleep 2
+ wait_for_ajax
expect(thumbsup_emoji).to have_text("1")
end
@@ -41,7 +45,7 @@ describe 'Awards Emoji', feature: true do
context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
- sleep 2
+ wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
@@ -49,13 +53,45 @@ describe 'Awards Emoji', feature: true do
expect(thumbsup_emoji).to have_text("0")
end
end
+
+ it 'toggles the smiley emoji on a note', js: true do
+ toggle_smiley_emoji(true)
+
+ within('.note-awards') do
+ expect(find(emoji_counter)).to have_text("1")
+ end
+
+ toggle_smiley_emoji(false)
+
+ within('.note-awards') do
+ expect(page).not_to have_selector(emoji_counter)
+ end
+ end
end
def thumbsup_emoji
- page.all('span.js-counter').first
+ page.all(emoji_counter).first
end
def thumbsdown_emoji
- page.all('span.js-counter').last
+ page.all(emoji_counter).last
+ end
+
+ def emoji_counter
+ 'span.js-counter'
+ end
+
+ def toggle_smiley_emoji(status)
+ within('.note') do
+ find('.note-emoji-button').click
+ end
+
+ unless status
+ first('[data-emoji="smiley"]').click
+ else
+ find('[data-emoji="smiley"]').click
+ end
+
+ wait_for_ajax
end
end
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 d1501c9791a..78208aed46d 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
@@ -96,9 +96,9 @@ describe 'Filter issues', feature: true do
wait_for_ajax
page.within '.labels-filter' do
- expect(page).to have_content 'No Label'
+ expect(page).to have_content 'Labels'
end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
end
it 'filters by a label' do
@@ -110,29 +110,37 @@ describe 'Filter issues', feature: true do
end
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
+ expect(page).to have_content wontfix.title
+ click_link label.title
end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
+
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more")
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
+ page.within '.labels-filter' do
+ click_link wontfix.title
+ expect(page).to have_content wontfix.title
+ end
+
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(wontfix.title)
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
@@ -146,7 +154,7 @@ describe 'Filter issues', feature: true do
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)
@@ -226,6 +234,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and label' do
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
@@ -236,6 +245,7 @@ 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
@@ -244,6 +254,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and milestone' do
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
@@ -253,6 +264,7 @@ 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
@@ -261,6 +273,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and assignee' do
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
@@ -270,6 +283,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
@@ -278,6 +292,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and author' do
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
@@ -287,6 +302,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
@@ -315,6 +331,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/form_spec.rb b/spec/features/issues/form_spec.rb
new file mode 100644
index 00000000000..8771cc8e157
--- /dev/null
+++ b/spec/features/issues/form_spec.rb
@@ -0,0 +1,119 @@
+require 'rails_helper'
+
+describe 'New/edit issue', feature: true, js: true do
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+ let!(:issue) { create(:issue, project: project, assignee: user, milestone: milestone) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'new issue' do
+ before do
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'allows user to create new issue' do
+ fill_in 'issue_title', with: 'title'
+ fill_in 'issue_description', with: 'title'
+
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Submit issue'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+
+ context 'edit issue' do
+ before do
+ visit edit_namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'allows user to update issue' do
+ expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+
+ page.within '.js-user-search' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 7773c486b4e..055210399a7 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -55,7 +55,7 @@ feature 'issue move to another project' do
first('.select2-choice').click
end
- fill_in('s2id_autogen2_search', with: new_project_search.name)
+ fill_in('s2id_autogen1_search', with: new_project_search.name)
page.within '.select2-drop' do
expect(page).to have_content(new_project_search.name)
diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
index f4d0f13c3d5..c9a3ecf16ea 100644
--- a/spec/features/issues/reset_filters_spec.rb
+++ b/spec/features/issues/reset_filters_spec.rb
@@ -75,6 +75,14 @@ feature 'Issues filter reset button', feature: true, js: true do
end
end
+ context 'when no filters have been applied' do
+ it 'the reset link should not be visible' do
+ visit_issues(project)
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).not_to have_css '.reset_filters'
+ end
+ end
+
def reset_filters
find('.reset-filters').click
end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index bf2b93c92fb..3f2da1c380c 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -99,5 +99,15 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
end
end
end
+
+ describe 'toggling the WIP prefix from the title from note' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'does not recognize the command nor create a note' do
+ write_note("/wip")
+
+ expect(page).not_to have_content '/wip'
+ end
+ end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 9fe40ea0892..b504329656f 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -51,9 +51,8 @@ describe 'Issues', feature: true do
expect(page).to have_content "Assignee #{@user.name}"
- first('#s2id_issue_assignee_id').click
- sleep 2 # wait for ajax stuff to complete
- first('.user-result').click
+ first('.js-user-search').click
+ click_link 'Unassigned'
click_button 'Save changes'
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 2523b4b7898..996f39ea06d 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -29,7 +29,7 @@ feature 'Login', feature: true do
describe 'with two-factor authentication' do
def enter_code(code)
- fill_in 'Two-Factor Authentication code', with: code
+ fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
new file mode 100644
index 00000000000..43cc6f2a2a7
--- /dev/null
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -0,0 +1,51 @@
+require 'rails_helper'
+
+feature 'Merge request issue assignment', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue1.to_reference} and #{issue2.to_reference}") }
+ let(:service) { MergeRequests::AssignIssuesService.new(merge_request, user, user, project) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ def visit_merge_request(current_user = nil)
+ login_as(current_user || user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'logged in as author' do
+ scenario 'updates related issues' do
+ visit_merge_request
+ click_link "Assign yourself to these issues"
+
+ expect(page).to have_content "2 issues have been assigned to you"
+ end
+
+ it 'returns user to the merge request' do
+ visit_merge_request
+ click_link "Assign yourself to these issues"
+
+ expect(page).to have_content merge_request.description
+ end
+
+ it "doesn't display if related issues are already assigned" do
+ [issue1, issue2].each { |issue| issue.update!(assignee: user) }
+
+ visit_merge_request
+
+ expect(page).not_to have_content "Assign yourself"
+ end
+ end
+
+ context 'not MR author' do
+ it "doesn't not show assignment link" do
+ visit_merge_request(create(:user))
+
+ expect(page).not_to have_content "Assign yourself"
+ end
+ end
+end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 759edf8ec80..d258ff52bbb 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do
end
end
- context 'when a merge request can be resolved in the UI' do
- let(:merge_request) { create_merge_request('conflict-resolvable') }
+ shared_examples "conflicts are resolved in Interactive mode" do
+ it 'conflicts are resolved in Interactive mode' do
+ within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
+ click_button 'Use ours'
+ end
+
+ within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
+ all('button', text: 'Use ours').each do |button|
+ button.click
+ end
+ end
+
+ click_button 'Commit conflict resolution'
+ wait_for_ajax
+
+ expect(page).to have_content('All merge conflicts were resolved')
+ merge_request.reload_diff
+
+ click_on 'Changes'
+ wait_for_ajax
+
+ within find('.diff-file', text: 'files/ruby/popen.rb') do
+ expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }")
+ expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }")
+ end
+
+ within find('.diff-file', text: 'files/ruby/regex.rb') do
+ expect(page).to have_selector('.line_content.new', text: "def username_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def project_name_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def path_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def default_regexp")
+ end
+ end
+ end
+ shared_examples "conflicts are resolved in Edit inline mode" do
+ it 'conflicts are resolved in Edit inline mode' do
+ expect(find('#conflicts')).to have_content('popen.rb')
+
+ within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
+ click_button 'Edit inline'
+ wait_for_ajax
+ execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");')
+ end
+
+ within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
+ click_button 'Edit inline'
+ wait_for_ajax
+ execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
+ end
+
+ click_button 'Commit conflict resolution'
+ wait_for_ajax
+ expect(page).to have_content('All merge conflicts were resolved')
+ merge_request.reload_diff
+
+ click_on 'Changes'
+ wait_for_ajax
+
+ expect(page).to have_content('One morning')
+ expect(page).to have_content('Gregor Samsa woke from troubled dreams')
+ end
+ end
+
+ context 'can be resolved in the UI' do
before do
project.team << [user, :developer]
login_as(user)
-
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'shows a link to the conflict resolution page' do
- expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ context 'the conflicts are resolvable' do
+ let(:merge_request) { create_merge_request('conflict-resolvable') }
+
+ before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) }
+
+ it 'shows a link to the conflict resolution page' do
+ expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ context 'in Inline view mode' do
+ before { click_link('conflicts', href: /\/conflicts\Z/) }
+
+ include_examples "conflicts are resolved in Interactive mode"
+ include_examples "conflicts are resolved in Edit inline mode"
+ end
+
+ context 'in Parallel view mode' do
+ before do
+ click_link('conflicts', href: /\/conflicts\Z/)
+ click_button 'Side-by-side'
+ end
+
+ include_examples "conflicts are resolved in Interactive mode"
+ include_examples "conflicts are resolved in Edit inline mode"
+ end
end
- context 'visiting the conflicts resolution page' do
- before { click_link('conflicts', href: /\/conflicts\Z/) }
+ context 'the conflict contain markers' do
+ let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') }
- it 'shows the conflicts' do
- begin
- expect(find('#conflicts')).to have_content('popen.rb')
- rescue Capybara::Poltergeist::JavascriptError
- retry
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ click_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ it 'conflicts can not be resolved in Interactive mode' do
+ within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
+ expect(page).not_to have_content 'Interactive mode'
+ expect(page).not_to have_content 'Edit inline'
+ end
+ end
+
+ it 'conflicts are resolved in Edit inline mode' do
+ within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
+ wait_for_ajax
+ execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
end
+
+ click_button 'Commit conflict resolution'
+ wait_for_ajax
+
+ expect(page).to have_content('All merge conflicts were resolved')
+
+ merge_request.reload_diff
+
+ click_on 'Changes'
+ wait_for_ajax
+ find('.click-to-expand').click
+ wait_for_ajax
+
+ expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end
end
end
@@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do
UNRESOLVABLE_CONFLICTS = {
'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file',
- 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
}
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 4d5d4aa121a..cfc1244429f 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -25,6 +25,20 @@ feature 'Merge request created from fork' do
expect(page).to have_content 'Test merge request'
end
+ context 'source project is deleted' do
+ background do
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ fork_project.destroy!
+ end
+
+ scenario 'user can access merge request' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_content 'Test merge request'
+ expect(page).to have_content "(removed):#{merge_request.source_branch}"
+ end
+ end
+
context 'pipeline present in source project' do
include WaitForAjax
@@ -45,7 +59,7 @@ feature 'Merge request created from fork' do
page.within('.merge-request-tabs') { click_link 'Builds' }
wait_for_ajax
- page.within('table.builds') do
+ page.within('table.ci-table') do
expect(page).to have_content 'rspec'
expect(page).to have_content 'spinach'
end
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/form_spec.rb b/spec/features/merge_requests/form_spec.rb
new file mode 100644
index 00000000000..7594cbf54e8
--- /dev/null
+++ b/spec/features/merge_requests/form_spec.rb
@@ -0,0 +1,273 @@
+require 'rails_helper'
+
+describe 'New/edit merge request', feature: true, js: true do
+ let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ context 'owned projects' do
+ before do
+ login_as(user)
+ end
+
+ context 'new merge request' do
+ before do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+
+ it 'creates new merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Submit merge request'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+
+ context 'edit merge request' do
+ before do
+ merge_request = create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'master'
+ )
+
+ visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'updates merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+ end
+
+ context 'forked project' do
+ before do
+ fork_project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'new merge request' do
+ before do
+ visit new_namespace_project_merge_request_path(
+ fork_project.namespace,
+ fork_project,
+ merge_request: {
+ source_project_id: fork_project.id,
+ target_project_id: project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+
+ it 'creates new merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Submit merge request'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+
+ context 'edit merge request' do
+ before do
+ merge_request = create(:merge_request,
+ source_project: fork_project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'master'
+ )
+
+ visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should update merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
index 22d9e42119d..23cee891bac 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -1,12 +1,13 @@
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
@@ -47,6 +48,16 @@ feature 'Merge Request versions', js: true, feature: true do
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'
@@ -61,10 +72,6 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
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
diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
index 60bc07bd1a0..c3c3ab33872 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -2,18 +2,26 @@ require 'spec_helper'
feature 'Merge When Build Succeeds', feature: true, js: true do
let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
- let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project,
+ author: user,
+ title: 'Bug NS-04')
+ end
- before do
- project.team << [user, :master]
- project.enable_ci
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
end
- context "Active build for Merge Request" do
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
+ before { project.team << [user, :master] }
+
+ context 'when there is active build for merge request' do
+ background do
+ create(:ci_build, pipeline: pipeline)
+ end
before do
login_as user
@@ -41,26 +49,30 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
end
end
- context 'When it is enabled' do
+ context 'when merge when build succeeds is enabled' do
let(:merge_request) do
- create(:merge_request_with_diffs, :simple, source_project: project, author: user,
- merge_user: user, title: "MepMep", merge_when_build_succeeds: true)
+ create(:merge_request_with_diffs, :simple, source_project: project,
+ author: user,
+ merge_user: user,
+ title: 'MepMep',
+ merge_when_build_succeeds: true)
end
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline)
+ end
before do
login_as user
visit_merge_request(merge_request)
end
- it 'cancels the automatic merge' do
+ it 'allows to cancel the automatic merge' do
click_link "Cancel Automatic Merge"
expect(page).to have_button "Merge When Build Succeeds"
- visit_merge_request(merge_request) # Needed to refresh the page
+ visit_merge_request(merge_request) # refresh the page
expect(page).to have_content "Canceled the automatic merge"
end
@@ -70,15 +82,26 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
click_link "Remove Source Branch When Merged"
expect(page).to have_content "The source branch will be removed"
end
+
+ context 'when build succeeds' do
+ background { build.success }
+
+ it 'merges merge request' do
+ visit_merge_request(merge_request) # refresh the page
+
+ expect(page).to have_content 'The changes were merged'
+ expect(merge_request.reload).to be_merged
+ end
+ end
end
- context 'Build is not active' do
- it "does not allow for enabling" do
+ context 'when build is not active' do
+ it "does not allow to enable merge when build succeeds" do
visit_merge_request(merge_request)
expect(page).not_to have_link "Merge When Build Succeeds"
end
end
-
+
def visit_merge_request(merge_request)
visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
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 22d9d1b9fd5..7b8af555f0e 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -14,7 +14,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
end
- describe 'adding a due date from note' do
+ describe 'merge-request-only commands' do
before do
project.team << [user, :master]
login_with(user)
@@ -25,10 +25,55 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
wait_for_ajax
end
- it 'does not recognize the command nor create a note' do
- write_note("/due 2016-08-28")
+ describe 'toggling the WIP prefix in the title from note' do
+ context 'when the current user can toggle the WIP prefix' do
+ it 'adds the WIP: prefix to the title' do
+ write_note("/wip")
- expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload.work_in_progress?).to eq true
+ end
+
+ it 'removes the WIP: prefix from the title' do
+ merge_request.title = merge_request.wip_title
+ merge_request.save
+ write_note("/wip")
+
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ end
+ end
+
+ context 'when the current user cannot toggle the WIP prefix' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not change the WIP prefix' do
+ write_note("/wip")
+
+ expect(page).not_to have_content '/wip'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ end
+ end
+ end
+
+ describe 'adding a due date from note' do
+ it 'does not recognize the command nor create a note' do
+ write_note('/due 2016-08-28')
+
+ expect(page).not_to have_content '/due 2016-08-28'
+ end
end
end
end
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
new file mode 100644
index 00000000000..6676821b807
--- /dev/null
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+feature 'Widget Deployments Header', feature: true, js: true do
+ include WaitForAjax
+
+ describe 'when deployed to an environment' do
+ given(:user) { create(:user) }
+ given(:project) { merge_request.target_project }
+ given(:merge_request) { create(:merge_request, :merged) }
+ given(:environment) { create(:environment, project: project) }
+ given(:role) { :developer }
+ given(:sha) { project.commit('master').id }
+ given!(:deployment) { create(:deployment, environment: environment, sha: sha) }
+ given!(:manual) { }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ scenario 'displays that the environment is deployed' do
+ wait_for_ajax
+
+ expect(page).to have_content("Deployed to #{environment.name}")
+ expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
+ end
+
+ context 'with stop action' do
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) do
+ create(:deployment, environment: environment, ref: merge_request.target_branch,
+ sha: sha, deployable: build, on_stop: 'close_app')
+ end
+
+ background do
+ wait_for_ajax
+ end
+
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop environment')
+ end
+
+ scenario 'does start build when stop button clicked' do
+ click_link('Stop environment')
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ given(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop environment')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index f1c522155d3..5d7247e2a62 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -240,6 +240,18 @@ describe 'Comments', feature: true do
is_expected.to have_css('.notes_holder .note', count: 1)
is_expected.to have_button('Reply...')
end
+
+ it 'adds code to discussion' do
+ click_button 'Reply...'
+
+ page.within(first('.js-discussion-note-form')) do
+ fill_in 'note[note]', with: '```{{ test }}```'
+
+ click_button('Comment')
+ end
+
+ expect(page).to have_content('{{ test }}')
+ end
end
end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index 3b20d38c520..eb1050d21c6 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -1,18 +1,57 @@
require 'rails_helper'
-describe 'Profile > SSH Keys', feature: true do
+feature '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
+ describe 'User adds a key' do
+ before do
+ visit profile_keys_path
+ end
+
+ scenario '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
+
+ scenario 'saves the new key' do
+ attrs = attributes_for(:key)
+
+ fill_in('Key', with: attrs[:key])
+ fill_in('Title', with: attrs[:title])
+ click_button('Add key')
+
+ expect(page).to have_content("Title: #{attrs[:title]}")
+ expect(page).to have_content(attrs[:key])
+ end
+ end
+
+ scenario 'User sees their keys' do
+ key = create(:key, user: user)
+ visit profile_keys_path
+
+ expect(page).to have_content(key.title)
+ end
+
+ scenario 'User removes a key via the key index' do
+ create(:key, user: user)
+ visit profile_keys_path
+
+ click_link('Remove')
+
+ expect(page).to have_content('Your SSH keys (0)')
+ end
+
+ scenario 'User removes a key via its details page' do
+ key = create(:key, user: user)
+ visit profile_key_path(key)
+
+ click_link('Remove')
+
+ expect(page).to have_content('Your SSH keys (0)')
end
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 5972e7f31c2..01a95bf49ac 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -59,7 +59,7 @@ feature 'test coverage badge' do
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
- pipeline.build_updated
+ pipeline.update_status
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 714b54d2e4b..45d333184bb 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -2,8 +2,11 @@ require 'spec_helper'
include WaitForAjax
describe 'Edit Project Settings', feature: true do
+ include WaitForAjax
+
let(:member) { create(:user) }
let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
+ let!(:issue) { create(:issue, project: project) }
let(:non_member) { create(:user) }
describe 'project features visibility selectors', js: true do
@@ -119,4 +122,31 @@ describe 'Edit Project Settings', feature: true do
end
end
end
+
+ describe 'repository visibility', js: true do
+ before do
+ project.team << [member, :master]
+ login_as(member)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ it "disables repository related features" do
+ select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+
+ expect(find(".edit-project")).to have_selector("select.disabled", count: 2)
+ end
+
+ it "shows empty features project homepage" do
+ select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+ select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+ select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+
+ click_button "Save changes"
+ wait_for_ajax
+
+ visit namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content "Customize your workflow!"
+ 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/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
new file mode 100644
index 00000000000..fc88fd74af8
--- /dev/null
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+feature 'Find file keyboard shortcuts', 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 namespace_project_find_file_path(project.namespace, project, project.repository.root_ref)
+
+ wait_for_ajax
+ end
+
+ it 'opens file when pressing enter key' do
+ fill_in 'file_find', with: 'CHANGELOG'
+
+ find('#file_find').native.send_keys(:enter)
+
+ expect(page).to have_selector('.blob-content-holder')
+
+ page.within('.file-title') do
+ expect(page).to have_content('CHANGELOG')
+ end
+ end
+
+ it 'navigates files with arrow keys' do
+ fill_in 'file_find', with: 'application.'
+
+ find('#file_find').native.send_keys(:down)
+ find('#file_find').native.send_keys(:enter)
+
+ expect(page).to have_selector('.blob-content-holder')
+
+ page.within('.file-title') do
+ expect(page).to have_content('application.js')
+ end
+ end
+end
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
new file mode 100644
index 00000000000..c22441f8929
--- /dev/null
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe "Guest navigation menu" do
+ let(:project) { create :empty_project, :private }
+ let(:guest) { create :user }
+
+ before do
+ project.team << [guest, :guest]
+
+ login_as(guest)
+ end
+
+ it "shows allowed tabs only" do
+ visit namespace_project_path(project.namespace, project)
+
+ within(".nav-links") do
+ expect(page).to have_content 'Project'
+ expect(page).to have_content 'Activity'
+ expect(page).to have_content 'Issues'
+ expect(page).to have_content 'Wiki'
+
+ expect(page).not_to have_content 'Repository'
+ expect(page).not_to have_content 'Pipelines'
+ expect(page).not_to have_content 'Graphs'
+ expect(page).not_to have_content 'Merge Requests'
+ end
+ end
+end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 27c986c5187..52d08982c7a 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -47,6 +47,8 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
expect(page).to have_content('Download export')
+ expect(file_permissions(project.export_path)).to eq(0700)
+
in_directory_with_expanded_export(project) do |exit_status, tmpdir|
expect(exit_status).to eq(0)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 09cd6369881..3015576f6f8 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,13 +3,8 @@ require 'spec_helper'
feature 'Import/Export - project import integration test', feature: true, js: true do
include Select2Helper
- let(:admin) { create(:admin) }
- let(:normal_user) { create(:user) }
- let!(:namespace) { create(:namespace, name: "asd", owner: admin) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
- let(:project) { Project.last }
- let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
@@ -19,41 +14,43 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
FileUtils.rm_rf(export_path, secure: true)
end
- context 'admin user' do
+ context 'when selecting the namespace' do
+ let(:user) { create(:admin) }
+ let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+
before do
- login_as(admin)
+ login_as(user)
end
scenario 'user imports an exported project successfully' do
- expect(Project.all.count).to be_zero
-
visit new_project_path
- select2('2', from: '#project_namespace_id')
+ select2(namespace.id, from: '#project_namespace_id')
fill_in :project_path, with: 'test-project-path', visible: true
click_link 'GitLab export'
expect(page).to have_content('GitLab project export')
- expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+ expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path")
attach_file('file', file)
- click_on 'Import project' # import starts
+ expect { click_on 'Import project' }.to change { Project.count }.from(0).to(1)
+ project = Project.last
expect(project).not_to be_nil
expect(project.issues).not_to be_empty
expect(project.merge_requests).not_to be_empty
- expect(project_hook).to exist
- expect(wiki_exists?).to be true
+ expect(project_hook_exists?(project)).to be true
+ expect(wiki_exists?(project)).to be true
expect(project.import_status).to eq('finished')
end
scenario 'invalid project' do
- project = create(:project, namespace_id: 2)
+ project = create(:project, namespace: namespace)
visit new_project_path
- select2('2', from: '#project_namespace_id')
+ select2(namespace.id, from: '#project_namespace_id')
fill_in :project_path, with: project.name, visible: true
click_link 'GitLab export'
@@ -66,11 +63,11 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
end
scenario 'project with no name' do
- create(:project, namespace_id: 2)
+ create(:project, namespace: namespace)
visit new_project_path
- select2('2', from: '#project_namespace_id')
+ select2(namespace.id, from: '#project_namespace_id')
# click on disabled element
find(:link, 'GitLab export').trigger('click')
@@ -81,24 +78,30 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
end
end
- context 'normal user' do
+ context 'when limited to the default user namespace' do
+ let(:user) { create(:user) }
before do
- login_as(normal_user)
+ login_as(user)
end
- scenario 'non-admin user is not allowed to import a project' do
- expect(Project.all.count).to be_zero
-
+ scenario 'passes correct namespace ID in the URL' do
visit new_project_path
fill_in :project_path, with: 'test-project-path', visible: true
- expect(page).not_to have_content('GitLab export')
+ click_link 'GitLab export'
+
+ expect(page).to have_content('GitLab project export')
+ expect(URI.parse(current_url).query).to eq("namespace_id=#{user.namespace.id}&path=test-project-path")
end
end
- def wiki_exists?
+ def wiki_exists?(project)
wiki = ProjectWiki.new(project)
File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
end
+
+ def project_hook_exists?(project)
+ Gitlab::Git::Hook.new('post-receive', project.repository.path).exists?
+ end
end
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 d04bdea0fe4..bfe59bdb90e 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 f76c4fe8b57..d886909ce85 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -15,6 +15,7 @@ feature 'issuable templates', feature: true, js: true 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) }
+ let(:description_addition) { ' appending to description' }
background do
project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
@@ -30,6 +31,25 @@ feature 'issuable templates', feature: true, js: true do
save_changes
end
+ scenario 'user selects "bug" template and then "no template"' do
+ select_template 'bug'
+ wait_for_ajax
+ select_option 'No template'
+ wait_for_ajax
+ preview_template('')
+ save_changes('')
+ end
+
+ scenario 'user selects "bug" template, edits description and then selects "reset template"' do
+ select_template 'bug'
+ wait_for_ajax
+ find_field('issue_description').send_keys(description_addition)
+ preview_template(template_content + description_addition)
+ select_option 'Reset template'
+ preview_template
+ save_changes
+ end
+
it 'updates height of markdown textarea' do
start_height = page.evaluate_script('$(".markdown-area").outerHeight()')
@@ -37,11 +57,31 @@ feature 'issuable templates', feature: true, js: true do
wait_for_ajax
end_height = page.evaluate_script('$(".markdown-area").outerHeight()')
-
+
expect(end_height).not_to eq(start_height)
end
end
+ context 'user creates an issue using templates, with a prior description' do
+ let(:prior_description) { 'test issue description' }
+ let(:template_content) { 'this is a test "bug" template' }
+ 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)
+ visit edit_namespace_project_issue_path project.namespace, project, issue
+ fill_in :'issue[title]', with: 'test issue title'
+ fill_in :'issue[description]', with: prior_description
+ end
+
+ scenario 'user selects "bug" template' do
+ select_template 'bug'
+ wait_for_ajax
+ preview_template("#{prior_description}\n\n#{template_content}")
+ save_changes
+ end
+ end
+
context 'user creates a merge request using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
@@ -89,18 +129,24 @@ feature 'issuable templates', feature: true, js: true do
end
end
- def preview_template
+ def preview_template(expected_content = template_content)
click_link 'Preview'
- expect(page).to have_content template_content
+ expect(page).to have_content expected_content
+ click_link 'Write'
end
- def save_changes
+ def save_changes(expected_content = template_content)
click_button "Save changes"
- expect(page).to have_content template_content
+ expect(page).to have_content expected_content
end
def select_template(name)
first('.js-issuable-selector').click
first('.js-issuable-selector-wrap .dropdown-content a', text: name).click
end
+
+ def select_option(name)
+ first('.js-issuable-selector').click
+ first('.js-issuable-selector-wrap .dropdown-footer-list a', text: name).click
+ end
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index cb7495da8eb..c9fa8315e79 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -3,18 +3,56 @@ require 'spec_helper'
feature 'Prioritize labels', feature: true do
include WaitForAjax
- context 'when project belongs to user' do
- let(:user) { create(:user) }
- let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
+ let!(:feature) { create(:group_label, group: group, title: 'feature') }
- scenario 'user can prioritize a label', js: true do
- bug = create(:label, title: 'bug')
- wontfix = create(:label, title: 'wontfix')
-
- project.labels << bug
- project.labels << wontfix
+ context 'when user belongs to project team' do
+ before do
+ project.team << [user, :developer]
login_as user
+ end
+
+ scenario 'user can prioritize a group label', js: true do
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('No prioritized labels yet')
+
+ page.within('.other-labels') do
+ all('.js-toggle-priority')[1].click
+ wait_for_ajax
+ expect(page).not_to have_content('feature')
+ end
+
+ page.within('.prioritized-labels') do
+ expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).to have_content('feature')
+ end
+ end
+
+ scenario 'user can unprioritize a group label', js: true do
+ create(:label_priority, project: project, label: feature, priority: 1)
+
+ visit namespace_project_labels_path(project.namespace, project)
+
+ page.within('.prioritized-labels') do
+ expect(page).to have_content('feature')
+
+ first('.js-toggle-priority').click
+ wait_for_ajax
+ expect(page).not_to have_content('bug')
+ end
+
+ page.within('.other-labels') do
+ expect(page).to have_content('feature')
+ end
+ end
+
+ scenario 'user can prioritize a project label', js: true do
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content('No prioritized labels yet')
@@ -31,19 +69,14 @@ feature 'Prioritize labels', feature: true do
end
end
- scenario 'user can unprioritize a label', js: true do
- bug = create(:label, title: 'bug', priority: 1)
- wontfix = create(:label, title: 'wontfix')
-
- project.labels << bug
- project.labels << wontfix
+ scenario 'user can unprioritize a project label', js: true do
+ create(:label_priority, project: project, label: bug, priority: 1)
- login_as user
visit namespace_project_labels_path(project.namespace, project)
- expect(page).to have_content('bug')
-
page.within('.prioritized-labels') do
+ expect(page).to have_content('bug')
+
first('.js-toggle-priority').click
wait_for_ajax
expect(page).not_to have_content('bug')
@@ -56,23 +89,20 @@ feature 'Prioritize labels', feature: true do
end
scenario 'user can sort prioritized labels and persist across reloads', js: true do
- bug = create(:label, title: 'bug', priority: 1)
- wontfix = create(:label, title: 'wontfix', priority: 2)
-
- project.labels << bug
- project.labels << wontfix
+ create(:label_priority, project: project, label: bug, priority: 1)
+ create(:label_priority, project: project, label: feature, priority: 2)
- login_as user
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug'
+ expect(page).to have_content 'feature'
expect(page).to have_content 'wontfix'
# Sort labels
- find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}")
+ find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('wontfix')
+ expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug')
end
@@ -80,7 +110,7 @@ feature 'Prioritize labels', feature: true do
wait_for_ajax
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('wontfix')
+ expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug')
end
end
@@ -88,28 +118,26 @@ feature 'Prioritize labels', feature: true do
context 'as a guest' do
it 'does not prioritize labels' do
- user = create(:user)
guest = create(:user)
- project = create(:project, name: 'test', namespace: user.namespace)
-
- create(:label, title: 'bug')
login_as guest
+
visit namespace_project_labels_path(project.namespace, project)
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels')
end
end
context 'as a non signed in user' do
it 'does not prioritize labels' do
- user = create(:user)
- project = create(:project, name: 'test', namespace: user.namespace)
-
- create(:label, title: 'bug')
-
visit namespace_project_labels_path(project.namespace, project)
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels')
end
end
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
new file mode 100644
index 00000000000..cc2f695211c
--- /dev/null
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Anonymous user sees members', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:empty_project, :public) }
+
+ background do
+ project.team << [user, :master]
+ @group_link = create(:project_group_link, project: project, group: group)
+
+ login_as(user)
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ it 'updates group access level' do
+ select 'Guest', from: "member_access_level_#{group.id}"
+ wait_for_ajax
+
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest')
+ end
+
+ it 'updates expiry date' do
+ tomorrow = Date.today + 3
+
+ fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
+ wait_for_ajax
+
+ page.within(find('li.group_member')) do
+ expect(page).to have_content('Expires in')
+ end
+ end
+
+ it 'deletes group link' do
+ page.within(first('.group_member')) do
+ find('.btn-remove').click
+ end
+ wait_for_ajax
+
+ expect(page).not_to have_selector('.group_member')
+ end
+
+ context 'search' do
+ it 'finds no results' do
+ page.within '.member-search-form' do
+ fill_in 'search', with: 'testing 123'
+ find('.member-search-btn').click
+ end
+
+ expect(page).not_to have_selector('.group_member')
+ end
+
+ it 'finds results' do
+ page.within '.member-search-form' do
+ fill_in 'search', with: group.name
+ find('.member-search-btn').click
+ end
+
+ expect(page).to have_selector('.group_member', count: 1)
+ end
+ end
+end
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index 430c384ac2e..27a83fdcd1f 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
+ include WaitForAjax
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
@@ -20,7 +21,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: '2016-08-10'
- click_on 'Add users to project'
+ click_on 'Add to project'
end
page.within '.project_member:first-child' do
@@ -35,9 +36,8 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
visit namespace_project_project_members_path(project.namespace, project)
page.within '.project_member:first-child' do
- click_on 'Edit'
- fill_in 'Access expiration date', with: '2016-08-09'
- click_on 'Save'
+ find('.js-access-expiration-date').set '2016-08-09'
+ wait_for_ajax
expect(page).to have_content('Expires in 3 days')
end
end
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index f7fcd9b6731..d15376931c3 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -41,7 +41,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do
def expect_visible_access_request(project, user)
expect(project.requesters.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "#{project.name} access requests 1"
+ expect(page).to have_content "Users requesting access to #{project.name} 1"
expect(page).to have_content user.name
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/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index 47482bc3cc9..db56a50e058 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -177,7 +177,7 @@ describe "Pipelines" do
before { click_on 'Retry failed' }
it { expect(page).not_to have_content('Retry failed') }
- it { expect(page).to have_content('retried') }
+ it { expect(page).to have_selector('.retried') }
end
end
diff --git a/spec/features/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index dcc364a3d01..76cb240ea98 100644
--- a/spec/features/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -24,11 +24,12 @@ feature "Pipelines settings", feature: true do
context 'for master' do
given(:role) { :master }
- scenario 'be allowed to change' do
+ scenario 'be allowed to change', js: true do
fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes'
expect(page.status_code).to eq(200)
+ expect(page).to have_button('Save changes', disabled: false)
expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 2242cb6236a..c30d38b6508 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -82,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
@@ -101,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/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 17af1d62dfa..799f54cbc31 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -216,7 +216,7 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index a752c1d7235..65544f79eba 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -14,7 +14,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq users_almost_there_path
expect(page).to have_content("Please check your email to confirm your account")
@@ -33,7 +33,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq dashboard_projects_path
expect(page).to have_content("Welcome! You have signed up successfully.")
@@ -52,7 +52,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq user_registration_path
expect(page).to have_content("error prohibited this user from being saved")
@@ -69,7 +69,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq user_registration_path
expect(page.body).not_to match(/#{user.password}/)
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index fc555a74f30..bf93c1d1251 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
- let(:issue) { create(:issue) }
+ let(:issue) { create(:issue, due_date: Date.today) }
describe 'GET /dashboard/todos' do
context 'User does not have todos' do
@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.todos-list .todo', count: 1)
end
+ it 'shows due date as today' do
+ page.within first('.todo') do
+ expect(page).to have_content 'Due today'
+ end
+ end
+
describe 'deleting the todo' do
before do
first('.done-todo').click
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index ff6933dc8d9..b750f27ea72 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -160,7 +160,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user)
@u2f_device.respond_to_u2f_authentication
- click_on "Login Via U2F Device"
+ click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
@@ -174,7 +174,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user)
@u2f_device.respond_to_u2f_authentication
- click_on "Login Via U2F Device"
+ click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
@@ -186,7 +186,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user, remember: true)
@u2f_device.respond_to_u2f_authentication
- click_on "Login Via U2F Device"
+ click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
within 'div#js-authenticate-u2f' do
@@ -209,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the old U2F device
login_as(current_user)
@u2f_device.respond_to_u2f_authentication
- click_on "Login Via U2F Device"
+ click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
@@ -230,7 +230,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the same U2F device
login_as(current_user)
@u2f_device.respond_to_u2f_authentication
- click_on "Login Via U2F Device"
+ click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
@@ -244,7 +244,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user)
unregistered_device.respond_to_u2f_authentication
- click_on "Login Via U2F Device"
+ click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
@@ -271,7 +271,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
[first_device, second_device].each do |device|
login_as(user)
device.respond_to_u2f_authentication
- click_on "Login Via U2F Device"
+ click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index b5a94fe0383..111ca7f7a70 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,15 +1,16 @@
require 'spec_helper'
-feature 'Users', feature: true do
+feature 'Users', feature: true, js: true do
let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'GET /users/sign_in creates a new user account' do
visit new_user_session_path
+ click_link 'Register'
fill_in 'new_user_name', with: 'Name Surname'
fill_in 'new_user_username', with: 'Great'
fill_in 'new_user_email', with: 'name@mail.com'
fill_in 'new_user_password', with: 'password1234'
- expect { click_button 'Sign up' }.to change { User.count }.by(1)
+ expect { click_button 'Register' }.to change { User.count }.by(1)
end
scenario 'Successful user signin invalidates password reset token' do
@@ -31,15 +32,61 @@ feature 'Users', feature: true do
scenario 'Should show one error if email is already taken' do
visit new_user_session_path
+ click_link 'Register'
fill_in 'new_user_name', with: 'Another user name'
fill_in 'new_user_username', with: 'anotheruser'
fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: '12341234'
- expect { click_button 'Sign up' }.to change { User.count }.by(0)
+ expect { click_button 'Register' }.to change { User.count }.by(0)
expect(page).to have_text('Email has already been taken')
expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
end
+ describe 'redirect alias routes' do
+ before { user }
+
+ scenario '/u/user1 redirects to user page' do
+ visit '/u/user1'
+
+ expect(current_path).to eq user_path(user)
+ expect(page).to have_text(user.name)
+ end
+
+ scenario '/u/user1/groups redirects to user groups page' do
+ visit '/u/user1/groups'
+
+ expect(current_path).to eq user_groups_path(user)
+ end
+
+ scenario '/u/user1/projects redirects to user projects page' do
+ visit '/u/user1/projects'
+
+ expect(current_path).to eq user_projects_path(user)
+ end
+ end
+
+ feature 'username validation' do
+ include WaitForAjax
+ let(:loading_icon) { '.fa.fa-spinner' }
+ let(:username_input) { 'new_user_username' }
+
+ before(:each) do
+ visit new_user_session_path
+ click_link 'Register'
+ end
+ scenario 'shows an error border if the username already exists' do
+ fill_in username_input, with: user.username
+ wait_for_ajax
+ expect(find('.username')).to have_css '.gl-field-error-outline'
+ end
+
+ scenario 'doesn\'t show an error border if the username is available' do
+ fill_in username_input, with: 'new-user'
+ wait_for_ajax
+ expect(find('#new_user_username')).not_to have_css '.gl-field-error-outline'
+ end
+ end
+
def errors_on_page(page)
page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n")
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..8cfea9659cb
--- /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, :public) }
+ let(:group) { create(:group, :public) }
+
+ 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}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/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/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
new file mode 100644
index 00000000000..27acc464ea2
--- /dev/null
+++ b/spec/finders/labels_finder_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe LabelsFinder do
+ describe '#execute' do
+ let(:group_1) { create(:group) }
+ let(:group_2) { create(:group) }
+ let(:group_3) { create(:group) }
+
+ let(:project_1) { create(:empty_project, namespace: group_1) }
+ let(:project_2) { create(:empty_project, namespace: group_2) }
+ let(:project_3) { create(:empty_project) }
+ let(:project_4) { create(:empty_project, :public) }
+ let(:project_5) { create(:empty_project, namespace: group_1) }
+
+ let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') }
+ let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') }
+ let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
+ let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
+
+ let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') }
+ let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
+ let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
+
+ let(:user) { create(:user) }
+
+ before do
+ create(:label, project: project_3, title: 'Label 3')
+ create(:group_label, group: group_3, title: 'Group Label 4')
+
+ project_1.team << [user, :developer]
+ end
+
+ context 'with no filter' do
+ it 'returns labels from projects the user have access' do
+ group_2.add_developer(user)
+
+ finder = described_class.new(user)
+
+ expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4]
+ end
+ end
+
+ context 'filtering by group_id' do
+ it 'returns labels available for any project within the group' do
+ group_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: group_1.id)
+
+ expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5]
+ end
+ end
+
+ context 'filtering by project_id' do
+ it 'returns labels available for the project' do
+ finder = described_class.new(user, project_id: project_1.id)
+
+ expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
+ end
+ end
+
+ context 'filtering by title' do
+ it 'returns label with that title' do
+ finder = described_class.new(user, title: 'Group Label 2')
+
+ expect(finder.execute).to eq [group_label_2]
+ end
+ end
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 7a012b90193..a3a078ce521 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -40,7 +40,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/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb
deleted file mode 100644
index a49cbfd5160..00000000000
--- a/spec/finders/trending_projects_finder_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe TrendingProjectsFinder do
- let(:user) { build(:user) }
-
- describe '#execute' do
- describe 'without an explicit start date' do
- subject { described_class.new }
-
- it 'returns the trending projects' do
- relation = double(:ar_relation)
-
- allow(subject).to receive(:projects_for)
- .with(user)
- .and_return(relation)
-
- allow(relation).to receive(:trending)
- .with(an_instance_of(ActiveSupport::TimeWithZone))
- end
- end
-
- describe 'with an explicit start date' do
- let(:date) { 2.months.ago }
-
- subject { described_class.new }
-
- it 'returns the trending projects' do
- relation = double(:ar_relation)
-
- allow(subject).to receive(:projects_for)
- .with(user)
- .and_return(relation)
-
- allow(relation).to receive(:trending)
- .with(date)
- end
- end
- end
-end
diff --git a/spec/fixtures/api/schemas/board.json b/spec/fixtures/api/schemas/board.json
new file mode 100644
index 00000000000..03aca4a3cc0
--- /dev/null
+++ b/spec/fixtures/api/schemas/board.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required" : [
+ "id"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "name": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/boards.json b/spec/fixtures/api/schemas/boards.json
new file mode 100644
index 00000000000..117564ef77a
--- /dev/null
+++ b/spec/fixtures/api/schemas/boards.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "board.json" }
+}
diff --git a/spec/fixtures/api/schemas/conflicts.json b/spec/fixtures/api/schemas/conflicts.json
new file mode 100644
index 00000000000..a947783d505
--- /dev/null
+++ b/spec/fixtures/api/schemas/conflicts.json
@@ -0,0 +1,137 @@
+{
+ "type": "object",
+ "required": [
+ "commit_message",
+ "commit_sha",
+ "source_branch",
+ "target_branch",
+ "files"
+ ],
+ "properties": {
+ "commit_message": {"type": "string"},
+ "commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
+ "source_branch": {"type": "string"},
+ "target_branch": {"type": "string"},
+ "files": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ { "$ref": "#/definitions/conflict-text-with-sections" },
+ { "$ref": "#/definitions/conflict-text-for-editor" }
+ ]
+ }
+ }
+ },
+ "definitions": {
+ "conflict-base": {
+ "type": "object",
+ "required": [
+ "old_path",
+ "new_path",
+ "blob_icon",
+ "blob_path"
+ ],
+ "properties": {
+ "old_path": {"type": "string"},
+ "new_path": {"type": "string"},
+ "blob_icon": {"type": "string"},
+ "blob_path": {"type": "string"}
+ }
+ },
+ "conflict-text-for-editor": {
+ "allOf": [
+ {"$ref": "#/definitions/conflict-base"},
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "content_path"
+ ],
+ "properties": {
+ "type": {"type": {"enum": ["text-editor"]}},
+ "content_path": {"type": "string"}
+ }
+ }
+ ]
+ },
+ "conflict-text-with-sections": {
+ "allOf": [
+ {"$ref": "#/definitions/conflict-base"},
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "content_path",
+ "sections"
+ ],
+ "properties": {
+ "type": {"type": {"enum": ["text"]}},
+ "content_path": {"type": "string"},
+ "sections": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ { "$ref": "#/definitions/section-context" },
+ { "$ref": "#/definitions/section-conflict" }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ },
+ "section-base": {
+ "type": "object",
+ "required": [
+ "conflict",
+ "lines"
+ ],
+ "properties": {
+ "conflict": {"type": "boolean"},
+ "lines": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "old_line",
+ "new_line",
+ "text",
+ "rich_text"
+ ],
+ "properties": {
+ "type": {"type": "string"},
+ "old_line": {"type": "string"},
+ "new_line": {"type": "string"},
+ "text": {"type": "string"},
+ "rich_text": {"type": "string"}
+ }
+ }
+ }
+ }
+ },
+ "section-context": {
+ "allOf": [
+ {"$ref": "#/definitions/section-base"},
+ {
+ "type": "object",
+ "properties": {
+ "conflict": {"enum": [false]}
+ }
+ }
+ ]
+ },
+ "section-conflict": {
+ "allOf": [
+ {"$ref": "#/definitions/section-base"},
+ {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "conflict": {"enum": [true]},
+ "id": {"type": "string"}
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index f070fa3b254..8d94cf26ecb 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -13,7 +13,7 @@
"enum": ["backlog", "label", "done"]
},
"label": {
- "type": ["object"],
+ "type": ["object", "null"],
"required": [
"id",
"color",
diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml
index 06bf60ab734..712f6f797b4 100644
--- a/spec/fixtures/emails/commands_in_reply.eml
+++ b/spec/fixtures/emails/commands_in_reply.eml
@@ -23,8 +23,6 @@ Cool!
/close
/todo
-/due tomorrow
-
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
diff --git a/spec/fixtures/emails/commands_only_reply.eml b/spec/fixtures/emails/commands_only_reply.eml
index aed64224b06..2d2e2f94290 100644
--- a/spec/fixtures/emails/commands_only_reply.eml
+++ b/spec/fixtures/emails/commands_only_reply.eml
@@ -21,8 +21,6 @@ X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
/close
/todo
-/due tomorrow
-
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index 157cc4665a2..c6e3c5c2368 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -7,7 +7,7 @@ describe BroadcastMessagesHelper do
end
it 'includes the current message' do
- current = double(message: 'Current Message')
+ current = BroadcastMessage.new(message: 'Current Message')
allow(helper).to receive(:broadcast_message_style).and_return(nil)
@@ -15,7 +15,7 @@ describe BroadcastMessagesHelper do
end
it 'includes custom style' do
- current = double(message: 'Current Message')
+ current = BroadcastMessage.new(message: 'Current Message')
allow(helper).to receive(:broadcast_message_style).and_return('foo')
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 022aba0c0d0..594b40303bc 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -62,4 +62,21 @@ describe EventsHelper do
expect(helper.event_note(input)).to eq(expected)
end
end
+
+ describe '#event_commit_title' do
+ let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
+ subject { helper.event_commit_title(message) }
+
+ it "returns the first line, truncated to 70 chars" do
+ is_expected.to eq(message[0..66] + "...")
+ end
+
+ it "is not html-safe" do
+ is_expected.not_to be_a(ActiveSupport::SafeBuffer)
+ end
+
+ it "handles empty strings" do
+ expect(helper.event_commit_title("")).to eq("")
+ end
+ end
end
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/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 67bac782591..abe08d95ece 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -63,28 +63,38 @@ describe IssuesHelper do
end
describe '#award_user_list' do
- let!(:awards) { build_list(:award_emoji, 15) }
+ it "returns a comma-separated list of the first X users" do
+ user = build_stubbed(:user, name: 'Joe')
+ awards = Array.new(3, build_stubbed(:award_emoji, user: user))
- it "returns a comma seperated list of 1-9 users" do
- expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
+ expect(award_user_list(awards, nil, limit: 3))
+ .to eq('Joe, Joe, and Joe')
end
it "displays the current user's name as 'You'" do
- expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
- end
+ user = build_stubbed(:user, name: 'Joe')
+ award = build_stubbed(:award_emoji, user: user)
- it "truncates lists of larger than 9 users" do
- expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
+ expect(award_user_list([award], user)).to eq('You')
+ expect(award_user_list([award], nil)).to eq 'Joe'
end
- it "displays the current user in front of 0-9 other users" do
- expect(award_user_list(awards, awards[0].user)).
- to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ it "truncates lists" do
+ user = build_stubbed(:user, name: 'Jane')
+ awards = Array.new(5, build_stubbed(:award_emoji, user: user))
+
+ expect(award_user_list(awards, nil, limit: 3))
+ .to eq('Jane, Jane, Jane, and 2 more.')
end
- it "displays the current user in front regardless of position in the list" do
- expect(award_user_list(awards, awards[12].user)).
- to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ it "displays the current user in front of other users" do
+ current_user = build_stubbed(:user)
+ my_award = build_stubbed(:award_emoji, user: current_user)
+ award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane'))
+ awards = Array.new(5, award).push(my_award)
+
+ expect(award_user_list(awards, current_user, limit: 2)).
+ to eq("You, Jane, and 4 more.")
end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 501f150cfda..d30daf47543 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -5,27 +5,26 @@ describe LabelsHelper do
let(:project) { create(:empty_project) }
let(:label) { create(:label, project: project) }
- context 'with @project set' do
- before do
- @project = project
- end
-
- it 'uses the instance variable' do
- expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name%5B%5D=#{label.name}"><span class="[\w\s\-]*has-tooltip".*</span></a>}
+ context 'without subject' do
+ it "uses the label's project" do
+ expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
- context 'without @project set' do
- it "uses the label's project" do
- expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ context 'with a project as subject' do
+ let(:namespace) { build(:namespace, name: 'foo3') }
+ let(:another_project) { build(:empty_project, namespace: namespace, name: 'bar3') }
+
+ it 'links to project issues page' do
+ expect(link_to_label(label, subject: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
- context 'with a project argument' do
- let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') }
+ context 'with a group as subject' do
+ let(:group) { build(:group, name: 'bar') }
- it 'links to merge requests page' do
- expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ it 'links to group issues page' do
+ expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/issues\?label_name%5B%5D=#{label.name}">.*</a>}
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/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 70032e7df94..8113742923b 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -11,7 +11,7 @@ describe ProjectsHelper do
describe "can_change_visibility_level?" do
let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:fork_project) { Projects::ForkService.new(project, user).execute }
it "returns false if there are no appropriate permissions" do
@@ -72,7 +72,7 @@ describe ProjectsHelper do
it 'returns an HTML link to the user' do
link = helper.link_to_member(project, user)
- expect(link).to match(%r{/u/#{user.username}})
+ expect(link).to match(%r{/#{user.username}})
end
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index c5b5aa8c445..64aa41020c9 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -19,7 +19,7 @@ describe SearchHelper do
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.startline).to eq(188)
expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
end
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
new file mode 100644
index 00000000000..743b15460c6
--- /dev/null
+++ b/spec/javascripts/activities_spec.js.es6
@@ -0,0 +1,61 @@
+/*= require jquery.cookie.js */
+/*= require jquery.endless-scroll.js */
+/*= require pager */
+/*= require activities */
+
+(() => {
+ window.gon || (window.gon = {});
+ const fixtureTemplate = 'event_filter.html';
+ const filters = [
+ {
+ id: 'all',
+ }, {
+ id: 'push',
+ name: 'push events',
+ }, {
+ id: 'merged',
+ name: 'merge events',
+ }, {
+ id: 'comments',
+ },{
+ id: 'team',
+ }];
+
+ function getEventName(index) {
+ let filter = filters[index];
+ return filter.hasOwnProperty('name') ? filter.name : filter.id;
+ }
+
+ function getSelector(index) {
+ let filter = filters[index];
+ return `#${filter.id}_event_filter`
+ }
+
+ describe('Activities', () => {
+ beforeEach(() => {
+ fixture.load(fixtureTemplate);
+ new Activities();
+ });
+
+ for(let i = 0; i < filters.length; i++) {
+ ((i) => {
+ describe(`when selecting ${getEventName(i)}`, () => {
+ beforeEach(() => {
+ $(getSelector(i)).click();
+ });
+
+ for(let x = 0; x < filters.length; x++) {
+ ((x) => {
+ let shouldHighlight = i === x;
+ let testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+
+ it(`${testName} ${getEventName(x)}`, () => {
+ expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
+ });
+ })(x);
+ }
+ });
+ })(i);
+ }
+ });
+})();
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index 078e4b00023..15c305ce321 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -14,7 +14,7 @@
(() => {
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
$.cookie('issue_board_welcome_hidden', 'false');
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index 3569d1b98bd..328c6f82ab5 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -16,7 +16,7 @@ describe('Issue model', () => {
let issue;
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
issue = new ListIssue({
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index 1688b996162..ec78d82e919 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -16,7 +16,7 @@ describe('List model', () => {
let list;
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index f3797ed44d4..052455f2ca6 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -26,7 +26,7 @@ const listObjDuplicate = {
const BoardsMockData = {
'GET': {
- '/test/issue-boards/board/lists{/id}/issues': {
+ '/test/issue-boards/board/1/lists{/id}/issues': {
issues: [{
title: 'Testing',
iid: 1,
@@ -37,13 +37,13 @@ const BoardsMockData = {
}
},
'POST': {
- '/test/issue-boards/board/lists{/id}': listObj
+ '/test/issue-boards/board/1/lists{/id}': listObj
},
'PUT': {
- '/test/issue-boards/board/lists{/id}': {}
+ '/test/issue-boards/board/1/lists{/id}': {}
},
'DELETE': {
- '/test/issue-boards/board/lists{/id}': {}
+ '/test/issue-boards/board/1/lists{/id}': {}
}
};
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
new file mode 100644
index 00000000000..95e248cadf8
--- /dev/null
+++ b/spec/javascripts/fixtures/event_filter.html.haml
@@ -0,0 +1,21 @@
+%ul.nav-links.event-filter.scrolling-tabs
+ %li.active
+ %a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"}
+ %span
+ All
+ %li
+ %a.event-filter-link{ id: "push_event_filter", title: "Filter by push events", href: "/dashboard/activity"}
+ %span
+ Push events
+ %li
+ %a.event-filter-link{ id: "merged_event_filter", title: "Filter by merge events", href: "/dashboard/activity"}
+ %span
+ Merge events
+ %li
+ %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"}
+ %span
+ Comments
+ %li
+ %a.event-filter-link{ id: "team_event_filter", title: "Filter by team", href: "/dashboard/activity"}
+ %span
+ Team \ No newline at end of file
diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml
new file mode 100644
index 00000000000..2526e5e33a5
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_field_errors.html.haml
@@ -0,0 +1,15 @@
+%form.show-gl-field-errors{action: 'submit', method: 'post'}
+ .form-group
+ %input.required-text{required: true, type: 'text'} Text
+ .form-group
+ %input.email{type: 'email', title: 'Please provide a valid email address.', required: true } Email
+ .form-group
+ %input.password{type: 'password', required: true} Password
+ .form-group
+ %input.alphanumeric{type: 'text', pattern: '[a-zA-Z0-9]', required: true} Alphanumeric
+ .form-group
+ %input.hidden{ type:'hidden' }
+ .form-group
+ %input.custom.no-gl-field-errors{ type:'text' } Custom, do not validate
+ .form-group
+ %input.submit{type: 'submit'} Submit
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
new file mode 100644
index 00000000000..36feb2b2aa5
--- /dev/null
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -0,0 +1,111 @@
+//= require jquery
+//= require gl_field_errors
+
+((global) => {
+ fixture.preload('gl_field_errors.html');
+
+ describe('GL Style Field Errors', function() {
+ beforeEach(function() {
+ fixture.load('gl_field_errors.html');
+ const $form = this.$form = $('form.show-gl-field-errors');
+ this.fieldErrors = new global.GlFieldErrors($form);
+ });
+
+ it('should properly initialize the form', function() {
+ expect(this.$form).toBeDefined();
+ expect(this.$form.length).toBe(1);
+ expect(this.fieldErrors).toBeDefined();
+ const inputs = this.fieldErrors.state.inputs;
+ expect(inputs.length).toBe(5);
+ });
+
+ it('should ignore elements with custom error handling', function() {
+ const customErrorFlag = 'no-gl-field-errors';
+ const customErrorElem = $(`.${customErrorFlag}`);
+
+ expect(customErrorElem.length).toBe(1);
+
+ const customErrors = this.fieldErrors.state.inputs.filter((input) => {
+ return input.inputElement.hasClass(customErrorFlag);
+ });
+ expect(customErrors.length).toBe(0);
+ });
+
+ it('should not show any errors before submit attempt', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
+
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(0);
+ });
+
+ it('should show errors when input valid is submitted', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
+
+ this.$form.submit();
+
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(4);
+ });
+
+ it('should properly track validity state on input after invalid submission attempt', function() {
+ this.$form.submit();
+
+ const emailInputModel = this.fieldErrors.state.inputs[1];
+ const fieldState = emailInputModel.state;
+ const emailInputElement = emailInputModel.inputElement;
+
+ // No input
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then empty input
+ emailInputElement.val('').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+ });
+
+ it('should properly infer error messages', function() {
+ this.$form.submit();
+ const trackedInputs = this.fieldErrors.state.inputs;
+ const inputHasTitle = trackedInputs[1];
+ const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
+ const inputNoTitle = trackedInputs[2];
+ const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
+
+ expect(noTitleErrorElem.text()).toBe('This field is required.');
+ expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
+ });
+
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 395032a7416..96ee5235acf 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,5 +1,6 @@
/*= require merge_request_tabs */
+//= require breakpoints
(function() {
describe('MergeRequestTabs', function() {
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 17b32914ec3..c9175e2b704 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,5 +1,5 @@
-
/*= require merge_request_widget */
+/*= require lib/utils/jquery.timeago.js */
(function() {
describe('MergeRequestWidget', function() {
@@ -8,6 +8,7 @@
window.notify = function() {};
this.opts = {
ci_status_url: "http://sampledomain.local/ci/getstatus",
+ ci_environments_status_url: "http://sampledomain.local/ci/getenvironmentsstatus",
ci_status: "",
ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
@@ -20,17 +21,48 @@
gitlab_icon: "gitlab_logo.png",
builds_path: "http://sampledomain.local/sampleBuildsPath"
};
- this["class"] = new MergeRequestWidget(this.opts);
- return this.ciStatusData = {
- "title": "Sample MR title",
- "sha": "12a34bc5",
- "status": "success",
- "coverage": 98
- };
+ this["class"] = new window.gl.MergeRequestWidget(this.opts);
});
+
+ describe('getCIEnvironmentsStatus', function() {
+ beforeEach(function() {
+ this.ciEnvironmentsStatusData = [{
+ created_at: '2016-09-12T13:38:30.636Z',
+ environment_id: 1,
+ environment_name: 'env1',
+ external_url: 'https://test-url.com',
+ external_url_formatted: 'test-url.com'
+ }];
+
+ spyOn(jQuery, 'getJSON').and.callFake((req, cb) => {
+ cb(this.ciEnvironmentsStatusData);
+ });
+ });
+
+ it('should call renderEnvironments when the environments property is set', function() {
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
+ });
+
+ it('should not call renderEnvironments when the environments property is not set', function() {
+ this.ciEnvironmentsStatusData = null;
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
return describe('getCIStatus', function() {
beforeEach(function() {
- return spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
+ this.ciStatusData = {
+ "title": "Sample MR title",
+ "sha": "12a34bc5",
+ "status": "success",
+ "coverage": 98
+ };
+
+ spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
return function(req, cb) {
return cb(_this.ciStatusData);
};
@@ -61,10 +93,10 @@
this["class"].getCIStatus(false);
return expect(spy).not.toHaveBeenCalled();
});
- return it('should not display a notification on the first check after the widget has been created', function() {
+ it('should not display a notification on the first check after the widget has been created', function() {
var spy;
spy = spyOn(window, 'notify');
- this["class"] = new MergeRequestWidget(this.opts);
+ this["class"] = new window.gl.MergeRequestWidget(this.opts);
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
});
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 00d9fc1302a..333128782a2 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -5,6 +5,8 @@
/*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */
/*= require fuzzaldrin-plus */
+/*= require turbolinks */
+/*= require jquery.turbolinks */
(function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -112,7 +114,7 @@
fixture.preload('search_autocomplete.html');
beforeEach(function() {
fixture.load('search_autocomplete.html');
- return widget = new SearchAutocomplete;
+ return widget = new gl.SearchAutocomplete;
});
it('should show Dashboard specific dropdown menu', function() {
var list;
@@ -138,7 +140,7 @@
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
- return it('should not show category related menu if there is text in the input', function() {
+ it('should not show category related menu if there is text in the input', function() {
var link, list;
addBodyAttributes('project');
mockProjectOptions();
@@ -148,6 +150,23 @@
link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
return expect(list.find(link).length).toBe(0);
});
+ return it('should not submit the search form when selecting an autocomplete row with the keyboard', function() {
+ var ENTER = 13;
+ var DOWN = 40;
+ addBodyAttributes();
+ mockDashboardOptions(true);
+ var submitSpy = spyOnEvent('form', 'submit');
+ widget.searchInput.focus();
+ widget.wrap.trigger($.Event('keydown', { which: DOWN }));
+ var enterKeyEvent = $.Event('keydown', { which: ENTER });
+ widget.searchInput.trigger(enterKeyEvent);
+ // This does not currently catch failing behavior. For security reasons,
+ // browsers will not trigger default behavior (form submit, in this
+ // example) on JavaScript-created keypresses.
+ expect(submitSpy).not.toHaveBeenTriggered();
+ // Does a worse job at capturing the intent of the test, but works.
+ expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
+ });
});
}).call(this);
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 7ce3884f844..784b43d4846 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -21,7 +21,7 @@
setupButton = this.container.find("#js-login-u2f-device");
setupMessage = this.container.find("p");
expect(setupMessage.text()).toContain('Insert your security key');
- expect(setupButton.text()).toBe('Login Via U2F Device');
+ expect(setupButton.text()).toBe('Sign in via U2F device');
setupButton.trigger('click');
inProgressMessage = this.container.find("p");
expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index b5b38cf0c8c..c8e62f528df 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -12,11 +12,16 @@ describe Banzai::Filter::EmojiFilter, lib: true do
ActionController::Base.asset_host = @original_asset_host
end
- it 'replaces supported emoji' do
+ it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
end
+ it 'replaces supported unicode emoji' do
+ doc = filter('<p>❤️</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+ end
+
it 'ignores unsupported emoji' do
exp = act = '<p>:foo:</p>'
doc = filter(act)
@@ -28,46 +33,96 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
end
+ it 'correctly encodes unicode to the URL' do
+ doc = filter('<p>👍</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+ end
+
it 'matches at the start of a string' do
doc = filter(':+1:')
expect(doc.css('img').size).to eq 1
end
+ it 'unicode matches at the start of a string' do
+ doc = filter("'👍'")
+ expect(doc.css('img').size).to eq 1
+ end
+
it 'matches at the end of a string' do
doc = filter('This gets a :-1:')
expect(doc.css('img').size).to eq 1
end
+ it 'unicode matches at the end of a string' do
+ doc = filter('This gets a 👍')
+ expect(doc.css('img').size).to eq 1
+ end
+
it 'matches with adjacent text' do
doc = filter('+1 (:+1:)')
expect(doc.css('img').size).to eq 1
end
+ it 'unicode matches with adjacent text' do
+ doc = filter('+1 (👍)')
+ expect(doc.css('img').size).to eq 1
+ end
+
it 'matches multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
expect(doc.css('img').size).to eq 3
end
+ it 'unicode matches multiple emoji in a row' do
+ doc = filter("'🙈🙉🙊'")
+ expect(doc.css('img').size).to eq 3
+ end
+
+ it 'mixed matches multiple emoji in a row' do
+ doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'")
+ expect(doc.css('img').size).to eq 6
+ end
+
it 'has a title attribute' do
doc = filter(':-1:')
expect(doc.css('img').first.attr('title')).to eq ':-1:'
end
+ it 'unicode has a title attribute' do
+ doc = filter("'👎'")
+ expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:'
+ end
+
it 'has an alt attribute' do
doc = filter(':-1:')
expect(doc.css('img').first.attr('alt')).to eq ':-1:'
end
+ it 'unicode has an alt attribute' do
+ doc = filter("'👎'")
+ expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:'
+ end
+
it 'has an align attribute' do
doc = filter(':8ball:')
expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
end
+ it 'unicode has an align attribute' do
+ doc = filter("'🎱'")
+ expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
+ end
+
it 'has an emoji class' do
doc = filter(':cat:')
expect(doc.css('img').first.attr('class')).to eq 'emoji'
end
+ it 'unicode has an emoji class' do
+ doc = filter("'🐱'")
+ expect(doc.css('img').first.attr('class')).to eq 'emoji'
+ end
+
it 'has height and width attributes' do
doc = filter(':dog:')
img = doc.css('img').first
@@ -76,12 +131,26 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(img.attr('height')).to eq '20'
end
+ it 'unicode has height and width attributes' do
+ doc = filter("'🐶'")
+ img = doc.css('img').first
+
+ expect(img.attr('width')).to eq '20'
+ expect(img.attr('height')).to eq '20'
+ end
+
it 'keeps whitespace intact' do
doc = filter('This deserves a :+1:, big time.')
expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
end
+ it 'unicode keeps whitespace intact' do
+ doc = filter('This deserves a 🎱, big time.')
+
+ expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+ end
+
it 'uses a custom asset_root context' do
root = Gitlab.config.gitlab.url + 'gitlab/root'
@@ -95,4 +164,18 @@ describe Banzai::Filter::EmojiFilter, lib: true do
doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
end
+
+ it 'uses a custom asset_root context' do
+ root = Gitlab.config.gitlab.url + 'gitlab/root'
+
+ doc = filter("'🎱'", asset_root: root)
+ expect(doc.css('img').first.attr('src')).to start_with(root)
+ end
+
+ it 'uses a custom asset_host context' do
+ ActionController::Base.asset_host = 'https://cdn.example.com'
+
+ doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?')
+ expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+ end
end
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 7116c09fb21..2f9343fadaf 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -7,12 +7,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
IssuesHelper
end
- let(:project) { create(:jira_project) }
-
- context 'JIRA issue references' do
- let(:issue) { ExternalIssue.new('JIRA-123', project) }
- let(:reference) { issue.to_reference }
-
+ shared_examples_for "external issue tracker" do
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
@@ -20,6 +15,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
+
expect(filter(act).to_html).to eq exp
end
end
@@ -33,25 +29,30 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'links to a valid reference' do
doc = filter("Issue #{reference}")
+ issue_id = doc.css('a').first.attr("data-external-issue")
+
expect(doc.css('a').first.attr('href'))
- .to eq helper.url_for_issue(reference, project)
+ .to eq helper.url_for_issue(issue_id, project)
end
it 'links to the external tracker' do
doc = filter("Issue #{reference}")
+
link = doc.css('a').first.attr('href')
+ issue_id = doc.css('a').first.attr("data-external-issue")
- expect(link).to eq "http://jira.example/browse/#{reference}"
+ expect(link).to eq(helper.url_for_issue(issue_id, project))
end
it 'links with adjacent text' do
doc = filter("Issue (#{reference}.)")
+
expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
end
it 'includes a title attribute' do
doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
+ expect(doc.css('a').first.attr('title')).to include("Issue in #{project.issues_tracker.title}")
end
it 'escapes the title attribute' do
@@ -69,9 +70,60 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'supports an :only_path context' do
doc = filter("Issue #{reference}", only_path: true)
+
link = doc.css('a').first.attr('href')
+ issue_id = doc.css('a').first["data-external-issue"]
+
+ expect(link).to eq helper.url_for_issue(issue_id, project, only_path: true)
+ end
+
+ context 'with RequestStore enabled' do
+ let(:reference_filter) { HTML::Pipeline.new([described_class]) }
+
+ before { allow(RequestStore).to receive(:active?).and_return(true) }
+
+ it 'queries the collection on the first call' do
+ expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original
+ expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original
+
+ not_cached = reference_filter.call("look for #{reference}", { project: project })
+
+ expect_any_instance_of(Project).not_to receive(:default_issues_tracker?)
+ expect_any_instance_of(Project).not_to receive(:issue_reference_pattern)
+
+ cached = reference_filter.call("look for #{reference}", { project: project })
- expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
+ # Links must be the same
+ expect(cached[:output].css('a').first[:href]).to eq(not_cached[:output].css('a').first[:href])
+ end
+ end
+ end
+
+ context "redmine project" do
+ let(:project) { create(:redmine_project) }
+ let(:issue) { ExternalIssue.new("#123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "jira project" do
+ let(:project) { create(:jira_project) }
+ let(:reference) { issue.to_reference }
+
+ context "with right markdown" do
+ let(:issue) { ExternalIssue.new("JIRA-123", project) }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with wrong markdown" do
+ let(:issue) { ExternalIssue.new("#123", project) }
+
+ it "ignores reference" do
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
end
end
end
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 695a5bc6fd4..167397c736b 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -46,4 +46,38 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(doc.at_css('a')['rel']).to include 'noreferrer'
end
end
+
+ context 'for non-lowercase scheme links' do
+ let(:doc_with_http) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
+ let(:doc_with_https) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc_with_http.at_css('a')).to have_attribute('rel')
+ expect(doc_with_https.at_css('a')).to have_attribute('rel')
+
+ expect(doc_with_http.at_css('a')['rel']).to include 'nofollow'
+ expect(doc_with_https.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc_with_http.at_css('a')).to have_attribute('rel')
+ expect(doc_with_https.at_css('a')).to have_attribute('rel')
+
+ expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer'
+ expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer'
+ end
+
+ it 'skips internal links' do
+ internal_link = Gitlab.config.gitlab.url + "/sign_in"
+ url = internal_link.gsub(/\Ahttp/, 'HtTp')
+ act = %Q(<a href="#{url}">Login</a>)
+ exp = %Q(<a href="#{internal_link}">Login</a>)
+ expect(filter(act).to_html).to eq(exp)
+ end
+
+ it 'skips relative links' do
+ exp = act = %q(<a href="http_spec/foo.rb">Relative URL</a>)
+ expect(filter(act).to_html).to eq(exp)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb
new file mode 100644
index 00000000000..f9e6bd609f0
--- /dev/null
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Banzai::Filter::HtmlEntityFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' }
+ let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;&amp;&lt;/strike&gt;' }
+
+ it 'converts common entities to their HTML-escaped equivalents' do
+ output = filter(unescaped)
+
+ expect(output).to eq(escaped)
+ end
+
+ it 'does not double-escape' do
+ escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'")
+ expect(filter(escaped)).to eq(escaped)
+ end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index fce86a9b6ad..a2025672ad9 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -25,9 +25,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
- expect_any_instance_of(described_class).to receive(:find_object).
- with(project, issue.iid).
- and_return(nil)
+ allow(project).to receive(:default_issues_tracker?).and_return(false)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
@@ -199,19 +197,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
- context 'referencing external issues' do
- let(:project) { create(:redmine_project) }
-
- it 'renders internal issue IDs as external issue links' do
- doc = reference_filter('#1')
- link = doc.css('a').first
-
- expect(link.attr('data-reference-type')).to eq('external_issue')
- expect(link.attr('title')).to eq('Issue in Redmine')
- expect(link.attr('data-external-issue')).to eq('1')
- end
- end
-
describe '#issues_per_Project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 908ccebbf87..9c09f00ae8a 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -305,6 +305,58 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ describe 'group label references' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:group_label) { create(:group_label, name: 'gfm references', group: group) }
+
+ context 'without project reference' do
+ let(:reference) { group_label.to_reference(format: :name) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}", project: project)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = %(Label #{Label.reference_prefix}"#{group_label.name.reverse}")
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with project reference' do
+ let(:reference) { project.to_reference + group_label.to_reference(format: :name) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}", project: project)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}")
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+ end
+
describe 'cross project label references' do
context 'valid project referenced' do
let(:another_project) { create(:empty_project, :public) }
@@ -339,4 +391,34 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
end
+
+ describe 'cross group label references' do
+ context 'valid project referenced' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:another_group) { create(:group) }
+ let(:another_project) { create(:empty_project, :public, namespace: another_group) }
+ let(:project_name) { another_project.name_with_namespace }
+ let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') }
+ let(:reference) { another_project.to_reference + group_label.to_reference }
+
+ let!(:result) { reference_filter("See #{reference}", project: project) }
+
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(another_project.namespace,
+ another_project,
+ label_name: group_label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style'))
+ .to match /background-color: #00ff00/
+ end
+
+ it 'contains cross project content' do
+ expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}"
+ end
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index b1370bca833..d265d29ee86 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>def fun end</code></pre>')
end
end
context "when a valid language is specified" do
it "highlights as that language" do
result = filter('<pre><code class="ruby">def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
end
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>This is a test</code></pre>')
end
end
@@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
it "highlights as plaintext" do
result = filter('<pre><code class="ruby">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight" v-pre="true"><code>This is a test</code></pre>')
end
end
end
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
deleted file mode 100644
index 569cbc885c7..00000000000
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::TaskListFilter, lib: true do
- include FilterSpecHelper
-
- it 'does not apply `task-list` class to non-task lists' do
- exp = act = %(<ul><li>Item</li></ul>)
- expect(filter(act).to_html).to eq exp
- end
-
- it 'applies `task-list` to single-item task lists' do
- act = filter('<ul><li>[ ] Task 1</li></ul>')
-
- expect(act.to_html).to start_with '<ul class="task-list">'
- end
-end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index fdbdb21eac1..729e77fd43f 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -31,13 +31,16 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
it 'supports a special @all mention' do
+ project.team << [user, :developer]
doc = reference_filter("Hey #{reference}", author: user)
+
expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project)
end
it 'includes a data-author attribute when there is an author' do
+ project.team << [user, :developer]
doc = reference_filter(reference, author: user)
expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
@@ -48,6 +51,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
end
+
+ it 'ignores reference to all when the user is not a project member' do
+ doc = reference_filter("Hey #{reference}", author: user)
+
+ expect(doc.css('a').length).to eq 0
+ end
end
context 'mentioning a user' do
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
index 98f76f36fd5..49556074278 100644
--- a/spec/lib/banzai/note_renderer_spec.rb
+++ b/spec/lib/banzai/note_renderer_spec.rb
@@ -12,8 +12,7 @@ describe Banzai::NoteRenderer do
with(project, user,
requested_path: 'foo',
project_wiki: wiki,
- ref: 'bar',
- pipeline: :note).
+ ref: 'bar').
and_call_original
expect_any_instance_of(Banzai::ObjectRenderer).
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index bcdb95250ca..6bcda87c999 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -4,10 +4,18 @@ describe Banzai::ObjectRenderer do
let(:project) { create(:empty_project) }
let(:user) { project.owner }
+ def fake_object(attrs = {})
+ object = double(attrs.merge("new_record?" => true, "destroyed?" => true))
+ allow(object).to receive(:markdown_cache_field_for).with(:note).and_return(:note_html)
+ allow(object).to receive(:banzai_render_context).with(:note).and_return(project: nil, author: nil)
+ allow(object).to receive(:update_column).with(:note_html, anything).and_return(true)
+ object
+ end
+
describe '#render' do
it 'renders and redacts an Array of objects' do
renderer = described_class.new(project, user)
- object = double(:object, note: 'hello', note_html: nil)
+ object = fake_object(note: 'hello', note_html: nil)
expect(renderer).to receive(:render_objects).with([object], :note).
and_call_original
@@ -16,7 +24,7 @@ describe Banzai::ObjectRenderer do
with(an_instance_of(Array)).
and_call_original
- expect(object).to receive(:note_html=).with('<p>hello</p>')
+ expect(object).to receive(:redacted_note_html=).with('<p dir="auto">hello</p>')
expect(object).to receive(:user_visible_reference_count=).with(0)
renderer.render([object], :note)
@@ -25,7 +33,7 @@ describe Banzai::ObjectRenderer do
describe '#render_objects' do
it 'renders an Array of objects' do
- object = double(:object, note: 'hello')
+ object = fake_object(note: 'hello', note_html: nil)
renderer = described_class.new(project, user)
@@ -57,74 +65,50 @@ describe Banzai::ObjectRenderer do
end
describe '#context_for' do
- let(:object) { double(:object, note: 'hello') }
+ let(:object) { fake_object(note: 'hello') }
let(:renderer) { described_class.new(project, user) }
it 'returns a Hash' do
expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash)
end
- it 'includes the cache key' do
+ it 'includes the banzai render context for the object' do
+ expect(object).to receive(:banzai_render_context).with(:note).and_return(foo: :bar)
context = renderer.context_for(object, :note)
-
- expect(context[:cache_key]).to eq([object, :note])
- end
-
- context 'when the object responds to "author"' do
- it 'includes the author in the context' do
- expect(object).to receive(:author).and_return('Alice')
-
- context = renderer.context_for(object, :note)
-
- expect(context[:author]).to eq('Alice')
- end
- end
-
- context 'when the object does not respond to "author"' do
- it 'does not include the author in the context' do
- context = renderer.context_for(object, :note)
-
- expect(context.key?(:author)).to eq(false)
- end
+ expect(context).to have_key(:foo)
+ expect(context[:foo]).to eq(:bar)
end
end
describe '#render_attributes' do
it 'renders the attribute of a list of objects' do
- objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')]
- renderer = described_class.new(project, user, pipeline: :note)
+ objects = [fake_object(note: 'hello', note_html: nil), fake_object(note: 'bye', note_html: nil)]
+ renderer = described_class.new(project, user)
- expect(Banzai).to receive(:cache_collection_render).
- with([
- { text: 'hello', context: renderer.context_for(objects[0], :note) },
- { text: 'bye', context: renderer.context_for(objects[1], :note) }
- ]).
- and_call_original
+ objects.each do |object|
+ expect(Banzai).to receive(:render_field).with(object, :note).and_call_original
+ end
docs = renderer.render_attributes(objects, :note)
expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
- expect(docs[0].to_html).to eq('<p>hello</p>')
+ expect(docs[0].to_html).to eq('<p dir="auto">hello</p>')
expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
- expect(docs[1].to_html).to eq('<p>bye</p>')
+ expect(docs[1].to_html).to eq('<p dir="auto">bye</p>')
end
it 'returns when no objects to render' do
objects = []
renderer = described_class.new(project, user, pipeline: :note)
- expect(Banzai).to receive(:cache_collection_render).
- with([]).
- and_call_original
-
expect(renderer.render_attributes(objects, :note)).to eq([])
end
end
describe '#base_context' do
let(:context) do
- described_class.new(project, user, pipeline: :note).base_context
+ described_class.new(project, user, foo: :bar).base_context
end
it 'returns a Hash' do
@@ -132,7 +116,7 @@ describe Banzai::ObjectRenderer do
end
it 'includes the custom attributes' do
- expect(context[:pipeline]).to eq(:note)
+ expect(context[:foo]).to eq(:bar)
end
it 'includes the current user' do
diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
index 76f42071810..8cce1b96698 100644
--- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
@@ -4,11 +4,11 @@ describe Banzai::Pipeline::DescriptionPipeline do
def parse(html)
# When we pass HTML to Redcarpet, it gets wrapped in `p` tags...
# ...except when we pass it pre-wrapped text. Rabble rabble.
- unwrap = !html.start_with?('<p>')
+ unwrap = !html.start_with?('<p ')
output = described_class.to_html(html, project: spy)
- output.gsub!(%r{\A<p>(.*)</p>(.*)\z}, '\1\2') if unwrap
+ output.gsub!(%r{\A<p dir="auto">(.*)</p>(.*)\z}, '\1\2') if unwrap
output
end
@@ -27,11 +27,17 @@ describe Banzai::Pipeline::DescriptionPipeline do
end
end
- %w(b i strong em a ins del sup sub p).each do |elem|
+ %w(b i strong em a ins del sup sub).each do |elem|
it "still allows '#{elem}' elements" do
exp = act = "<#{elem}>Description</#{elem}>"
expect(parse(act).strip).to eq exp
end
end
+
+ it "still allows 'p' elements" do
+ exp = act = "<p dir=\"auto\">Description</p>"
+
+ expect(parse(act).strip).to eq exp
+ end
end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
new file mode 100644
index 00000000000..aaa6b12e67e
--- /dev/null
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Banzai::Renderer do
+ def expect_render(project = :project)
+ expected_context = { project: project }
+ expect(renderer).to receive(:cacheless_render) { :html }.with(:markdown, expected_context)
+ end
+
+ def expect_cache_update
+ expect(object).to receive(:update_column).with("field_html", :html)
+ end
+
+ def fake_object(*features)
+ markdown = :markdown if features.include?(:markdown)
+ html = :html if features.include?(:html)
+
+ object = double(
+ "object",
+ banzai_render_context: { project: :project },
+ field: markdown,
+ field_html: html
+ )
+
+ allow(object).to receive(:markdown_cache_field_for).with(:field).and_return("field_html")
+ allow(object).to receive(:new_record?).and_return(features.include?(:new))
+ allow(object).to receive(:destroyed?).and_return(features.include?(:destroyed))
+
+ object
+ end
+
+ describe "#render_field" do
+ let(:renderer) { Banzai::Renderer }
+ let(:subject) { renderer.render_field(object, :field) }
+
+ context "with an empty cache" do
+ let(:object) { fake_object(:markdown) }
+ it "caches and returns the result" do
+ expect_render
+ expect_cache_update
+ expect(subject).to eq(:html)
+ end
+ end
+
+ context "with a filled cache" do
+ let(:object) { fake_object(:markdown, :html) }
+
+ it "uses the cache" do
+ expect_render.never
+ expect_cache_update.never
+ should eq(:html)
+ end
+ end
+
+ context "new object" do
+ let(:object) { fake_object(:new, :markdown) }
+
+ it "doesn't cache the result" do
+ expect_render
+ expect_cache_update.never
+ expect(subject).to eq(:html)
+ end
+ end
+
+ context "destroyed object" do
+ let(:object) { fake_object(:destroyed, :markdown) }
+
+ it "doesn't cache the result" do
+ expect_render
+ expect_cache_update.never
+ expect(subject).to eq(:html)
+ end
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 6dedd25e9d3..84f21631719 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -754,7 +754,7 @@ 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 })
+ expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
end
end
@@ -796,6 +796,52 @@ module Ci
expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end
end
+
+ context 'when on_stop is specified' do
+ let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
+ let(:config) { { review: review, close_review: close_review }.compact }
+
+ context 'with matching job' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
+
+ it 'does return a list of builds' do
+ expect(builds.size).to eq(2)
+ expect(builds.first[:environment]).to eq('review')
+ end
+ end
+
+ context 'without matching job' do
+ let(:close_review) { nil }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
+ end
+ end
+
+ context 'with close job without environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
+ end
+ end
+
+ context 'with close job for different environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
+ end
+ end
+
+ context 'with close job without stop action' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
+ end
+ end
+ end
end
describe "Dependencies" do
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
new file mode 100644
index 00000000000..f0b75a664f2
--- /dev/null
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe GroupUrlConstrainer, lib: true do
+ let!(:username) { create(:group, path: 'gitlab-org') }
+
+ describe '#find_resource' do
+ it { expect(!!subject.find_resource('gitlab-org')).to be_truthy }
+ it { expect(!!subject.find_resource('gitlab-com')).to be_falsey }
+ end
+end
diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb
new file mode 100644
index 00000000000..7814711fe27
--- /dev/null
+++ b/spec/lib/constraints/namespace_url_constrainer_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe NamespaceUrlConstrainer, lib: true do
+ let!(:group) { create(:group, path: 'gitlab') }
+
+ describe '#matches?' do
+ context 'existing namespace' do
+ it { expect(subject.matches?(request '/gitlab')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab/')).to be_truthy }
+ it { expect(subject.matches?(request '//gitlab/')).to be_truthy }
+ end
+
+ context 'non-existing namespace' do
+ it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey }
+ it { expect(subject.matches?(request '/gitlab.ce')).to be_falsey }
+ it { expect(subject.matches?(request '/g/gitlab')).to be_falsey }
+ it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
+ end
+
+ context 'relative url' do
+ before do
+ allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' }
+ end
+
+ it { expect(subject.matches?(request '/gitlab/gitlab')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab/gitlab-ce')).to be_falsey }
+ it { expect(subject.matches?(request '/gitlab/')).to be_falsey }
+ end
+ end
+
+ def request(path)
+ OpenStruct.new(path: path)
+ end
+end
diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb
new file mode 100644
index 00000000000..4b26692672f
--- /dev/null
+++ b/spec/lib/constraints/user_url_constrainer_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe UserUrlConstrainer, lib: true do
+ let!(:username) { create(:user, username: 'dz') }
+
+ describe '#find_resource' do
+ it { expect(!!subject.find_resource('dz')).to be_truthy }
+ it { expect(!!subject.find_resource('john')).to be_falsey }
+ end
+end
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
new file mode 100644
index 00000000000..a6d8e6927e0
--- /dev/null
+++ b/spec/lib/event_filter_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe EventFilter, lib: true do
+ describe '#apply_filter' do
+ let(:source_user) { create(:user) }
+ let!(:public_project) { create(:project, :public) }
+
+ let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
+ let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
+ let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
+ let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
+ let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
+
+ it 'applies push filter' do
+ events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event)
+ end
+
+ it 'applies merged filter' do
+ events = EventFilter.new(EventFilter.merged).apply_filter(Event.all)
+ expect(events).to contain_exactly(merged_event)
+ end
+
+ it 'applies comments filter' do
+ events = EventFilter.new(EventFilter.comments).apply_filter(Event.all)
+ expect(events).to contain_exactly(comments_event)
+ end
+
+ it 'applies team filter' do
+ events = EventFilter.new(EventFilter.team).apply_filter(Event.all)
+ expect(events).to contain_exactly(joined_event, left_event)
+ end
+
+ it 'applies all filter' do
+ events = EventFilter.new(EventFilter.all).apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ end
+
+ it 'applies no filter' do
+ events = EventFilter.new(nil).apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ end
+
+ it 'applies unknown filter' do
+ events = EventFilter.new('').apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ end
+ end
+end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index e10c1f5c547..0e85e302f29 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -6,6 +6,7 @@ describe ExtractsPath, lib: true do
include Gitlab::Routing.url_helpers
let(:project) { double('project') }
+ let(:request) { double('request') }
before do
@project = project
@@ -15,9 +16,10 @@ describe ExtractsPath, lib: true do
allow(project).to receive(:repository).and_return(repo)
allow(project).to receive(:path_with_namespace).
and_return('gitlab/gitlab-ci')
+ allow(request).to receive(:format=)
end
- describe '#assign_ref' do
+ describe '#assign_ref_vars' do
let(:ref) { sample_commit[:id] }
let(:params) { { path: sample_commit[:line_code_path], ref: ref } }
@@ -61,6 +63,75 @@ describe ExtractsPath, lib: true do
expect(@id).to eq(get_id)
end
end
+
+ context 'ref only exists without .atom suffix' do
+ context 'with a path' do
+ let(:params) { { ref: 'v1.0.0.atom', path: 'README.md' } }
+
+ it 'renders a 404' do
+ expect(self).to receive(:render_404)
+
+ assign_ref_vars
+ end
+ end
+
+ context 'without a path' do
+ let(:params) { { ref: 'v1.0.0.atom' } }
+ before { assign_ref_vars }
+
+ it 'sets the un-suffixed version as @ref' do
+ expect(@ref).to eq('v1.0.0')
+ end
+
+ it 'sets the request format to Atom' do
+ expect(request).to have_received(:format=).with(:atom)
+ end
+ end
+ end
+
+ context 'ref exists with .atom suffix' do
+ context 'with a path' do
+ let(:params) { { ref: 'master.atom', path: 'README.md' } }
+
+ before do
+ repository = @project.repository
+ allow(repository).to receive(:commit).and_call_original
+ allow(repository).to receive(:commit).with('master.atom').and_return(repository.commit('master'))
+
+ assign_ref_vars
+ end
+
+ it 'sets the suffixed version as @ref' do
+ expect(@ref).to eq('master.atom')
+ end
+
+ it 'does not change the request format' do
+ expect(request).not_to have_received(:format=)
+ end
+ end
+
+ context 'without a path' do
+ let(:params) { { ref: 'master.atom' } }
+
+ before do
+ repository = @project.repository
+ allow(repository).to receive(:commit).and_call_original
+ allow(repository).to receive(:commit).with('master.atom').and_return(repository.commit('master'))
+ end
+
+ it 'sets the suffixed version as @ref' do
+ assign_ref_vars
+
+ expect(@ref).to eq('master.atom')
+ end
+
+ it 'does not change the request format' do
+ expect(request).not_to receive(:format=)
+
+ assign_ref_vars
+ end
+ end
+ end
end
describe '#extract_ref' do
@@ -115,4 +186,18 @@ describe ExtractsPath, lib: true do
end
end
end
+
+ describe '#extract_ref_without_atom' do
+ it 'ignores any matching refs suffixed with atom' do
+ expect(extract_ref_without_atom('master.atom')).to eq('master')
+ end
+
+ it 'returns the longest matching ref' do
+ expect(extract_ref_without_atom('release/app/v1.0.0.atom')).to eq('release/app/v1.0.0')
+ end
+
+ it 'returns nil if there are no matching refs' do
+ expect(extract_ref_without_atom('foo.atom')).to eq(nil)
+ end
+ end
end
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 07407f212aa..f826d0d1b04 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -22,15 +22,15 @@ describe Gitlab::Shell, lib: true do
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
- describe 'generate_and_link_secret_token' do
+ describe 'memoized secret_token' do
let(:secret_file) { 'tmp/tests/.secret_shell_test' }
let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' }
before do
- allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file)
+ allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
FileUtils.mkdir('tmp/tests/shell-secret-test')
- gitlab_shell.generate_and_link_secret_token
+ Gitlab::Shell.ensure_secret_token!
end
after do
@@ -39,7 +39,10 @@ describe Gitlab::Shell, lib: true do
end
it 'creates and links the secret token file' do
+ secret_token = Gitlab::Shell.secret_token
+
expect(File.exist?(secret_file)).to be(true)
+ expect(File.read(secret_file).chomp).to eq(secret_token)
expect(File.symlink?(link_file)).to be(true)
expect(File.readlink(link_file)).to eq(secret_file)
end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
index ab0cce6e091..1547bd3228c 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -100,7 +100,7 @@ describe Gitlab::Badge::Coverage::Report do
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
- pipeline.build_updated
+ pipeline.update_status
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb
index df453223da7..df925ff1afd 100644
--- a/spec/lib/gitlab/ci/config/node/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb
@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Node::Environment do
describe '#value' do
it 'returns valid hash' do
- expect(entry.value).to eq(name: 'production')
+ expect(entry.value).to include(name: 'production')
end
end
@@ -87,6 +87,68 @@ describe Gitlab::Ci::Config::Node::Environment do
end
end
+ context 'when valid action is used' do
+ let(:config) do
+ { name: 'production',
+ action: 'start' }
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when invalid action is used' do
+ let(:config) do
+ { name: 'production',
+ action: 'invalid' }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid action' do
+ expect(entry.errors)
+ .to include 'environment action should be start or stop'
+ end
+ end
+ end
+
+ context 'when on_stop is used' do
+ let(:config) do
+ { name: 'production',
+ on_stop: 'close_app' }
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when invalid on_stop is used' do
+ let(:config) do
+ { name: 'production',
+ on_stop: false }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid action' do
+ expect(entry.errors)
+ .to include 'environment on stop should be a string'
+ end
+ end
+ end
+
context 'when variables are used for environment' do
let(:config) do
{ name: 'review/$CI_BUILD_REF_NAME',
diff --git a/spec/lib/gitlab/ci/trace_reader_spec.rb b/spec/lib/gitlab/ci/trace_reader_spec.rb
new file mode 100644
index 00000000000..f06d78694d6
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace_reader_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::TraceReader do
+ let(:path) { __FILE__ }
+ let(:lines) { File.readlines(path) }
+ let(:bytesize) { lines.sum(&:bytesize) }
+
+ it 'returns last few lines' do
+ 10.times do
+ subject = build_subject
+ last_lines = random_lines
+
+ expected = lines.last(last_lines).join
+
+ expect(subject.read(last_lines: last_lines)).to eq(expected)
+ end
+ end
+
+ it 'returns everything if trying to get too many lines' do
+ expect(build_subject.read(last_lines: lines.size * 2)).to eq(lines.join)
+ end
+
+ it 'raises an error if not passing an integer for last_lines' do
+ expect do
+ build_subject.read(last_lines: lines)
+ end.to raise_error(ArgumentError)
+ end
+
+ def random_lines
+ Random.rand(lines.size) + 1
+ end
+
+ def random_buffer
+ Random.rand(bytesize) + 1
+ end
+
+ def build_subject
+ described_class.new(__FILE__, buffer_size: random_buffer)
+ end
+end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 60020487061..648d342ecf8 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -257,5 +257,16 @@ FILE
it 'includes the blob icon for the file' do
expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
end
+
+ context 'with the full_content option passed' do
+ it 'includes the full content of the conflict' do
+ expect(conflict_file.as_json(full_content: true)).to have_key(:content)
+ end
+
+ it 'includes the detected language of the conflict file' do
+ expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]).
+ to eq('ruby')
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index b73434e8dd7..a379f798a16 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -8,13 +8,13 @@ describe Gitlab::DataBuilder::Push, lib: true do
let(:data) { described_class.build_sample(project, user) }
it { expect(data).to be_a(Hash) }
- it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(data[:before]).to eq('1b12f15a11fc6e62177bef08f47bc7b5ce50b141') }
+ it { expect(data[:after]).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') }
it { expect(data[:ref]).to eq('refs/heads/master') }
it { expect(data[:commits].size).to eq(3) }
it { expect(data[:total_commits_count]).to eq(3) }
- it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) }
- it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) }
+ it { expect(data[:commits].first[:added]).to eq(['bar/branch-test.txt']) }
+ it { expect(data[:commits].first[:modified]).to eq([]) }
it { expect(data[:commits].first[:removed]).to eq([]) }
include_examples 'project hook data with deprecateds'
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 4909fed6b77..48660d1dd1b 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -12,10 +12,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
let(:email_raw) { fixture_file('emails/valid_reply.eml') }
let(:project) { create(:project, :public) }
- let(:noteable) { create(:issue, project: project) }
let(:user) { create(:user) }
+ let(:note) { create(:diff_note_on_merge_request, project: project) }
+ let(:noteable) { note.noteable }
- let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
+ let!(:sent_notification) do
+ SentNotification.record_note(note, user.id, mail_key)
+ end
context "when the recipient address doesn't include a mail key" do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") }
@@ -82,7 +85,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
expect(noteable.reload).to be_closed
- expect(noteable.due_date).to eq(Date.tomorrow)
expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
@@ -100,7 +102,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
expect(noteable.reload).to be_open
- expect(noteable.due_date).to be_nil
expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
end
end
@@ -117,7 +118,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
expect { receiver.execute }.to change { noteable.notes.count }.by(2)
expect(noteable.reload).to be_closed
- expect(noteable.due_date).to eq(Date.tomorrow)
expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
@@ -138,10 +138,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
it "creates a comment" do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
+ new_note = noteable.notes.last
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include("I could not disagree more.")
+ expect(new_note.author).to eq(sent_notification.recipient)
+ expect(new_note.position).to eq(note.position)
+ expect(new_note.note).to include("I could not disagree more.")
end
it "adds all attachments" do
@@ -160,10 +161,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
shared_examples 'an email that contains a mail key' do |header|
it "fetches the mail key from the #{header} header and creates a comment" do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
+ new_note = noteable.notes.last
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include('I could not disagree more.')
+ expect(new_note.author).to eq(sent_notification.recipient)
+ expect(new_note.position).to eq(note.position)
+ expect(new_note.note).to include('I could not disagree more.')
end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 0af249d8690..f045463c1cb 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'some text' }
- let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:old_project) { create(:project, name: 'old') }
+ let(:new_project) { create(:project, name: 'new') }
let(:user) { create(:user) }
before { old_project.team << [user, :guest] }
@@ -62,7 +62,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" }
end
- context 'description with labels' do
+ context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
let(:project_ref) { old_project.to_reference }
@@ -76,6 +76,26 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
end
end
+
+ context 'description with group labels' do
+ let(:old_group) { create(:group) }
+ let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
+ let(:project_ref) { old_project.to_reference }
+
+ before do
+ old_project.update(namespace: old_group)
+ end
+
+ context 'label referenced by id' do
+ let(:text) { '#1 and ~321' }
+ it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ end
+
+ context 'label referenced by text' do
+ let(:text) { '#1 and ~"group label"' }
+ it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ end
+ end
end
context 'reference contains milestone' do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 7c6c7b08107..09e1b5488f6 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -185,6 +185,7 @@ describe Gitlab::GitAccess, lib: true do
end
end
+ # Run permission checks for a user
def self.run_permission_checks(permissions_matrix)
permissions_matrix.keys.each do |role|
describe "#{role} access" do
@@ -194,13 +195,12 @@ describe Gitlab::GitAccess, lib: true do
else
project.team << [user, role]
end
- end
-
- permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.push_access_check(changes[action]) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ permissions_matrix[role].each do |action, allowed|
+ context action do
+ subject { access.push_access_check(changes[action]) }
+ it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ 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 8854c8431b5..1af553f8f03 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -157,7 +157,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
{ 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: :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/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index ab06b7bc5bb..a73b1f4ff5d 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
end
- context 'when Github project is private' do
+ context 'when GitHub project is private' do
it 'sets project visibility to private' do
repo.private = true
@@ -43,7 +43,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
end
end
- context 'when Github project is public' do
+ context 'when GitHub project is public' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
end
@@ -56,5 +56,25 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
+
+ context 'when GitHub project has wiki' do
+ it 'does not create the wiki repository' do
+ allow(repo).to receive(:has_wiki?).and_return(true)
+
+ project = service.execute
+
+ expect(project.wiki.repository_exists?).to eq false
+ end
+ end
+
+ context 'when GitHub project does not have wiki' do
+ it 'creates the wiki repository' do
+ allow(repo).to receive(:has_wiki?).and_return(false)
+
+ project = service.execute
+
+ expect(project.wiki.repository_exists?).to eq true
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 54f85f8cffc..097861fd34d 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -15,6 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
subject { described_class.new(project) }
before do
+ project.team << [project.creator, :master]
project.create_import_data(data: import_data)
end
@@ -31,9 +32,9 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
subject.execute
%w(
- Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
- Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
- Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
+ Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
+ Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
+ Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New
).each do |label|
label.sub!("-", ": ")
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
new file mode 100644
index 00000000000..47d6f1007d1
--- /dev/null
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+
+describe Gitlab::Identifier do
+ let(:identifier) do
+ Class.new { include Gitlab::Identifier }.new
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:key) { create(:key, user: user) }
+
+ describe '#identify' do
+ context 'without an identifier' do
+ it 'identifies the user using a commit' do
+ expect(identifier).to receive(:identify_using_commit).
+ with(project, '123')
+
+ identifier.identify('', project, '123')
+ end
+ end
+
+ context 'with a user identifier' do
+ it 'identifies the user using a user ID' do
+ expect(identifier).to receive(:identify_using_user).
+ with("user-#{user.id}")
+
+ identifier.identify("user-#{user.id}", project, '123')
+ end
+ end
+
+ context 'with an SSH key identifier' do
+ it 'identifies the user using an SSH key ID' do
+ expect(identifier).to receive(:identify_using_ssh_key).
+ with("key-#{key.id}")
+
+ identifier.identify("key-#{key.id}", project, '123')
+ end
+ end
+ end
+
+ describe '#identify_using_commit' do
+ it "returns the User for an existing commit author's Email address" do
+ commit = double(:commit, author_email: user.email)
+
+ expect(project).to receive(:commit).with('123').and_return(commit)
+
+ expect(identifier.identify_using_commit(project, '123')).to eq(user)
+ end
+
+ it 'returns nil when no user could be found' do
+ allow(project).to receive(:commit).with('123').and_return(nil)
+
+ expect(identifier.identify_using_commit(project, '123')).to be_nil
+ end
+
+ it 'returns nil when the commit does not have an author Email' do
+ commit = double(:commit, author_email: nil)
+
+ expect(project).to receive(:commit).with('123').and_return(commit)
+
+ expect(identifier.identify_using_commit(project, '123')).to be_nil
+ end
+
+ it 'caches the found users per Email' do
+ commit = double(:commit, author_email: user.email)
+
+ expect(project).to receive(:commit).with('123').twice.and_return(commit)
+ expect(User).to receive(:find_by).once.and_call_original
+
+ 2.times do
+ expect(identifier.identify_using_commit(project, '123')).to eq(user)
+ end
+ end
+ end
+
+ describe '#identify_using_user' do
+ it 'returns the User for an existing ID in the identifier' do
+ found = identifier.identify_using_user("user-#{user.id}")
+
+ expect(found).to eq(user)
+ end
+
+ it 'returns nil for a non existing user ID' do
+ found = identifier.identify_using_user('user--1')
+
+ expect(found).to be_nil
+ end
+
+ it 'caches the found users per ID' do
+ expect(User).to receive(:find_by).once.and_call_original
+
+ 2.times do
+ found = identifier.identify_using_user("user-#{user.id}")
+
+ expect(found).to eq(user)
+ end
+ end
+ end
+
+ describe '#identify_using_ssh_key' do
+ it 'returns the User for an existing SSH key' do
+ found = identifier.identify_using_ssh_key("key-#{key.id}")
+
+ expect(found).to eq(user)
+ end
+
+ it 'returns nil for an invalid SSH key' do
+ found = identifier.identify_using_ssh_key('key--1')
+
+ expect(found).to be_nil
+ end
+
+ it 'caches the found users per key' do
+ expect(User).to receive(:find_by_ssh_key_id).once.and_call_original
+
+ 2.times do
+ found = identifier.identify_using_ssh_key("key-#{key.id}")
+
+ expect(found).to eq(user)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 006569254a6..02b11bd999a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -38,6 +38,7 @@ label:
- label_links
- issues
- merge_requests
+- priorities
milestone:
- project
- issues
@@ -118,13 +119,14 @@ project:
- creator
- group
- namespace
-- board
+- boards
- last_event
- services
- campfire_service
- drone_ci_service
- emails_on_push_service
- builds_email_service
+- pipelines_email_service
- irker_service
- pivotaltracker_service
- hipchat_service
@@ -184,4 +186,6 @@ project:
- project_feature
award_emoji:
- awardable
-- user \ No newline at end of file
+- user
+priorities:
+- label \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
new file mode 100644
index 00000000000..b8e7932eb4a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AttributeCleaner, lib: true do
+ let(:unsafe_hash) do
+ {
+ 'service_id' => 99,
+ 'moved_to_id' => 99,
+ 'namespace_id' => 99,
+ 'ci_id' => 99,
+ 'random_project_id' => 99,
+ 'random_id' => 99,
+ 'milestone_id' => 99,
+ 'project_id' => 99,
+ 'user_id' => 99,
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99
+ }
+ end
+
+ let(:post_safe_hash) do
+ {
+ 'project_id' => 99,
+ 'user_id' => 99,
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99
+ }
+ end
+
+ it 'removes unwanted attributes from the hash' do
+ described_class.clean!(relation_hash: unsafe_hash)
+
+ expect(unsafe_hash).to eq(post_safe_hash)
+ end
+end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 2ba344092ce..ea65a5dfed1 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -26,10 +26,11 @@ describe 'Import/Export attribute configuration', lib: true do
it 'has no new columns' do
relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name)
+ relation_attributes = relation_class.new.attributes.keys
- 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"
+ expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
- current_attributes = parsed_attributes(relation_name, relation_class.attribute_names)
+ current_attributes = parsed_attributes(relation_name, relation_attributes)
safe_attributes = safe_model_attributes[relation_class.to_s]
new_attributes = current_attributes - safe_attributes
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 98323fe6be4..ed9df468ced 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2,6 +2,21 @@
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10,
"archived": false,
+ "labels": [
+ {
+ "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": "",
+ "type": "ProjectLabel",
+ "priorities": [
+ ]
+ }
+ ],
"issues": [
{
"id": 40,
@@ -64,7 +79,37 @@
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
- "priority": null
+ "type": "ProjectLabel"
+ }
+ },
+ {
+ "id": 3,
+ "label_id": 3,
+ "target_id": 40,
+ "target_type": "Issue",
+ "created_at": "2016-07-22T08:57:02.841Z",
+ "updated_at": "2016-07-22T08:57:02.841Z",
+ "label": {
+ "id": 3,
+ "title": "test3",
+ "color": "#428bca",
+ "group_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "project_id": null,
+ "type": "GroupLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
}
}
],
@@ -536,7 +581,7 @@
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
- "priority": null
+ "type": "ProjectLabel"
}
}
],
@@ -2227,9 +2272,6 @@
]
}
],
- "labels": [
-
- ],
"milestones": [
{
"id": 1,
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 7582a732cdf..069ea960321 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
it 'has the same label associated to two issues' do
restored_project_json
- expect(Label.first.issues.count).to eq(2)
+ expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end
it 'has milestones associated to two separate issues' do
@@ -107,6 +107,41 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
end
+ it 'has project labels' do
+ restored_project_json
+
+ expect(ProjectLabel.count).to eq(2)
+ end
+
+ it 'has no group labels' do
+ restored_project_json
+
+ expect(GroupLabel.count).to eq(0)
+ end
+
+ context 'with group' do
+ let!(:project) do
+ create(:empty_project,
+ name: 'project',
+ path: 'project',
+ builds_access_level: ProjectFeature::DISABLED,
+ issues_access_level: ProjectFeature::DISABLED,
+ group: create(:group))
+ end
+
+ it 'has group labels' do
+ restored_project_json
+
+ expect(GroupLabel.count).to eq(1)
+ end
+
+ it 'has label priorities' do
+ restored_project_json
+
+ expect(GroupLabel.first.priorities).not_to be_empty
+ end
+ end
+
it 'has a project feature' do
restored_project_json
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 cf8f2200c57..c8bba553558 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,18 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end
+ it 'has project and group labels' do
+ label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']}
+
+ expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
+ end
+
+ it 'has priorities associated to labels' do
+ priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']}
+
+ expect(priorities.flatten).not_to be_empty
+ end
+
it 'saves the correct service type' do
expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
end
@@ -135,15 +147,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
issue = create(:issue, assignee: user)
snippet = create(:project_snippet)
release = create(:release)
+ group = create(:group)
project = create(:project,
:public,
issues: [issue],
snippets: [snippet],
- releases: [release]
+ releases: [release],
+ group: group
)
- label = create(:label, project: project)
- create(:label_link, label: label, target: issue)
+ project_label = create(:label, project: project)
+ group_label = create(:group_label, group: group)
+ create(:label_link, label: project_label, target: issue)
+ create(:label_link, label: group_label, target: issue)
+ create(:label_priority, label: group_label, priority: 1)
milestone = create(:milestone, project: project)
merge_request = create(:merge_request, source_project: project, milestone: milestone)
commit_status = create(:commit_status, project: project)
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
new file mode 100644
index 00000000000..3aa492a8ab1
--- /dev/null
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RelationFactory, lib: true do
+ let(:project) { create(:empty_project) }
+ let(:members_mapper) { double('members_mapper').as_null_object }
+ let(:user) { create(:user) }
+ let(:created_object) do
+ described_class.create(relation_sym: relation_sym,
+ relation_hash: relation_hash,
+ members_mapper: members_mapper,
+ user: user,
+ project_id: project.id)
+ end
+
+ context 'hook object' do
+ let(:relation_sym) { :hooks }
+ let(:id) { 999 }
+ let(:service_id) { 99 }
+ let(:original_project_id) { 8 }
+ let(:token) { 'secret' }
+
+ let(:relation_hash) do
+ {
+ 'id' => id,
+ 'url' => 'https://example.json',
+ 'project_id' => original_project_id,
+ 'created_at' => '2016-08-12T09:41:03.462Z',
+ 'updated_at' => '2016-08-12T09:41:03.462Z',
+ 'service_id' => service_id,
+ 'push_events' => true,
+ 'issues_events' => false,
+ 'merge_requests_events' => true,
+ 'tag_push_events' => false,
+ 'note_events' => true,
+ 'enable_ssl_verification' => true,
+ 'build_events' => false,
+ 'wiki_page_events' => true,
+ 'token' => token
+ }
+ end
+
+ it 'does not have the original ID' do
+ expect(created_object.id).not_to eq(id)
+ end
+
+ it 'does not have the original service_id' do
+ expect(created_object.service_id).not_to eq(service_id)
+ end
+
+ it 'does not have the original project_id' do
+ expect(created_object.project_id).not_to eq(original_project_id)
+ end
+
+ it 'has the new project_id' do
+ expect(created_object.project_id).to eq(project.id)
+ end
+
+ it 'has a token' do
+ expect(created_object.token).to eq(token)
+ end
+
+ context 'original service exists' do
+ let(:service_id) { Service.create(project: project).id }
+
+ it 'does not have the original service_id' do
+ expect(created_object.service_id).not_to eq(service_id)
+ end
+ end
+ end
+
+ # Mocks an ActiveRecordish object with the dodgy columns
+ class FooModel
+ include ActiveModel::Model
+
+ def initialize(params)
+ params.each { |key, value| send("#{key}=", value) }
+ end
+
+ def values
+ instance_variables.map { |ivar| instance_variable_get(ivar) }
+ end
+ end
+
+ # `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
+ # re-assigned by described_class.
+ context 'Potentially hazardous foreign keys' do
+ let(:relation_sym) { :hazardous_foo_model }
+ let(:relation_hash) do
+ {
+ 'service_id' => 99,
+ 'moved_to_id' => 99,
+ 'namespace_id' => 99,
+ 'ci_id' => 99,
+ 'random_project_id' => 99,
+ 'random_id' => 99,
+ 'milestone_id' => 99,
+ 'project_id' => 99,
+ 'user_id' => 99,
+ }
+ end
+
+ class HazardousFooModel < FooModel
+ attr_accessor :service_id, :moved_to_id, :namespace_id, :ci_id, :random_project_id, :random_id, :milestone_id, :project_id
+ end
+
+ it 'does not preserve any foreign key IDs' do
+ expect(created_object.values).not_to include(99)
+ end
+ end
+
+ context 'Project references' do
+ let(:relation_sym) { :project_foo_model }
+ let(:relation_hash) do
+ Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
+ end
+
+ class ProjectFooModel < FooModel
+ attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES)
+ end
+
+ it 'does not preserve any project foreign key IDs' do
+ expect(created_object.values).not_to include(99)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 8bccd313d6c..feee0f025d8 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -60,11 +60,13 @@ LabelLink:
- target_type
- created_at
- updated_at
-Label:
+ProjectLabel:
- id
- title
- color
+- group_id
- project_id
+- type
- created_at
- updated_at
- template
@@ -307,6 +309,7 @@ ProjectFeature:
- wiki_access_level
- snippets_access_level
- builds_access_level
+- repository_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
@@ -328,3 +331,10 @@ AwardEmoji:
- awardable_type
- created_at
- updated_at
+LabelPriority:
+- id
+- project_id
+- label_id
+- priority
+- created_at
+- updated_at \ No newline at end of file
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index cb54c020b31..74ff85e132a 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -88,6 +88,34 @@ describe Gitlab::Redis do
end
end
+ describe '.with' do
+ before { clear_pool }
+ after { clear_pool }
+
+ context 'when running not on sidekiq workers' do
+ before { allow(Sidekiq).to receive(:server?).and_return(false) }
+
+ it 'instantiates a connection pool with size 5' do
+ expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original
+
+ described_class.with { |_redis| true }
+ end
+ end
+
+ context 'when running on sidekiq workers' do
+ before do
+ allow(Sidekiq).to receive(:server?).and_return(true)
+ allow(Sidekiq).to receive(:options).and_return({ concurrency: 18 })
+ end
+
+ it 'instantiates a connection pool with a size based on the concurrency of the worker' do
+ expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original
+
+ described_class.with { |_redis| true }
+ end
+ 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 }
@@ -114,4 +142,10 @@ describe Gitlab::Redis do
rescue NameError
# raised if @_raw_config was not set; ignore
end
+
+ def clear_pool
+ described_class.remove_instance_variable(:@pool)
+ rescue NameError
+ # raised if @pool was not set; ignore
+ end
end
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/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
index 0df89938e97..d968096783c 100644
--- a/spec/mailers/emails/builds_spec.rb
+++ b/spec/mailers/emails/builds_spec.rb
@@ -1,6 +1,5 @@
require 'spec_helper'
require 'email_spec'
-require 'mailers/shared/notify'
describe Notify do
include EmailSpec::Matchers
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
index 4d3811af254..e22858d1d8f 100644
--- a/spec/mailers/emails/merge_requests_spec.rb
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -1,6 +1,5 @@
require 'spec_helper'
require 'email_spec'
-require 'mailers/shared/notify'
describe Notify, "merge request notifications" do
include EmailSpec::Matchers
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 781472d0c00..14bc062ef12 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -1,6 +1,5 @@
require 'spec_helper'
require 'email_spec'
-require 'mailers/shared/notify'
describe Notify do
include EmailSpec::Matchers
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 0363bc74939..f5f3f58613d 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1,6 +1,5 @@
require 'spec_helper'
require 'email_spec'
-require 'mailers/shared/notify'
describe Notify do
include EmailSpec::Helpers
@@ -402,7 +401,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 +428,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 +491,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 +525,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 +552,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
@@ -627,7 +627,7 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do
- is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
+ is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/
end
it 'contains a link to the commit' do
@@ -744,21 +744,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 +778,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 +805,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
@@ -829,6 +830,7 @@ describe Notify do
let(:user) { create(:user, email: 'old-email@mail.com') }
before do
+ stub_config_setting(email_subject_suffix: 'A Nice Suffix')
perform_enqueued_jobs do
user.email = "new-email@mail.com"
user.save
@@ -845,7 +847,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject "Confirmation instructions"
+ is_expected.to have_subject /^Confirmation instructions/
end
it 'includes a link to the site' do
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 305f8bc88cc..c4486a32082 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe AbuseReport, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:reporter).class_name('User') }
it { is_expected.to belong_to(:user) }
+
+ it "aliases reporter to author" do
+ expect(subject.author).to be(subject.reporter)
+ end
end
describe 'validations' do
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index c5658bd26e1..0b72a2f979b 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Appearance, type: :model do
- subject { create(:appearance) }
+ subject { build(:appearance) }
it { is_expected.to be_valid }
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index e7864b7ad33..ae185de9ca3 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -39,8 +39,8 @@ describe Ci::Build, models: true do
end
end
- describe '#ignored?' do
- subject { build.ignored? }
+ describe '#failed_but_allowed?' do
+ subject { build.failed_but_allowed? }
context 'when build is not allowed to fail' do
before do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 550a890797e..43397c5ae39 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -88,24 +88,38 @@ describe Ci::Pipeline, models: true do
context 'no failed builds' do
before do
- FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+ create_build('rspec', 'success')
end
- it 'be not retryable' do
+ it 'is not retryable' do
is_expected.to be_falsey
end
+
+ context 'one canceled job' do
+ before do
+ create_build('rubocop', 'canceled')
+ end
+
+ it 'is retryable' do
+ is_expected.to be_truthy
+ end
+ end
end
context 'with failed builds' do
before do
- FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
- FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
+ create_build('rspec', 'running')
+ create_build('rubocop', 'failed')
end
- it 'be retryable' do
+ it 'is retryable' do
is_expected.to be_truthy
end
end
+
+ def create_build(name, status)
+ create(:ci_build, name: name, status: status, pipeline: pipeline)
+ end
end
describe '#stages' do
@@ -187,33 +201,24 @@ describe Ci::Pipeline, models: true do
end
end
- describe "merge request metrics" do
+ 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
+ before do
+ expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id)
+ end
- expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
+ context 'when transitioning to running' do
+ it 'schedules metrics workers' do
+ pipeline.run
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)
+ it 'schedules metrics workers' do
+ pipeline.succeed
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index d3e6a6648cc..51be3f36135 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -164,10 +164,10 @@ eos
let(:data) { commit.hook_attrs(with_changed_files: true) }
it { expect(data).to be_a(Hash) }
- it { expect(data[:message]).to include('Add submodule from gitlab.com') }
- it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') }
- it { expect(data[:added]).to eq(["gitlab-grack"]) }
- it { expect(data[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
+ it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') }
+ it { expect(data[:added]).to eq(["bar/branch-test.txt"]) }
+ it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) }
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 2f1baff5d66..80c2a1bc7a9 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -7,7 +7,11 @@ describe CommitStatus, models: true do
create(:ci_pipeline, project: project, sha: project.commit.id)
end
- let(:commit_status) { create(:commit_status, pipeline: pipeline) }
+ let(:commit_status) { create_status }
+
+ def create_status(args = {})
+ create(:commit_status, args.merge(pipeline: pipeline))
+ end
it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
@@ -125,32 +129,53 @@ describe CommitStatus, models: true do
describe '.latest' do
subject { CommitStatus.latest.order(:id) }
- before do
- @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success'
- @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success'
- @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
+ let(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running'),
+ create_status(name: 'cc', ref: 'cc', status: 'pending'),
+ create_status(name: 'aa', ref: 'cc', status: 'success'),
+ create_status(name: 'cc', ref: 'bb', status: 'success'),
+ create_status(name: 'aa', ref: 'bb', status: 'success')]
end
it 'returns unique statuses' do
- is_expected.to eq([@commit4, @commit5])
+ is_expected.to eq(statuses.values_at(3, 4))
end
end
describe '.running_or_pending' do
subject { CommitStatus.running_or_pending.order(:id) }
- before do
- @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success'
- @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed'
- @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
+ let(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running'),
+ create_status(name: 'cc', ref: 'cc', status: 'pending'),
+ create_status(name: 'aa', ref: nil, status: 'success'),
+ create_status(name: 'dd', ref: nil, status: 'failed'),
+ create_status(name: 'ee', ref: nil, status: 'canceled')]
end
it 'returns statuses that are running or pending' do
- is_expected.to eq([@commit1, @commit2])
+ is_expected.to eq(statuses.values_at(0, 1))
+ end
+ end
+
+ describe '.exclude_ignored' do
+ subject { CommitStatus.exclude_ignored.order(:id) }
+
+ let(:statuses) do
+ [create_status(when: 'manual', status: 'skipped'),
+ create_status(when: 'manual', status: 'success'),
+ create_status(when: 'manual', status: 'failed'),
+ create_status(when: 'on_failure', status: 'skipped'),
+ create_status(when: 'on_failure', status: 'success'),
+ create_status(when: 'on_failure', status: 'failed'),
+ create_status(allow_failure: true, status: 'success'),
+ create_status(allow_failure: true, status: 'failed'),
+ create_status(allow_failure: false, status: 'success'),
+ create_status(allow_failure: false, status: 'failed')]
+ end
+
+ it 'returns statuses without what we want to ignore' do
+ is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9))
end
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
new file mode 100644
index 00000000000..2e3702f7520
--- /dev/null
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -0,0 +1,181 @@
+require 'spec_helper'
+
+describe CacheMarkdownField do
+ CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields"
+
+ # The minimum necessary ActiveModel to test this concern
+ class ThingWithMarkdownFields
+ include ActiveModel::Model
+ include ActiveModel::Dirty
+
+ include ActiveModel::Serialization
+
+ class_attribute :attribute_names
+ self.attribute_names = []
+
+ def attributes
+ attribute_names.each_with_object({}) do |name, hsh|
+ hsh[name.to_s] = send(name)
+ end
+ end
+
+ extend ActiveModel::Callbacks
+ define_model_callbacks :save
+
+ include CacheMarkdownField
+ cache_markdown_field :foo
+ cache_markdown_field :baz, pipeline: :single_line
+
+ def self.add_attr(attr_name)
+ self.attribute_names += [attr_name]
+ define_attribute_methods(attr_name)
+ attr_reader(attr_name)
+ define_method("#{attr_name}=") do |val|
+ send("#{attr_name}_will_change!") unless val == send(attr_name)
+ instance_variable_set("@#{attr_name}", val)
+ end
+ end
+
+ [:foo, :foo_html, :bar, :baz, :baz_html].each do |attr_name|
+ add_attr(attr_name)
+ end
+
+ def initialize(*)
+ super
+
+ # Pretend new is load
+ clear_changes_information
+ end
+
+ def save
+ run_callbacks :save do
+ changes_applied
+ end
+ end
+ end
+
+ CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields")
+
+ def thing_subclass(new_attr)
+ Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
+ end
+
+ let(:markdown) { "`Foo`" }
+ let(:html) { "<p><code>Foo</code></p>" }
+
+ let(:updated_markdown) { "`Bar`" }
+ let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" }
+
+ subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
+
+ describe ".attributes" do
+ it "excludes cache attributes" do
+ expect(thing_subclass(:qux).new.attributes.keys.sort).to eq(%w[bar baz foo qux])
+ end
+ end
+
+ describe ".cache_markdown_field" do
+ it "refuses to allow untracked classes" do
+ expect { thing_subclass(:qux).__send__(:cache_markdown_field, :qux) }.to raise_error(RuntimeError)
+ end
+ end
+
+ context "an unchanged markdown field" do
+ before do
+ subject.foo = subject.foo
+ subject.save
+ end
+
+ it { expect(subject.foo).to eq(markdown) }
+ it { expect(subject.foo_html).to eq(html) }
+ it { expect(subject.foo_html_changed?).not_to be_truthy }
+ end
+
+ context "a changed markdown field" do
+ before do
+ subject.foo = updated_markdown
+ subject.save
+ end
+
+ it { expect(subject.foo_html).to eq(updated_html) }
+ end
+
+ context "a non-markdown field changed" do
+ before do
+ subject.bar = "OK"
+ subject.save
+ end
+
+ it { expect(subject.bar).to eq("OK") }
+ it { expect(subject.foo).to eq(markdown) }
+ it { expect(subject.foo_html).to eq(html) }
+ end
+
+ describe '#banzai_render_context' do
+ it "sets project to nil if the object lacks a project" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).to have_key(:project)
+ expect(context[:project]).to be_nil
+ end
+
+ it "excludes author if the object lacks an author" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).not_to have_key(:author)
+ end
+
+ it "raises if the context for an unrecognised field is requested" do
+ expect{subject.banzai_render_context(:not_found)}.to raise_error(ArgumentError)
+ end
+
+ it "includes the pipeline" do
+ context = subject.banzai_render_context(:baz)
+ expect(context[:pipeline]).to eq(:single_line)
+ end
+
+ it "returns copies of the context template" do
+ template = subject.cached_markdown_fields[:baz]
+ copy = subject.banzai_render_context(:baz)
+ expect(copy).not_to be(template)
+ end
+
+ context "with a project" do
+ subject { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project) }
+
+ it "sets the project in the context" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).to have_key(:project)
+ expect(context[:project]).to eq(:project)
+ end
+
+ it "invalidates the cache when project changes" do
+ subject.project = :new_project
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ subject.save
+
+ expect(subject.foo_html).to eq(updated_html)
+ expect(subject.baz_html).to eq(updated_html)
+ end
+ end
+
+ context "with an author" do
+ subject { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author) }
+
+ it "sets the author in the context" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).to have_key(:author)
+ expect(context[:author]).to eq(:author)
+ end
+
+ it "invalidates the cache when author changes" do
+ subject.author = :new_author
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ subject.save
+
+ expect(subject.foo_html).to eq(updated_html)
+ expect(subject.baz_html).to eq(updated_html)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/expirable_spec.rb b/spec/models/concerns/expirable_spec.rb
new file mode 100644
index 00000000000..f7b436f32e6
--- /dev/null
+++ b/spec/models/concerns/expirable_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Expirable do
+ describe 'ProjectMember' do
+ let(:no_expire) { create(:project_member) }
+ let(:expire_later) { create(:project_member, expires_at: Time.current + 6.days) }
+ let(:expired) { create(:project_member, expires_at: Time.current - 6.days) }
+
+ describe '.expired' do
+ it { expect(ProjectMember.expired).to match_array([expired]) }
+ end
+
+ describe '#expired?' do
+ it { expect(no_expire.expired?).to eq(false) }
+ it { expect(expire_later.expired?).to eq(false) }
+ it { expect(expired.expired?).to eq(true) }
+ end
+
+ describe '#expires?' do
+ it { expect(no_expire.expires?).to eq(false) }
+ it { expect(expire_later.expires?).to eq(true) }
+ it { expect(expired.expires?).to eq(true) }
+ end
+
+ describe '#expires_soon?' do
+ it { expect(no_expire.expires_soon?).to eq(false) }
+ it { expect(expire_later.expires_soon?).to eq(true) }
+ it { expect(expired.expires_soon?).to eq(true) }
+ end
+ end
+end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index e118432d098..87bffbdc54e 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -1,26 +1,17 @@
require 'spec_helper'
describe HasStatus do
- before do
- @object = Object.new
- @object.extend(HasStatus::ClassMethods)
- end
-
describe '.status' do
- before do
- allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses))
- end
-
- subject { @object.status }
+ subject { CommitStatus.status }
shared_examples 'build status summary' do
context 'all successful' do
- let(:statuses) { Array.new(2) { create(type, status: :success) } }
+ let!(:statuses) { Array.new(2) { create(type, status: :success) } }
it { is_expected.to eq 'success' }
end
context 'at least one failed' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :failed)]
end
@@ -28,7 +19,7 @@ describe HasStatus do
end
context 'at least one running' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :running)]
end
@@ -36,7 +27,7 @@ describe HasStatus do
end
context 'at least one pending' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :pending)]
end
@@ -44,7 +35,7 @@ describe HasStatus do
end
context 'success and failed but allowed to fail' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success),
create(type, status: :failed, allow_failure: true)]
end
@@ -53,12 +44,15 @@ describe HasStatus do
end
context 'one failed but allowed to fail' do
- let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
+ let!(:statuses) do
+ [create(type, status: :failed, allow_failure: true)]
+ end
+
it { is_expected.to eq 'success' }
end
context 'success and canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :canceled)]
end
@@ -66,7 +60,7 @@ describe HasStatus do
end
context 'one failed and one canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :failed), create(type, status: :canceled)]
end
@@ -74,7 +68,7 @@ describe HasStatus do
end
context 'one failed but allowed to fail and one canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :failed, allow_failure: true),
create(type, status: :canceled)]
end
@@ -83,7 +77,7 @@ describe HasStatus do
end
context 'one running one canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :running), create(type, status: :canceled)]
end
@@ -91,14 +85,15 @@ describe HasStatus do
end
context 'all canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :canceled), create(type, status: :canceled)]
end
+
it { is_expected.to eq 'canceled' }
end
context 'success and canceled but allowed to fail' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success),
create(type, status: :canceled, allow_failure: true)]
end
@@ -107,7 +102,7 @@ describe HasStatus do
end
context 'one finished and second running but allowed to fail' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success),
create(type, status: :running, allow_failure: true)]
end
@@ -118,11 +113,13 @@ describe HasStatus do
context 'ci build statuses' do
let(:type) { :ci_build }
+
it_behaves_like 'build status summary'
end
context 'generic commit statuses' do
let(:type) { :generic_commit_status }
+
it_behaves_like 'build status summary'
end
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 549b0042038..132858950d5 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -1,18 +1,27 @@
require 'spec_helper'
describe Mentionable do
- include Mentionable
+ class Example
+ include Mentionable
- def author
- nil
+ attr_accessor :project, :message
+ attr_mentionable :message
+
+ def author
+ nil
+ end
end
describe 'references' do
let(:project) { create(:project) }
+ let(:mentionable) { Example.new }
it 'excludes JIRA references' do
allow(project).to receive_messages(jira_tracker?: true)
- expect(referenced_mentionables(project, 'JIRA-123')).to be_empty
+
+ mentionable.project = project
+ mentionable.message = 'JIRA-123'
+ expect(mentionable.referenced_mentionables).to be_empty
end
end
end
@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do
let(:user) { create(:user) }
def referenced_issues(current_user)
- text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
-
- issue.referenced_mentionables(current_user, text)
+ issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
+ issue.referenced_mentionables(current_user)
end
context 'when the current user can see the issue' do
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index b9381e33914..7691d690db0 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -8,35 +8,69 @@ describe 'CycleAnalytics#code', feature: true do
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
+ context 'with deployment' do
+ 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
- expect(subject.code).to be_nil
+ context 'without deployment' do
+ 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])
+ 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)
+ end
+
+ expect(subject.code).to be_nil
+ end
end
end
end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index e9cc71254ab..f649b44d367 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -28,7 +28,6 @@ describe 'CycleAnalytics#issue', models: true 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)
@@ -41,7 +40,6 @@ describe 'CycleAnalytics#issue', models: true do
create_merge_request_closing_issue(issue)
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.issue).to be_nil
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 5b8c96dc992..2cdefbeef21 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -31,7 +31,6 @@ describe 'CycleAnalytics#plan', feature: true do
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
@@ -44,7 +43,6 @@ describe 'CycleAnalytics#plan', feature: true do
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
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index b6e26d8f261..0ed080a42b1 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -19,14 +19,12 @@ describe 'CycleAnalytics#review', feature: true do
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
end]],
- post_fn: -> (context, data) { context.deploy_master })
+ post_fn: nil)
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
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 89ace0b2742..02ddfeed9c1 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -20,7 +20,6 @@ describe 'CycleAnalytics#test', feature: true do
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
@@ -34,7 +33,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.succeed!
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.test).to be_nil
@@ -48,8 +46,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.run!
pipeline.succeed!
-
- deploy_master
end
expect(subject.test).to be_nil
@@ -67,7 +63,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.drop!
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.test).to be_nil
@@ -85,7 +80,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.cancel!
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.test).to be_nil
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 6a90598a629..93623e8e99b 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -1,9 +1,6 @@
require 'spec_helper'
describe DeployKey, models: true do
- let(:project) { create(:project) }
- let(:deploy_key) { create(:deploy_key, projects: [project]) }
-
describe "Associations" do
it { is_expected.to have_many(:deploy_keys_projects) }
it { is_expected.to have_many(:projects) }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index bfff639ad78..ca594a320c0 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -38,5 +38,60 @@ describe Deployment, models: true do
expect(deployment.includes_commit?(commit)).to be true
end
end
+
+ context 'when the SHA for the deployment does not exist in the repo' do
+ it 'returns false' do
+ deployment.update(sha: Gitlab::Git::BLANK_SHA)
+ commit = project.commit
+
+ expect(deployment.includes_commit?(commit)).to be false
+ end
+ end
+ end
+
+ describe '#stop_action' do
+ let(:build) { create(:ci_build) }
+
+ subject { deployment.stop_action }
+
+ context 'when no other actions' do
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with other actions' do
+ let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+ context 'when matching action is defined' do
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when no matching action is defined' do
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+
+ it { is_expected.to eq(close_action) }
+ end
+ end
+ end
+
+ describe '#stoppable?' do
+ subject { deployment.stoppable? }
+
+ context 'when no other actions' do
+ let(:deployment) { build(:deployment) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when matching action is defined' do
+ let(:build) { create(:ci_build) }
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+ let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+ it { is_expected.to be_truthy }
+ end
end
end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index d9df9e0f907..fe4de1b2afb 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -6,4 +6,9 @@ describe Email, models: true do
subject { build(:email) }
end
end
+
+ it 'normalize email value' do
+ expect(described_class.new(email: ' inFO@exAMPLe.com ').email)
+ .to eq 'info@example.com'
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 6b1867a44e1..a94e6d0165f 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -8,6 +8,8 @@ describe Environment, models: true do
it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
+ it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
+
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_within(0..255) }
@@ -64,6 +66,23 @@ describe Environment, models: true do
end
end
+ describe '#first_deployment_for' do
+ let(:project) { create(:project) }
+ let!(:environment) { create(:environment, project: project) }
+ let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
+ let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) }
+ let(:head_commit) { project.commit }
+ let(:commit) { project.commit.parent }
+
+ it 'returns deployment id for the environment' do
+ expect(environment.first_deployment_for(commit)).to eq deployment1
+ end
+
+ it 'return nil when no deployment is found' do
+ expect(environment.first_deployment_for(head_commit)).to eq nil
+ end
+ end
+
describe '#environment_type' do
subject { environment.environment_type }
@@ -79,4 +98,72 @@ describe Environment, models: true do
is_expected.to be_nil
end
end
+
+ describe '#stoppable?' do
+ subject { environment.stoppable? }
+
+ context 'when no other actions' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when matching action is defined' do
+ let(:build) { create(:ci_build) }
+ let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+ context 'when environment is available' do
+ before do
+ environment.start
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is stopped' do
+ before do
+ environment.stop
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#stop!' do
+ let(:user) { create(:user) }
+
+ subject { environment.stop!(user) }
+
+ before do
+ expect(environment).to receive(:stoppable?).and_call_original
+ end
+
+ context 'when no other actions' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when matching action is defined' do
+ let(:build) { create(:ci_build) }
+ let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ context 'when action did not yet finish' do
+ let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
+
+ it 'returns the same action' do
+ expect(subject).to eq(close_action)
+ expect(subject.user).to eq(user)
+ end
+ end
+
+ context 'if action did finish' do
+ let!(:close_action) { create(:ci_build, :manual, :success, pipeline: build.pipeline, name: 'close_app') }
+
+ it 'returns a new action of the same type' do
+ is_expected.to be_persisted
+ expect(subject.name).to eq(close_action.name)
+ expect(subject.user).to eq(user)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 8600eb4d2c4..aca49be2942 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -27,17 +27,44 @@ describe Event, models: true do
end
describe "Push event" do
- before do
- project = create(:project)
- @user = project.owner
- @event = create_event(project, @user)
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:event) { create_event(project, user) }
+
+ it do
+ expect(event.push?).to be_truthy
+ expect(event.visible_to_user?).to be_truthy
+ expect(event.tag?).to be_falsey
+ expect(event.branch_name).to eq("master")
+ expect(event.author).to eq(user)
end
+ end
- it { expect(@event.push?).to be_truthy }
- it { expect(@event.visible_to_user?).to be_truthy }
- it { expect(@event.tag?).to be_falsey }
- it { expect(@event.branch_name).to eq("master") }
- it { expect(@event.author).to eq(@user) }
+ describe '#membership_changed?' do
+ context "created" do
+ subject { build(:event, action: Event::CREATED).membership_changed? }
+ it { is_expected.to be_falsey }
+ end
+
+ context "updated" do
+ subject { build(:event, action: Event::UPDATED).membership_changed? }
+ it { is_expected.to be_falsey }
+ end
+
+ context "expired" do
+ subject { build(:event, action: Event::EXPIRED).membership_changed? }
+ it { is_expected.to be_truthy }
+ end
+
+ context "left" do
+ subject { build(:event, action: Event::LEFT).membership_changed? }
+ it { is_expected.to be_truthy }
+ end
+
+ context "joined" do
+ subject { build(:event, action: Event::JOINED).membership_changed? }
+ it { is_expected.to be_truthy }
+ end
end
describe '#note?' do
@@ -59,8 +86,8 @@ describe Event, models: true do
describe '#visible_to_user?' do
let(:project) { create(:empty_project, :public) }
let(:non_member) { create(:user) }
- let(:member) { create(:user) }
- let(:guest) { create(:user) }
+ let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
@@ -79,23 +106,27 @@ describe Event, models: true do
context 'for non confidential issues' do
let(:target) { issue }
- it { expect(event.visible_to_user?(non_member)).to eq true }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq true }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
context 'for confidential issues' do
let(:target) { confidential_issue }
- it { expect(event.visible_to_user?(non_member)).to eq false }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq false }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
end
@@ -103,23 +134,27 @@ describe Event, models: true do
context 'on non confidential issues' do
let(:target) { note_on_issue }
- it { expect(event.visible_to_user?(non_member)).to eq true }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq true }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
context 'on confidential issues' do
let(:target) { note_on_confidential_issue }
- it { expect(event.visible_to_user?(non_member)).to eq false }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq false }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
end
@@ -129,12 +164,27 @@ describe Event, models: true do
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
- it { expect(event.visible_to_user?(non_member)).to eq true }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq true }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+
+ context 'private project' do
+ let(:project) { create(:project, :private) }
+
+ it do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
end
end
@@ -173,13 +223,11 @@ describe Event, models: true 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)
+ create_event(project, project.owner)
- expect(project).to receive(:update_column).
- with(:last_activity_at, a_kind_of(Time))
+ project.reload
- create_event(project, project.owner)
+ project.last_activity_at <= 1.minute.ago
end
end
end
@@ -205,6 +253,6 @@ describe Event, models: true do
action: Event::PUSHED,
data: data,
author_id: user.id
- }.merge(attrs))
+ }.merge!(attrs))
end
end
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 4fc3b065592..ebba6e14578 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -10,21 +10,6 @@ describe ExternalIssue, models: true do
it { is_expected.to include_module(Referable) }
end
- describe '.reference_pattern' do
- it 'allows underscores in the project name' do
- expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
- end
-
- it 'allows numbers in the project name' do
- expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
- end
-
- it 'requires the project name to begin with A-Z' do
- expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil
- expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
- end
- end
-
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(issue.to_reference).to eq issue.id
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 9c81d159cdf..1863581f57b 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do
let(:user) { create(:user, namespace: namespace) }
before do
+ create(:project_member, :reporter, user: user, project: project_from)
@project_to = fork_project(project_from, user)
end
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
new file mode 100644
index 00000000000..85eb889225b
--- /dev/null
+++ b/spec/models/group_label_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe GroupLabel, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:group) }
+ end
+
+ describe '#subject' do
+ it 'aliases group to subject' do
+ subject = described_class.new(group: build(:group))
+
+ expect(subject.subject).to be(subject.group)
+ end
+ end
+
+ describe '#to_reference' do
+ let(:label) { create(:group_label) }
+
+ context 'using id' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using name' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+ end
+
+ it 'uses id when name contains double quote' do
+ label = create(:label, name: %q{"irony"})
+ expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using invalid format' do
+ it 'raises error' do
+ expect { label.to_reference(format: :invalid) }
+ .to raise_error StandardError, /Unknown format/
+ end
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0b3ef9b98fd..ac862055ebc 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -12,6 +12,7 @@ describe Group, models: true do
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
+ it { is_expected.to have_many(:labels).class_name('GroupLabel') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
index e170b087ebc..2459a49f095 100644
--- a/spec/models/issue/metrics_spec.rb
+++ b/spec/models/issue/metrics_spec.rb
@@ -13,7 +13,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
- expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ expect(metrics.first_associated_with_milestone_at).to be_like_time(time)
end
it "does not record the second time an issue is associated with a milestone" do
@@ -24,7 +24,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
- expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ expect(metrics.first_associated_with_milestone_at).to be_like_time(time)
end
end
@@ -36,7 +36,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
- expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ expect(metrics.first_added_to_board_at).to be_like_time(time)
end
it "does not record the second time an issue is associated with a list label" do
@@ -48,7 +48,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
- expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ expect(metrics.first_added_to_board_at).to be_like_time(time)
end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index c14d90e619e..ac73c2975eb 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -100,11 +100,17 @@ describe Issue, models: true do
end
it 'returns the merge request to close this issue' do
- allow(mr).to receive(:closes_issue?).with(issue).and_return(true)
+ mr
expect(issue.closed_by_merge_requests).to eq([mr])
end
+ it "returns an empty array when the merge request is closed already" do
+ closed_mr
+
+ expect(issue.closed_by_merge_requests).to eq([])
+ end
+
it "returns an empty array when the current issue is closed already" do
expect(closed_issue.closed_by_merge_requests).to eq([])
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index fd4a2beff58..7fc6ed1dd54 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -5,9 +5,6 @@ describe Key, models: true do
it { is_expected.to belong_to(:user) }
end
- describe "Mass assignment" do
- end
-
describe "Validation" do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:key) }
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index 5e6f8ca1528..c18ed8574b1 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
describe LabelLink, models: true do
- let(:label) { create(:label_link) }
- it { expect(label).to be_valid }
+ it { expect(build(:label_link)).to be_valid }
it { is_expected.to belong_to(:label) }
it { is_expected.to belong_to(:target) }
diff --git a/spec/models/label_priority_spec.rb b/spec/models/label_priority_spec.rb
new file mode 100644
index 00000000000..d18c2f7949a
--- /dev/null
+++ b/spec/models/label_priority_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe LabelPriority, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:label) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:label) }
+ it { is_expected.to validate_numericality_of(:priority).only_integer.is_greater_than_or_equal_to(0) }
+
+ it 'validates uniqueness of label_id scoped to project_id' do
+ create(:label_priority)
+
+ expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:project_id)
+ end
+ end
+end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 5a5d1a5d60c..0c163659a71 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -1,46 +1,42 @@
require 'spec_helper'
describe Label, models: true do
- let(:label) { create(:label) }
+ describe 'modules' do
+ it { is_expected.to include_module(Referable) }
+ it { is_expected.to include_module(Subscribable) }
+ end
describe 'associations' do
- it { is_expected.to belong_to(:project) }
-
- it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
+ it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:lists).dependent(:destroy) }
- end
-
- describe 'modules' do
- subject { described_class }
-
- it { is_expected.to include_module(Referable) }
+ it { is_expected.to have_many(:priorities).class_name('LabelPriority') }
end
describe 'validation' do
- it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_uniqueness_of(:title).scoped_to([:group_id, :project_id]) }
it 'validates color code' do
- expect(label).not_to allow_value('G-ITLAB').for(:color)
- expect(label).not_to allow_value('AABBCC').for(:color)
- expect(label).not_to allow_value('#AABBCCEE').for(:color)
- expect(label).not_to allow_value('GGHHII').for(:color)
- expect(label).not_to allow_value('#').for(:color)
- expect(label).not_to allow_value('').for(:color)
-
- expect(label).to allow_value('#AABBCC').for(:color)
- expect(label).to allow_value('#abcdef').for(:color)
+ is_expected.not_to allow_value('G-ITLAB').for(:color)
+ is_expected.not_to allow_value('AABBCC').for(:color)
+ is_expected.not_to allow_value('#AABBCCEE').for(:color)
+ is_expected.not_to allow_value('GGHHII').for(:color)
+ is_expected.not_to allow_value('#').for(:color)
+ is_expected.not_to allow_value('').for(:color)
+
+ is_expected.to allow_value('#AABBCC').for(:color)
+ is_expected.to allow_value('#abcdef').for(:color)
end
it 'validates title' do
- expect(label).not_to allow_value('G,ITLAB').for(:title)
- expect(label).not_to allow_value('').for(:title)
-
- expect(label).to allow_value('GITLAB').for(:title)
- expect(label).to allow_value('gitlab').for(:title)
- expect(label).to allow_value('G?ITLAB').for(:title)
- expect(label).to allow_value('G&ITLAB').for(:title)
- expect(label).to allow_value("customer's request").for(:title)
+ is_expected.not_to allow_value('G,ITLAB').for(:title)
+ is_expected.not_to allow_value('').for(:title)
+
+ is_expected.to allow_value('GITLAB').for(:title)
+ is_expected.to allow_value('gitlab').for(:title)
+ is_expected.to allow_value('G?ITLAB').for(:title)
+ is_expected.to allow_value('G&ITLAB').for(:title)
+ is_expected.to allow_value("customer's request").for(:title)
end
end
@@ -51,45 +47,59 @@ describe Label, models: true do
end
end
- describe '#to_reference' do
- context 'using id' do
- it 'returns a String reference to the object' do
- expect(label.to_reference).to eq "~#{label.id}"
- end
- end
+ describe 'priorization' do
+ subject(:label) { create(:label) }
- context 'using name' do
- it 'returns a String reference to the object' do
- expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+ let(:project) { label.project }
+
+ describe '#prioritize!' do
+ context 'when label is not prioritized' do
+ it 'creates a label priority' do
+ expect { label.prioritize!(project, 1) }.to change(label.priorities, :count).by(1)
+ end
+
+ it 'sets label priority' do
+ label.prioritize!(project, 1)
+
+ expect(label.priorities.first.priority).to eq 1
+ end
end
- it 'uses id when name contains double quote' do
- label = create(:label, name: %q{"irony"})
- expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+ context 'when label is prioritized' do
+ let!(:priority) { create(:label_priority, project: project, label: label, priority: 0) }
+
+ it 'does not create a label priority' do
+ expect { label.prioritize!(project, 1) }.not_to change(label.priorities, :count)
+ end
+
+ it 'updates label priority' do
+ label.prioritize!(project, 1)
+
+ expect(priority.reload.priority).to eq 1
+ end
end
end
- context 'using invalid format' do
- it 'raises error' do
- expect { label.to_reference(format: :invalid) }
- .to raise_error StandardError, /Unknown format/
+ describe '#unprioritize!' do
+ it 'removes label priority' do
+ create(:label_priority, project: project, label: label, priority: 0)
+
+ expect { label.unprioritize!(project) }.to change(label.priorities, :count).by(-1)
end
end
- context 'cross project reference' do
- let(:project) { create(:project) }
-
- context 'using name' do
- it 'returns cross reference with label name' do
- expect(label.to_reference(project, format: :name))
- .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+ describe '#priority' do
+ context 'when label is not prioritized' do
+ it 'returns nil' do
+ expect(label.priority(project)).to be_nil
end
end
- context 'using id' do
- it 'returns cross reference with label id' do
- expect(label.to_reference(project, format: :id))
- .to eq %Q(#{label.project.to_reference}~#{label.id})
+ context 'when label is prioritized' do
+ it 'returns label priority' do
+ create(:label_priority, project: project, label: label, priority: 1)
+
+ expect(label.priority(project)).to eq 1
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 0b1634f654a..485121701af 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(:empty_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)
@@ -74,22 +74,17 @@ describe Member, models: true do
@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)
- 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')
+ @invited_member = create(:project_member, :developer,
+ project: project,
+ invite_token: '1234',
+ invite_email: 'toto1@example.com')
accepted_invite_user = build(:user, state: :active)
- 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) }
+ @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)
@@ -176,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, :public) }
+ 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 56fa7fa6134..370aeb9e0a9 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -1,6 +1,33 @@
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 805c15a4e5e..b2fe96e2e02 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -15,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 }
@@ -34,6 +54,17 @@ describe ProjectMember, models: true do
master_todos
end
+ it "creates an expired event when left due to expiry" do
+ expired = create(:project_member, project: project, expires_at: Time.now - 6.days)
+ expired.destroy
+ expect(Event.first.action).to eq(Event::EXPIRED)
+ end
+
+ it "creates a left event when left due to leave" do
+ master.destroy
+ expect(Event.first.action).to eq(Event::LEFT)
+ end
+
it "destroys itself and delete associated todos" do
expect(owner.user.todos.size).to eq(2)
expect(master.user.todos.size).to eq(3)
@@ -50,7 +81,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
@@ -81,25 +112,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
+ described_class.add_users_to_projects(
+ [projects.first.id, projects.second],
+ [users.first.id, users.second],
+ described_class::MASTER)
- ProjectMember.add_users_to_projects(
- [@project_1.id, @project_2.id],
- [@user_1.id, @user_2.id],
- ProjectMember::MASTER
- )
- end
+ expect(projects.first.users).to include(users.first)
+ expect(projects.first.users).to include(users.second)
- it { expect(@project_1.users).to include(@user_1) }
- it { expect(@project_1.users).to include(@user_2) }
-
- 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
index a79dd215d41..255db41cb19 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -12,7 +12,7 @@ describe MergeRequest::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
- expect(metrics.merged_at).to be_within(1.second).of(time)
+ expect(metrics.merged_at).to be_like_time(time)
end
end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 530a7def553..e5007424041 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -6,9 +6,9 @@ describe MergeRequestDiff, models: true do
it { expect(subject).to be_valid }
it { expect(subject).to be_persisted }
- it { expect(subject.commits.count).to eq(5) }
- it { expect(subject.diffs.count).to eq(8) }
- it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(subject.commits.count).to eq(29) }
+ it { expect(subject.diffs.count).to eq(20) }
+ it { expect(subject.head_commit_sha).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') }
it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
end
@@ -44,6 +44,16 @@ describe MergeRequestDiff, models: true do
end
end
+ context 'when the raw diffs have invalid content' do
+ before { mr_diff.update_attributes(st_diffs: ["--broken-diff"]) }
+
+ it 'returns an empty DiffCollection' do
+ expect(mr_diff.raw_diffs.to_a).to be_empty
+ expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(mr_diff.raw_diffs).to be_empty
+ end
+ end
+
context 'when the raw diffs exist' do
it 'returns the diffs' do
expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
@@ -64,27 +74,43 @@ describe MergeRequestDiff, models: true do
end
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
+ 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
+ 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'
+ context 'when commits were loaded' do
+ before do
+ subject.commits
end
- context 'when commits were not loaded' do
- it_behaves_like 'returning all commits SHA'
- 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
+
+ describe '#compare_with' do
+ subject { create(:merge_request, source_branch: 'fix').merge_request_diff }
+
+ it 'delegates compare to the service' do
+ expect(CompareService).to receive(:new).and_call_original
+
+ subject.compare_with(nil)
+ end
+
+ it 'uses git diff A..B approach by default' do
+ diffs = subject.compare_with('0b4bc9a49b562e85de7cc9e834518ea6828729b9').diffs
+
+ expect(diffs.size).to eq(3)
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 580a3235127..6e5137602aa 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
end
end
+ describe '#cache_merge_request_closes_issues!' do
+ before do
+ subject.project.team << [subject.author, :developer]
+ subject.target_branch = subject.project.default_branch
+ end
+
+ it 'caches closed issues' do
+ issue = create :issue, project: subject.project
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
+ end
+
+ it 'does not cache issues from external trackers' do
+ subject.project.update_attribute(:has_external_issue_tracker, true)
+ issue = ExternalIssue.new('JIRA-123', subject.project)
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
+ end
+ end
+
describe '#source_branch_sha' do
let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
@@ -287,6 +311,46 @@ describe MergeRequest, models: true do
end
end
+ describe "#wipless_title" do
+ ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
+ it "removes the '#{wip_prefix}' prefix" do
+ wipless_title = subject.title
+ subject.title = "#{wip_prefix}#{subject.title}"
+
+ expect(subject.wipless_title).to eq wipless_title
+ end
+
+ it "is satisfies the #work_in_progress? method" do
+ subject.title = "#{wip_prefix}#{subject.title}"
+ subject.title = subject.wipless_title
+
+ expect(subject.work_in_progress?).to eq false
+ end
+ end
+ end
+
+ describe "#wip_title" do
+ it "adds the WIP: prefix to the title" do
+ wip_title = "WIP: #{subject.title}"
+
+ expect(subject.wip_title).to eq wip_title
+ end
+
+ it "does not add the WIP: prefix multiple times" do
+ wip_title = "WIP: #{subject.title}"
+ subject.title = subject.wip_title
+ subject.title = subject.wip_title
+
+ expect(subject.wip_title).to eq wip_title
+ end
+
+ it "is satisfies the #work_in_progress? method" do
+ subject.title = subject.wip_title
+
+ expect(subject.work_in_progress?).to eq true
+ end
+ end
+
describe '#can_remove_source_branch?' do
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -425,7 +489,7 @@ describe MergeRequest, models: true do
subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
- expect(subject.diverged_commits_count).to eq(5)
+ expect(subject.diverged_commits_count).to eq(29)
end
end
@@ -433,7 +497,7 @@ describe MergeRequest, models: true do
subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
- expect(subject.diverged_commits_count).to eq(5)
+ expect(subject.diverged_commits_count).to eq(29)
end
end
@@ -482,7 +546,7 @@ describe MergeRequest, models: true do
end
it_behaves_like 'an editable mentionable' do
- subject { create(:merge_request) }
+ subject { create(:merge_request, :simple) }
let(:backref_text) { "merge request #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
@@ -576,32 +640,56 @@ describe MergeRequest, models: true do
end
describe '#all_commits_sha' do
- let(:all_commits_sha) do
- subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
- end
+ context 'when merge request is persisted' 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)
+ 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
- end
- context 'with a completely different branch' do
- before do
- subject.update(target_branch: 'v1.0.0')
+ context 'with a completely different branch' do
+ before do
+ subject.update(target_branch: 'v1.0.0')
+ end
+
+ it_behaves_like 'returning all SHA'
end
- it_behaves_like 'returning all SHA'
+ 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
- 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
+ context 'when merge request is not persisted' do
+ context 'when compare commits are set in the service' do
+ let(:commit) { spy('commit') }
+
+ subject do
+ build(:merge_request, compare_commits: [commit, commit])
+ end
+
+ it 'returns commits from compare commits temporary data' do
+ expect(subject.all_commits_sha).to eq [commit, commit]
+ end
end
- it_behaves_like 'returning all SHA'
+ context 'when compare commits are not set in the service' do
+ subject { build(:merge_request) }
+
+ it 'returns array with diff head sha element only' do
+ expect(subject.all_commits_sha).to eq [subject.diff_head_sha]
+ end
+ end
end
end
@@ -1091,12 +1179,6 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
end
- it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
- merge_request = create_merge_request('conflict-contains-conflict-markers')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
merge_request = create_merge_request('conflict-missing-side')
@@ -1108,9 +1190,15 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
end
+
+ it 'returns a truthy value when the conflicts have to be resolved in an editor' do
+ merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
+ end
end
- describe "#forked_source_project_missing?" do
+ describe "#source_project_missing?" do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:user) { create(:user) }
@@ -1123,13 +1211,13 @@ describe MergeRequest, models: true do
target_project: project)
end
- it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ it { expect(merge_request.source_project_missing?).to be_falsey }
end
context "when the source project is the same as the target project" do
let(:merge_request) { create(:merge_request, source_project: project) }
- it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ it { expect(merge_request.source_project_missing?).to be_falsey }
end
context "when the fork does not exist" do
@@ -1143,7 +1231,7 @@ describe MergeRequest, models: true do
unlink_project.execute
merge_request.reload
- expect(merge_request.forked_source_project_missing?).to be_truthy
+ expect(merge_request.source_project_missing?).to be_truthy
end
end
end
@@ -1186,38 +1274,6 @@ describe MergeRequest, models: true do
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
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/namespace_spec.rb b/spec/models/namespace_spec.rb
index 544920d1824..431b3e4435f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -114,6 +114,7 @@ describe Namespace, models: true do
it "cleans the path and makes sure it's available" do
expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2")
+ expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name")
end
end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index a7c587d4d68..93317c6db7a 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -5,7 +5,7 @@ describe ProjectFeature do
let(:user) { create(:user) }
describe '#feature_available?' do
- let(:features) { %w(issues wiki builds merge_requests snippets) }
+ let(:features) { %w(issues wiki builds merge_requests snippets repository) }
context 'when features are disabled' do
it "returns false" do
@@ -64,6 +64,27 @@ describe ProjectFeature do
end
end
+ context 'repository related features' do
+ before do
+ project.project_feature.update_attributes(
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED,
+ repository_access_level: ProjectFeature::PRIVATE
+ )
+ end
+
+ it "does not allow repository related features have higher level" do
+ features = %w(builds merge_requests)
+ project_feature = project.project_feature
+
+ features.each do |feature|
+ field = "#{feature}_access_level".to_sym
+ project_feature.update_attribute(field, ProjectFeature::ENABLED)
+ expect(project_feature.valid?).to be_falsy
+ end
+ end
+ end
+
describe '#*_enabled?' do
let(:features) { %w(wiki builds merge_requests) }
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 2fa6715fcaf..c5ff1941378 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -11,7 +11,7 @@ describe ProjectGroupLink do
it { should validate_presence_of(:project_id) }
it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
- it { should validate_presence_of(:group_id) }
+ it { should validate_presence_of(:group) }
it { should validate_presence_of(:group_access) }
end
end
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
new file mode 100644
index 00000000000..18c9d449ee5
--- /dev/null
+++ b/spec/models/project_label_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe ProjectLabel, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+
+ context 'validates if title must not exist at group level' do
+ let(:group) { create(:group, name: 'gitlab-org') }
+ let(:project) { create(:empty_project, group: group) }
+
+ before do
+ create(:group_label, group: group, title: 'Bug')
+ end
+
+ it 'returns error if title already exists at group level' do
+ label = described_class.new(project: project, title: 'Bug')
+
+ label.valid?
+
+ expect(label.errors[:title]).to include 'already exists at group level for gitlab-org. Please choose another one.'
+ end
+
+ it 'does not returns error if title does not exist at group level' do
+ label = described_class.new(project: project, title: 'Security')
+
+ label.valid?
+
+ expect(label.errors[:title]).to be_empty
+ end
+
+ it 'does not returns error if project does not belong to group' do
+ another_project = create(:empty_project)
+ label = described_class.new(project: another_project, title: 'Bug')
+
+ label.valid?
+
+ expect(label.errors[:title]).to be_empty
+ end
+
+ it 'does not returns error when title does not change' do
+ project_label = create(:label, project: project, name: 'Security')
+ create(:group_label, group: group, name: 'Security')
+ project_label.description = 'Security related stuff.'
+
+ project_label.valid?
+
+ expect(project_label.errors[:title]).to be_empty
+ end
+ end
+
+ context 'when attempting to add more than one priority to the project label' do
+ it 'returns error' do
+ subject.priorities.build
+ subject.priorities.build
+
+ subject.valid?
+
+ expect(subject.errors[:priorities]).to include 'Number of permitted priorities exceeded'
+ end
+ end
+ end
+
+ describe '#subject' do
+ it 'aliases project to subject' do
+ subject = described_class.new(project: build(:empty_project))
+
+ expect(subject.subject).to be(subject.project)
+ end
+ end
+
+ describe '#to_reference' do
+ let(:label) { create(:label) }
+
+ context 'using id' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using name' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+ end
+
+ it 'uses id when name contains double quote' do
+ label = create(:label, name: %q{"irony"})
+ expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using invalid format' do
+ it 'raises error' do
+ expect { label.to_reference(format: :invalid) }
+ .to raise_error StandardError, /Unknown format/
+ end
+ end
+
+ context 'cross project reference' do
+ let(:project) { create(:project) }
+
+ context 'using name' do
+ it 'returns cross reference with label name' do
+ expect(label.to_reference(project, format: :name))
+ .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+ end
+ end
+
+ context 'using id' do
+ it 'returns cross reference with label id' do
+ expect(label.to_reference(project, format: :id))
+ .to eq %Q(#{label.project.to_reference}~#{label.id})
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index cf713684463..2da3a9cb09f 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -117,7 +117,7 @@ describe HipchatService, models: true do
end
context 'issue events' do
- let(:issue) { create(:issue, title: 'Awesome issue', description: 'please fix') }
+ let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') }
let(:issue_service) { Issues::CreateService.new(project, user) }
let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
@@ -135,12 +135,12 @@ describe HipchatService, models: true do
"<a href=\"#{obj_attr[:url]}\">issue ##{obj_attr["iid"]}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>Awesome issue</b>" \
- "<pre>please fix</pre>")
+ "<pre><strong>please</strong> fix</pre>")
end
end
context 'merge request events' do
- let(:merge_request) { create(:merge_request, description: 'please fix', title: 'Awesome merge request', target_project: project, source_project: project) }
+ let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) }
let(:merge_service) { MergeRequests::CreateService.new(project, user) }
let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
@@ -159,7 +159,7 @@ describe HipchatService, models: true do
"<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>Awesome merge request</b>" \
- "<pre>please fix</pre>")
+ "<pre><strong>please</strong> fix</pre>")
end
end
@@ -203,7 +203,7 @@ describe HipchatService, models: true do
let(:merge_request_note) do
create(:note_on_merge_request, noteable: merge_request,
project: project,
- note: "merge request note")
+ note: "merge request **note**")
end
it "calls Hipchat API for merge request comment events" do
@@ -222,7 +222,7 @@ describe HipchatService, models: true do
"<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>#{title}</b>" \
- "<pre>merge request note</pre>")
+ "<pre>merge request <strong>note</strong></pre>")
end
end
@@ -230,7 +230,7 @@ describe HipchatService, models: true do
let(:issue) { create(:issue, project: project) }
let(:issue_note) do
create(:note_on_issue, noteable: issue, project: project,
- note: "issue note")
+ note: "issue **note**")
end
it "calls Hipchat API for issue comment events" do
@@ -247,7 +247,7 @@ describe HipchatService, models: true do
"<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>#{title}</b>" \
- "<pre>issue note</pre>")
+ "<pre>issue <strong>note</strong></pre>")
end
end
@@ -283,7 +283,7 @@ describe HipchatService, models: true do
context 'build events' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
context 'for failed' do
before { build.drop }
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index b48a3176007..6ff32aea018 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -30,6 +30,15 @@ describe JiraService, models: true do
end
end
+ describe '#reference_pattern' do
+ it_behaves_like 'allows project key on reference pattern'
+
+ it 'does not allow # on the code' do
+ expect(subject.reference_pattern.match('#123')).to be_nil
+ expect(subject.reference_pattern.match('1#23#12')).to be_nil
+ end
+ end
+
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
new file mode 100644
index 00000000000..1368a2925e8
--- /dev/null
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe PipelinesEmailService do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit('master').sha)
+ end
+
+ let(:project) { create(:project) }
+ let(:recipient) { 'test@gitlab.com' }
+
+ let(:data) do
+ Gitlab::DataBuilder::Pipeline.build(pipeline)
+ end
+
+ before do
+ ActionMailer::Base.deliveries.clear
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:recipients) }
+
+ context 'when pusher is added' do
+ before do
+ subject.add_pusher = true
+ end
+
+ it { is_expected.not_to validate_presence_of(:recipients) }
+ end
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:recipients) }
+ end
+ end
+
+ describe '#test_data' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'builds test data' do
+ data = subject.test_data(project, user)
+
+ expect(data[:object_kind]).to eq('pipeline')
+ end
+ end
+
+ shared_examples 'sending email' do
+ before do
+ perform_enqueued_jobs do
+ run
+ end
+ end
+
+ it 'sends email' do
+ sent_to = ActionMailer::Base.deliveries.flat_map(&:to)
+ expect(sent_to).to contain_exactly(recipient)
+ end
+ end
+
+ shared_examples 'not sending email' do
+ before do
+ perform_enqueued_jobs do
+ run
+ end
+ end
+
+ it 'does not send email' do
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+ end
+
+ describe '#test' do
+ def run
+ subject.test(data)
+ end
+
+ before do
+ subject.recipients = recipient
+ end
+
+ context 'when pipeline is failed' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'when pipeline is succeeded' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'sending email'
+ end
+ end
+
+ describe '#execute' do
+ def run
+ subject.execute(data)
+ end
+
+ context 'with recipients' do
+ before do
+ subject.recipients = recipient
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with succeeded pipeline' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+
+ context 'with notify_only_broken_pipelines on' do
+ before do
+ subject.notify_only_broken_pipelines = true
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with succeeded pipeline' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
+ end
+
+ context 'with empty recipients list' do
+ before do
+ subject.recipients = ' ,, '
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index b8679cd2563..0a7b237a051 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -26,4 +26,12 @@ describe RedmineService, models: true do
it { is_expected.not_to validate_presence_of(:new_issue_url) }
end
end
+
+ describe '#reference_pattern' do
+ it_behaves_like 'allows project key on reference pattern'
+
+ it 'does allow # on the reference' do
+ expect(subject.reference_pattern.match('#123')[:issue]).to eq('123')
+ end
+ end
end
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
index 0f8889bdf3c..98c36ec088d 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/slack_service/issue_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::IssueMessage, models: true do
{
user: {
name: 'Test User',
- username: 'Test User'
+ username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
@@ -40,7 +40,7 @@ describe SlackService::IssueMessage, models: true do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- '<somewhere.com|[project_name>] Issue opened by Test User')
+ '<somewhere.com|[project_name>] Issue opened by test.user')
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
@@ -60,7 +60,7 @@ describe SlackService::IssueMessage, models: true do
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User')
+ '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
expect(subject.attachments).to be_empty
end
end
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb
index 224c7ceabe8..c5c052d9af1 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/slack_service/merge_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::MergeMessage, models: true do
{
user: {
name: 'Test User',
- username: 'Test User'
+ username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
@@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'Test User opened <somewhere.com/merge_requests/100|merge request !100> '\
+ 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
@@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'Test User closed <somewhere.com/merge_requests/100|merge request !100> '\
+ 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 41b93f08050..38cfe4ad3e3 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::NoteMessage, models: true do
@args = {
user: {
name: 'Test User',
- username: 'username',
+ username: 'test.user',
avatar_url: 'http://fakeavatar'
},
project_name: 'project_name',
@@ -37,7 +37,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on commits' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("Test User commented on " \
+ expect(message.pretext).to eq("test.user commented on " \
"<url|commit 5f163b2b> in <somewhere.com|project_name>: " \
"*Added a commit message*")
expected_attachments = [
@@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("Test User commented on " \
+ expect(message.pretext).to eq("test.user commented on " \
"<url|merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*")
expected_attachments = [
@@ -90,7 +90,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on an issue' do
message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq(
- "Test User commented on " \
+ "test.user commented on " \
"<url|issue #20> in <somewhere.com|project_name>: " \
"*issue title*")
expected_attachments = [
@@ -115,7 +115,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a project snippet' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("Test User commented on " \
+ expect(message.pretext).to eq("test.user commented on " \
"<url|snippet #5> in <somewhere.com|project_name>: " \
"*snippet title*")
expected_attachments = [
diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb
index cda9ee670b0..17cd05e24f1 100644
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ b/spec/models/project_services/slack_service/push_message_spec.rb
@@ -9,7 +9,7 @@ describe SlackService::PushMessage, models: true do
before: 'before',
project_name: 'project_name',
ref: 'refs/heads/master',
- user_name: 'user_name',
+ user_name: 'test.user',
project_url: 'url'
}
end
@@ -26,7 +26,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'user_name pushed to branch <url/commits/master|master> of '\
+ 'test.user pushed to branch <url/commits/master|master> of '\
'<url|project_name> (<url/compare/before...after|Compare changes>)'
)
expect(subject.attachments).to eq([
@@ -46,13 +46,13 @@ describe SlackService::PushMessage, models: true do
before: Gitlab::Git::BLANK_SHA,
project_name: 'project_name',
ref: 'refs/tags/new_tag',
- user_name: 'user_name',
+ user_name: 'test.user',
project_url: 'url'
}
end
it 'returns a message regarding pushes' do
- expect(subject.pretext).to eq('user_name pushed new tag ' \
+ expect(subject.pretext).to eq('test.user pushed new tag ' \
'<url/commits/new_tag|new_tag> to ' \
'<url|project_name>')
expect(subject.attachments).to be_empty
@@ -66,7 +66,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'user_name pushed new branch <url/commits/master|master> to '\
+ 'test.user pushed new branch <url/commits/master|master> to '\
'<url|project_name>'
)
expect(subject.attachments).to be_empty
@@ -80,7 +80,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
- 'user_name removed branch master from <url|project_name>'
+ 'test.user removed branch master from <url|project_name>'
)
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
index 13aea0b0600..093911598b0 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::WikiPageMessage, models: true do
{
user: {
name: 'Test User',
- username: 'Test User'
+ username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
@@ -25,7 +25,7 @@ describe SlackService::WikiPageMessage, models: true do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'Test User created <url|wiki page> in <somewhere.com|project_name>: '\
+ 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
@@ -35,7 +35,7 @@ describe SlackService::WikiPageMessage, models: true do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\
+ 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 83f61f0af0a..f4dda1ee558 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -24,7 +24,7 @@ describe Project, models: true do
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
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_many(:boards).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) }
@@ -56,7 +56,7 @@ 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(:labels).class_name('ProjectLabel').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) }
@@ -67,8 +67,16 @@ describe Project, models: true do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
+ context 'after create' do
+ it "creates project feature" do
+ project = FactoryGirl.build(:project)
+
+ expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true)
+ end
+ end
+
describe '#members & #requesters' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
@@ -94,6 +102,15 @@ describe Project, models: true do
end
end
end
+
+ describe '#boards' do
+ it 'raises an error when attempting to add more than one board to the project' do
+ subject.boards.build
+
+ expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
+ expect(subject.boards.size).to eq 1
+ end
+ end
end
describe 'modules' do
@@ -219,7 +236,6 @@ describe Project, models: true do
describe 'Respond to' do
it { is_expected.to respond_to(:url_to_repo) }
it { is_expected.to respond_to(:repo_exists?) }
- it { is_expected.to respond_to(:update_merge_requests) }
it { is_expected.to respond_to(:execute_hooks) }
it { is_expected.to respond_to(:owner) }
it { is_expected.to respond_to(:path_with_namespace) }
@@ -308,7 +324,8 @@ describe Project, models: true do
end
describe 'last_activity methods' do
- let(:timestamp) { Time.now - 2.hours }
+ let(:timestamp) { 2.hours.ago }
+ # last_activity_at gets set to created_at upon creation
let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
describe 'last_activity' do
@@ -321,9 +338,9 @@ describe Project, models: true do
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- expect_any_instance_of(Event).to receive(:try_obtain_lease).and_return(true)
new_event = create(:event, project: project, created_at: Time.now)
+ project.reload
expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
end
@@ -379,26 +396,6 @@ describe Project, models: true do
end
end
- describe '#update_merge_requests' do
- let(:project) { create(:project) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let(:key) { create(:key, user_id: project.owner.id) }
- let(:prev_commit_id) { merge_request.commits.last.id }
- let(:commit_id) { merge_request.commits.first.id }
-
- it 'closes merge request if last commit from source branch was pushed to target branch' do
- project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user)
- merge_request.reload
- expect(merge_request.merged?).to be_truthy
- end
-
- it 'updates merge request commits with new one if pushed to source branch' do
- project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user)
- merge_request.reload
- expect(merge_request.diff_head_sha).to eq(commit_id)
- end
- end
-
describe '.find_with_namespace' do
context 'with namespace' do
before do
@@ -520,7 +517,7 @@ describe Project, models: true do
end
describe '#cache_has_external_issue_tracker' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, has_external_issue_tracker: nil) }
it 'stores true if there is any external_issue_tracker' do
services = double(:service, external_issue_trackers: [RedmineService.new])
@@ -542,9 +539,9 @@ describe Project, models: true do
end
describe '#has_wiki?' do
- let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) }
- let(:wiki_enabled_project) { build(:project) }
- let(:external_wiki_project) { build(:project, has_external_wiki: true) }
+ let(:no_wiki_project) { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
+ let(:wiki_enabled_project) { create(:project) }
+ let(:external_wiki_project) { create(:project, has_external_wiki: true) }
it 'returns true if project is wiki enabled or has external wiki' do
expect(wiki_enabled_project).to have_wiki
@@ -799,32 +796,22 @@ describe Project, models: true do
end
create(:note_on_commit, project: project2)
- end
- describe 'without an explicit start date' do
- subject { described_class.trending.to_a }
-
- it 'sorts Projects by the amount of notes in descending order' do
- expect(subject).to eq([project1, project2])
- end
+ TrendingProject.refresh!
end
- describe 'with an explicit start date' do
- let(:date) { 2.months.ago }
+ subject { described_class.trending.to_a }
- subject { described_class.trending(date).to_a }
+ it 'sorts projects by the amount of notes in descending order' do
+ expect(subject).to eq([project1, project2])
+ end
- before do
- 2.times do
- # Little fix for special issue related to Fractional Seconds support for MySQL.
- # See: https://github.com/rails/rails/pull/14359/files
- create(:note_on_commit, project: project2, created_at: date + 1)
- end
+ it 'does not take system notes into account' do
+ 10.times do
+ create(:note_on_commit, project: project2, system: true)
end
- it 'sorts Projects by the amount of notes in descending order' do
- expect(subject).to eq([project2, project1])
- end
+ expect(described_class.trending.to_a).to eq([project1, project2])
end
end
@@ -836,7 +823,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]) }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index f979d66c88c..e0f2dadf189 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -137,7 +137,7 @@ describe ProjectTeam, models: true do
describe '#find_member' do
context 'personal project' do
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
let(:requester) { create(:user) }
before do
@@ -200,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 db29f4d353b..187a1bf2d79 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -7,15 +7,18 @@ describe Repository, models: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:user) { create(:user) }
+
let(:commit_options) do
author = repository.user_to_committer(user)
{ message: 'Test message', committer: author, author: author }
end
+
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
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
@@ -90,6 +93,26 @@ describe Repository, models: true do
end
end
+ describe '#ref_name_for_sha' do
+ context 'ref found' do
+ it 'returns the ref' do
+ allow_any_instance_of(Gitlab::Popen).to receive(:popen).
+ and_return(["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0])
+
+ expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
+ end
+ end
+
+ context 'ref not found' do
+ it 'returns nil' do
+ allow_any_instance_of(Gitlab::Popen).to receive(:popen).
+ and_return(["", 0])
+
+ expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq nil
+ end
+ end
+ end
+
describe '#last_commit_for_path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
@@ -97,12 +120,20 @@ describe Repository, models: true do
end
describe '#find_commits_by_message' do
- subject { repository.find_commits_by_message('submodule').map{ |k| k.id } }
+ it 'returns commits with messages containing a given string' do
+ commit_ids = repository.find_commits_by_message('submodule').map(&:id)
+
+ expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ expect(commit_ids).to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ expect(commit_ids).to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660')
+ expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
+ end
- it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
- it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- it { is_expected.to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') }
- it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+ it 'is case insensitive' do
+ commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
+
+ expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ end
end
describe '#blob_at' do
@@ -114,11 +145,30 @@ describe Repository, models: true do
end
describe '#merged_to_root_ref?' do
- context 'merged branch' do
+ context 'merged branch without ff' do
+ subject { repository.merged_to_root_ref?('branch-merged') }
+
+ it { is_expected.to be_truthy }
+ end
+
+ # If the HEAD was ff then it will be false
+ context 'merged with ff' do
subject { repository.merged_to_root_ref?('improve/awesome') }
it { is_expected.to be_truthy }
end
+
+ context 'not merged branch' do
+ subject { repository.merged_to_root_ref?('not-merged-branch') }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'default branch' do
+ subject { repository.merged_to_root_ref?('master') }
+
+ it { is_expected.to be_falsey }
+ end
end
describe '#can_be_merged?' do
@@ -316,7 +366,17 @@ describe Repository, models: true do
subject { results.first }
it { is_expected.to be_an String }
- it { expect(subject.lines[2]).to eq("master:CHANGELOG:188: - Feature: Replace teams with group membership\n") }
+ it { expect(subject.lines[2]).to eq("master:CHANGELOG:190: - Feature: Replace teams with group membership\n") }
+ end
+ end
+
+ describe '#create_ref' do
+ it 'redirects the call to fetch_ref' do
+ ref, ref_path = '1', '2'
+
+ expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path)
+
+ repository.create_ref(ref, ref_path)
end
end
@@ -950,10 +1010,10 @@ describe Repository, models: true do
context 'cherry-picking a merge commit' do
it 'cherry-picks the changes' do
- expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
- repository.cherry_pick(user, pickable_merge, 'master')
- expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).not_to be_nil
+ repository.cherry_pick(user, pickable_merge, 'improve/awesome')
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
end
end
end
@@ -1086,28 +1146,17 @@ describe Repository, models: true do
end
describe '#before_import' do
- it 'flushes the emptiness cachess' do
- expect(repository).to receive(:expire_emptiness_caches)
-
- repository.before_import
- end
-
- it 'flushes the exists cache' do
- expect(repository).to receive(:expire_exists_cache)
+ it 'flushes the repository caches' do
+ expect(repository).to receive(:expire_content_cache)
repository.before_import
end
end
describe '#after_import' do
- it 'flushes the emptiness cachess' do
- expect(repository).to receive(:expire_emptiness_caches)
-
- repository.after_import
- end
-
- it 'flushes the exists cache' do
- expect(repository).to receive(:expire_exists_cache)
+ it 'flushes and builds the cache' do
+ expect(repository).to receive(:expire_content_cache)
+ expect(repository).to receive(:build_cache)
repository.after_import
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 05056a4bb47..43937a54b2c 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -203,6 +203,23 @@ describe Service, models: true do
end
end
+ describe 'initialize service with no properties' do
+ let(:service) do
+ GitlabIssueTrackerService.create(
+ project: create(:project),
+ title: 'random title'
+ )
+ end
+
+ it 'does not raise error' do
+ expect { service }.not_to raise_error
+ end
+
+ it 'creates the properties' do
+ expect(service.properties).to eq({ "title" => "random title" })
+ end
+ end
+
describe "callbacks" do
let(:project) { create(:project) }
let!(:service) do
@@ -221,7 +238,7 @@ describe Service, models: true do
it "updates the has_external_issue_tracker boolean" do
expect do
service.save!
- end.to change { service.project.has_external_issue_tracker }.from(nil).to(true)
+ end.to change { service.project.has_external_issue_tracker }.from(false).to(true)
end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index e6bc5296398..f62f6bacbaa 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -46,6 +46,13 @@ describe Snippet, models: true do
end
end
+ describe "#content_html_invalidated?" do
+ let(:snippet) { create(:snippet, content: "md", content_html: "html", file_name: "foo.md") }
+ it "invalidates the HTML cache of content when the filename changes" do
+ expect { snippet.file_name = "foo.rb" }.to change { snippet.content_html_invalidated? }.from(false).to(true)
+ end
+ end
+
describe '.search' do
let(:snippet) { create(:snippet) }
diff --git a/spec/models/trending_project_spec.rb b/spec/models/trending_project_spec.rb
new file mode 100644
index 00000000000..cc28c6d4004
--- /dev/null
+++ b/spec/models/trending_project_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe TrendingProject do
+ let(:user) { create(:user) }
+ let(:public_project1) { create(:empty_project, :public) }
+ let(:public_project2) { create(:empty_project, :public) }
+ let(:public_project3) { create(:empty_project, :public) }
+ let(:private_project) { create(:empty_project, :private) }
+ let(:internal_project) { create(:empty_project, :internal) }
+
+ before do
+ 3.times do
+ create(:note_on_commit, project: public_project1)
+ end
+
+ 2.times do
+ create(:note_on_commit, project: public_project2)
+ end
+
+ create(:note_on_commit, project: public_project3, created_at: 5.weeks.ago)
+ create(:note_on_commit, project: private_project)
+ create(:note_on_commit, project: internal_project)
+ end
+
+ describe '.refresh!' do
+ before do
+ described_class.refresh!
+ end
+
+ it 'populates the trending projects table' do
+ expect(described_class.count).to eq(2)
+ end
+
+ it 'removes existing rows before populating the table' do
+ described_class.refresh!
+
+ expect(described_class.count).to eq(2)
+ end
+
+ it 'stores the project IDs for every trending project' do
+ rows = described_class.order(id: :asc).all
+
+ expect(rows[0].project_id).to eq(public_project1.id)
+ expect(rows[1].project_id).to eq(public_project2.id)
+ end
+
+ it 'does not store projects that fall out of the trending time range' do
+ expect(described_class.where(project_id: public_project3).any?).to eq(false)
+ end
+
+ it 'stores only public projects' do
+ expect(described_class.where(project_id: [public_project1.id, public_project2.id]).count).to eq(2)
+ expect(described_class.where(project_id: [private_project.id, internal_project.id]).count).to eq(0)
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index feb6b62bce4..ccfbffafb10 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -610,6 +610,23 @@ describe User, models: true do
end
end
+ describe '.find_by_ssh_key_id' do
+ context 'using an existing SSH key ID' do
+ let(:user) { create(:user) }
+ let(:key) { create(:key, user: user) }
+
+ it 'returns the corresponding User' do
+ expect(described_class.find_by_ssh_key_id(key.id)).to eq(user)
+ end
+ end
+
+ context 'using an invalid SSH key ID' do
+ it 'returns nil' do
+ expect(described_class.find_by_ssh_key_id(-1)).to be_nil
+ end
+ end
+ end
+
describe '.by_login' do
let(:username) { 'John' }
let!(:user) { create(:user, username: username) }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index a7a06744428..658e3c13a73 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1,20 +1,69 @@
require 'spec_helper'
describe ProjectPolicy, models: true do
- let(:project) { create(:empty_project, :public) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
let(:dev) { create(:user) }
let(:master) { create(:user) }
let(:owner) { create(:user) }
- let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
- let(:users_ordered_by_permissions) do
- [nil, guest, reporter, dev, master, owner, admin]
+ let(:guest_permissions) do
+ [
+ :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label,
+ :read_milestone, :read_project_snippet, :read_project_member,
+ :read_note, :create_project, :create_issue, :create_note,
+ :upload_file
+ ]
end
- let(:users_permissions) do
- users_ordered_by_permissions.map { |u| Ability.allowed(u, project).size }
+ let(:reporter_permissions) do
+ [
+ :download_code, :fork_project, :create_project_snippet, :update_issue,
+ :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build,
+ :read_container_image, :read_pipeline, :read_environment, :read_deployment,
+ :read_merge_request
+ ]
+ end
+
+ let(:team_member_reporter_permissions) do
+ [
+ :build_download_code, :build_read_container_image
+ ]
+ end
+
+ let(:developer_permissions) do
+ [
+ :admin_merge_request, :update_merge_request, :create_commit_status,
+ :update_commit_status, :create_build, :update_build, :create_pipeline,
+ :update_pipeline, :create_merge_request, :create_wiki, :push_code,
+ :resolve_note, :create_container_image, :update_container_image,
+ :create_environment, :create_deployment
+ ]
+ end
+
+ let(:master_permissions) do
+ [
+ :push_code_to_protected_branches, :update_project_snippet, :update_environment,
+ :update_deployment, :admin_milestone, :admin_project_snippet,
+ :admin_project_member, :admin_note, :admin_wiki, :admin_project,
+ :admin_commit_status, :admin_build, :admin_container_image,
+ :admin_pipeline, :admin_environment, :admin_deployment
+ ]
+ end
+
+ let(:public_permissions) do
+ [
+ :download_code, :fork_project, :read_commit_status, :read_pipeline,
+ :read_container_image, :build_download_code, :build_read_container_image
+ ]
+ end
+
+ let(:owner_permissions) do
+ [
+ :change_namespace, :change_visibility_level, :rename_project, :remove_project,
+ :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue
+ ]
end
before do
@@ -22,16 +71,6 @@ describe ProjectPolicy, models: true do
project.team << [master, :master]
project.team << [dev, :developer]
project.team << [reporter, :reporter]
-
- group = create(:group)
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::MASTER)
- group.add_owner(owner)
- end
-
- 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
@@ -46,4 +85,81 @@ describe ProjectPolicy, models: true do
expect(Ability.allowed?(user, :read_issue, project)).to be_falsy
end
+
+ context 'abilities for non-public projects' do
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ subject { described_class.abilities(current_user, project).to_set }
+
+ context 'with no user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'guests' do
+ let(:current_user) { guest }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.not_to include(*reporter_permissions)
+ is_expected.not_to include(*team_member_reporter_permissions)
+ is_expected.not_to include(*developer_permissions)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.to include(*team_member_reporter_permissions)
+ is_expected.not_to include(*developer_permissions)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'developer' do
+ let(:current_user) { dev }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.to include(*team_member_reporter_permissions)
+ is_expected.to include(*developer_permissions)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'master' do
+ let(:current_user) { master }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.to include(*team_member_reporter_permissions)
+ is_expected.to include(*developer_permissions)
+ is_expected.to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.not_to include(*team_member_reporter_permissions)
+ is_expected.to include(*developer_permissions)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index d78494b76fa..b467890a403 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)
@@ -181,7 +195,7 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as the access requester' do
- it 'returns 200' do
+ it 'deletes the access requester' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
@@ -191,7 +205,7 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as a master/owner' do
- it 'returns 200' do
+ it 'deletes the access requester' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
@@ -199,6 +213,16 @@ describe API::AccessRequests, api: true do
end.to change { source.requesters.count }.by(-1)
end
+ context 'user_id matches a member, not an access requester' do
+ it 'returns 404' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master)
+
+ expect(response).to have_http_status(404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
context 'user_id does not match an existing access requester' do
it 'returns 404' do
expect do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index e66faeed705..0f41f8dc7f1 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -10,7 +10,8 @@ describe API::Helpers, api: true do
let(:key) { create(:key, user: user) }
let(:params) { {} }
- let(:env) { {} }
+ let(:env) { { 'REQUEST_METHOD' => 'GET' } }
+ let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier)
clear_env
@@ -52,17 +53,43 @@ describe API::Helpers, api: true do
describe ".current_user" do
subject { current_user }
- describe "when authenticating via Warden" do
+ describe "Warden authentication" do
before { doorkeeper_guard_returns false }
- context "fails" do
- it { is_expected.to be_nil }
+ context "with invalid credentials" do
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to be_nil }
+ end
end
- context "succeeds" do
+ context "with valid credentials" do
before { warden_authenticate_returns user }
- it { is_expected.to eq(user) }
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "HEAD request" do
+ before { env['REQUEST_METHOD'] = 'HEAD' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "PUT request" do
+ before { env['REQUEST_METHOD'] = 'PUT' }
+ it { is_expected.to be_nil }
+ end
+
+ context "POST request" do
+ before { env['REQUEST_METHOD'] = 'POST' }
+ it { is_expected.to be_nil }
+ end
+
+ context "DELETE request" do
+ before { env['REQUEST_METHOD'] = 'DELETE' }
+ it { is_expected.to be_nil }
+ end
end
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
new file mode 100644
index 00000000000..4f5c09a3029
--- /dev/null
+++ b/spec/requests/api/boards_spec.rb
@@ -0,0 +1,201 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+ let!(:dev_label) do
+ create(:label, title: 'Development', color: '#FFAABB', project: project)
+ end
+
+ let!(:test_label) do
+ create(:label, title: 'Testing', color: '#FFAACC', project: project)
+ end
+
+ let!(:ux_label) do
+ create(:label, title: 'UX', color: '#FF0000', project: project)
+ end
+
+ let!(:dev_list) do
+ create(:list, label: dev_label, position: 1)
+ end
+
+ let!(:test_list) do
+ create(:list, label: test_label, position: 2)
+ end
+
+ let!(:board) do
+ create(:board, project: project, lists: [dev_list, test_list])
+ end
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /projects/:id/boards" do
+ let(:base_url) { "/projects/#{project.id}/boards" }
+
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get api(base_url)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns the project issue board" do
+ get api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(board.id)
+ expect(json_response.first['lists']).to be_an Array
+ expect(json_response.first['lists'].length).to eq(2)
+ expect(json_response.first['lists'].last).to have_key('position')
+ end
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns issue board lists' do
+ get api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['label']['name']).to eq(dev_label.title)
+ end
+
+ it 'returns 404 if board not found' do
+ get api("/projects/#{project.id}/boards/22343/lists", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns a list' do
+ get api("#{base_url}/#{dev_list.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(dev_list.id)
+ expect(json_response['label']['name']).to eq(dev_label.title)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it 'returns 404 if list not found' do
+ get api("#{base_url}/5324", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/board/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'creates a new issue board list for group labels' do
+ group = create(:group)
+ group_label = create(:group_label, group: group)
+ project.update(group: group)
+
+ post api(base_url, user), label_id: group_label.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['label']['name']).to eq(group_label.title)
+ expect(json_response['position']).to eq(3)
+ end
+
+ it 'creates a new issue board list for project labels' do
+ post api(base_url, user), label_id: ux_label.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['label']['name']).to eq(ux_label.title)
+ expect(json_response['position']).to eq(3)
+ end
+
+ it 'returns 400 when creating a new list if label_id is invalid' do
+ post api(base_url, user), label_id: 23423
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 403 for project members with guest role' do
+ put api("#{base_url}/#{test_list.id}", guest), position: 1
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "updates a list" do
+ put api("#{base_url}/#{test_list.id}", user),
+ position: 1
+
+ expect(response).to have_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it "returns 404 error if list id not found" do
+ put api("#{base_url}/44444", user),
+ position: 1
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put api("#{base_url}/#{test_list.id}", guest),
+ position: 1
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe "DELETE /projects/:id/board/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "rejects a non member from deleting a list" do
+ delete api("#{base_url}/#{dev_list.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a user with guest role from deleting a list" do
+ delete api("#{base_url}/#{dev_list.id}", guest)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 404 error if list id not found" do
+ delete api("#{base_url}/44444", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ it "deletes the list if an admin requests it" do
+ delete api("#{base_url}/#{dev_list.id}", owner)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 3fd989dd7a6..905f762d578 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -48,92 +48,154 @@ describe API::API, api: true do
end
describe 'PUT /projects/:id/repository/branches/:branch/protect' do
- it 'protects a single branch' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+ context "when a protected branch doesn't already exist" do
+ it 'protects a single branch' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
- it 'protects a single branch and developers can push' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_push: true
+ it 'protects a single branch and developers can push' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: true
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(true)
- expect(json_response['developers_can_merge']).to eq(false)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
- it 'protects a single branch and developers can merge' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_merge: true
+ it 'protects a single branch and developers can merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_merge: true
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(true)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
- it 'protects a single branch and developers can push and merge' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_push: true, developers_can_merge: true
+ it 'protects a single branch and developers can push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: true, developers_can_merge: true
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(true)
- expect(json_response['developers_can_merge']).to eq(true)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
- it 'protects a single branch and developers cannot push and merge' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_push: 'tru', developers_can_merge: 'tr'
+ it 'protects a single branch and developers cannot push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: 'tru', developers_can_merge: 'tr'
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
end
- context 'on a protected branch' do
- let(:protected_branch) { 'foo' }
-
+ context 'for an existing protected branch' do
before do
- project.repository.add_branch(user, protected_branch, 'master')
- create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch)
+ project.repository.add_branch(user, protected_branch.name, 'master')
end
- it 'updates that a developer can push' do
- put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
- developers_can_push: false, developers_can_merge: false
+ context "when developers can push and merge" do
+ let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') }
+
+ it 'updates that a developer cannot push or merge' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_push: false, developers_can_merge: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_push: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(protected_branch.reload.push_access_levels.first).to be_present
+ expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
+ end
+
+ it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_merge: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(protected_branch.reload.merge_access_levels.first).to be_present
+ expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context "when developers cannot push or merge" do
+ let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') }
+
+ it 'updates that a developer can push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_push: true, developers_can_merge: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
+ end
+ end
+
+ context "multiple API calls" do
+ it "returns success when `protect` is called twice" do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['name']).to eq(branch_name)
expect(json_response['protected']).to eq(true)
expect(json_response['developers_can_push']).to eq(false)
expect(json_response['developers_can_merge']).to eq(false)
end
- it 'does not update that a developer can push' do
- put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
- developers_can_push: 'foobar', developers_can_merge: 'foo'
+ it "returns success when `protect` is called twice with `developers_can_push` turned on" do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['name']).to eq(branch_name)
expect(json_response['protected']).to eq(true)
expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ it "returns success when `protect` is called twice with `developers_can_merge` turned on" do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
expect(json_response['developers_can_merge']).to eq(true)
end
end
@@ -147,12 +209,6 @@ describe API::API, api: true do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
expect(response).to have_http_status(403)
end
-
- it "returns success when protect branch again" do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- expect(response).to have_http_status(200)
- end
end
describe "PUT /projects/:id/repository/branches/:branch/unprotect" 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 7aa7e85a9e2..335efc4db6c 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -196,7 +196,7 @@ describe API::CommitStatuses, api: true do
end
context 'reporter user' do
- before { post api(post_url, reporter) }
+ before { post api(post_url, reporter), state: 'running' }
it 'does not create commit status' do
expect(response).to have_http_status(403)
@@ -204,7 +204,7 @@ describe API::CommitStatuses, api: true do
end
context 'guest user' do
- before { post api(post_url, guest) }
+ before { post api(post_url, guest), state: 'running' }
it 'does not create commit status' do
expect(response).to have_http_status(403)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 10f772c5b1a..a6e8550fac3 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
@@ -13,7 +13,7 @@ describe API::API, api: true do
before { project.team << [user, :reporter] }
- describe "GET /projects/:id/repository/commits" do
+ describe "List repository commits" do
context "authorized user" do
before { project.team << [user2, :reporter] }
@@ -53,7 +53,12 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
- expect(json_response.size).to eq(commits.size - 1)
+ if commits.size >= 20
+ expect(json_response.size).to eq(20)
+ else
+ expect(json_response.size).to eq(commits.size - 1)
+ end
+
expect(json_response.first["id"]).to eq(commits.second.id)
expect(json_response.second["id"]).to eq(commits.third.id)
end
@@ -67,9 +72,281 @@ describe API::API, api: true do
expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format"
end
end
+
+ context "path optional parameter" do
+ it "returns project commits matching provided path parameter" do
+ path = 'files/ruby/popen.rb'
+
+ get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+
+ expect(json_response.size).to eq(3)
+ expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ end
+ end
end
- describe "GET /projects:id/repository/commits/:sha" do
+ describe "Create a commit with multiple files and actions" do
+ let!(:url) { "/projects/#{project.id}/repository/commits" }
+
+ it 'returns a 403 unauthorized for user without permissions' do
+ post api(url, user2)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns a 400 bad request if no params are given' do
+ post api(url, user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context :create do
+ let(:message) { 'Created file' }
+ let!(:invalid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'a new file in project repo' do
+ post api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file exists' do
+ post api(url, user), invalid_c_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :delete do
+ let(:message) { 'Deleted file' }
+ let!(:invalid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ }
+ ]
+ }
+ end
+ let!(:valid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/users.md'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_d_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_d_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :move do
+ let(:message) { 'Moved file' }
+ let!(:invalid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+ let!(:valid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_m_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_m_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :update do
+ let(:message) { 'Updated file' }
+ let!(:invalid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_u_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_u_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context "multiple operations" do
+ let(:message) { 'Multiple actions' }
+ let!(:invalid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ },
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'Gemfile.zip'
+ },
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'are commited as one in project repo' do
+ post api(url, user), valid_mo_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'return a 400 bad request if there are any issues' do
+ post api(url, user), invalid_mo_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+ end
+
+ describe "Get a single commit" do
context "authorized user" do
it "returns a commit by sha" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -122,7 +399,7 @@ describe API::API, api: true do
end
end
- describe "GET /projects:id/repository/commits/:sha/diff" do
+ describe "Get the diff of a commit" do
context "authorized user" do
before { project.team << [user2, :reporter] }
@@ -149,7 +426,7 @@ describe API::API, api: true do
end
end
- describe 'GET /projects:id/repository/commits/:sha/comments' do
+ describe 'Get the comments of a commit' do
context 'authorized user' do
it 'returns merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
@@ -174,7 +451,7 @@ describe API::API, api: true do
end
end
- describe 'POST /projects:id/repository/commits/:sha/comments' do
+ describe 'Post comment to commit' do
context 'authorized user' do
it 'returns comment' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
@@ -186,11 +463,12 @@ describe API::API, api: true do
end
it 'returns the inline comment' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 7, line_type: 'new'
+ post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
+
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
- expect(json_response['line']).to eq(7)
+ expect(json_response['line']).to eq(1)
expect(json_response['line_type']).to eq('new')
end
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index 34f84f78952..e38d5745d44 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -18,7 +18,7 @@ describe API::API, api: true do
end
let(:project_user2) do
- create(:project_member, :guest, user: user2, project: project)
+ create(:project_member, :reporter, user: user2, project: project)
end
describe 'POST /projects/fork/:id' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 1f68ef1af8f..3ba257256a0 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -45,6 +45,16 @@ describe API::API, api: true do
expect(json_response.length).to eq(2)
end
end
+
+ context "when using skip_groups in request" do
+ it "returns all groups excluding skipped groups" do
+ get api("/groups", admin), skip_groups: [group2.id]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
end
describe "GET /groups/:id" do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index f840778ae9b..beed53d1e5c 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -694,7 +694,7 @@ describe API::API, api: true do
title: 'new issue', labels: 'label, label2', created_at: creation_time
expect(response).to have_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
end
@@ -895,7 +895,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'label3'
- expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time)
+ expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 83789223019..46641fcd846 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -12,12 +12,17 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/labels' do
- it 'returns project labels' do
+ it 'returns all available labels to the project' do
+ group = create(:group)
+ group_label = create(:group_label, group: group)
+ project.update(group: group)
+
get api("/projects/#{project.id}/labels", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response.first['name']).to eq(label1.name)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, label1.name])
end
end
@@ -154,14 +159,14 @@ describe API::API, api: true do
it 'returns 400 if no label name given' do
put api("/projects/#{project.id}/labels", user), new_name: 'label2'
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "name" not given')
+ expect(json_response['error']).to eq('name is missing')
end
it 'returns 400 if no new parameters given' do
put api("/projects/#{project.id}/labels", user), name: 'label1'
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('Required parameters '\
- '"new_name" or "color" missing')
+ expect(json_response['error']).to eq('new_name, color, description are missing, '\
+ 'at least one parameter must be provided')
end
it 'returns 400 for invalid name' do
diff --git a/spec/requests/api/license_templates_spec.rb b/spec/requests/api/license_templates_spec.rb
deleted file mode 100644
index 9a1894d63a2..00000000000
--- a/spec/requests/api/license_templates_spec.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true do
- include ApiHelpers
-
- describe 'Entity' do
- before { get api('/licenses/mit') }
-
- it { expect(json_response['key']).to eq('mit') }
- it { expect(json_response['name']).to eq('MIT License') }
- it { expect(json_response['nickname']).to be_nil }
- it { expect(json_response['popular']).to be true }
- it { expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') }
- it { expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') }
- it { expect(json_response['description']).to include('A permissive license that is short and to the point.') }
- it { expect(json_response['conditions']).to eq(%w[include-copyright]) }
- it { expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) }
- it { expect(json_response['limitations']).to eq(%w[no-liability]) }
- it { expect(json_response['content']).to include('The MIT License (MIT)') }
- end
-
- describe 'GET /licenses' do
- it 'returns a list of available license templates' do
- get api('/licenses')
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(15)
- expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
- end
-
- describe 'the popular parameter' do
- context 'with popular=1' do
- it 'returns a list of available popular license templates' do
- get api('/licenses?popular=1')
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(3)
- expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
- end
- end
- end
- end
-
- describe 'GET /licenses/:key' do
- context 'with :project and :fullname given' do
- before do
- get api("/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
- end
-
- context 'for the mit license' do
- let(:license_type) { 'mit' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('The MIT License (MIT)')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
- end
- end
-
- context 'for the agpl-3.0 license' do
- let(:license_type) { 'agpl-3.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the gpl-3.0 license' do
- let(:license_type) { 'gpl-3.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the gpl-2.0 license' do
- let(:license_type) { 'gpl-2.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the apache-2.0 license' do
- let(:license_type) { 'apache-2.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('Apache License')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
- end
- end
-
- context 'for an uknown license' do
- let(:license_type) { 'muth-over9000' }
-
- it 'returns a 404' do
- expect(response).to have_http_status(404)
- end
- end
- end
-
- context 'with no :fullname given' do
- context 'with an authenticated user' do
- let(:user) { create(:user) }
-
- it 'replaces the copyright owner placeholder with the name of the current user' do
- get api('/licenses/mit', user)
-
- expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
- end
- end
- end
- end
-end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 92032f09b17..493c0a893d1 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -97,7 +97,10 @@ describe API::Members, api: true do
shared_examples 'POST /:sources/:id/members' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/members", stranger),
+ user_id: access_requester.id, access_level: Member::MASTER
+ end
end
context 'when authenticated as a non-member or member with insufficient rights' do
@@ -105,7 +108,8 @@ describe API::Members, api: true do
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
- post api("/#{source_type.pluralize}/#{source.id}/members", user)
+ post api("/#{source_type.pluralize}/#{source.id}/members", user),
+ user_id: access_requester.id, access_level: Member::MASTER
expect(response).to have_http_status(403)
end
@@ -174,7 +178,10 @@ describe API::Members, api: true do
shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
+ access_level: Member::MASTER
+ end
end
context 'when authenticated as a non-member or member with insufficient rights' do
@@ -182,7 +189,8 @@ describe API::Members, api: true do
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
- put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
+ access_level: Member::MASTER
expect(response).to have_http_status(403)
end
@@ -320,4 +328,15 @@ describe API::Members, api: true do
it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
let(:source) { group }
end
+
+ context 'Adding owner to project' do
+ it 'returns 403' do
+ expect do
+ post api("/projects/#{project.id}/members", master),
+ user_id: stranger.id, access_level: Member::OWNER
+
+ expect(response).to have_http_status(422)
+ end.to change { project.members.count }.by(0)
+ end
+ end
end
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 b89dac01040..dd192bea432 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -104,6 +104,14 @@ describe API::API, api: true do
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
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 063a8706e76..0124b7271b3 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -217,16 +217,27 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
- expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
- context 'when the user is posting an award emoji' do
+ context 'when the user is posting an award emoji on an issue created by someone else' do
+ let(:issue2) { create(:issue, project: project) }
+
it 'returns an award emoji' do
+ post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['awardable_id']).to eq issue2.id
+ end
+ end
+
+ context 'when the user is posting an award emoji on his/her own issue' do
+ it 'creates a new issue note' 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
+ expect(json_response['body']).to eq(':+1:')
end
end
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 765dc8a8f66..cfcdcad74cd 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -163,9 +163,10 @@ describe API::API, 'ProjectHooks', api: true do
expect(response).to have_http_status(404)
end
- it "returns a 405 error if hook id not given" do
+ it "returns a 404 error if hook id not given" do
delete api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(405)
+
+ expect(response).to have_http_status(404)
end
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 192c7d14c13..973928d007a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -175,6 +175,36 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/visible' do
+ let(:public_project) { create(:project, :public) }
+
+ before do
+ public_project
+ project
+ project2
+ project3
+ project4
+ end
+
+ it 'returns the projects viewable by the user' do
+ get api('/projects/visible', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).
+ to contain_exactly(public_project.id, project.id, project2.id, project3.id)
+ end
+
+ it 'shows only public projects when the user only has access to those' do
+ get api('/projects/visible', user2)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).
+ to contain_exactly(public_project.id)
+ end
+ end
+
describe 'GET /projects/starred' do
let(:public_project) { create(:project, :public) }
@@ -232,7 +262,7 @@ describe API::API, api: true do
post api('/projects', user), project
project.each_pair do |k, v|
- next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k)
+ next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
@@ -360,7 +390,7 @@ describe API::API, api: true do
post api("/projects/user/#{user.id}", admin), project
project.each_pair do |k, v|
- next if k == :path
+ next if %i[has_external_issue_tracker path].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
end
@@ -558,37 +588,39 @@ describe API::API, api: true do
before do
note = create(:note_on_issue, note: 'What an awesome day!', project: project)
EventCreateService.new.leave_note(note, note.author)
- get api("/projects/#{project.id}/events", user)
end
- it { expect(response).to have_http_status(200) }
+ it 'returns all events' do
+ get api("/projects/#{project.id}/events", user)
- context 'joined event' do
- let(:json_event) { json_response[1] }
+ expect(response).to have_http_status(200)
- it { expect(json_event['action_name']).to eq('joined') }
- it { expect(json_event['project_id'].to_i).to eq(project.id) }
- it { expect(json_event['author_username']).to eq(user3.username) }
- it { expect(json_event['author']['name']).to eq(user3.name) }
- end
+ first_event = json_response.first
- context 'comment event' do
- let(:json_event) { json_response.first }
+ expect(first_event['action_name']).to eq('commented on')
+ expect(first_event['note']['body']).to eq('What an awesome day!')
- it { expect(json_event['action_name']).to eq('commented on') }
- it { expect(json_event['note']['body']).to eq('What an awesome day!') }
+ last_event = json_response.last
+
+ expect(last_event['action_name']).to eq('joined')
+ expect(last_event['project_id'].to_i).to eq(project.id)
+ expect(last_event['author_username']).to eq(user3.username)
+ expect(last_event['author']['name']).to eq(user3.name)
end
end
it 'returns a 404 error if not found' do
get api('/projects/42/events', user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
+
get api("/projects/#{project.id}/events", other_user)
+
expect(response).to have_http_status(404)
end
end
@@ -761,13 +793,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
@@ -786,6 +821,20 @@ describe API::API, api: true do
expect(response.status).to eq 400
end
+ it 'returns a 404 error when user cannot read group' do
+ private_group = create(:group, :private)
+
+ post api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response.status).to eq 404
+ end
+
+ it 'returns a 404 error when group does not exist' do
+ post api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response.status).to eq 404
+ end
+
it "returns a 409 error when wrong params passed" do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
expect(response.status).to eq 409
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 80a856a6e90..c4dc2d9006a 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -21,7 +21,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['name']).to eq('encoding')
+ expect(json_response.first['name']).to eq('bar')
expect(json_response.first['type']).to eq('tree')
expect(json_response.first['mode']).to eq('040000')
end
@@ -166,9 +166,9 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
contributor = json_response.first
- expect(contributor['email']).to eq('dmitriy.zaporozhets@gmail.com')
- expect(contributor['name']).to eq('Dmitriy Zaporozhets')
- expect(contributor['commits']).to eq(13)
+ expect(contributor['email']).to eq('tiagonbotelho@hotmail.com')
+ expect(contributor['name']).to eq('tiagonbotelho')
+ expect(contributor['commits']).to eq(1)
expect(contributor['additions']).to eq(0)
expect(contributor['deletions']).to eq(0)
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/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 1ce2658569e..f8a1aed5441 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -73,9 +73,10 @@ describe API::API, api: true do
end.to change { SystemHook.count }.by(-1)
end
- it "returns success if hook id not found" do
- delete api("/hooks/12345", admin)
- expect(response).to have_http_status(200)
+ it 'returns 404 if the system hook does not exist' do
+ delete api('/hooks/12345', admin)
+
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 5bd5b861792..d32ba60fc4c 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,53 +3,201 @@ require 'spec_helper'
describe API::Templates, api: true do
include ApiHelpers
- context 'global templates' do
- describe 'the Template Entity' do
- before { get api('/gitignores/Ruby') }
+ shared_examples_for 'the Template Entity' do |path|
+ before { get api(path) }
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
+
+ shared_examples_for 'the TemplateList Entity' do |path|
+ before { get api(path) }
+
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
+
+ shared_examples_for 'requesting gitignores' do |path|
+ it 'returns a list of available gitignore templates' do
+ get api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
end
+ end
- describe 'the TemplateList Entity' do
- before { get api('/gitignores') }
+ shared_examples_for 'requesting gitlab-ci-ymls' do |path|
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api(path)
- it { expect(json_response.first['name']).not_to be_nil }
- it { expect(json_response.first['content']).to be_nil }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
end
+ end
+
+ shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path|
+ it 'adds a disclaimer on the top' do
+ get api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
+ end
+
+ shared_examples_for 'the License Template Entity' do |path|
+ before { get api(path) }
+
+ it 'returns a license template' do
+ expect(json_response['key']).to eq('mit')
+ expect(json_response['name']).to eq('MIT License')
+ expect(json_response['nickname']).to be_nil
+ expect(json_response['popular']).to be true
+ expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
+ expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
+ expect(json_response['description']).to include('A permissive license that is short and to the point.')
+ expect(json_response['conditions']).to eq(%w[include-copyright])
+ expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
+ expect(json_response['limitations']).to eq(%w[no-liability])
+ expect(json_response['content']).to include('The MIT License (MIT)')
+ end
+ end
- context 'requesting gitignores' do
- describe 'GET /gitignores' do
- it 'returns a list of available gitignore templates' do
- get api('/gitignores')
+ shared_examples_for 'GET licenses' do |path|
+ it 'returns a list of available license templates' do
+ get api(path)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(15)
+ expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
+ end
+
+ describe 'the popular parameter' do
+ context 'with popular=1' do
+ it 'returns a list of available popular license templates' do
+ get api("#{path}?popular=1")
+
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
+ expect(json_response.size).to eq(3)
+ expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
end
end
end
+ end
- context 'requesting gitlab-ci-ymls' do
- describe 'GET /gitlab_ci_ymls' do
- it 'returns a list of available gitlab_ci_ymls' do
- get api('/gitlab_ci_ymls')
+ shared_examples_for 'GET licenses/:name' do |path|
+ context 'with :project and :fullname given' do
+ before do
+ get api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
+ end
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).not_to be_nil
+ context 'for the mit license' do
+ let(:license_type) { 'mit' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('The MIT License (MIT)')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the agpl-3.0 license' do
+ let(:license_type) { 'agpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the gpl-3.0 license' do
+ let(:license_type) { 'gpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the gpl-2.0 license' do
+ let(:license_type) { 'gpl-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the apache-2.0 license' do
+ let(:license_type) { 'apache-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('Apache License')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for an uknown license' do
+ let(:license_type) { 'muth-over9000' }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
end
end
end
- describe 'GET /gitlab_ci_ymls/Ruby' do
- it 'adds a disclaimer on the top' do
- get api('/gitlab_ci_ymls/Ruby')
+ context 'with no :fullname given' do
+ context 'with an authenticated user' do
+ let(:user) { create(:user) }
+
+ it 'replaces the copyright owner placeholder with the name of the current user' do
+ get api('/templates/licenses/mit', user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).not_to be_nil
- expect(json_response['content']).to start_with("# This file is a template,")
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
+ end
end
end
end
+
+ describe 'with /templates namespace' do
+ it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby'
+ it_behaves_like 'the TemplateList Entity', '/templates/gitignores'
+ it_behaves_like 'requesting gitignores', '/templates/gitignores'
+ it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls'
+ it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby'
+ it_behaves_like 'the License Template Entity', '/templates/licenses/mit'
+ it_behaves_like 'GET licenses', '/templates/licenses'
+ it_behaves_like 'GET licenses/:name', '/templates/licenses'
+ end
+
+ describe 'without /templates namespace' do
+ it_behaves_like 'the Template Entity', '/gitignores/Ruby'
+ it_behaves_like 'the TemplateList Entity', '/gitignores'
+ it_behaves_like 'requesting gitignores', '/gitignores'
+ it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls'
+ it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby'
+ it_behaves_like 'the License Template Entity', '/licenses/mit'
+ it_behaves_like 'GET licenses', '/licenses'
+ it_behaves_like 'GET licenses/:name', '/licenses'
+ end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 1498c02629b..e44521fa682 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -90,8 +90,9 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "returns a 404 if invalid ID" do
+ it "returns a 404 for invalid ID" do
get api("/users/1ASDF", user)
+
expect(response).to have_http_status(404)
end
end
@@ -386,8 +387,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "raises error for invalid ID" do
- expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 if invalid ID" do
+ put api("/users/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
it 'returns 400 error if user does not validate' do
@@ -539,8 +542,9 @@ describe API::API, api: true do
end.to change{ user.emails.count }.by(1)
end
- it "raises error for invalid ID" do
+ it "returns a 400 for invalid ID" do
post api("/users/999999/emails", admin)
+
expect(response).to have_http_status(400)
end
end
@@ -571,9 +575,10 @@ describe API::API, api: true do
expect(json_response.first['email']).to eq(email.email)
end
- it "raises error for invalid ID" do
+ it "returns a 404 for invalid ID" do
put api("/users/ASDF/emails", admin)
- expect(response).to have_http_status(405)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -612,8 +617,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Email Not Found')
end
- it "raises error for invalid ID" do
- expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/ASDF/emails/bar", admin)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -646,8 +653,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it "raises error for invalid ID" do
- expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -700,6 +709,7 @@ describe API::API, api: true do
it "returns 404 Not Found within invalid ID" do
get api("/user/keys/42", user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -715,6 +725,7 @@ describe API::API, api: true do
it "returns 404 for invalid ID" do
get api("/users/keys/ASDF", admin)
+
expect(response).to have_http_status(404)
end
end
@@ -773,8 +784,10 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "raises error for invalid ID" do
- expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/keys/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -824,6 +837,7 @@ describe API::API, api: true do
it "returns 404 for invalid ID" do
get api("/users/emails/ASDF", admin)
+
expect(response).to have_http_status(404)
end
end
@@ -871,12 +885,14 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "raises error for invalid ID" do
- expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/emails/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
- describe 'PUT /user/:id/block' do
+ describe 'PUT /users/:id/block' do
before { admin }
it 'blocks existing user' do
put api("/users/#{user.id}/block", admin)
@@ -903,7 +919,7 @@ describe API::API, api: true do
end
end
- describe 'PUT /user/:id/unblock' do
+ describe 'PUT /users/:id/unblock' do
let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
@@ -937,8 +953,64 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it "raises error for invalid ID" do
- expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ put api("/users/ASDF/block", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /users/:id/events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
+
+ before do
+ project.add_user(user, :developer)
+ EventCreateService.new.leave_note(note, user)
+ end
+
+ context "as a user than cannot see the event's project" do
+ it 'returns no events' do
+ other_user = create(:user)
+
+ get api("/users/#{user.id}/events", other_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_empty
+ end
+ end
+
+ context "as a user than can see the event's project" do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/users/#{user.id}/events", user) }
+ end
+
+ context 'joined event' do
+ it 'returns the "joined" event' do
+ get api("/users/#{user.id}/events", user)
+
+ comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_event['project_id'].to_i).to eq(project.id)
+ expect(comment_event['author_username']).to eq(user.username)
+ expect(comment_event['note']['id']).to eq(note.id)
+ expect(comment_event['note']['body']).to eq('What an awesome day!')
+
+ joined_event = json_response.find { |e| e['action_name'] == 'joined' }
+
+ expect(joined_event['project_id'].to_i).to eq(project.id)
+ expect(joined_event['author_username']).to eq(user.username)
+ expect(joined_event['author']['name']).to eq(user.name)
+ end
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get api('/users/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
end
end
end
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
new file mode 100644
index 00000000000..54b69a0cae7
--- /dev/null
+++ b/spec/requests/api/version_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ describe 'GET /version' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/version')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ let(:user) { create(:user) }
+
+ it 'returns the version information' do
+ get api('/version', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['version']).to eq(Gitlab::VERSION)
+ expect(json_response['revision']).to eq(Gitlab::REVISION)
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index df97f1bf7b6..7b7d62feb2c 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -35,18 +35,24 @@ describe Ci::API::API do
end
end
- it "starts a build" do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response['sha']).to eq(build.sha)
- expect(runner.reload.platform).to eq("darwin")
- expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
- expect(json_response["variables"]).to include(
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true }
- )
+ context 'when there is a pending build' do
+ it 'starts a build' do
+ register_builds info: { platform: :darwin }
+
+ expect(response).to have_http_status(201)
+ expect(json_response['sha']).to eq(build.sha)
+ expect(runner.reload.platform).to eq("darwin")
+ expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ expect(json_response["variables"]).to include(
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true }
+ )
+ end
+
+ it 'updates runner info' do
+ expect { register_builds }.to change { runner.reload.contacted_at }
+ end
end
context 'when builds are finished' do
@@ -159,13 +165,18 @@ describe Ci::API::API do
end
context 'when runner is paused' do
- let(:inactive_runner) { create(:ci_runner, :inactive, token: "InactiveRunner") }
+ let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') }
- before do
- register_builds inactive_runner.token
+ it 'responds with 404' do
+ register_builds
+
+ expect(response).to have_http_status 404
end
- it { expect(response).to have_http_status 404 }
+ it 'does not update runner info' do
+ expect { register_builds }
+ .not_to change { runner.reload.contacted_at }
+ end
end
def register_builds(token = runner.token, **params)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index c0c1e62e910..27f0fd22ae6 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -440,8 +440,8 @@ describe 'Git HTTP requests', lib: true 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')
+ allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
+ Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt')
end
get "/#{project.path_with_namespace}/blob/master/info/refs"
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 77842057a10..2322430d212 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -337,7 +337,7 @@ describe Projects::CommitsController, 'routing' do
end
it 'to #show' do
- expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'atom')
+ expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 4bc3cddd9c2..c18a2d55e43 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -9,31 +9,33 @@ require 'spec_helper'
# user_calendar_activities GET /u/:username/calendar_activities(.:format)
describe UsersController, "routing" do
it "to #show" do
- expect(get("/u/User")).to route_to('users#show', username: 'User')
+ allow(User).to receive(:find_by).and_return(true)
+
+ expect(get("/User")).to route_to('users#show', username: 'User')
end
it "to #groups" do
- expect(get("/u/User/groups")).to route_to('users#groups', username: 'User')
+ expect(get("/users/User/groups")).to route_to('users#groups', username: 'User')
end
it "to #projects" do
- expect(get("/u/User/projects")).to route_to('users#projects', username: 'User')
+ expect(get("/users/User/projects")).to route_to('users#projects', username: 'User')
end
it "to #contributed" do
- expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User')
+ expect(get("/users/User/contributed")).to route_to('users#contributed', username: 'User')
end
it "to #snippets" do
- expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User')
+ expect(get("/users/User/snippets")).to route_to('users#snippets', username: 'User')
end
it "to #calendar" do
- expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User')
+ expect(get("/users/User/calendar")).to route_to('users#calendar', username: 'User')
end
it "to #calendar_activities" do
- expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
+ expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
end
end
@@ -264,7 +266,15 @@ describe "Groups", "routing" do
end
it "also display group#show on the short path" do
- expect(get('/1')).to route_to('namespaces#show', id: '1')
+ allow(Group).to receive(:find_by_path).and_return(true)
+
+ expect(get('/1')).to route_to('groups#show', id: '1')
+ end
+
+ it "also display group#show with dot in the path" do
+ allow(Group).to receive(:find_by_path).and_return(true)
+
+ expect(get('/group.with.dot')).to route_to('groups#show', id: 'group.with.dot')
end
end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index a1a4dd4c57c..fde807cc410 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -2,33 +2,31 @@ require 'spec_helper'
describe Boards::CreateService, services: true do
describe '#execute' do
+ let(:project) { create(:empty_project) }
+
subject(:service) { described_class.new(project, double) }
context 'when project does not have a board' do
- let(:project) { create(:empty_project, board: nil) }
-
it 'creates a new board' do
expect { service.execute }.to change(Board, :count).by(1)
end
it 'creates default lists' do
- service.execute
+ board = service.execute
- expect(project.board.lists.size).to eq 2
- expect(project.board.lists.first).to be_backlog
- expect(project.board.lists.last).to be_done
+ expect(board.lists.size).to eq 2
+ expect(board.lists.first).to be_backlog
+ expect(board.lists.last).to be_done
end
end
context 'when project has a board' do
- let!(:project) { create(:project_with_board) }
-
- it 'does not create a new board' do
- expect { service.execute }.not_to change(Board, :count)
+ before do
+ create(:board, project: project)
end
- it 'does not create board lists' do
- expect { service.execute }.not_to change(project.board.lists, :count)
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(project.boards, :count)
end
end
end
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
new file mode 100644
index 00000000000..360ee398f77
--- /dev/null
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Boards::Issues::CreateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, project: project, name: 'in-progress') }
+ let!(:list) { create(:list, board: board, label: label, position: 0) }
+
+ subject(:service) { described_class.new(project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'delegates the create proceedings to Issues::CreateService' do
+ expect_any_instance_of(Issues::CreateService).to receive(:execute).once
+
+ service.execute
+ end
+
+ it 'creates a new issue' do
+ expect { service.execute }.to change(project.issues, :count).by(1)
+ end
+
+ it 'adds the label of the list to the issue' do
+ issue = service.execute
+
+ expect(issue.labels).to eq [label]
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index e65da15aca8..7c206cf3ce7 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe Boards::Issues::ListService, services: true do
describe '#execute' do
let(:user) { create(:user) }
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
@@ -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) { project.board.backlog_list }
+ let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { project.board.done_list }
+ let!(:done) { create(:done_list, board: board) }
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
@@ -30,14 +30,14 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
- let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
before do
project.team << [user, :developer]
end
it 'delegates search to IssuesFinder' do
- params = { id: list1.id }
+ params = { board_id: board.id, id: list1.id }
expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
@@ -46,7 +46,7 @@ describe Boards::Issues::ListService, services: true do
context 'sets default order to priority' do
it 'returns opened issues when listing issues from Backlog' do
- params = { id: backlog.id }
+ params = { board_id: board.id, id: backlog.id }
issues = described_class.new(project, user, params).execute
@@ -54,19 +54,36 @@ describe Boards::Issues::ListService, services: true do
end
it 'returns closed issues when listing issues from Done' do
- params = { id: done.id }
+ params = { board_id: board.id, id: done.id }
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1]
+ expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
end
- it 'returns opened/closed issues that have label list applied when listing issues from a label list' do
- params = { id: list1.id }
+ it 'returns opened issues that have label list applied when listing issues from a label list' do
+ params = { board_id: board.id, id: list1.id }
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2]
+ expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+ end
+ end
+
+ context 'with list that does not belong to the board' do
+ it 'raises an error' do
+ list = create(:list)
+ service = described_class.new(project, user, board_id: board.id, id: list.id)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'raises an error' do
+ service = described_class.new(project, user, board_id: board.id, id: nil)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 180f1b08631..c43b2aec490 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe Boards::Issues::MoveService, services: true do
describe '#execute' do
let(:user) { create(:user) }
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board1) { create(:board, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
- 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) { project.board.done_list }
+ let!(:backlog) { create(:backlog_list, board: board1) }
+ let!(:list1) { create(:list, board: board1, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board1) }
before do
project.team << [user, :developer]
@@ -22,7 +22,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from backlog' do
it 'adds the label of the list it goes to' do
issue = create(:labeled_issue, project: project, labels: [bug])
- params = { from_list_id: backlog.id, to_list_id: list1.id }
+ params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id }
described_class.new(project, user, params).execute(issue)
@@ -33,7 +33,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving to backlog' do
it 'removes all list-labels' do
issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
- params = { from_list_id: list1.id, to_list_id: backlog.id }
+ params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id }
described_class.new(project, user, params).execute(issue)
@@ -44,7 +44,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from backlog to done' do
it 'closes the issue' do
issue = create(:labeled_issue, project: project, labels: [bug])
- params = { from_list_id: backlog.id, to_list_id: done.id }
+ params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id }
described_class.new(project, user, params).execute(issue)
issue.reload
@@ -56,7 +56,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
- let(:params) { { from_list_id: list1.id, to_list_id: list2.id } }
+ let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
it 'delegates the label changes to Issues::UpdateService' do
expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
@@ -72,8 +72,12 @@ describe Boards::Issues::MoveService, services: true do
end
context 'when moving to done' do
- let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing]) }
- let(:params) { { from_list_id: list2.id, to_list_id: done.id } }
+ let(:board2) { create(:board, project: project) }
+ let(:regression) { create(:label, project: project, name: 'Regression') }
+ let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
+
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
+ let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: done.id } }
it 'delegates the close proceedings to Issues::CloseService' do
expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
@@ -81,7 +85,7 @@ describe Boards::Issues::MoveService, services: true do
described_class.new(project, user, params).execute(issue)
end
- it 'removes all list-labels and close the issue' do
+ it 'removes all list-labels from project boards and close the issue' do
described_class.new(project, user, params).execute(issue)
issue.reload
@@ -92,7 +96,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from done' do
let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
- let(:params) { { from_list_id: done.id, to_list_id: list2.id } }
+ let(:params) { { board_id: board1.id, from_list_id: done.id, to_list_id: list2.id } }
it 'delegates the re-open proceedings to Issues::ReopenService' do
expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
@@ -112,7 +116,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from done to backlog' do
it 'reopens the issue' do
issue = create(:labeled_issue, :closed, project: project, labels: [bug])
- params = { from_list_id: done.id, to_list_id: backlog.id }
+ params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id }
described_class.new(project, user, params).execute(issue)
issue.reload
@@ -124,7 +128,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
- let(:params) { { from_list_id: list1.id, to_list_id: list1.id } }
+ let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
it 'returns false' do
expect(described_class.new(project, user, params).execute(issue)).to eq false
diff --git a/spec/services/boards/list_service_spec.rb b/spec/services/boards/list_service_spec.rb
new file mode 100644
index 00000000000..dff33e4bcbb
--- /dev/null
+++ b/spec/services/boards/list_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Boards::ListService, services: true do
+ describe '#execute' do
+ let(:project) { create(:empty_project) }
+
+ subject(:service) { described_class.new(project, double) }
+
+ context 'when project does not have a board' do
+ it 'creates a new project board' do
+ expect { service.execute }.to change(project.boards, :count).by(1)
+ end
+
+ it 'delegates the project board creation to Boards::CreateService' do
+ expect_any_instance_of(Boards::CreateService).to receive(:execute).once
+
+ service.execute
+ end
+ end
+
+ context 'when project has a board' do
+ before do
+ create(:board, project: project)
+ end
+
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(project.boards, :count)
+ end
+ end
+
+ it 'returns project boards' do
+ board = create(:board, project: project)
+
+ expect(service.execute).to match_array [board]
+ end
+ end
+end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index bff9c1fd1fe..a7e9efcf93f 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -2,16 +2,20 @@ require 'spec_helper'
describe Boards::Lists::CreateService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project, name: 'in-progress') }
subject(:service) { described_class.new(project, user, label_id: label.id) }
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
- list = service.execute
+ list = service.execute(board)
expect(list.position).to eq 0
end
@@ -19,7 +23,7 @@ describe Boards::Lists::CreateService, services: true do
context 'when board lists has backlog, and done lists' do
it 'creates a new list at beginning of the list' do
- list = service.execute
+ list = service.execute(board)
expect(list.position).to eq 0
end
@@ -30,7 +34,7 @@ describe Boards::Lists::CreateService, services: true do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
- list = service.execute
+ list = service.execute(board)
expect(list.position).to eq 2
end
@@ -40,7 +44,7 @@ describe Boards::Lists::CreateService, services: true do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
- list2 = service.execute
+ list2 = service.execute(board)
expect(list1.reload.position).to eq 0
expect(list2.reload.position).to eq 1
@@ -52,7 +56,7 @@ describe Boards::Lists::CreateService, services: true do
label = create(:label, name: 'in-development')
service = described_class.new(project, user, label_id: label.id)
- expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 474c4512471..628caf03476 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Boards::Lists::DestroyService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
context 'when list type is label' do
@@ -15,11 +15,11 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
- backlog = project.board.backlog_list
+ backlog = 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 = project.board.done_list
+ done = 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 = project.board.backlog_list
+ list = 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 = project.board.done_list
+ list = 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/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
index 9fd39122737..ed0337662af 100644
--- a/spec/services/boards/lists/generate_service_spec.rb
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -2,15 +2,19 @@ require 'spec_helper'
describe Boards::Lists::GenerateService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
subject(:service) { described_class.new(project, user) }
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when board lists is empty' do
it 'creates the default lists' do
- expect { service.execute }.to change(board.lists, :count).by(4)
+ expect { service.execute(board) }.to change(board.lists, :count).by(2)
end
end
@@ -18,22 +22,21 @@ describe Boards::Lists::GenerateService, services: true do
it 'does not creates the default lists' do
create(:list, board: board)
- expect { service.execute }.not_to change(board.lists, :count)
+ expect { service.execute(board) }.not_to change(board.lists, :count)
end
end
context 'when project labels does not contains any list label' do
it 'creates labels' do
- expect { service.execute }.to change(project.labels, :count).by(4)
+ expect { service.execute(board) }.to change(project.labels, :count).by(2)
end
end
context 'when project labels contains some of list label' do
it 'creates the missing labels' do
- create(:label, project: project, name: 'Development')
- create(:label, project: project, name: 'Ready')
+ create(:label, project: project, name: 'Doing')
- expect { service.execute }.to change(project.labels, :count).by(2)
+ expect { service.execute(board) }.to change(project.labels, :count).by(1)
end
end
end
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
new file mode 100644
index 00000000000..334cee3f06d
--- /dev/null
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Boards::Lists::ListService, services: true do
+ describe '#execute' do
+ it "returns board's lists" do
+ project = create(:empty_project)
+ board = create(:board, project: project)
+ label = create(:label, project: project)
+ list = create(:list, board: board, label: label)
+
+ service = described_class.new(project, double)
+
+ expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list]
+ end
+ end
+end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 102ed67449d..63fa0bb8c5f 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -2,16 +2,16 @@ require 'spec_helper'
describe Boards::Lists::MoveService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
- let!(:backlog) { project.board.backlog_list }
+ let!(:backlog) { create(:backlog_list, board: board) }
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) { project.board.done_list }
+ let!(:done) { create(:done_list, board: board) }
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/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 8326e5cd313..ff113efd916 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -18,7 +18,7 @@ describe Ci::ProcessPipelineService, services: true do
all_builds.where.not(status: [:created, :skipped])
end
- def create_builds
+ def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
@@ -36,26 +36,26 @@ describe Ci::ProcessPipelineService, services: true do
end
it 'processes a pipeline' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
succeed_pending
expect(builds.success.count).to eq(2)
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
succeed_pending
expect(builds.success.count).to eq(4)
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
succeed_pending
expect(builds.success.count).to eq(5)
- expect(create_builds).to be_falsey
+ expect(process_pipeline).to be_falsey
end
it 'does not process pipeline if existing stage is running' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pending.count).to eq(2)
- expect(create_builds).to be_falsey
+ expect(process_pipeline).to be_falsey
expect(builds.pending.count).to eq(2)
end
end
@@ -67,7 +67,7 @@ describe Ci::ProcessPipelineService, services: true do
end
it 'automatically triggers a next stage when build finishes' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:drop)
@@ -88,7 +88,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when builds are successful' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -113,7 +113,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when test job fails' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -138,7 +138,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when test and test_failure jobs fail' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -164,7 +164,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when deploy job fails' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -189,7 +189,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when build is canceled in the second stage' do
it 'does not schedule builds after build has been canceled' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -208,7 +208,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when listing manual actions' do
it 'returns only for skipped builds' do
# currently all builds are created
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(manual_actions).to be_empty
# succeed stage build
@@ -230,6 +230,69 @@ describe Ci::ProcessPipelineService, services: true do
end
end
+ context 'when there are manual/on_failure jobs in earlier stages' do
+ before do
+ builds
+ process_pipeline
+ builds.each(&:reload)
+ end
+
+ context 'when first stage has only manual jobs' do
+ let(:builds) do
+ [create_build('build', 0, 'manual'),
+ create_build('check', 1),
+ create_build('test', 2)]
+ end
+
+ it 'starts from the second stage' do
+ expect(builds.map(&:status)).to eq(%w[skipped pending created])
+ end
+ end
+
+ context 'when second stage has only manual jobs' do
+ let(:builds) do
+ [create_build('check', 0),
+ create_build('build', 1, 'manual'),
+ create_build('test', 2)]
+ end
+
+ it 'skips second stage and continues on third stage' do
+ expect(builds.map(&:status)).to eq(%w[pending created created])
+
+ builds.first.success
+ builds.each(&:reload)
+
+ expect(builds.map(&:status)).to eq(%w[success skipped pending])
+ end
+ end
+
+ context 'when second stage has only on_failure jobs' do
+ let(:builds) do
+ [create_build('check', 0),
+ create_build('build', 1, 'on_failure'),
+ create_build('test', 2)]
+ end
+
+ it 'skips second stage and continues on third stage' do
+ expect(builds.map(&:status)).to eq(%w[pending created created])
+
+ builds.first.success
+ builds.each(&:reload)
+
+ expect(builds.map(&:status)).to eq(%w[success skipped pending])
+ end
+ end
+
+ def create_build(name, stage_idx, when_value = nil)
+ create(:ci_build,
+ :created,
+ pipeline: pipeline,
+ name: name,
+ stage_idx: stage_idx,
+ when: when_value)
+ end
+ end
+
context 'when failed build in the middle stage is retried' do
context 'when failed build is the only unsuccessful build in the stage' do
before do
@@ -242,7 +305,7 @@ describe Ci::ProcessPipelineService, services: true do
end
it 'does trigger builds in the next stage' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
pipeline.builds.running_or_pending.each(&:success)
@@ -297,14 +360,14 @@ describe Ci::ProcessPipelineService, services: true do
expect(all_builds.count).to eq(2)
# Create builds will mark the created as pending
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.count).to eq(2)
expect(all_builds.count).to eq(2)
# When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml
# We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy)
succeed_pending
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.success.count).to eq(2)
expect(builds.pending.count).to eq(2)
expect(all_builds.count).to eq(5)
@@ -312,14 +375,14 @@ describe Ci::ProcessPipelineService, services: true do
# When we succeed the 2 pending from stage test,
# We will queue a deploy stage, no new builds will be created
succeed_pending
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pending.count).to eq(1)
expect(builds.success.count).to eq(4)
expect(all_builds.count).to eq(5)
# When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created
succeed_pending
- expect(create_builds).to be_falsey
+ expect(process_pipeline).to be_falsey
expect(builds.success.count).to eq(5)
expect(all_builds.count).to eq(5)
end
diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb
new file mode 100644
index 00000000000..288302cc94f
--- /dev/null
+++ b/spec/services/ci/send_pipeline_notification_service_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Ci::SendPipelineNotificationService, services: true do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('master').sha,
+ user: user,
+ status: status)
+ end
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ subject{ described_class.new(pipeline) }
+
+ describe '#execute' do
+ before do
+ reset_delivered_emails!
+ end
+
+ shared_examples 'sending emails' do
+ it 'sends an email to pipeline user' do
+ perform_enqueued_jobs do
+ subject.execute([user.email])
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email.subject).to include(email_subject)
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context 'with success pipeline' do
+ let(:status) { 'success' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
+
+ it_behaves_like 'sending emails'
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+
+ it_behaves_like 'sending emails'
+ end
+ end
+end
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
new file mode 100644
index 00000000000..3760f19aaa2
--- /dev/null
+++ b/spec/services/compare_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe CompareService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new }
+
+ describe '#execute' do
+ context 'compare with base, like feature...fix' do
+ subject { service.execute(project, 'feature', project, 'fix', straight: false) }
+
+ it { expect(subject.diffs.size).to eq(1) }
+ end
+
+ context 'straight compare, like feature..fix' do
+ subject { service.execute(project, 'feature', project, 'fix', straight: true) }
+
+ it { expect(subject.diffs.size).to eq(3) }
+ end
+ end
+end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 343b4385bf2..cf0a18aacec 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -7,11 +7,13 @@ describe CreateDeploymentService, services: true do
let(:service) { described_class.new(project, user, params) }
describe '#execute' do
+ let(:options) { nil }
let(:params) do
{ environment: 'production',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
+ options: options
}
end
@@ -28,7 +30,7 @@ describe CreateDeploymentService, services: true do
end
context 'when environment exist' do
- before { create(:environment, project: project, name: 'production') }
+ let!(:environment) { create(:environment, project: project, name: 'production') }
it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count }
@@ -37,6 +39,46 @@ describe CreateDeploymentService, services: true do
it 'does create a deployment' do
expect(subject).to be_persisted
end
+
+ context 'and start action is defined' do
+ let(:options) { { action: 'start' } }
+
+ context 'and environment is stopped' do
+ before do
+ environment.stop
+ end
+
+ it 'makes environment available' do
+ subject
+
+ expect(environment.reload).to be_available
+ end
+
+ it 'does create a deployment' do
+ expect(subject).to be_persisted
+ end
+ end
+ end
+
+ context 'and stop action is defined' do
+ let(:options) { { action: 'stop' } }
+
+ context 'and environment is available' do
+ before do
+ environment.start
+ end
+
+ it 'makes environment stopped' do
+ subject
+
+ expect(environment.reload).to be_stopped
+ end
+
+ it 'does not create a deployment' do
+ expect(subject).to be_nil
+ end
+ end
+ end
end
context 'for environment with invalid name' do
@@ -53,7 +95,7 @@ describe CreateDeploymentService, services: true do
end
it 'does not create a deployment' do
- expect(subject).not_to be_persisted
+ expect(subject).to be_nil
end
end
@@ -83,12 +125,42 @@ describe CreateDeploymentService, services: true do
it 'does create a new deployment' do
expect(subject).to be_persisted
end
+
+ context 'and environment exist' do
+ let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') }
+
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'updates external url' do
+ subject
+
+ 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
+
+ context 'when project was removed' do
+ let(:project) { nil }
+
+ it 'does not create deployment or environment' do
+ expect { subject }.not_to raise_error
+
+ expect(Environment.count).to be_zero
+ expect(Deployment.count).to be_zero
+ end
end
end
describe 'processing of builds' do
let(:environment) { nil }
-
+
shared_examples 'does not create environment and deployment' do
it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count }
@@ -133,12 +205,12 @@ describe CreateDeploymentService, services: true do
context 'without environment specified' do
let(:build) { create(:ci_build, project: project) }
-
+
it_behaves_like 'does not create environment and deployment' do
subject { build.success }
end
end
-
+
context 'when environment is specified' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) }
@@ -190,7 +262,7 @@ describe CreateDeploymentService, services: true 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)
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
end
it "doesn't set the time if the deploy's environment is not 'production'" do
@@ -216,13 +288,13 @@ describe CreateDeploymentService, services: true 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)
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(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)
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 16a9956fe7f..b7dc99ed887 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -110,4 +110,23 @@ describe EventCreateService, services: true do
end
end
end
+
+ describe 'Project' do
+ let(:user) { create :user }
+ let(:project) { create(:empty_project) }
+
+ describe '#join_project' do
+ subject { service.join_project(project, user) }
+
+ it { is_expected.to be_truthy }
+ it { expect { subject }.to change { Event.count }.from(0).to(1) }
+ end
+
+ describe '#expired_leave_project' do
+ subject { service.expired_leave_project(project, user) }
+
+ it { is_expected.to be_truthy }
+ it { expect { subject }.to change { Event.count }.from(0).to(1) }
+ end
+ end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index d019e50649f..d3c37c7820f 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -41,7 +41,7 @@ describe Files::UpdateService do
it "returns a hash with the :success status " do
results = subject.execute
- expect(results).to match({ status: :success })
+ expect(results[:status]).to match(:success)
end
it "updates the file with the new contents" do
@@ -69,7 +69,7 @@ describe Files::UpdateService do
it "returns a hash with the :success status " do
results = subject.execute
- expect(results).to match({ status: :success })
+ expect(results[:status]).to match(:success)
end
it "updates the file with the new contents" do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 22991c5bc86..ad5170afc21 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -184,8 +184,8 @@ describe GitPushService, services: true do
context "Updates merge requests" do
it "when pushing a new branch for the first time" do
- expect(project).to receive(:update_merge_requests).
- with(@blankrev, 'newrev', 'refs/heads/master', user)
+ expect(UpdateMergeRequestsWorker).to receive(:perform_async).
+ with(project.id, user.id, @blankrev, 'newrev', 'refs/heads/master')
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
end
end
@@ -364,7 +364,7 @@ describe GitPushService, services: true 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)
+ expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_like_time(commit_time)
end
it 'does not set the metric for non-referenced issues' do
@@ -415,7 +415,7 @@ describe GitPushService, services: true do
it "doesn't close issues when external issue tracker is in use" do
allow_any_instance_of(Project).to receive(:default_issues_tracker?).
and_return(false)
- external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid)
+ external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern)
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
# The push still shouldn't create cross-reference notes.
@@ -448,6 +448,8 @@ describe GitPushService, services: true do
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
before do
+ # project.create_jira_service doesn't seem to invalidate the cache here
+ project.has_external_issue_tracker = true
jira_service_settings
WebMock.stub_request(:post, jira_api_transition_url)
@@ -482,30 +484,46 @@ describe GitPushService, services: true do
end
context "closing an issue" do
- let(:message) { "this is some work.\n\ncloses JIRA-1" }
-
- it "initiates one api call to jira server to close the issue" do
- transition_body = {
- transition: {
- id: '2'
- }
- }.to_json
-
- execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
- body: transition_body
- ).once
+ let(:message) { "this is some work.\n\ncloses JIRA-1" }
+ let(:transition_body) { { transition: { id: '2' } }.to_json }
+ let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+
+ context "using right markdown" do
+ it "initiates one api call to jira server to close the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
+ body: transition_body
+ ).once
+ end
+
+ it "initiates one api call to jira server to comment on the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
+ body: comment_body
+ ).once
+ end
end
- it "initiates one api call to jira server to comment on the issue" do
- comment_body = {
- body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
- }.to_json
+ context "using wrong markdown" do
+ let(:message) { "this is some work.\n\ncloses #1" }
- execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
- body: comment_body
- ).once
+ it "does not initiates one api call to jira server to close the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).not_to have_requested(:post, jira_api_transition_url).with(
+ body: transition_body
+ )
+ end
+
+ it "does not initiates one api call to jira server to comment on the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).not_to have_requested(:post, jira_api_comment_url).with(
+ body: comment_body
+ ).once
+ end
end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 1050502fa19..5c0331ebe66 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -67,6 +67,27 @@ describe Issues::CreateService, services: true do
expect(Todo.where(attributes).count).to eq 1
end
+ context 'when label belongs to project group' do
+ let(:group) { create(:group) }
+ let(:group_labels) { create_pair(:group_label, group: group) }
+
+ let(:opts) do
+ {
+ title: 'Title',
+ description: 'Description',
+ label_ids: group_labels.map(&:id)
+ }
+ end
+
+ before do
+ project.update(group: group)
+ end
+
+ it 'assigns group labels' do
+ expect(issue.labels).to match_array group_labels
+ end
+ end
+
context 'when label belongs to different project' do
let(:label) { create(:label) }
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
new file mode 100644
index 00000000000..cbfc63de811
--- /dev/null
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Labels::FindOrCreateService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ let(:params) do
+ {
+ title: 'Security',
+ description: 'Security related stuff.',
+ color: '#FF0000'
+ }
+ end
+
+ subject(:service) { described_class.new(user, project, params) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when label does not exist at group level' do
+ it 'creates a new label at project level' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
+ end
+
+ context 'when label exists at group level' do
+ it 'returns the group label' do
+ group_label = create(:group_label, group: group, title: 'Security')
+
+ expect(service.execute).to eq group_label
+ end
+ end
+
+ context 'when label does not exist at group level' do
+ it 'creates a new label at project leve' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
+ end
+
+ context 'when label exists at project level' do
+ it 'returns the project label' do
+ project_label = create(:label, project: project, title: 'Security')
+
+ expect(service.execute).to eq project_label
+ end
+ end
+ end
+end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
new file mode 100644
index 00000000000..ddf3527dc0f
--- /dev/null
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe Labels::TransferService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:group_1) { create(:group) }
+ let(:group_2) { create(:group) }
+ let(:group_3) { create(:group) }
+ let(:project_1) { create(:project, namespace: group_2) }
+ let(:project_2) { create(:project, namespace: group_3) }
+
+ let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
+ let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
+ let(:group_label_3) { create(:group_label, group: group_1, name: 'Group Label 3') }
+ let(:group_label_4) { create(:group_label, group: group_2, name: 'Group Label 4') }
+ let(:group_label_5) { create(:group_label, group: group_3, name: 'Group Label 5') }
+ let(:project_label_1) { create(:label, project: project_1, name: 'Project Label 1') }
+
+ subject(:service) { described_class.new(user, group_1, project_1) }
+
+ before do
+ create(:labeled_issue, project: project_1, labels: [group_label_1])
+ create(:labeled_issue, project: project_1, labels: [group_label_4])
+ create(:labeled_issue, project: project_1, labels: [project_label_1])
+ create(:labeled_issue, project: project_2, labels: [group_label_5])
+ create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2])
+ create(:labeled_merge_request, source_project: project_2, labels: [group_label_5])
+ end
+
+ it 'recreates the missing group labels at project level' do
+ expect { service.execute }.to change(project_1.labels, :count).by(2)
+ end
+
+ it 'recreates label priorities related to the missing group labels' do
+ create(:label_priority, project: project_1, label: group_label_1, priority: 1)
+
+ service.execute
+
+ new_project_label = project_1.labels.find_by(title: group_label_1.title)
+ expect(new_project_label.id).not_to eq group_label_1.id
+ expect(new_project_label.priorities).not_to be_empty
+ end
+
+ it 'does not recreate missing group labels that are not applied to issues or merge requests' do
+ service.execute
+
+ expect(project_1.labels.where(title: group_label_3.title)).to be_empty
+ end
+
+ it 'does not recreate missing group labels that already exist in the project group' do
+ service.execute
+
+ expect(project_1.labels.where(title: group_label_4.title)).to be_empty
+ end
+ end
+end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 6fca80b5613..03e296259f9 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -26,7 +26,7 @@ describe Members::ApproveAccessRequestService, services: true do
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).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_nil
end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 2395445e7fd..9995f3488af 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -2,70 +2,111 @@ require 'spec_helper'
describe Members::DestroyService, services: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let!(:member) { create(:project_member, source: project) }
+ let(:member_user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :public) }
- context 'when member is nil' do
- before do
- project.team << [user, :developer]
+ 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
- it 'does not destroy the member' do
- expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ 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
- context 'when current user cannot destroy the given member' do
- before do
- project.team << [user, :developer]
+ shared_examples 'a service destroying a member' do
+ it 'destroys the member' do
+ expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1)
+ end
+
+ context 'when the given member is an access requester' do
+ before do
+ source.members.find_by(user_id: member_user).destroy
+ source.request_access(member_user)
+ end
+ let(:access_requester) { source.requesters.find_by(user_id: member_user) }
+
+ it_behaves_like 'a service raising ActiveRecord::RecordNotFound'
+
+ %i[requesters all].each do |scope|
+ context "and #{scope} scope is passed" do
+ it 'destroys the access requester' do
+ expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1)
+ end
+
+ it 'calls Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester)
+
+ described_class.new(source, user, params).execute(scope)
+ end
+
+ context 'when current user is the member' do
+ it 'does not call Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester)
+
+ described_class.new(source, member_user, params).execute(scope)
+ end
+ end
+ end
+ end
end
+ end
+
+ context 'when no member are found' do
+ let(:params) { { user_id: 42 } }
- it 'does not destroy the member' do
- expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ 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 current user can destroy the given member' do
+ context 'when a member is found' do
before do
- project.team << [user, :master]
+ project.team << [member_user, :developer]
+ group.add_developer(member_user)
end
+ let(:params) { { user_id: member_user.id } }
- it 'destroys the member' do
- destroy_member(member, user)
+ context 'when current user cannot destroy the given member' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
- expect(member).to be_destroyed
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
end
- context 'when the given member is a requester' do
+ context 'when current user can destroy the given member' do
before do
- member.update_column(:requested_at, Time.now)
+ project.team << [user, :master]
+ group.add_owner(user)
end
- it 'calls Member#after_decline_request' do
- expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
-
- destroy_member(member, user)
+ it_behaves_like 'a service destroying a member' do
+ let(:source) { project }
end
- context 'when current user is the member' do
- it 'does not call Member#after_decline_request' do
- expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
-
- destroy_member(member, member.user)
- end
+ it_behaves_like 'a service destroying a member' do
+ let(:source) { group }
end
- context 'when current user is the member and ' do
- it 'does not call Member#after_decline_request' do
- expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
+ context 'when given a :id' do
+ let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
- destroy_member(member, member.user)
+ it 'destroys the member' do
+ expect { described_class.new(project, user, params).execute }.
+ to change { project.members.count }.by(-1)
end
end
end
end
-
- def destroy_member(member, user)
- Members::DestroyService.new(member, user).execute
- 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..0d2d5f03199
--- /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}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/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
new file mode 100644
index 00000000000..5034b6ef33f
--- /dev/null
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe MergeRequests::AssignIssuesService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue.to_reference}") }
+ let(:service) { described_class.new(project, user, merge_request: merge_request) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'finds unassigned issues fixed in merge request' do
+ expect(service.assignable_issues.map(&:id)).to include(issue.id)
+ end
+
+ it 'ignores issues already assigned to any user' do
+ issue.update!(assignee: create(:user))
+
+ expect(service.assignable_issues).to be_empty
+ end
+
+ it 'ignores issues the user cannot update assignee on' do
+ project.team.truncate
+
+ expect(service.assignable_issues).to be_empty
+ end
+
+ it 'ignores all issues unless current_user is merge_request.author' do
+ merge_request.update!(author: create(:user))
+
+ expect(service.assignable_issues).to be_empty
+ end
+
+ it 'accepts precomputed data for closes_issues' do
+ issue2 = create(:issue, project: project)
+ service2 = described_class.new(project,
+ user,
+ merge_request: merge_request,
+ closes_issues: [issue, issue2])
+
+ expect(service2.assignable_issues.count).to eq 2
+ end
+
+ it 'assigns these to the merge request owner' do
+ expect { service.execute }.to change { issue.reload.assignee }.to(user)
+ end
+
+ it 'ignores external issues' do
+ external_issue = ExternalIssue.new('JIRA-123', project)
+ service = described_class.new(
+ project,
+ user,
+ merge_request: merge_request,
+ closes_issues: [external_issue]
+ )
+
+ expect(service.assignable_issues.count).to eq 0
+ end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 0d586e2216b..3a3f07ddcb9 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -52,12 +52,28 @@ describe MergeRequests::BuildService, services: true do
end
end
- context 'no commits in the diff' do
- let(:commits) { [] }
+ context 'same source and target branch' do
+ let(:source_branch) { 'master' }
it 'forbids the merge request from being created' do
expect(merge_request.can_be_created).to eq(false)
end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly('You must select different branches')
+ end
+ end
+
+ context 'no commits in the diff' do
+ let(:commits) { [] }
+
+ it 'allows the merge request to be created' do
+ expect(merge_request.can_be_created).to eq(true)
+ end
+
+ it 'adds a WIP prefix to the merge request title' do
+ expect(merge_request.title).to eq('WIP: Feature branch')
+ end
end
context 'one commit in the diff' do
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 159f6817e8d..f93d7732a9a 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -38,6 +38,81 @@ describe MergeRequests::MergeService, services: true do
end
end
+ context 'closes related issues' do
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
+ before do
+ allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
+ end
+
+ it 'closes GitLab issue tracker issues' do
+ issue = create :issue, project: project
+ commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
+ allow(merge_request).to receive(:commits).and_return([commit])
+
+ service.execute(merge_request)
+
+ expect(issue.reload.closed?).to be_truthy
+ end
+
+ context 'with JIRA integration' do
+ include JiraServiceHelper
+
+ let(:jira_tracker) { project.create_jira_service }
+
+ before do
+ project.update_attributes!(has_external_issue_tracker: true)
+ jira_service_settings
+ end
+
+ it 'closes issues on JIRA issue tracker' do
+ jira_issue = ExternalIssue.new('JIRA-123', project)
+ commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
+ allow(merge_request).to receive(:commits).and_return([commit])
+
+ expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
+
+ service.execute(merge_request)
+ end
+
+ context "wrong issue markdown" do
+ it 'does not close issues on JIRA issue tracker' do
+ jira_issue = ExternalIssue.new('#123', project)
+ commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
+ allow(merge_request).to receive(:commits).and_return([commit])
+
+ expect_any_instance_of(JiraService).not_to receive(:close_issue)
+
+ service.execute(merge_request)
+ end
+ end
+ 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'
@@ -57,13 +132,13 @@ describe MergeRequests::MergeService, services: true do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
it 'saves error if there is an exception' do
- allow(service).to receive(:repository).and_raise("error")
+ allow(service).to receive(:repository).and_raise("error message")
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.merge_error).to eq("Something went wrong during merge")
+ expect(merge_request.merge_error).to eq("Something went wrong during merge: error message")
end
it 'saves error if there is an PreReceiveError exception' do
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index 520e906b21f..b80cfd8f450 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -58,61 +58,83 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end
describe "#trigger" do
- context 'build with ref' do
- let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+ let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
+ let(:merge_request_head) do
+ project.commit(mr_merge_if_green_enabled.source_branch).id
+ end
- it "merges all merge requests with merge when build succeeds enabled" do
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
- allow(pipeline).to receive(:success?).and_return(true)
+ context 'when triggered by pipeline with valid ref and sha' do
+ let(:triggering_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: merge_request_head, status: 'success')
+ end
+ it "merges all merge requests with merge when build succeeds enabled" do
expect(MergeWorker).to receive(:perform_async)
- service.trigger(build)
+ service.trigger(triggering_pipeline)
end
end
- context 'triggered by an old build' do
- let(:old_build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
- let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
-
- it "merges all merge requests with merge when build succeeds enabled" do
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
- allow(pipeline).to receive(:success?).and_return(true)
- allow(old_build).to receive(:sha).and_return('1234abcdef')
+ context 'when triggered by an old pipeline' do
+ let(:old_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: '1234abcdef', status: 'success')
+ end
+ it 'it does not merge merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(old_build)
+ service.trigger(old_pipeline)
end
end
- context 'commit status without ref' do
- let(:commit_status) { create(:generic_commit_status, status: 'success') }
-
- before { mr_merge_if_green_enabled }
-
- it "doesn't merge a requests for status on other branch" do
- allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([])
+ context 'when triggered by pipeline from a different branch' do
+ let(:unrelated_pipeline) do
+ create(:ci_pipeline, project: project, ref: 'feature',
+ sha: merge_request_head, status: 'success')
+ end
+ it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(commit_status)
+ service.trigger(unrelated_pipeline)
end
+ end
+ end
- it 'discovers branches and merges all merge requests when status is success' do
- allow(project.repository).to receive(:branch_names_contains).
- with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch])
- allow(pipeline).to receive(:success?).and_return(true)
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
- allow(pipeline).to receive(:success?).and_return(true)
+ describe "#cancel" do
+ before do
+ service.cancel(mr_merge_if_green_enabled)
+ end
- expect(MergeWorker).to receive(:perform_async)
- service.trigger(commit_status)
- end
+ it "resets all the merge_when_build_succeeds params" do
+ expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
+ expect(mr_merge_if_green_enabled.merge_params).to eq({})
+ expect(mr_merge_if_green_enabled.merge_user).to be nil
end
- context 'properly handles multiple stages' do
+ it 'Posts a system note' do
+ note = mr_merge_if_green_enabled.notes.last
+ expect(note.note).to include 'Canceled the automatic merge'
+ end
+ end
+
+ describe 'pipeline integration' do
+ context 'when there are multiple stages in the pipeline' do
let(:ref) { mr_merge_if_green_enabled.source_branch }
- let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
- let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
- let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) }
+ let(:sha) { project.commit(ref).id }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, ref: ref, sha: sha, project: project)
+ end
+
+ let!(:build) do
+ create(:ci_build, :created, pipeline: pipeline, ref: ref,
+ name: 'build', stage: 'build')
+ end
+
+ let!(:test) do
+ create(:ci_build, :created, pipeline: pipeline, ref: ref,
+ name: 'test', stage: 'test')
+ end
before do
# This behavior of MergeRequest: we instantiate a new object
@@ -121,34 +143,19 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end
end
- it "doesn't merge if some stages failed" do
+ it "doesn't merge if any of stages failed" do
expect(MergeWorker).not_to receive(:perform_async)
+
build.success
test.drop
end
- it 'merge when all stages succeeded' do
+ it 'merges when all stages succeeded' do
expect(MergeWorker).to receive(:perform_async)
+
build.success
test.success
end
end
end
-
- describe "#cancel" do
- before do
- service.cancel(mr_merge_if_green_enabled)
- end
-
- it "resets all the merge_when_build_succeeds params" do
- expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
- expect(mr_merge_if_green_enabled.merge_params).to eq({})
- expect(mr_merge_if_green_enabled.merge_user).to be nil
- end
-
- it 'Posts a system note' do
- note = mr_merge_if_green_enabled.notes.last
- expect(note.note).to include 'Canceled the automatic merge'
- end
- end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index a162df5fc34..e515bc9f89c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -62,7 +62,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes).not_to be_empty }
it { expect(@merge_request).to be_open }
- it { expect(@merge_request.merge_when_build_succeeds).to be_falsey}
+ it { expect(@merge_request.merge_when_build_succeeds).to be_falsey }
+ it { expect(@merge_request.diff_head_sha).to eq(@newrev) }
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@build_failed_todo).to be_done }
@@ -79,8 +80,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 +100,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
@@ -118,7 +119,7 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes).to be_empty }
it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') }
+ it { expect(@fork_merge_request.notes.last.note).to include('Added 28 commits') }
it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
@@ -149,8 +150,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
@@ -169,7 +170,7 @@ describe MergeRequests::RefreshService, services: true do
notes = @fork_merge_request.notes.reorder(:created_at).map(&:note)
expect(notes[0]).to include('Restored source branch `master`')
- expect(notes[1]).to include('Added 4 commits')
+ expect(notes[1]).to include('Added 28 commits')
expect(@fork_merge_request).to be_open
end
end
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index d71932458fa..388abb6a0df 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do
end
describe '#execute' do
- context 'with valid params' do
+ context 'with section params' do
let(:params) do
{
- sections: {
- '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
- },
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ sections: {
+ '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
+ }
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ sections: {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ }
+ }
+ ],
commit_message: 'This is a commit message!'
}
end
@@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
- '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b'])
+ '824be604a34828eb682305f0d963056cfac87b2d'])
end
end
@@ -74,8 +85,96 @@ describe MergeRequests::ResolveService do
end
end
- context 'when a resolution is missing' do
- let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } }
+ context 'with content and sections params' do
+ let(:popen_content) { "class Popen\nend" }
+
+ let(:params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: popen_content
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ sections: {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ }
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ before do
+ MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+ end
+
+ it 'creates a commit with the message' do
+ expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
+ end
+
+ it 'creates a commit with the correct parents' do
+ expect(merge_request.source_branch_head.parents.map(&:id)).
+ to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
+ '824be604a34828eb682305f0d963056cfac87b2d'])
+ end
+
+ it 'sets the content to the content given' do
+ blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha,
+ 'files/ruby/popen.rb')
+
+ expect(blob.data).to eq(popen_content)
+ end
+ end
+
+ context 'when a resolution section is missing' do
+ let(:invalid_params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: ''
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ sections: { '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' }
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+ it 'raises a MissingResolution error' do
+ expect { service.execute(merge_request) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+ end
+ end
+
+ context 'when the content of a file is unchanged' do
+ let(:invalid_params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: ''
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
it 'raises a MissingResolution error' do
@@ -83,5 +182,27 @@ describe MergeRequests::ResolveService do
to raise_error(Gitlab::Conflict::File::MissingResolution)
end
end
+
+ context 'when a file is missing' do
+ let(:invalid_params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: ''
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+ it 'raises a MissingFiles error' do
+ expect { service.execute(merge_request) }.
+ to raise_error(MergeRequests::ResolveService::MissingFiles)
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 33db34c0f62..2433a7dad06 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -17,6 +17,7 @@ describe MergeRequests::UpdateService, services: true do
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [user3, :developer]
end
describe 'execute' do
@@ -104,6 +105,18 @@ describe MergeRequests::UpdateService, services: true do
expect(note).not_to be_nil
expect(note.note).to eq 'Target branch changed from `master` to `target`'
end
+
+ context 'when not including source branch removal options' do
+ before do
+ opts.delete(:force_remove_source_branch)
+ end
+
+ it 'maintains the original options' do
+ update_merge_request(opts)
+
+ expect(@merge_request.merge_params["force_remove_source_branch"]).to eq("1")
+ end
+ end
end
context 'todos' do
@@ -188,6 +201,11 @@ describe MergeRequests::UpdateService, services: true do
let!(:non_subscriber) { create(:user) }
let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ before do
+ project.team << [non_subscriber, :developer]
+ project.team << [subscriber, :developer]
+ end
+
it 'sends notifications for subscribers of newly added labels' do
opts = { label_ids: [label.id] }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 0d152534c38..699b9925b4e 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -331,7 +331,7 @@ describe NotificationService, services: true do
describe '#new_note' do
it "records sent notifications" do
# Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note
- expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original
+ expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original
notification.new_note(note)
@@ -962,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
@@ -1155,6 +1169,61 @@ describe NotificationService, services: true do
end
end
+ context 'guest user in private project' do
+ let(:private_project) { create(:empty_project, :private) }
+ let(:guest) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) }
+ let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") }
+ let(:note) { create(:note, noteable: merge_request, project: private_project) }
+
+ before do
+ private_project.team << [assignee, :developer]
+ private_project.team << [developer, :developer]
+ private_project.team << [guest, :guest]
+
+ ActionMailer::Base.deliveries.clear
+ end
+
+ it 'filters out guests when new note is created' do
+ expect(SentNotification).to receive(:record).with(merge_request, any_args).exactly(1).times
+
+ notification.new_note(note)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when new merge request is created' do
+ notification.new_merge_request(merge_request1, @u_disabled)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when merge request is closed' do
+ notification.close_mr(merge_request, developer)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when merge request is reopened' do
+ notification.reopen_mr(merge_request, developer)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when merge request is merged' do
+ notification.merge_mr(merge_request, developer)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 29341c5e57e..7dcd03496bb 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -5,6 +5,7 @@ describe Projects::DestroyService, services: true do
let!(:project) { create(:project, namespace: user.namespace) }
let!(:path) { project.repository.path_to_repo }
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
+ let!(:async) { false } # execute or async_execute
context 'Sidekiq inline' do
before do
@@ -28,6 +29,22 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
+ context 'async delete of project with private issue visibility' do
+ let!(:async) { true }
+
+ before do
+ project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
+ # Run sidekiq immediately to check that renamed repository will be removed
+ Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+ end
+
+ it 'deletes the project' do
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_falsey
+ expect(Dir.exist?(remove_path)).to be_falsey
+ end
+ end
+
context 'container registry' do
before do
stub_container_registry_config(enabled: true)
@@ -52,6 +69,10 @@ describe Projects::DestroyService, services: true do
end
def destroy_project(project, user, params)
- Projects::DestroyService.new(project, user, params).execute
+ if async
+ Projects::DestroyService.new(project, user, params).async_execute
+ else
+ Projects::DestroyService.new(project, user, params).execute
+ end
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index ef2036c78b1..64d15c0523c 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do
description: 'wow such project')
@to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace)
+ @from_project.add_user(@to_user, :developer)
end
context 'fork project' do
+ context 'when forker is a guest' do
+ before do
+ @guest = create(:user)
+ @from_project.add_user(@guest, :guest)
+ end
+ subject { fork_project(@from_project, @guest) }
+
+ it { is_expected.not_to be_persisted }
+ it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
+ end
+
describe "successfully creates project in the user namespace" do
let(:to_project) { fork_project(@from_project, @to_user) }
+ it { expect(to_project).to be_persisted }
+ it { expect(to_project.errors).to be_empty }
it { expect(to_project.owner).to eq(@to_user) }
it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero }
@@ -29,7 +43,9 @@ describe Projects::ForkService, services: true do
it "fails due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
- expect(@existing_project.persisted?).to be_truthy
+ expect(@existing_project).to be_persisted
+
+ expect(@to_project).not_to be_persisted
expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:path]).to eq(['has already been taken'])
end
@@ -81,18 +97,23 @@ describe Projects::ForkService, services: true do
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
+ @project.add_user(@developer, :developer)
+ @project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts)
+
+ expect(to_project).to be_persisted
+ expect(to_project.errors).to be_empty
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
- expect(to_project.star_count).to be_zero
+ expect(to_project.star_count).to be_zero
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/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 57c71544dff..1540b90163a 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -71,4 +71,14 @@ describe Projects::TransferService, services: true do
it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
end
end
+
+ context 'missing group labels applied to issues or merge requests' do
+ it 'delegates tranfer to Labels::TransferService' do
+ group.add_owner(user)
+
+ expect_any_instance_of(Labels::TransferService).to receive(:execute).once.and_call_original
+
+ transfer_project(project, user, group)
+ end
+ end
end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index 5b1edba87a1..b57e338b782 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -86,6 +86,25 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'multiple label command' do
+ it 'fetches label ids and populates add_label_ids if content contains multiple /label' do
+ bug # populate the label
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [inprogress.id, bug.id])
+ end
+ end
+
+ shared_examples 'multiple label with same argument' do
+ it 'prevents duplicate label ids and populates add_label_ids if content contains multiple /label' do
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [inprogress.id])
+ end
+ end
+
shared_examples 'unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
issuable.update(label_ids: [inprogress.id]) # populate the label
@@ -95,6 +114,15 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'multiple unlabel command' do
+ it 'fetches label ids and populates remove_label_ids if content contains mutiple /unlabel' do
+ issuable.update(label_ids: [inprogress.id, bug.id]) # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
+ end
+ end
+
shared_examples 'unlabel command with no argument' do
it 'populates label_ids: [] if content contains /unlabel with no arguments' do
issuable.update(label_ids: [inprogress.id]) # populate the label
@@ -165,6 +193,23 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'wip command' do
+ it 'returns wip_event: "wip" if content contains /wip' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(wip_event: 'wip')
+ end
+ end
+
+ shared_examples 'unwip command' do
+ it 'returns wip_event: "unwip" if content contains /wip' do
+ issuable.update(title: issuable.wip_title)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(wip_event: 'unwip')
+ end
+ end
+
shared_examples 'empty command' do
it 'populates {} if content contains an unsupported command' do
_, updates = service.execute(content, issuable)
@@ -268,6 +313,16 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ it_behaves_like 'multiple label command' do
+ let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{bug.title}) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'multiple label with same argument' do
+ let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{inprogress.title}) }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'unlabel command' do
let(:content) { %(/unlabel ~"#{inprogress.title}") }
let(:issuable) { issue }
@@ -278,6 +333,11 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ it_behaves_like 'multiple unlabel command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}" \n/unlabel ~#{bug.title}) }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'unlabel command with no argument' do
let(:content) { %(/unlabel) }
let(:issuable) { issue }
@@ -376,6 +436,16 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
end
+ it_behaves_like 'wip command' do
+ let(:content) { '/wip' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unwip command' do
+ let(:content) { '/wip' }
+ let(:issuable) { merge_request }
+ end
+
it_behaves_like 'empty command' do
let(:content) { '/remove_due_date' }
let(:issuable) { merge_request }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 3d854a959f3..b4ba28dfe8e 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:"
@@ -48,7 +54,7 @@ describe SystemNoteService, services: true do
it 'adds a message line for each commit' do
new_commits.each_with_index do |commit, i|
# Skip the header
- expect(note_lines[i + 1]).to eq "* #{commit.short_id} - #{commit.title}"
+ expect(HTMLEntities.new.decode(note_lines[i + 1])).to eq "* #{commit.short_id} - #{commit.title}"
end
end
end
@@ -75,7 +81,7 @@ describe SystemNoteService, services: true do
end
it 'includes a commit count' do
- expect(summary_line).to end_with " - 2 commits from branch `feature`"
+ expect(summary_line).to end_with " - 26 commits from branch `feature`"
end
end
@@ -85,7 +91,7 @@ describe SystemNoteService, services: true do
end
it 'includes a commit count' do
- expect(summary_line).to end_with " - 2 commits from branch `feature`"
+ expect(summary_line).to end_with " - 26 commits from branch `feature`"
end
end
@@ -445,7 +451,7 @@ describe SystemNoteService, services: true do
end
context 'commit with cross-reference from fork' do
- let(:author2) { create(:user) }
+ let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:forked_project) { Projects::ForkService.new(project, author2).execute }
let(:commit2) { forked_project.commit }
@@ -525,13 +531,13 @@ describe SystemNoteService, services: true do
include JiraServiceHelper
describe 'JIRA integration' do
- let(:project) { create(:project) }
+ let(:project) { create(:jira_project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
- let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
- let(:commit) { project.commit }
+ let(:jira_tracker) { project.jira_service }
+ let(:commit) { project.repository.commits('master').find { |commit| commit.id == '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } }
context 'in JIRA issue tracker' do
before do
@@ -539,10 +545,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:post, jira_api_comment_url)
end
- after do
- jira_tracker.destroy!
- end
-
describe "new reference" do
before do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
@@ -555,7 +557,7 @@ describe SystemNoteService, services: true do
describe "existing reference" do
before do
- message = %Q{[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
+ message = %Q{[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]}))
end
@@ -572,10 +574,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
end
- after do
- jira_tracker.destroy!
- end
-
subject { described_class.cross_reference(jira_issue, issue, author) }
it { is_expected.to eq(jira_status_message) }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index b41f6f14fbd..ed55791d24e 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -345,7 +345,7 @@ describe TodoService, services: true do
service.new_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
- should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -357,7 +357,7 @@ describe TodoService, services: true do
service.update_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
- should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -381,6 +381,7 @@ describe TodoService, services: true do
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
it 'does not raise an error when description not change' do
@@ -430,6 +431,11 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
end
+
+ it 'does not create a todo for guests' do
+ service.reassigned_merge_request(mr_assigned, author)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ end
end
describe '#merge_merge_request' do
@@ -441,6 +447,11 @@ describe TodoService, services: true do
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
end
+
+ it 'does not create todo for guests' do
+ service.merge_merge_request(mr_assigned, john_doe)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ end
end
describe '#new_award_emoji' do
@@ -495,6 +506,13 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: legacy_diff_note_on_merge_request)
end
+
+ it 'does not create todo for guests' do
+ note_on_merge_request = create :note_on_merge_request, project: project, noteable: mr_assigned, note: mentions
+ service.new_note(note_on_merge_request, author)
+
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ end
end
end
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/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index be0772d6a4a..1b0a4583f5c 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -130,4 +130,8 @@ module ExportFileHelper
(parsed_model_attributes - parent.keys - excluded_attributes).empty?
end
+
+ def file_permissions(file)
+ File.stat(file).mode & 0777
+ end
end
diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb
index b6d7436c360..e70b3963d9d 100644
--- a/spec/support/issue_tracker_service_shared_example.rb
+++ b/spec/support/issue_tracker_service_shared_example.rb
@@ -5,3 +5,18 @@ RSpec.shared_examples 'issue tracker service URL attribute' do |url_attr|
it { is_expected.not_to allow_value('ftp://example.com').for(url_attr) }
it { is_expected.not_to allow_value('herp-and-derp').for(url_attr) }
end
+
+RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
+ it 'allows underscores in the project name' do
+ expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ end
+
+ it 'allows numbers in the project name' do
+ expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
+ end
+
+ it 'requires the project name to begin with A-Z' do
+ expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil
+ expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ end
+end
diff --git a/spec/support/matchers/be_like_time.rb b/spec/support/matchers/be_like_time.rb
new file mode 100644
index 00000000000..1f27390eab7
--- /dev/null
+++ b/spec/support/matchers/be_like_time.rb
@@ -0,0 +1,13 @@
+RSpec::Matchers.define :be_like_time do |expected|
+ match do |actual|
+ expect(actual).to be_within(1.second).of(expected)
+ end
+
+ description do
+ "within one second of #{expected}"
+ end
+
+ failure_message do |actual|
+ "expected #{actual} to be within one second of #{expected}"
+ end
+end
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/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index e876d44c166..f57c82809a6 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -9,7 +9,7 @@ shared_context 'mentionable context' do
let(:author) { subject.author }
let(:mentioned_issue) { create(:issue, project: project) }
- let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) }
+ let!(:mentioned_mr) { create(:merge_request, source_project: project) }
let(:mentioned_commit) { project.commit("HEAD~1") }
let(:ext_proj) { create(:project, :public) }
@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do
it 'creates new cross-reference notes when the mentionable text is edited' do
subject.save
+ subject.create_cross_references!
new_text = <<-MSG.strip_heredoc
These references already existed:
@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do
end
# These two issues are new and should receive reference notes
+ # In the case of MergeRequests remember that cannot mention commits included in the MergeRequest
new_issues.each do |newref|
expect(SystemNoteService).to receive(:cross_reference).
with(newref, subject.local_reference, author)
diff --git a/spec/mailers/shared/notify.rb b/spec/support/notify_shared_examples.rb
index 5c9851f14c7..3956d05060b 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -37,6 +37,16 @@ shared_examples 'an email sent from GitLab' do
reply_to = subject.header[:reply_to].addresses
expect(reply_to).to eq([gitlab_sender_reply_to])
end
+
+ context 'when custom suffix for email subject is set' do
+ before do
+ stub_config_setting(email_subject_suffix: 'A Nice Suffix')
+ end
+
+ it 'ends the subject with the suffix' do
+ is_expected.to have_subject /\ \| A Nice Suffix$/
+ end
+ end
end
shared_examples 'an email that contains a header with author username' do
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index 35cc51725c6..d30cc8ff9f2 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -17,9 +17,9 @@ module Select2Helper
selector = options.fetch(:from)
if options[:multiple]
- execute_script("$('#{selector}').select2('val', ['#{value}'], true);")
+ execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');")
else
- execute_script("$('#{selector}').select2('val', '#{value}', true);")
+ execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');")
end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 0097dbf8fad..c79975d8667 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,6 +5,8 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
+ 'not-merged-branch' => 'b83d6e3',
+ 'branch-merged' => '498214d',
'empty-branch' => '7efb185',
'ends-with.json' => '98b0d8b',
'flatten-dir' => 'e56497b',
@@ -14,7 +16,8 @@ module TestEnv
'improve/awesome' => '5937ac0',
'markdown' => '0ed8c6c',
'lfs' => 'be93687',
- 'master' => '5937ac0',
+ 'master' => 'b83d6e3',
+ 'merge-test' => '5937ac0',
"'test'" => 'e56497b',
'orphaned-branch' => '45127a9',
'binary-encoding' => '7b1cf43',
@@ -24,10 +27,10 @@ module TestEnv
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
'crlf-diff' => '5938907',
- 'conflict-start' => '75284c7',
+ 'conflict-start' => '824be60',
'conflict-resolvable' => '1450cd6',
'conflict-binary-file' => '259a6fb',
- 'conflict-contains-conflict-markers' => '5e0964c',
+ 'conflict-contains-conflict-markers' => '78a3086',
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
@@ -95,7 +98,9 @@ module TestEnv
def setup_gitlab_shell
unless File.directory?(Gitlab.config.gitlab_shell.path)
- `rake gitlab:shell:install`
+ unless system('rake', 'gitlab:shell:install')
+ raise 'Can`t clone gitlab-shell'
+ end
end
end
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index b90fc112671..0f9dc2dee75 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -8,4 +8,8 @@ module WaitForAjax
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
+
+ def javascript_test?
+ [:selenium, :webkit, :chrome, :poltergeist].include?(Capybara.current_driver)
+ end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 548e7780c36..73bc8326f02 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -9,6 +9,7 @@ describe 'gitlab:app namespace rake task' do
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'
Rake.application.rake_require 'tasks/gitlab/db'
+ Rake.application.rake_require 'tasks/cache'
# empty task as env is already loaded
Rake::Task.define_task :environment
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
new file mode 100644
index 00000000000..e6ebef82b78
--- /dev/null
+++ b/spec/tasks/gitlab/users_rake_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+require 'rake'
+
+describe 'gitlab:users namespace rake task' do
+ let(:enable_registry) { true }
+
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/users'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ def run_rake_task(task_name)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task task_name
+ end
+
+ describe 'clear_all_authentication_tokens' do
+ before do
+ # avoid writing task output to spec progress
+ allow($stdout).to receive :write
+ end
+
+ context 'gitlab version' do
+ it 'clears the authentication token for all users' do
+ create_list(:user, 2)
+
+ expect(User.pluck(:authentication_token)).to all(be_present)
+
+ run_rake_task('gitlab:users:clear_all_authentication_tokens')
+
+ expect(User.pluck(:authentication_token)).to all(be_nil)
+ end
+ end
+ 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..2dac5ee23c8
--- /dev/null
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe 'ci/lints/show' do
+ include Devise::TestHelpers
+
+ describe 'XSS protection' do
+ let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+ before do
+ assign(:status, true)
+ assign(:builds, config_processor.builds)
+ assign(:stages, config_processor.stages)
+ assign(:jobs, config_processor.jobs)
+ end
+
+ context 'when builds attrbiutes contain HTML nodes' do
+ let(:content) do
+ {
+ rspec: {
+ script: '<h1>rspec</h1>',
+ stage: 'test'
+ }
+ }
+ end
+
+ it 'does not render HTML elements' do
+ render
+
+ expect(rendered).not_to have_css('h1', text: 'rspec')
+ end
+ end
+
+ context 'when builds attributes do not contain HTML nodes' do
+ let(:content) do
+ {
+ rspec: {
+ script: 'rspec',
+ stage: 'test'
+ }
+ }
+ end
+
+ it 'shows configuration in the table' do
+ render
+
+ expect(rendered).to have_css('td pre', text: 'rspec')
+ end
+ end
+ end
+
+ 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/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index ee362e6fcb3..1397bfa5864 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -12,13 +12,13 @@ describe 'devise/shared/_signin_box' do
render
- expect(rendered).to have_selector('#tab-crowd form')
+ expect(rendered).to have_selector('#crowd form')
end
it 'is not shown when Crowd is disabled' do
render
- expect(rendered).not_to have_selector('#tab-crowd')
+ expect(rendered).not_to have_selector('#crowd')
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 446ba3bfa14..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
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
deleted file mode 100644
index 21f49d396e7..00000000000
--- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'spec_helper'
-
-describe 'projects/merge_requests/widget/_heading' do
- include Devise::TestHelpers
-
- context 'when released to an environment' do
- let(:project) { merge_request.target_project }
- let(:merge_request) { create(:merge_request, :merged) }
- let(:environment) { create(:environment, project: project) }
- let!(:deployment) do
- create(:deployment, environment: environment, sha: project.commit('master').id)
- end
-
- before do
- assign(:merge_request, merge_request)
- assign(:project, project)
-
- allow(view).to receive(:can?).and_return(true)
-
- render
- end
-
- it 'displays that the environment is deployed' do
- expect(rendered).to match("Deployed to")
- expect(rendered).to match("#{environment.name}")
- end
- end
-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 31bbb150698..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,18 +1,21 @@
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) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:milestone) { create(:milestone, project: project) }
let(:closed_merge_request) do
create(:closed_merge_request,
source_project: fork_project,
target_project: project,
- author: user)
+ author: user,
+ assignee: user,
+ milestone: milestone)
end
before do
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
index 932d6756ad2..b14b1ece2d0 100644
--- a/spec/views/projects/notes/_form.html.haml_spec.rb
+++ b/spec/views/projects/notes/_form.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/notes/_form' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index ac7f3ffb157..bf027499c94 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/pipelines/show' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
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/build_coverage_worker_spec.rb b/spec/workers/build_coverage_worker_spec.rb
new file mode 100644
index 00000000000..ba20488f663
--- /dev/null
+++ b/spec/workers/build_coverage_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildCoverageWorker do
+ describe '#perform' do
+ context 'when build exists' do
+ let!(:build) { create(:ci_build) }
+
+ it 'updates code coverage' do
+ expect_any_instance_of(Ci::Build)
+ .to receive(:update_coverage)
+
+ described_class.new.perform(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
new file mode 100644
index 00000000000..2868167c7d4
--- /dev/null
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe BuildFinishedWorker do
+ describe '#perform' do
+ context 'when build exists' do
+ let(:build) { create(:ci_build) }
+
+ it 'calculates coverage and calls hooks' do
+ expect(BuildCoverageWorker)
+ .to receive(:new).ordered.and_call_original
+ expect(BuildHooksWorker)
+ .to receive(:new).ordered.and_call_original
+
+ expect_any_instance_of(BuildCoverageWorker)
+ .to receive(:perform)
+ expect_any_instance_of(BuildHooksWorker)
+ .to receive(:perform)
+
+ described_class.new.perform(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/build_hooks_worker_spec.rb b/spec/workers/build_hooks_worker_spec.rb
new file mode 100644
index 00000000000..97654a93f5c
--- /dev/null
+++ b/spec/workers/build_hooks_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildHooksWorker do
+ describe '#perform' do
+ context 'when build exists' do
+ let!(:build) { create(:ci_build) }
+
+ it 'calls build hooks' do
+ expect_any_instance_of(Ci::Build)
+ .to receive(:execute_hooks)
+
+ described_class.new.perform(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
new file mode 100644
index 00000000000..dba70883130
--- /dev/null
+++ b/spec/workers/build_success_worker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe BuildSuccessWorker do
+ describe '#perform' do
+ context 'when build exists' do
+ context 'when build belogs to the environment' do
+ let!(:build) { create(:ci_build, environment: 'production') }
+
+ it 'executes deployment service' do
+ expect_any_instance_of(CreateDeploymentService)
+ .to receive(:execute)
+
+ described_class.new.perform(build.id)
+ end
+ end
+
+ context 'when build is not associated with project' do
+ let!(:build) { create(:ci_build, project: nil) }
+
+ it 'does not create deployment' do
+ expect_any_instance_of(CreateDeploymentService)
+ .not_to receive(:execute)
+
+ described_class.new.perform(build.id)
+ end
+ end
+ end
+
+ context 'when build does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/build_queue_spec.rb b/spec/workers/concerns/build_queue_spec.rb
new file mode 100644
index 00000000000..6bf955e0be2
--- /dev/null
+++ b/spec/workers/concerns/build_queue_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe BuildQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include BuildQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('build')
+ end
+end
diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb
new file mode 100644
index 00000000000..5d1336c21a6
--- /dev/null
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe CronjobQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include CronjobQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+ end
+
+ it 'disables retrying of failed jobs' do
+ expect(worker.sidekiq_options['retry']).to eq(false)
+ end
+end
diff --git a/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
new file mode 100644
index 00000000000..512baec8b7e
--- /dev/null
+++ b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe DedicatedSidekiqQueue do
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'Foo::Bar::DummyWorker'
+ end
+
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+ end
+ end
+
+ describe 'queue names' do
+ it 'sets the queue name based on the class name' do
+ expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy')
+ end
+ end
+end
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
new file mode 100644
index 00000000000..40794d0e42a
--- /dev/null
+++ b/spec/workers/concerns/pipeline_queue_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe PipelineQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include PipelineQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('pipeline')
+ end
+end
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
new file mode 100644
index 00000000000..8868e969829
--- /dev/null
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe RepositoryCheckQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include RepositoryCheckQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check')
+ end
+
+ it 'disables retrying of failed jobs' do
+ expect(worker.sidekiq_options['retry']).to eq(false)
+ end
+end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 7ca2c29da1c..036d037f3f9 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -57,7 +57,7 @@ describe EmailsOnPushWorker do
end
it "sends a mail with the correct subject" do
- expect(email.subject).to include('Change some files')
+ expect(email.subject).to include('adds bar folder and branch-test text file')
end
it "mentions force pushing in the body" do
@@ -73,7 +73,7 @@ describe EmailsOnPushWorker do
before { perform }
it "sends a mail with the correct subject" do
- expect(email.subject).to include('Change some files')
+ expect(email.subject).to include('adds bar folder and branch-test text file')
end
it "does not mention force pushing in the body" do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
new file mode 100644
index 00000000000..fc9adf47c1e
--- /dev/null
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Every Sidekiq worker' do
+ let(:workers) do
+ root = Rails.root.join('app', 'workers')
+ concerns = root.join('concerns').to_s
+
+ workers = Dir[root.join('**', '*.rb')].
+ reject { |path| path.start_with?(concerns) }
+
+ workers.map do |path|
+ ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '')
+
+ ns.camelize.constantize
+ end
+ end
+
+ it 'does not use the default queue' do
+ workers.each do |worker|
+ expect(worker.sidekiq_options['queue'].to_s).not_to eq('default')
+ end
+ end
+
+ it 'uses the cronjob queue when the worker runs as a cronjob' do
+ cron_workers = Settings.cron_jobs.
+ map { |job_name, options| options['job_class'].constantize }.
+ to_set
+
+ workers.each do |worker|
+ next unless cron_workers.include?(worker)
+
+ expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+ end
+ end
+
+ it 'defines the queue in the Sidekiq configuration file' do
+ config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
+ queue_names = config[:queues].map { |(queue, _)| queue }.to_set
+
+ workers.each do |worker|
+ expect(queue_names).to include(worker.sidekiq_options['queue'].to_s)
+ end
+ end
+end
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index 7d6668920c0..73cbadc13d9 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -5,65 +5,42 @@ describe ExpireBuildArtifactsWorker do
let(:worker) { described_class.new }
+ before { Sidekiq::Worker.clear_all }
+
describe '#perform' do
before { build }
- subject! { worker.perform }
+ subject! do
+ Sidekiq::Testing.fake! { worker.perform }
+ end
context 'with expired artifacts' do
let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
- it 'does expire' do
- expect(build.reload.artifacts_expired?).to be_truthy
- end
-
- it 'does remove files' do
- expect(build.reload.artifacts_file.exists?).to be_falsey
- end
-
- it 'does nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).to be_nil
+ it 'enqueues that build' do
+ expect(jobs_enqueued.size).to eq(1)
+ expect(jobs_enqueued[0]["args"]).to eq([build.id])
end
end
context 'with not yet expired artifacts' do
let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
- it 'does not expire' do
- expect(build.reload.artifacts_expired?).to be_falsey
- end
-
- it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
- end
-
- it 'does not nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).not_to be_nil
+ it 'does not enqueue that build' do
+ expect(jobs_enqueued.size).to eq(0)
end
end
context 'without expire date' do
let(:build) { create(:ci_build, :artifacts) }
- it 'does not expire' do
- expect(build.reload.artifacts_expired?).to be_falsey
- end
-
- it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
- end
-
- it 'does not nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).not_to be_nil
+ it 'does not enqueue that build' do
+ expect(jobs_enqueued.size).to eq(0)
end
end
- context 'for expired artifacts' do
- let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
-
- it 'is still expired' do
- expect(build.reload.artifacts_expired?).to be_truthy
- end
+ def jobs_enqueued
+ Sidekiq::Queues.jobs_by_worker['ExpireBuildInstanceArtifactsWorker']
end
end
end
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
new file mode 100644
index 00000000000..d202b3de77e
--- /dev/null
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe ExpireBuildInstanceArtifactsWorker do
+ include RepoHelpers
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before do
+ worker.perform(build.id)
+ end
+
+ context 'with expired artifacts' do
+ let(:artifacts_expiry) { { artifacts_expire_at: Time.now - 7.days } }
+
+ context 'when associated project is valid' do
+ let(:build) do
+ create(:ci_build, :artifacts, artifacts_expiry)
+ end
+
+ it 'does expire' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+
+ it 'does remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_falsey
+ end
+
+ it 'does nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).to be_nil
+ end
+ end
+
+ context 'when associated project was removed' do
+ let(:build) do
+ create(:ci_build, :artifacts, artifacts_expiry) do |build|
+ build.project.delete
+ end
+ end
+
+ it 'does not remove artifacts' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+ end
+ end
+
+ context 'with not yet expired artifacts' do
+ let(:build) do
+ create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
+ end
+
+ context 'without expire date' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
+ end
+
+ context 'for expired artifacts' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'is still expired' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_hooks_worker_spec.rb b/spec/workers/pipeline_hooks_worker_spec.rb
new file mode 100644
index 00000000000..035e329839f
--- /dev/null
+++ b/spec/workers/pipeline_hooks_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe PipelineHooksWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'executes hooks for the pipeline' do
+ expect_any_instance_of(Ci::Pipeline)
+ .to receive(:execute_hooks)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
new file mode 100644
index 00000000000..2c9e7c2cd02
--- /dev/null
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe PipelineMetricsWorker do
+ let(:project) { create(:project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline,
+ status: status,
+ project: project,
+ ref: 'master',
+ sha: project.repository.commit('master').id,
+ started_at: 1.hour.ago,
+ finished_at: Time.now)
+ end
+
+ describe '#perform' do
+ subject { described_class.new.perform(pipeline.id) }
+
+ context 'when pipeline is running' do
+ let(:status) { 'running' }
+
+ it 'records the build start time' do
+ subject
+
+ expect(merge_request.reload.metrics.latest_build_started_at).to be_like_time(pipeline.started_at)
+ end
+
+ it 'clears the build end time' do
+ subject
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
+ end
+ end
+
+ context 'when pipeline succeeded' do
+ let(:status) { 'success' }
+
+ it 'records the build end time' do
+ subject
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_like_time(pipeline.finished_at)
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_proccess_worker_spec.rb b/spec/workers/pipeline_proccess_worker_spec.rb
new file mode 100644
index 00000000000..86e9d7f6684
--- /dev/null
+++ b/spec/workers/pipeline_proccess_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe PipelineProcessWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'processes pipeline' do
+ expect_any_instance_of(Ci::Pipeline).to receive(:process!)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb
new file mode 100644
index 00000000000..5e31cc2c8e7
--- /dev/null
+++ b/spec/workers/pipeline_success_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe PipelineSuccessWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline, status: 'success') }
+
+ it 'performs "merge when pipeline succeeds"' do
+ expect_any_instance_of(
+ MergeRequests::MergeWhenBuildSucceedsService
+ ).to receive(:trigger)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_update_worker_spec.rb b/spec/workers/pipeline_update_worker_spec.rb
new file mode 100644
index 00000000000..0b456cfd0da
--- /dev/null
+++ b/spec/workers/pipeline_update_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe PipelineUpdateWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'updates pipeline status' do
+ expect_any_instance_of(Ci::Pipeline).to receive(:update_status)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 1d2cf7acddd..984acdade36 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -79,7 +79,9 @@ describe PostReceive do
end
it "does not run if the author is not in the project" do
- allow(Key).to receive(:find_by).with(hash_including(id: anything())) { nil }
+ allow_any_instance_of(Gitlab::GitPostReceive).
+ to receive(:identify_using_ssh_key).
+ and_return(nil)
expect(project).not_to receive(:execute_hooks)
@@ -90,7 +92,13 @@ describe PostReceive do
allow(Project).to receive(:find_with_namespace).and_return(project)
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
- expect(project).to receive(:update_merge_requests)
+
+ PostReceive.new.perform(pwd(project), key_id, base64_changes)
+ end
+
+ it "enqueues a UpdateMergeRequestsWorker job" do
+ allow(Project).to receive(:find_with_namespace).and_return(project)
+ expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 5785a6a06ff..f5b60b90d11 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -6,21 +6,39 @@ describe ProjectCacheWorker do
subject { described_class.new }
describe '#perform' do
- it 'updates project cache data' do
- expect_any_instance_of(Repository).to receive(:size)
- expect_any_instance_of(Repository).to receive(:commit_count)
+ context 'when an exclusive lease can be obtained' do
+ before do
+ allow(subject).to receive(:try_obtain_lease_for).with(project.id).
+ and_return(true)
+ end
- expect_any_instance_of(Project).to receive(:update_repository_size)
- expect_any_instance_of(Project).to receive(:update_commit_count)
+ it 'updates project cache data' do
+ expect_any_instance_of(Repository).to receive(:size)
+ expect_any_instance_of(Repository).to receive(:commit_count)
- subject.perform(project.id)
+ expect_any_instance_of(Project).to receive(:update_repository_size)
+ expect_any_instance_of(Project).to receive(:update_commit_count)
+
+ subject.perform(project.id)
+ end
+
+ it 'handles missing repository data' do
+ expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
+ expect_any_instance_of(Repository).not_to receive(:size)
+
+ subject.perform(project.id)
+ end
end
- it 'handles missing repository data' do
- expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect_any_instance_of(Repository).not_to receive(:size)
+ context 'when an exclusive lease can not be obtained' do
+ it 'does nothing' do
+ allow(subject).to receive(:try_obtain_lease_for).with(project.id).
+ and_return(false)
+
+ expect(subject).not_to receive(:update_caches)
- subject.perform(project.id)
+ subject.perform(project.id)
+ end
end
end
end
diff --git a/spec/workers/trending_projects_worker_spec.rb b/spec/workers/trending_projects_worker_spec.rb
new file mode 100644
index 00000000000..c3c6fdcf2d5
--- /dev/null
+++ b/spec/workers/trending_projects_worker_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe TrendingProjectsWorker do
+ describe '#perform' do
+ it 'refreshes the trending projects' do
+ expect(TrendingProject).to receive(:refresh!)
+
+ described_class.new.perform
+ end
+ end
+end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
new file mode 100644
index 00000000000..c78a69eda67
--- /dev/null
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe UpdateMergeRequestsWorker do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ let(:oldrev) { "123456" }
+ let(:newrev) { "789012" }
+ let(:ref) { "refs/heads/test" }
+
+ def perform
+ subject.perform(project.id, user.id, oldrev, newrev, ref)
+ end
+
+ it 'executes MergeRequests::RefreshService with expected values' do
+ expect(MergeRequests::RefreshService).to receive(:new).with(project, user).and_call_original
+ expect_any_instance_of(MergeRequests::RefreshService).to receive(:execute).with(oldrev, newrev, ref)
+
+ perform
+ end
+
+ it 'executes SystemHooksService with expected values' do
+ push_data = double('push_data')
+ expect(Gitlab::DataBuilder::Push).to receive(:build).with(project, user, oldrev, newrev, ref, []).and_return(push_data)
+
+ system_hook_service = double('system_hook_service')
+ expect(SystemHooksService).to receive(:new).and_return(system_hook_service)
+ expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks)
+
+ perform
+ end
+ end
+end
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/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/Node.gitignore b/vendor/gitignore/Node.gitignore
index bf7525f9912..bc7fc55724c 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -39,3 +39,6 @@ jspm_packages
# Optional REPL history
.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
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 d56f8b53288..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/
@@ -258,3 +263,6 @@ paket-files/
# 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/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/