summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes2
-rw-r--r--.gitlab-ci.yml14
-rw-r--r--.gitlab/merge_request_templates/Documentation.md2
-rw-r--r--.rubocop.yml2
-rw-r--r--.scss-lint.yml12
-rw-r--r--CHANGELOG.md (renamed from CHANGELOG)475
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock8
-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/application.js5
-rw-r--r--app/assets/javascripts/build.js36
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es6 (renamed from app/assets/javascripts/compare_autocomplete.js)13
-rw-r--r--app/assets/javascripts/cycle_analytics.js.es68
-rw-r--r--app/assets/javascripts/dispatcher.js.es6 (renamed from app/assets/javascripts/dispatcher.js)24
-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/gl_dropdown.js88
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es6170
-rw-r--r--app/assets/javascripts/groups.js13
-rw-r--r--app/assets/javascripts/labels_select.js2
-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.es6347
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es682
-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_tabs.js39
-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/pipelines.js.es6 (renamed from app/assets/javascripts/pipeline.js.es6)2
-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.js38
-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/templates/issuable_template_selector.js.es68
-rw-r--r--app/assets/javascripts/username_validator.js.es6133
-rw-r--r--app/assets/javascripts/users_select.js4
-rw-r--r--app/assets/stylesheets/behaviors.scss1
-rw-r--r--app/assets/stylesheets/framework/animations.scss7
-rw-r--r--app/assets/stylesheets/framework/blocks.scss5
-rw-r--r--app/assets/stylesheets/framework/buttons.scss12
-rw-r--r--app/assets/stylesheets/framework/callout.scss5
-rw-r--r--app/assets/stylesheets/framework/common.scss47
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss10
-rw-r--r--app/assets/stylesheets/framework/files.scss19
-rw-r--r--app/assets/stylesheets/framework/flash.scss6
-rw-r--r--app/assets/stylesheets/framework/forms.scss13
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss6
-rw-r--r--app/assets/stylesheets/framework/header.scss14
-rw-r--r--app/assets/stylesheets/framework/layout.scss12
-rw-r--r--app/assets/stylesheets/framework/lists.scss19
-rw-r--r--app/assets/stylesheets/framework/logo.scss85
-rw-r--r--app/assets/stylesheets/framework/mixins.scss8
-rw-r--r--app/assets/stylesheets/framework/mobile.scss12
-rw-r--r--app/assets/stylesheets/framework/modal.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss23
-rw-r--r--app/assets/stylesheets/framework/panels.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss14
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss8
-rw-r--r--app/assets/stylesheets/framework/tables.scss3
-rw-r--r--app/assets/stylesheets/framework/timeline.scss1
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss13
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss122
-rw-r--r--app/assets/stylesheets/framework/typography.scss66
-rw-r--r--app/assets/stylesheets/framework/variables.scss46
-rw-r--r--app/assets/stylesheets/highlight/dark.scss147
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss137
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss155
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss155
-rw-r--r--app/assets/stylesheets/highlight/white.scss15
-rw-r--r--app/assets/stylesheets/mailers/devise.scss10
-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.scss15
-rw-r--r--app/assets/stylesheets/pages/boards.scss3
-rw-r--r--app/assets/stylesheets/pages/builds.scss21
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss3
-rw-r--r--app/assets/stylesheets/pages/commit.scss12
-rw-r--r--app/assets/stylesheets/pages/commits.scss7
-rw-r--r--app/assets/stylesheets/pages/confirmation.scss10
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss18
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss9
-rw-r--r--app/assets/stylesheets/pages/diff.scss47
-rw-r--r--app/assets/stylesheets/pages/editor.scss9
-rw-r--r--app/assets/stylesheets/pages/environments.scss18
-rw-r--r--app/assets/stylesheets/pages/errors.scss4
-rw-r--r--app/assets/stylesheets/pages/events.scss10
-rw-r--r--app/assets/stylesheets/pages/groups.scss15
-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.scss4
-rw-r--r--app/assets/stylesheets/pages/labels.scss24
-rw-r--r--app/assets/stylesheets/pages/lint.scss1
-rw-r--r--app/assets/stylesheets/pages/login.scss238
-rw-r--r--app/assets/stylesheets/pages/members.scss98
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss52
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss40
-rw-r--r--app/assets/stylesheets/pages/milestone.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss7
-rw-r--r--app/assets/stylesheets/pages/notes.scss15
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss118
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss129
-rw-r--r--app/assets/stylesheets/pages/runners.scss1
-rw-r--r--app/assets/stylesheets/pages/search.scss6
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/tree.scss15
-rw-r--r--app/assets/stylesheets/pages/xterm.scss290
-rw-r--r--app/assets/stylesheets/print.scss25
-rw-r--r--app/controllers/admin/services_controller.rb8
-rw-r--r--app/controllers/application_controller.rb11
-rw-r--r--app/controllers/concerns/issuable_actions.rb5
-rw-r--r--app/controllers/dashboard/labels_controller.rb4
-rw-r--r--app/controllers/groups/group_members_controller.rb4
-rw-r--r--app/controllers/groups/labels_controller.rb92
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb4
-rw-r--r--app/controllers/projects/boards/issues_controller.rb4
-rw-r--r--app/controllers/projects/boards/lists_controller.rb5
-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/group_links_controller.rb20
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/labels_controller.rb38
-rw-r--r--app/controllers/projects/merge_requests_controller.rb80
-rw-r--r--app/controllers/projects/project_members_controller.rb27
-rw-r--r--app/controllers/projects_controller.rb35
-rw-r--r--app/controllers/users_controller.rb6
-rw-r--r--app/finders/issuable_finder.rb17
-rw-r--r--app/finders/labels_finder.rb94
-rw-r--r--app/helpers/award_emoji_helper.rb9
-rw-r--r--app/helpers/boards_helper.rb2
-rw-r--r--app/helpers/builds_helper.rb8
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb82
-rw-r--r--app/helpers/merge_requests_helper.rb10
-rw-r--r--app/helpers/preferences_helper.rb16
-rw-r--r--app/helpers/projects_helper.rb33
-rw-r--r--app/mailers/.gitkeep0
-rw-r--r--app/mailers/emails/pipelines.rb43
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/ci/build.rb49
-rw-r--r--app/models/ci/pipeline.rb29
-rw-r--r--app/models/ci/runner.rb6
-rw-r--r--app/models/ci/runner_project.rb4
-rw-r--r--app/models/ci/trigger.rb4
-rw-r--r--app/models/ci/trigger_request.rb6
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/expirable.rb6
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/issuable.rb22
-rw-r--r--app/models/concerns/protected_branch_access.rb5
-rw-r--r--app/models/concerns/sortable.rb10
-rw-r--r--app/models/deployment.rb30
-rw-r--r--app/models/email.rb6
-rw-r--r--app/models/environment.rb43
-rw-r--r--app/models/event.rb9
-rw-r--r--app/models/external_issue.rb5
-rw-r--r--app/models/group.rb3
-rw-r--r--app/models/group_label.rb11
-rw-r--r--app/models/issue.rb24
-rw-r--r--app/models/label.rb129
-rw-r--r--app/models/label_priority.rb8
-rw-r--r--app/models/list.rb11
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/project_member.rb8
-rw-r--r--app/models/merge_request.rb60
-rw-r--r--app/models/merge_request_diff.rb8
-rw-r--r--app/models/project.rb50
-rw-r--r--app/models/project_feature.rb19
-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/repository.rb44
-rw-r--r--app/models/service.rb5
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/models/user.rb10
-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.rb14
-rw-r--r--app/services/boards/lists/create_service.rb6
-rw-r--r--app/services/boards/lists/generate_service.rb3
-rw-r--r--app/services/ci/send_pipeline_notification_service.rb19
-rw-r--r--app/services/create_deployment_service.rb49
-rw-r--r--app/services/event_create_service.rb4
-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/merge_requests/assign_issues_service.rb2
-rw-r--r--app/services/merge_requests/merge_service.rb20
-rw-r--r--app/services/merge_requests/resolve_service.rb24
-rw-r--r--app/services/notes/create_service.rb6
-rw-r--r--app/services/projects/autocomplete_service.rb2
-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.rb6
-rw-r--r--app/views/admin/appearances/preview.html.haml17
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/admin/runners/show.html.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.haml6
-rw-r--r--app/views/devise/unlocks/new.html.haml10
-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/import/gitlab_projects/new.html.haml4
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml9
-rw-r--r--app/views/layouts/application.html.haml1
-rw-r--r--app/views/layouts/devise.html.haml59
-rw-r--r--app/views/layouts/nav/_group.html.haml4
-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/projects/_customize_workflow.html.haml8
-rw-r--r--app/views/projects/_home_panel.html.haml5
-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/edit.html.haml43
-rw-r--r--app/views/projects/boards/components/_card.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml17
-rw-r--r--app/views/projects/builds/_table.html.haml2
-rw-r--r--app/views/projects/builds/_user.html.haml7
-rw-r--r--app/views/projects/builds/show.html.haml95
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml9
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml4
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml47
-rw-r--r--app/views/projects/commit/_pipeline.html.haml2
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml2
-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/compare/_form.html.haml2
-rw-r--r--app/views/projects/compare/_ref_dropdown.html.haml4
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml4
-rw-r--r--app/views/projects/deployments/_actions.haml41
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml4
-rw-r--r--app/views/projects/deployments/_rollback.haml6
-rw-r--r--app/views/projects/edit.html.haml104
-rw-r--r--app/views/projects/environments/_environment.html.haml6
-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.haml21
-rw-r--r--app/views/projects/environments/new.html.haml11
-rw-r--r--app/views/projects/environments/show.html.haml8
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml13
-rw-r--r--app/views/projects/group_links/update.js.haml3
-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/show.html.haml4
-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_submit.html.haml12
-rw-r--r--app/views/projects/merge_requests/_show.html.haml92
-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/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml16
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml3
-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/new.html.haml6
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml13
-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/runners/_specific_runners.html.haml2
-rw-r--r--app/views/projects/show.html.haml130
-rw-r--r--app/views/shared/_label.html.haml53
-rw-r--r--app/views/shared/_label_row.html.haml7
-rw-r--r--app/views/shared/_labels_row.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml14
-rw-r--r--app/views/shared/issuable/_form.html.haml12
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml3
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml3
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/labels/_form.html.haml (renamed from app/views/projects/labels/_form.html.haml)4
-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/u2f/_authenticate.html.haml2
-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.rb1
-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.rb1
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb11
-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.rb3
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb3
-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.rb3
-rw-r--r--app/workers/update_merge_requests_worker.rb17
-rwxr-xr-xbin/background_jobs3
-rw-r--r--config/application.rb5
-rw-r--r--config/initializers/metrics.rb1
-rw-r--r--config/locales/en.yml1
-rw-r--r--config/mail_room.yml2
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/group.rb10
-rw-r--r--config/routes/project.rb10
-rw-r--r--config/routes/user.rb15
-rw-r--r--config/sidekiq_queues.yml47
-rw-r--r--db/fixtures/development/14_pipelines.rb3
-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/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.rb27
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/integration/koding.md1
-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/request_profiling.md16
-rw-r--r--doc/api/award_emoji.md18
-rw-r--r--doc/api/builds.md16
-rw-r--r--doc/api/commits.md10
-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/merge_requests.md18
-rw-r--r--doc/api/notes.md6
-rw-r--r--doc/api/pipelines.md10
-rw-r--r--doc/api/projects.md16
-rw-r--r--doc/api/system_hooks.md7
-rw-r--r--doc/api/todos.md18
-rw-r--r--doc/api/users.md26
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/docker/using_docker_images.md6
-rw-r--r--doc/ci/examples/README.md1
-rw-r--r--doc/ci/yaml/README.md167
-rw-r--r--doc/development/README.md3
-rw-r--r--doc/development/doc_styleguide.md10
-rw-r--r--doc/development/performance.md7
-rw-r--r--doc/development/sidekiq_style_guide.md38
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/monitoring/performance/gitlab_configuration.md2
-rw-r--r--doc/monitoring/performance/grafana_configuration.md2
-rw-r--r--doc/monitoring/performance/influxdb_configuration.md2
-rw-r--r--doc/monitoring/performance/influxdb_schema.md2
-rw-r--r--doc/monitoring/performance/introduction.md2
-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.md4
-rw-r--r--doc/university/README.md9
-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.11-to-8.12.md2
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/user/markdown.md8
-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/versions.md30
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md61
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/workflow/gitlab_flow.md2
-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/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/group/members.rb14
-rw-r--r--features/steps/groups.rb12
-rw-r--r--features/steps/project/active_tab.rb2
-rw-r--r--features/steps/project/commits/commits.rb26
-rw-r--r--features/steps/project/graph.rb4
-rw-r--r--features/steps/project/issues/labels.rb2
-rw-r--r--features/steps/project/merge_requests.rb14
-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/boards.rb80
-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.rb131
-rw-r--r--lib/api/helpers.rb19
-rw-r--r--lib/api/labels.rb93
-rw-r--r--lib/api/merge_requests.rb15
-rw-r--r--lib/api/system_hooks.rb66
-rw-r--r--lib/api/todos.rb45
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb16
-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.rb2
-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/relative_link_filter.rb4
-rw-r--r--lib/banzai/filter/set_direction_filter.rb15
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/banzai/renderer.rb4
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb30
-rw-r--r--lib/constraints/namespace_url_constrainer.rb13
-rw-r--r--lib/event_filter.rb11
-rw-r--r--lib/extracts_path.rb17
-rw-r--r--lib/gitlab/backend/shell.rb4
-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/ee_compat_check.rb261
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb4
-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/label_formatter.rb10
-rw-r--r--lib/gitlab/google_code_import/importer.rb24
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.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.rb6
-rw-r--r--lib/gitlab/import_export/relation_factory.rb43
-rw-r--r--lib/gitlab/issues_labels.rb4
-rw-r--r--lib/gitlab/project_search_results.rb6
-rw-r--r--lib/tasks/.gitkeep0
-rw-r--r--lib/tasks/cache.rake2
-rw-r--r--lib/tasks/ci/.gitkeep0
-rw-r--r--lib/tasks/ee_compat_check.rake4
-rw-r--r--lib/tasks/gitlab/backup.rake2
-rw-r--r--lib/tasks/gitlab/dev.rake22
-rwxr-xr-xscripts/lint-doc.sh6
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb163
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb76
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb203
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb261
-rw-r--r--spec/controllers/projects_controller_spec.rb40
-rw-r--r--spec/controllers/snippets_controller_spec.rb156
-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.rb1
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb4
-rw-r--r--spec/features/compare_spec.rb4
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb41
-rw-r--r--spec/features/environments_spec.rb112
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups_spec.rb82
-rw-r--r--spec/features/issues/award_emoji_spec.rb46
-rw-r--r--spec/features/issues/reset_filters_spec.rb8
-rw-r--r--spec/features/login_spec.rb67
-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/merge_when_build_succeeds_spec.rb2
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb6
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb61
-rw-r--r--spec/features/projects/features_visibility_spec.rb30
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb42
-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.rb40
-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/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/signup_spec.rb8
-rw-r--r--spec/features/u2f_spec.rb14
-rw-r--r--spec/features/users_spec.rb42
-rw-r--r--spec/finders/labels_finder_spec.rb101
-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/events_helper_spec.rb17
-rw-r--r--spec/helpers/labels_helper_spec.rb27
-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/u2f/authenticate_spec.js2
-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.rb5
-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/relative_link_filter_spec.rb40
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb6
-rw-r--r--spec/lib/banzai/pipeline/description_pipeline_spec.rb12
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb48
-rw-r--r--spec/lib/constraints/namespace_url_constrainer_spec.rb10
-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/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/google_code_import/importer_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml6
-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/safe_model_attributes.yml12
-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.rb1
-rw-r--r--spec/models/ci/pipeline_spec.rb51
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb2
-rw-r--r--spec/models/concerns/expirable_spec.rb31
-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.rb27
-rw-r--r--spec/models/external_issue_spec.rb15
-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/label_priority_spec.rb20
-rw-r--r--spec/models/label_spec.rb120
-rw-r--r--spec/models/members/project_member_spec.rb13
-rw-r--r--spec/models/merge_request/metrics_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb119
-rw-r--r--spec/models/project_feature_spec.rb23
-rw-r--r--spec/models/project_label_spec.rb120
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb16
-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_spec.rb37
-rw-r--r--spec/models/repository_spec.rb44
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--spec/requests/api/boards_spec.rb25
-rw-r--r--spec/requests/api/branches_spec.rb190
-rw-r--r--spec/requests/api/commit_statuses_spec.rb4
-rw-r--r--spec/requests/api/commits_spec.rb11
-rw-r--r--spec/requests/api/issues_spec.rb4
-rw-r--r--spec/requests/api/labels_spec.rb17
-rw-r--r--spec/requests/api/members_spec.rb11
-rw-r--r--spec/requests/api/notes_spec.rb17
-rw-r--r--spec/requests/api/system_hooks_spec.rb7
-rw-r--r--spec/requests/api/users_spec.rb6
-rw-r--r--spec/requests/git_http_spec.rb5
-rw-r--r--spec/routing/routing_spec.rb18
-rw-r--r--spec/services/boards/lists/create_service_spec.rb4
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb4
-rw-r--r--spec/services/ci/send_pipeline_notification_service_spec.rb48
-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/git_push_service_spec.rb66
-rw-r--r--spec/services/issues/create_service_spec.rb21
-rw-r--r--spec/services/issues/move_service_spec.rb43
-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/merge_requests/assign_issues_service_spec.rb12
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb16
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb3
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb141
-rw-r--r--spec/services/projects/transfer_service_spec.rb10
-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/notify_shared_examples.rb (renamed from spec/mailers/shared/notify.rb)0
-rw-r--r--spec/support/select2_helper.rb4
-rw-r--r--spec/support/test_env.rb9
-rw-r--r--spec/support/wait_for_ajax.rb4
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb1
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb38
-rw-r--r--spec/views/projects/merge_requests/_heading.html.haml_spec.rb28
-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/every_sidekiq_worker_spec.rb44
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb44
-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/post_receive_spec.rb8
-rw-r--r--spec/workers/project_cache_worker_spec.rb38
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb38
716 files changed, 13974 insertions, 4933 deletions
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 7d19f55aca3..9c4b4acbaf5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -22,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
@@ -99,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:
@@ -210,6 +210,13 @@ rake brakeman: *exec
rake flay: *exec
license_finder: *exec
rake downtime_check: *exec
+rake ee_compat_check:
+ <<: *exec
+ only:
+ - branches
+ except:
+ - tags
+ allow_failure: yes
rake db:migrate:reset:
stage: test
@@ -302,6 +309,9 @@ coverage:
# 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:
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 bec2464c740..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.
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 71df6be6a15..5c8e5ac0758 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:
@@ -172,7 +172,7 @@ linters:
# Split selectors onto separate lines after each comma, and have each
# individual selector occupy a single line.
SingleLinePerSelector:
- enabled: false
+ enabled: true
# Commas in lists should be followed by a space.
SpaceAfterComma:
@@ -191,7 +191,7 @@ linters:
# Variables should be formatted with a single space separating the colon
# from the variable's value.
SpaceAfterVariableColon:
- enabled: false
+ enabled: true
# Variables should be formatted with no space between the name and the
# colon.
@@ -201,7 +201,7 @@ linters:
# Operators should be formatted with a single space on both sides of an
# infix operator.
SpaceAroundOperator:
- enabled: false
+ enabled: true
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
@@ -219,11 +219,11 @@ 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:
- enabled: false
+ enabled: true
# Don't write trailing zeros for numeric values with a decimal point.
TrailingZero:
diff --git a/CHANGELOG b/CHANGELOG.md
index 7f96a9cba4e..60f932e1f76 100644
--- a/CHANGELOG
+++ b/CHANGELOG.md
@@ -1,39 +1,87 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.13.0 (unreleased)
+## 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 extra space on Build sidebar on Firefox !7060
+ - Fix HipChat notifications rendering (airatshigapov, eisnerd)
+ - Add hover to trash icon in notes !7008 (blackst0ne)
+ - Escape ref and path for relative links !6050 (winniehell)
+ - 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 bug where labels would be assigned to issues that were moved
+ - Fix error in generating labels
+ - Fix reply-by-email not working due to queue name mismatch
+ - Fixed hidden pipeline graph on commit and MR page !6895
+ - 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
+ - Fix unauthorized users dragging on issue boards
+ - Better handle when no users were selected for adding to group or project. (Linus Thiel)
+ - Only show register tab if signup enabled.
+
+## 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
- - Prevent running GfmAutocomplete setup for each diff note !6569
+ - 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
- - Do not alter 'force_remove_source_branch' options on MergeRequest unless specified
- Simplify Mentionable concern instance methods
- API: Ability to retrieve version information (Robert Schilling)
- Fix permission for setting an issue's due date
@@ -47,15 +95,20 @@ v 8.13.0 (unreleased)
- 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 showing commits from source project for merge request !6658
- 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)
@@ -68,14 +121,12 @@ v 8.13.0 (unreleased)
- 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:*)
- - Prevent flash alert text from being obscured when container is fluid
- 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)
- - Fix Long commit messages overflow viewport in file tree
- 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)
@@ -86,7 +137,7 @@ v 8.13.0 (unreleased)
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
- - Ignore deployment for statistics in Cycle Analytics, except in staging and production stages
+ - 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
@@ -94,10 +145,10 @@ v 8.13.0 (unreleased)
- 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)
- - Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein)
- 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)
@@ -105,21 +156,39 @@ v 8.13.0 (unreleased)
- 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)
-v 8.12.7
- - Use gitlab-markup gem instead of github-markup to fix `.rst` file rendering. !6659
+## 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
-v 8.12.6
- Update mailroom to 0.8.1 in Gemfile.lock !6814
-v 8.12.5
+## 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
@@ -127,7 +196,8 @@ v 8.12.5
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
-v 8.12.4
+## 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
@@ -142,10 +212,12 @@ v 8.12.4
- Set GitLab project exported file permissions to owner only
- Improve the way merge request versions are compared with each other
-v 8.12.3
+## 8.12.3
+
- Update Gitlab Shell to support low IO priority for storage moves
-v 8.12.2
+## 8.12.2
+
- Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted
@@ -161,11 +233,13 @@ v 8.12.2
- Fix resolve discussion buttons endpoint path
- Refactor remnants of CoffeeScript destructured opts and super !6261
-v 8.12.1
+## 8.12.1
+
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
- Fix issue with search filter labels not displaying
-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
@@ -221,6 +295,7 @@ v 8.12.0
- 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)
@@ -262,6 +337,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)
@@ -327,6 +403,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
@@ -354,22 +431,27 @@ v 8.12.0
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
-v 8.11.9
+## 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
-v 8.11.8
+## 8.11.8
+
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
- API: disable rails session auth for non-GET/HEAD requests
- Escape HTML nodes in builds commands in CI linter
-v 8.11.7
+## 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.
+ - 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
@@ -379,7 +461,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
@@ -392,7 +475,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
@@ -409,7 +493,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
@@ -420,17 +505,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)
@@ -583,48 +671,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.12
+## 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
-v 8.10.11
+## 8.10.11
+
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
- API: disable rails session auth for non-GET/HEAD requests
- Escape HTML nodes in builds commands in CI linter
-v 8.10.10
+## 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
@@ -635,7 +733,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
@@ -656,7 +755,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
@@ -665,7 +765,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)
@@ -829,26 +930,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.11
+## 8.9.11
+
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
- API: disable rails session auth for non-GET/HEAD requests
- Escape HTML nodes in builds commands in CI linter
-v 8.9.10
+## 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
@@ -857,7 +964,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
@@ -872,7 +980,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
@@ -886,7 +995,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
@@ -903,12 +1013,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
@@ -952,7 +1064,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
@@ -1107,21 +1220,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
@@ -1134,12 +1252,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
@@ -1158,7 +1278,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
@@ -1169,13 +1290,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
@@ -1251,34 +1374,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
@@ -1287,20 +1416,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
@@ -1310,7 +1442,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
@@ -1426,12 +1559,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
@@ -1444,12 +1579,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
@@ -1457,7 +1594,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
@@ -1466,11 +1604,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
@@ -1483,7 +1623,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
@@ -1505,7 +1646,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
@@ -1520,7 +1662,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)
@@ -1585,11 +1728,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
@@ -1600,41 +1745,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
@@ -1651,7 +1806,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
@@ -1671,7 +1827,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)
@@ -1750,11 +1907,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
@@ -1765,28 +1924,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
@@ -1797,7 +1963,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
@@ -1810,14 +1977,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
@@ -1890,11 +2059,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
@@ -1903,22 +2074,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
@@ -1934,17 +2111,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)
@@ -2012,11 +2192,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
@@ -2024,10 +2206,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
@@ -2036,7 +2220,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)
@@ -2046,11 +2231,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
@@ -2110,19 +2297,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
@@ -2132,10 +2322,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)
@@ -2220,11 +2412,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
@@ -2233,13 +2427,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
@@ -2253,10 +2449,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)
@@ -2321,5 +2519,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 0cdcb54b0ae..b4635e50c28 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -247,7 +247,7 @@ 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. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
-1. Add your changes to the [CHANGELOG](CHANGELOG):
+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
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index b60d71966ae..7ada0d303f3 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.8.4
+0.8.5
diff --git a/Gemfile b/Gemfile
index 5f754c1b66f..46245ab62d1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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'
@@ -262,8 +262,6 @@ group :development do
# thin instead webrick
gem 'thin', '~> 1.7.0'
-
- gem 'activerecord_sane_schema_dumper', '0.2'
end
group :development, :test do
@@ -310,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
diff --git a/Gemfile.lock b/Gemfile.lock
index a9892d1c130..442184b9228 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -473,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)
@@ -635,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)
@@ -915,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)
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/application.js b/app/assets/javascripts/application.js
index 8a61669822c..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 {
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index f336bfc36d6..f4c387a1a05 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 || document.documentElement.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');
}
};
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js.es6
index 294d2c9052c..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'),
@@ -42,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/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/dispatcher.js b/app/assets/javascripts/dispatcher.js.es6
index f3ef13ce20e..a1fe57562fa 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,6 +21,9 @@
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();
@@ -46,7 +50,7 @@
case 'projects:milestones:new':
case 'projects:milestones:edit':
new ZenMode();
- new DueDateSelect();
+ new gl.DueDateSelectors();
new GLForm($('.milestone-form'));
break;
case 'groups:milestones:new':
@@ -97,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();
@@ -116,6 +117,9 @@
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:commit:builds':
+ new gl.Pipelines();
+ break;
case 'projects:commits:show':
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
@@ -140,12 +144,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':
@@ -167,6 +171,8 @@
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();
@@ -291,6 +297,12 @@
}
};
+ 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/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index e034ca68645..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
@@ -487,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);
@@ -500,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
@@ -521,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;
}
@@ -529,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
@@ -567,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;
};
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..8e8f9f29ab3
--- /dev/null
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -0,0 +1,170 @@
+((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 () {
+ // register selectors here as needed
+ const validateSelectors = [':text', ':password', '[type=email]']
+ .map((selector) => `input${selector}`).join(',');
+
+ this.state.inputs = this.form.find(validateSelectors).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/labels_select.js b/app/assets/javascripts/labels_select.js
index f1e719937c7..b4f6e70f694 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -266,7 +266,7 @@
},
fieldName: $dropdown.data('field-name'),
id: function(label) {
- if (label.id <= 0) return;
+ if (label.id <= 0) return label.title;
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return label.id;
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 13ee794ba38..00000000000
--- a/app/assets/javascripts/merge_conflict_data_provider.js.es6
+++ /dev/null
@@ -1,347 +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() {
- // TODO: remove reliance on jQuery and DOM state introspection
- const diffViewType = $.cookie('diff_view');
- const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
-
- return {
- isLoading : true,
- hasError : false,
- isParallel : diffViewType === 'parallel',
- diffViewType : diffViewType,
- fixedLayout : fixedLayout,
- 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.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
- return;
- }
-
- vi.diffViewType = newType;
- vi.isParallel = newType === 'parallel';
- $.cookie('diff_view', newType, {
- path: (gon && gon.relative_url_root) || '/'
- });
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
- }
-
-
- 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 7e756433bf5..00000000000
--- a/app/assets/javascripts/merge_conflict_resolver.js.es6
+++ /dev/null
@@ -1,82 +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();
- });
-
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
- })
- }
-
-
- 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_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 8045d24a1bb..3dde979185b 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -71,6 +71,7 @@
this._location = location;
this.bindEvents();
this.activateTab(this.opts.action);
+ this.initAffix();
}
MergeRequestTabs.prototype.bindEvents = function() {
@@ -281,6 +282,7 @@
document.querySelector("div#builds").innerHTML = data.html;
gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
_this.buildsLoaded = true;
+ if (!this.pipelines) this.pipelines = new gl.Pipelines();
return _this.scrollToElement("#builds");
};
})(this)
@@ -380,6 +382,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/pipeline.js.es6 b/app/assets/javascripts/pipelines.js.es6
index 6bf63ee6979..a7624de6089 100644
--- a/app/assets/javascripts/pipeline.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -15,7 +15,7 @@
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
- graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+ graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
}
addMarginToBuildColumns() {
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 3cf41505814..478e82aa14d 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -4,9 +4,8 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
- this.$selects = $('.features select').filter(function () {
- return $(this).data('field');
- });
+ this.$selects = $('.features select');
+ this.$repoSelects = this.$selects.filter('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
@@ -16,6 +15,7 @@
})(this));
this.toggleSettings();
this.toggleSettingsOnclick();
+ this.toggleRepoVisibility();
}
ProjectNew.prototype.toggleSettings = function() {
@@ -43,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/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/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index 2ecf3b18975..bd4e3c3d00d 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -16,7 +16,13 @@
if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => {
- if (this.currentTemplate) this.setInputValueToTemplateContent(false);
+ this.setInputValueToTemplateContent();
+ });
+
+ $('.no-template', this.dropdown.parent()).on('click', () => {
+ this.currentTemplate = '';
+ this.setInputValueToTemplateContent();
+ $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
});
}
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 6aa0e1cd2b6..3020b7cc239 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -325,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/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 1e9a45c19b8..f1d36efb3de 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -1,10 +1,10 @@
// This file is based off animate.css 3.5.1, available here:
// https://github.com/daneden/animate.css/blob/3.5.1/animate.css
-//
+//
// animate.css - http://daneden.me/animate
// Version - 3.5.1
// Licensed under the MIT license - http://opensource.org/licenses/MIT
-//
+//
// Copyright (c) 2016 Daniel Eden
.animated {
@@ -37,7 +37,8 @@
}
@include keyframes(pulse) {
- from, to {
+ from,
+ to {
@include webkit-prefix(transform, scale3d(1, 1, 1));
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 8002e56724b..7e168092522 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -19,6 +19,7 @@
&.diff-collapsed {
padding: 5px;
+
.click-to-expand {
cursor: pointer;
}
@@ -127,7 +128,8 @@
position: relative;
.avatar-holder {
- .avatar, .identicon {
+ .avatar,
+ .identicon {
margin: 0 auto;
float: none;
}
@@ -203,6 +205,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 a7c8d782e9b..c0e9c8bf829 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -25,7 +25,7 @@
&:focus {
background-color: $hover-background;
color: $hover-text;
- border-color: $hover-border;;
+ border-color: $hover-border;
}
}
@@ -152,7 +152,8 @@
@include btn-blue-medium;
}
- &.btn-info {
+ &.btn-info,
+ &.btn-register {
@include btn-blue;
}
@@ -212,7 +213,8 @@
top: 2px;
}
- svg, .fa {
+ svg,
+ .fa {
&:not(:last-child) {
margin-right: 3px;
}
@@ -240,6 +242,7 @@
width: 100%;
margin: 0;
margin-bottom: 15px;
+
&.btn {
padding: 6px 0;
}
@@ -321,6 +324,7 @@
.btn-build {
margin-left: 10px;
+
i {
color: $gl-icon-color;
}
@@ -328,6 +332,7 @@
.clone-dropdown-btn a {
color: $dropdown-link-color;
+
&:hover {
text-decoration: none;
}
@@ -337,6 +342,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..ad5ac589d0f 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;
@@ -142,7 +143,8 @@ li.note {
}
}
-.wiki_content code, .readme code {
+.wiki_content code,
+.readme code {
background-color: inherit;
}
@@ -172,6 +174,7 @@ li.note {
@extend .col-md-6;
text-align: left;
margin-top: 40px;
+
pre {
background: white;
border: none;
@@ -197,6 +200,7 @@ li.note {
background: #c67;
color: #fff;
font-weight: bold;
+
a {
color: #fff;
text-decoration: underline;
@@ -227,6 +231,7 @@ li.note {
&.milestone-closed {
background: $gray-light;
}
+
.progress {
margin-bottom: 0;
margin-top: 4px;
@@ -286,6 +291,7 @@ table {
.footer-links {
margin-bottom: 20px;
+
a {
margin-right: 15px;
}
@@ -345,7 +351,8 @@ table {
margin-right: 10px;
}
-.alert, .progress {
+.alert,
+.progress {
margin-bottom: $gl-padding;
}
@@ -367,3 +374,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 baa95711329..1de246600fd 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -12,6 +12,7 @@
.dropdown-menu,
.dropdown-menu-nav {
display: block;
+
@media (max-width: $screen-xs-max) {
width: 100%;
}
@@ -48,6 +49,7 @@
margin-top: -6px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
+
&.fa-spinner {
font-size: 16px;
margin-top: -8px;
@@ -273,7 +275,8 @@
a {
padding-left: 25px;
- &.is-indeterminate, &.is-active {
+ &.is-indeterminate,
+ &.is-active {
&::before {
position: absolute;
left: 5px;
@@ -371,7 +374,8 @@
}
}
-.dropdown-input-field, .default-dropdown-input {
+.dropdown-input-field,
+.default-dropdown-input {
width: 100%;
min-height: 30px;
padding: 0 7px;
@@ -400,7 +404,7 @@
.dropdown-content {
max-height: 215px;
- overflow-y: scroll;
+ overflow-y: auto;
}
.dropdown-footer {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 81520500594..f49d7b92a00 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -26,15 +26,6 @@
padding: 10px $gl-padding;
word-wrap: break-word;
border-radius: 3px 3px 0 0;
- cursor: pointer;
-
- &:hover {
- background-color: $dark-background-color;
- }
-
- .diff-toggle-caret {
- padding-right: 6px;
- }
&.file-title-clear {
padding-left: 0;
@@ -66,6 +57,7 @@
margin-top: -3px;
}
}
+
.file-content {
background: #fff;
@@ -105,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;
@@ -130,6 +127,7 @@
margin-right: 0;
}
}
+
td.lines {
padding: 0;
}
@@ -146,8 +144,10 @@
border-left: 1px solid $border-color;
margin-bottom: 0;
background: white;
+
li {
color: #888;
+
p {
margin: 0;
color: #333;
@@ -167,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 a55dcf4a699..a9006de6d3e 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -18,7 +18,8 @@
margin: 0;
}
- .flash-notice, .flash-alert {
+ .flash-notice,
+ .flash-alert {
border-radius: $border-radius-default;
.container-fluid,
@@ -30,7 +31,8 @@
&.flash-container-page {
margin-bottom: 0;
- .flash-notice, .flash-alert {
+ .flash-notice,
+ .flash-alert {
border-radius: 0;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 05e8ee0190d..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 {
@@ -73,8 +73,8 @@ label {
}
.form-control {
- box-shadow: none;
- border-radius: 3px;
+ @include box-shadow(none);
+ border-radius: 2px;
padding: $gl-vert-padding $gl-input-padding;
}
@@ -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..3f877d86a26 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -25,7 +25,9 @@
a {
color: $color-light;
- &:hover, &:focus, &:active {
+ &:hover,
+ &:focus,
+ &:active {
background: $color-dark;
}
@@ -62,7 +64,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 9823abdde1f..142076f65b2 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -15,7 +15,8 @@ header {
margin: 8px 0;
text-align: center;
- .tanuki-logo, img {
+ .tanuki-logo,
+ img {
height: 36px;
}
}
@@ -54,7 +55,9 @@ header {
line-height: 28px;
text-align: center;
- &:hover, &:focus, &:active {
+ &:hover,
+ &:focus,
+ &:active {
background-color: $background-color;
}
@@ -125,7 +128,8 @@ header {
left: -50%;
}
- svg, img {
+ svg,
+ img {
height: 36px;
}
@@ -168,6 +172,7 @@ header {
a {
color: $gl-text-color;
+
&:hover {
text-decoration: underline;
}
@@ -221,7 +226,8 @@ header {
margin: 0;
float: none !important;
- .visible-xs, .visable-sm {
+ .visible-xs,
+ .visable-sm {
display: table-cell !important;
}
}
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..48e34a0066e 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;
@@ -75,14 +76,16 @@
/** light list with border-bottom between li **/
-ul.bordered-list, ul.unstyled-list {
+ul.bordered-list,
+ul.unstyled-list {
@include basic-list;
&.top-list {
li:first-child {
padding-top: 0;
- h4, h5 {
+ h4,
+ h5 {
margin-top: 0;
}
}
@@ -128,6 +131,10 @@ ul.content-list {
color: $gl-dark-link-color;
}
+ .member-group-link {
+ color: $blue-normal;
+ }
+
.description {
p {
@include str-truncated;
@@ -168,6 +175,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..429cfbe7235 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/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 7c207969b0a..f84ca36d10f 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -34,6 +34,7 @@
&.active {
background: $gray-light;
+
a {
font-weight: 600;
}
@@ -84,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 9fe390eb09d..c1ed43bc20f 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -79,7 +79,8 @@
padding-left: 15px !important;
}
- .nav-links, .nav-links {
+ .nav-links,
+ .nav-links {
li a {
font-size: 14px;
padding: 19px 10px;
@@ -99,18 +100,21 @@
@media (max-width: $screen-sm-max) {
.issues-filters {
- .milestone-filter, .labels-filter {
+ .milestone-filter,
+ .labels-filter {
display: none;
}
}
.page-title {
- .note-created-ago, .new-issue-link {
+ .note-created-ago,
+ .new-issue-link {
display: none;
}
}
- .issue_edited_ago, .note_edited_ago {
+ .issue_edited_ago,
+ .note_edited_ago {
display: none;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 8374f30d0b2..8cd49280e1c 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -3,7 +3,7 @@
padding: 15px;
.form-actions {
- margin: -$gl-padding+1;
+ margin: -$gl-padding + 1;
margin-top: 15px;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea43f4afc37..fcaf5e18633 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -54,7 +54,9 @@
color: #959494;
border-bottom: 2px solid transparent;
- &:hover, &:active, &:focus {
+ &:hover,
+ &:active,
+ &:focus {
text-decoration: none;
outline: none;
}
@@ -210,7 +212,12 @@
@media (max-width: $screen-xs-max) {
padding-bottom: 0;
width: 100%;
- .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
+
+ .btn,
+ form,
+ .dropdown,
+ .dropdown-menu-toggle,
+ .form-control {
margin: 0 0 10px;
display: block;
width: 100%;
@@ -244,7 +251,8 @@
}
&.adjust {
- .nav-text, .nav-controls {
+ .nav-text,
+ .nav-controls {
width: auto;
}
}
@@ -308,13 +316,15 @@
padding-top: 10px;
}
- a, i {
+ a,
+ i {
color: $layout-link-gray;
}
&.active {
- a, i {
+ a,
+ i {
color: $black;
}
@@ -327,7 +337,8 @@
}
&:hover {
- a, i {
+ a,
+ i {
color: $black;
}
}
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 79cd26714a3..ecdf0be1a05 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -3,7 +3,8 @@
width: 100% !important;
}
-.select2-container, .select2-container.select2-drop-above {
+.select2-container,
+.select2-container.select2-drop-above {
.select2-choice {
background: #fff;
border-color: $input-border;
@@ -71,7 +72,8 @@
}
.select2-container-active {
- .select2-choice, .select2-choices {
+ .select2-choice,
+ .select2-choices {
box-shadow: none;
}
}
@@ -93,7 +95,7 @@
background: none;
.select2-search-field input {
- padding: $gl-padding / 2;
+ padding: 5px $gl-padding / 2;
font-size: 13px;
height: auto;
font-family: inherit;
@@ -101,7 +103,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;
@@ -137,6 +139,7 @@
.select2-results {
max-height: 350px;
+
.select2-highlighted {
background: $gl-primary;
}
@@ -212,9 +215,11 @@
.group-image {
float: left;
}
+
.group-name {
font-weight: bold;
}
+
.group-path {
color: #999;
}
@@ -239,6 +244,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 ec52f326eb9..1d8e64a0e4b 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -185,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 {
@@ -207,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/tables.scss b/app/assets/stylesheets/framework/tables.scss
index b42075c98d0..9a90d3794fd 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -23,7 +23,8 @@ table {
}
tr {
- td, th {
+ td,
+ th {
padding: 10px $gl-padding;
line-height: 20px;
vertical-align: middle;
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..59f4594bb83 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;
}
@@ -117,7 +126,8 @@
box-shadow: none;
.panel-body {
- form, pre {
+ form,
+ pre {
margin: 0;
}
@@ -146,6 +156,7 @@
padding: 6px 15px;
font-size: 13px;
font-weight: normal;
+
a {
color: #777;
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 915aa631ef8..44fe37d3a4a 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -16,21 +16,21 @@
// $gray-light: lighten($gray-base, 46.7%) // #777
// $gray-lighter: lighten($gray-base, 93.5%) // #eee
-$brand-primary: $gl-primary;
-$brand-success: $gl-success;
-$brand-info: $gl-info;
-$brand-warning: $gl-warning;
-$brand-danger: $gl-danger;
+$brand-primary: $gl-primary;
+$brand-success: $gl-success;
+$brand-info: $gl-info;
+$brand-warning: $gl-warning;
+$brand-danger: $gl-danger;
-$border-radius-base: 3px !default;
-$border-radius-large: 3px !default;
-$border-radius-small: 3px !default;
+$border-radius-base: 3px !default;
+$border-radius-large: 3px !default;
+$border-radius-small: 3px !default;
//== Scaffolding
//
-$text-color: $gl-text-color;
-$link-color: $gl-link-color;
+$text-color: $gl-text-color;
+$link-color: $gl-link-color;
//== Typography
@@ -38,112 +38,112 @@ $link-color: $gl-link-color;
//## Font, line-height, and color for body text, headings, and more.
$font-family-sans-serif: $regular_font;
-$font-family-monospace: $monospace_font;
-$font-size-base: $gl-font-size;
+$font-family-monospace: $monospace_font;
+$font-size-base: $gl-font-size;
//== Components
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-$padding-base-vertical: $gl-vert-padding;
-$padding-base-horizontal: $gl-padding;
-$component-active-color: #fff;
-$component-active-bg: $brand-info;
+$padding-base-vertical: $gl-vert-padding;
+$padding-base-horizontal: $gl-padding;
+$component-active-color: #fff;
+$component-active-bg: $brand-info;
//== Forms
//
//##
-$input-color: $text-color;
-$input-border: $border-color;
-$input-border-focus: $focus-border-color;
-$legend-color: $text-color;
+$input-color: $text-color;
+$input-border: $border-color;
+$input-border-focus: $focus-border-color;
+$legend-color: $text-color;
//== Pagination
//
//##
-$pagination-color: $gl-gray;
-$pagination-bg: #fff;
-$pagination-border: $border-color;
+$pagination-color: $gl-gray;
+$pagination-bg: #fff;
+$pagination-border: $border-color;
-$pagination-hover-color: $gl-gray;
-$pagination-hover-bg: $row-hover;
-$pagination-hover-border: $border-color;
+$pagination-hover-color: $gl-gray;
+$pagination-hover-bg: $row-hover;
+$pagination-hover-border: $border-color;
-$pagination-active-color: $blue-dark;
-$pagination-active-bg: #fff;
-$pagination-active-border: $border-color;
+$pagination-active-color: $blue-dark;
+$pagination-active-bg: #fff;
+$pagination-active-border: $border-color;
-$pagination-disabled-color: #cdcdcd;
-$pagination-disabled-bg: $background-color;
-$pagination-disabled-border: $border-color;
+$pagination-disabled-color: #cdcdcd;
+$pagination-disabled-bg: $background-color;
+$pagination-disabled-border: $border-color;
//== Form states and alerts
//
//## Define colors for form feedback states and, by default, alerts.
-$state-success-text: #fff;
-$state-success-bg: $brand-success;
-$state-success-border: $brand-success;
+$state-success-text: #fff;
+$state-success-bg: $brand-success;
+$state-success-border: $brand-success;
-$state-info-text: #fff;
-$state-info-bg: $brand-info;
-$state-info-border: $brand-info;
+$state-info-text: #fff;
+$state-info-bg: $brand-info;
+$state-info-border: $brand-info;
-$state-warning-text: #fff;
-$state-warning-bg: $brand-warning;
-$state-warning-border: $brand-warning;
+$state-warning-text: #fff;
+$state-warning-bg: $brand-warning;
+$state-warning-border: $brand-warning;
-$state-danger-text: #fff;
-$state-danger-bg: $brand-danger;
-$state-danger-border: $brand-danger;
+$state-danger-text: #fff;
+$state-danger-bg: $brand-danger;
+$state-danger-border: $brand-danger;
//== Alerts
//
//## Define alert colors, border radius, and padding.
-$alert-border-radius: 0;
+$alert-border-radius: 0;
//== Panels
//
//##
-$panel-border-radius: 2px;
-$panel-default-text: $text-color;
-$panel-default-border: $border-color;
+$panel-border-radius: 2px;
+$panel-default-text: $text-color;
+$panel-default-border: $border-color;
$panel-default-heading-bg: $background-color;
-$panel-footer-bg: $background-color;
-$panel-inner-border: $border-color;
+$panel-footer-bg: $background-color;
+$panel-inner-border: $border-color;
//== Wells
//
//##
-$well-bg: $gray-light;
-$well-border: #eee;
+$well-bg: $gray-light;
+$well-border: #eee;
//== Code
//
//##
-$code-color: #c7254e;
-$code-bg: #f9f2f4;
+$code-color: #c7254e;
+$code-bg: #f9f2f4;
-$kbd-color: #fff;
-$kbd-bg: #333;
+$kbd-color: #fff;
+$kbd-bg: #333;
//== Buttons
//
//##
-$btn-default-color: $gl-text-color;
-$btn-default-bg: #fff;
-$btn-default-border: #e7e9ed;
+$btn-default-color: $gl-text-color;
+$btn-default-bg: #fff;
+$btn-default-border: #e7e9ed;
//== Nav
//
@@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding;
//== Code
//
//##
-$pre-bg: $background-color !default;
-$pre-color: $gl-gray !default;
+$pre-bg: $background-color !default;
+$pre-color: $gl-gray !default;
$pre-border-color: $border-color;
$table-bg-accent: $background-color;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index d099a884f54..266a8024809 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,11 +109,16 @@
@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;
@@ -123,16 +131,23 @@
font-weight: inherit;
}
- ul, ol {
+ ul,
+ ol {
padding: 0;
margin: 3px 0 3px 28px !important;
}
+ ul:dir(rtl),
+ ol:dir(rtl) {
+ margin: 3px 28px 3px 0 !important;
+ }
+
li {
line-height: 1.6em;
}
- a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] {
+ a[href*="/uploads/"],
+ a[href*="storage.googleapis.com/google-code-attachments/"] {
&:before {
margin-right: 4px;
@@ -155,7 +170,12 @@
}
/* Link to current header. */
- h1, h2, h3, h4, h5, h6 {
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
position: relative;
a.anchor {
@@ -203,7 +223,12 @@ body {
margin: 12px 7px;
}
-h1, h2, h3, h4, h5, h6 {
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
color: $gl-title-color;
font-weight: 600;
}
@@ -261,7 +286,10 @@ a > code {
text-decoration: line-through;
}
-h1, h2, h3, h4 {
+h1,
+h2,
+h3,
+h4 {
small {
color: $gl-gray;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4c34ed3ebf7..b271f8cf332 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -16,6 +16,7 @@ $white-light: #fff;
$white-normal: #ededed;
$white-dark: #ececec;
+$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
@@ -56,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;
@@ -82,39 +84,39 @@ $warning-message-border: #f0e2bb;
/*
* UI elements
*/
-$border-color: #e5e5e5;
-$focus-border-color: #3aabf0;
-$table-border-color: #f0f0f0;
-$background-color: $gray-light;
+$border-color: #e5e5e5;
+$focus-border-color: #3aabf0;
+$table-border-color: #f0f0f0;
+$background-color: $gray-light;
$dark-background-color: #f5f5f5;
-$table-text-gray: #8f8f8f;
+$table-text-gray: #8f8f8f;
/*
* Text
*/
-$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;
-$gl-link-color: #3084bb;
-$gl-dark-link-color: #333;
+$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;
+$gl-link-color: #3084bb;
+$gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f;
-$gl-icon-color: $gl-placeholder-color;
-$gl-grayish-blue: #7f8fa4;
-$gl-gray: $gl-text-color;
-$gl-gray-dark: #313236;
-$gl-gray-light: $gl-placeholder-color;
-$gl-header-color: #4c4e54;
+$gl-icon-color: $gl-placeholder-color;
+$gl-grayish-blue: #7f8fa4;
+$gl-gray: $gl-text-color;
+$gl-gray-dark: #313236;
+$gl-gray-light: $gl-placeholder-color;
+$gl-header-color: #4c4e54;
/*
* Lists
*/
-$list-font-size: $gl-font-size;
+$list-font-size: $gl-font-size;
$list-title-color: $gl-title-color;
-$list-text-color: $gl-text-color;
+$list-text-color: $gl-text-color;
$list-text-height: 42px;
/*
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 16ffbe57a99..d22d9b01495 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,20 +1,25 @@
/* https://github.com/MozMorris/tomorrow-pygments */
.code.dark {
// Line numbers
- .line-numbers, .diff-line-num {
+ .line-numbers,
+ .diff-line-num {
background-color: #1d1f21;
}
- .diff-line-num, .diff-line-num a {
+ .diff-line-num,
+ .diff-line-num a {
color: rgba(255, 255, 255, 0.3);
}
// Code itself
- pre.code, .diff-line-num {
+ pre.code,
+ .diff-line-num {
border-color: #666;
}
- &, pre.code, .line_holder .line_content {
+ &,
+ pre.code,
+ .line_holder .line_content {
background-color: #1d1f21;
color: #c5c8c6;
}
@@ -31,11 +36,13 @@
border-color: darken(#557, 15%);
}
- .diff-line-num.new, .line_content.new {
+ .diff-line-num.new,
+ .line_content.new {
@include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
}
- .diff-line-num.old, .line_content.old {
+ .diff-line-num.old,
+ .line_content.old {
@include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080);
}
@@ -55,68 +62,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..db8da8aab10 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,20 +1,25 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
.code.monokai {
// Line numbers
- .line-numbers, .diff-line-num {
+ .line-numbers,
+ .diff-line-num {
background-color: #272822;
}
- .diff-line-num, .diff-line-num a {
+ .diff-line-num,
+ .diff-line-num a {
color: rgba(255, 255, 255, 0.3);
}
// Code itself
- pre.code, .diff-line-num {
+ pre.code,
+ .diff-line-num {
border-color: #555;
}
- &, pre.code, .line_holder .line_content {
+ &,
+ pre.code,
+ .line_holder .line_content {
background-color: #272822;
color: #f8f8f2;
}
@@ -31,11 +36,13 @@
border-color: darken(#49483e, 15%);
}
- .diff-line-num.new, .line_content.new {
+ .diff-line-num.new,
+ .line_content.new {
@include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
}
- .diff-line-num.old, .line_content.old {
+ .diff-line-num.old,
+ .line_content.old {
@include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080);
}
@@ -55,65 +62,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..a87333146de 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,20 +1,25 @@
/* https://gist.github.com/qguv/7936275 */
.code.solarized-dark {
// Line numbers
- .line-numbers, .diff-line-num {
+ .line-numbers,
+ .diff-line-num {
background-color: #002b36;
}
- .diff-line-num, .diff-line-num a {
+ .diff-line-num,
+ .diff-line-num a {
color: rgba(255, 255, 255, 0.3);
}
// Code itself
- pre.code, .diff-line-num {
+ pre.code,
+ .diff-line-num {
border-color: #113b46;
}
- &, pre.code, .line_holder .line_content {
+ &,
+ pre.code,
+ .line_holder .line_content {
background-color: #002b36;
color: #93a1a1;
}
@@ -31,11 +36,13 @@
border-color: darken(#174652, 15%);
}
- .diff-line-num.new, .line_content.new {
+ .diff-line-num.new,
+ .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
}
- .diff-line-num.old, .line_content.old {
+ .diff-line-num.old,
+ .line_content.old {
@include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46);
}
@@ -72,72 +79,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..faff353ded7 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -7,20 +7,25 @@
.code.solarized-light {
// Line numbers
- .line-numbers, .diff-line-num {
+ .line-numbers,
+ .diff-line-num {
background-color: #fdf6e3;
}
- .diff-line-num, .diff-line-num a {
+ .diff-line-num,
+ .diff-line-num a {
color: $black-transparent;
}
// Code itself
- pre.code, .diff-line-num {
+ pre.code,
+ .diff-line-num {
border-color: #c5d0d4;
}
- &, pre.code, .line_holder .line_content {
+ &,
+ pre.code,
+ .line_holder .line_content {
background-color: #fdf6e3;
color: #586e75;
}
@@ -37,11 +42,13 @@
border-color: darken(#ddd8c5, 15%);
}
- .diff-line-num.new, .line_content.new {
+ .diff-line-num.new,
+ .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
}
- .diff-line-num.old, .line_content.old {
+ .diff-line-num.old,
+ .line_content.old {
@include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4);
}
@@ -78,72 +85,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..d5367d5f3f0 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -7,20 +7,25 @@
.code.white {
// Line numbers
- .line-numbers, .diff-line-num {
+ .line-numbers,
+ .diff-line-num {
background-color: $background-color;
}
- .diff-line-num, .diff-line-num a {
+ .diff-line-num,
+ .diff-line-num a {
color: $black-transparent;
}
// Code itself
- pre.code, .diff-line-num {
+ pre.code,
+ .diff-line-num {
border-color: $table-border-gray;
}
- &, pre.code, .line_holder .line_content {
+ &,
+ pre.code,
+ .line_holder .line_content {
background-color: #fff;
color: #333;
}
@@ -86,7 +91,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/devise.scss b/app/assets/stylesheets/mailers/devise.scss
index 9495c5b3f37..b2bce482fde 100644
--- a/app/assets/stylesheets/mailers/devise.scss
+++ b/app/assets/stylesheets/mailers/devise.scss
@@ -5,13 +5,13 @@
// Styles defined here are embedded directly into the resulting email HTML via
// the `premailer` gem.
-$body-background-color: #363636;
+$body-background-color: #363636;
$message-background-color: #fafafa;
-$header-color: #6b4fbb;
-$body-color: #444;
-$cta-color: #e14329;
-$footer-link-color: #7e7e7e;
+$header-color: #6b4fbb;
+$body-color: #444;
+$cta-color: #e14329;
+$footer-link-color: #7e7e7e;
$font-family: Helvetica, Arial, sans-serif;
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..63396a6bb29 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 {
@@ -56,7 +56,8 @@
padding: 10px;
text-align: center;
- > div, p {
+ > div,
+ p {
display: inline;
margin: 0;
@@ -106,26 +107,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 +145,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 6e81c12aa55..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;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 194a39a8377..d6a55fbd464 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -137,6 +137,7 @@
.retry-link {
color: $gl-link-color;
+
&:hover {
text-decoration: underline;
}
@@ -194,7 +195,7 @@
.build-job {
position: relative;
- .fa {
+ .fa-arrow-right {
position: absolute;
left: 15px;
top: 20px;
@@ -204,20 +205,30 @@
&.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;
}
@@ -233,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/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 67a9d7d2cf7..87c453a7a27 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -12,7 +12,8 @@
border-color: $border-color;
}
- th, td {
+ th,
+ td {
padding: 10px $gl-padding;
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 53ec0002afe..8ecac08137b 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -2,14 +2,16 @@
display: block;
}
-.commit-author, .commit-committer {
+.commit-author,
+.commit-committer {
display: block;
color: #999;
font-weight: normal;
font-style: italic;
}
-.commit-author strong, .commit-committer strong {
+.commit-author strong,
+.commit-committer strong {
font-weight: bold;
font-style: normal;
}
@@ -51,6 +53,7 @@
margin-left: 4px;
}
}
+
.commit-committer-link,
.commit-author-link {
color: $gl-gray;
@@ -108,21 +111,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 +165,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..ad315cfae62 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -63,7 +63,8 @@
display: inline-block;
}
- .btn-clipboard, .btn-transparent {
+ .btn-clipboard,
+ .btn-transparent {
padding-left: 0;
padding-right: 0;
}
@@ -161,7 +162,9 @@
.branch-commit {
color: $gl-gray;
- .commit-id, .commit-row-message {
+
+ .commit-id,
+ .commit-row-message {
color: $gl-gray;
}
}
diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss
index 292225c5261..81e5cee240d 100644
--- a/app/assets/stylesheets/pages/confirmation.scss
+++ b/app/assets/stylesheets/pages/confirmation.scss
@@ -2,7 +2,12 @@
margin-bottom: 20px;
border-bottom: 1px solid #eee;
- > h1, h2, h3, h4, h5, h6 {
+ > h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
font-weight: 400;
}
@@ -10,7 +15,8 @@
margin-bottom: 20px;
}
- ul, ol {
+ ul,
+ ol {
padding-left: 0;
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index d732008de3d..572e1e7d558 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -9,15 +9,15 @@
padding: 24px 0;
border-bottom: none;
position: relative;
-
+
@media (max-width: $screen-sm-min) {
padding: 6px 0 24px;
- }
+ }
}
.column {
text-align: center;
-
+
@media (max-width: $screen-sm-min) {
padding: 15px 0;
}
@@ -36,7 +36,7 @@
&:last-child {
text-align: right;
-
+
@media (max-width: $screen-sm-min) {
text-align: center;
}
@@ -51,7 +51,7 @@
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
-
+
}
.content-list {
@@ -73,10 +73,10 @@
font-weight: 600;
color: $gl-title-color;
}
-
+
&.text {
color: $layout-link-gray;
-
+
&.value-col {
color: $gl-title-color;
}
@@ -108,13 +108,13 @@
.svg-container {
text-align: center;
-
+
svg {
width: 136px;
height: 136px;
}
}
-
+
.inner-content {
@media (max-width: $screen-sm-min) {
padding: 0 28px;
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..0f0c0abe7ae 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -13,16 +13,19 @@
color: #5c5d5e;
}
- .issue_created_ago, .author_link {
+ .issue_created_ago,
+ .author_link {
white-space: nowrap;
}
}
.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..e0367d1d942 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;
@@ -112,7 +124,8 @@
}
}
- .old_line, .new_line {
+ .old_line,
+ .new_line {
margin: 0;
padding: 0;
border: none;
@@ -123,15 +136,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 +167,12 @@
white-space: pre-wrap;
}
}
+
.image {
background: #ddd;
text-align: center;
padding: 30px;
+
.wrap {
display: inline-block;
}
@@ -163,6 +181,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 +190,7 @@
background-position: 0 0, 5px 5px;
max-width: 100%;
}
+
&.deleted {
border: 1px solid $deleted;
}
@@ -179,6 +199,7 @@
border: 1px solid $added;
}
}
+
.image-info {
font-size: 12px;
margin: 5px 0 0;
@@ -193,6 +214,7 @@
margin: auto;
position: relative;
}
+
.swipe-wrap {
overflow: hidden;
border-left: 1px solid #999;
@@ -201,10 +223,12 @@
top: 13px;
right: 7px;
}
+
.frame {
top: 0;
right: 0;
position: absolute;
+
&.deleted {
margin: 0;
display: block;
@@ -212,6 +236,7 @@
right: 7px;
}
}
+
.swipe-bar {
display: block;
height: 100%;
@@ -219,14 +244,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 +263,7 @@
top: 0;
background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
}
+
.bottom-handle {
display: block;
height: 14px;
@@ -252,12 +281,15 @@
margin: auto;
position: relative;
}
- .frame.added, .frame.deleted {
+
+ .frame.added,
+ .frame.deleted {
position: absolute;
display: block;
top: 0;
left: 0;
}
+
.controls {
display: block;
height: 14px;
@@ -311,12 +343,14 @@
}
//.view.onion-skin
}
+
.view-modes {
padding: 10px;
text-align: center;
background: #eee;
- ul, li {
+ ul,
+ li {
list-style: none;
margin: 0;
padding: 0;
@@ -328,19 +362,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 fcc5f32c738..cb8cefaca97 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -15,6 +15,7 @@
.cancel-btn {
color: #b94a48;
+
&:hover {
color: #b94a48;
}
@@ -70,23 +71,29 @@
.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 {
+ .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 3f19e920166..fc49ff780fc 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -37,14 +37,22 @@
.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;
}
@@ -52,13 +60,13 @@
}
}
-.table.builds.environments {
+.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/errors.scss b/app/assets/stylesheets/pages/errors.scss
index 32d2d7b1dbf..11309817d31 100644
--- a/app/assets/stylesheets/pages/errors.scss
+++ b/app/assets/stylesheets/pages/errors.scss
@@ -2,7 +2,9 @@
max-width: 400px;
margin: 0 auto;
- h1, h2, h3 {
+ h1,
+ h2,
+ h3 {
text-align: center;
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 789d6237df8..3004959ff7b 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,6 +87,7 @@
margin-right: 5px;
}
}
+
.event_icon {
position: relative;
float: right;
@@ -95,12 +97,13 @@
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;
}
@@ -137,7 +142,7 @@
.event-last-push {
overflow: auto;
width: 100%;
-
+
.event-last-push-text {
@include str-truncated(100%);
padding: 4px 0;
@@ -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 185ce970e71..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;
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..3e7fc3fa52c 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -37,12 +37,14 @@ ul.related-merge-requests > li {
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
+
.merge-request-id {
flex-shrink: 0;
}
}
-.merge-requests-title, .related-branches-title {
+.merge-requests-title,
+.related-branches-title {
font-size: 16px;
font-weight: 600;
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 701c29a3986..397f89f501a 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -1,5 +1,6 @@
.suggest-colors {
margin-top: 5px;
+
a {
border-radius: 4px;
width: 30px;
@@ -65,7 +66,21 @@
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 {
@@ -208,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 a5ca509163d..3d2b024fe5c 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,15 @@
}
}
- .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 +54,7 @@
margin: 0 0 10px;
}
+
.login-footer {
margin-top: 10px;
@@ -58,42 +65,159 @@
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;
+ }
- &.top {
- border-radius: 5px 5px 0 0;
- margin-bottom: 0;
+ &: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;
+
+ &:hover {
+ border-bottom: 2px solid $link-underline-blue;
+ }
+ }
+ }
}
+ }
+
+ // 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.
- &.bottom {
- border-radius: 0 0 5px 5px;
- border-top: 0;
- margin-bottom: 20px;
+ .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;
- border-radius: 0;
+ a {
+ font-size: 16px;
}
+ }
+
- &:active, &:focus {
+ .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 +225,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 +244,64 @@
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..032feae8854 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -101,7 +101,8 @@ $colors: (
@mixin color-scheme($color) {
- .header.line_content, .diff-line-num {
+ .header.line_content,
+ .diff-line-num {
&.origin {
background-color: map-get($colors, #{$color}_header_origin_neutral);
border-color: map-get($colors, #{$color}_header_origin_neutral);
@@ -131,6 +132,7 @@ $colors: (
}
}
}
+
&.head {
background-color: map-get($colors, #{$color}_header_head_neutral);
border-color: map-get($colors, #{$color}_header_head_neutral);
@@ -174,6 +176,7 @@ $colors: (
background-color: map-get($colors, #{$color}_line_not_chosen);
}
}
+
&.head {
background-color: map-get($colors, #{$color}_line_head_neutral);
@@ -235,4 +238,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 7cf69c56d15..70afa568554 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -10,6 +10,7 @@
form {
margin-bottom: 0;
+
.clearfix {
margin-bottom: 0;
}
@@ -46,6 +47,7 @@
&.right {
float: right;
+
a {
color: $gl-gray;
}
@@ -121,6 +123,10 @@
color: #5c5d5e;
}
+ .js-deployment-link {
+ display: inline-block;
+ }
+
.mr-widget-body {
h4 {
font-weight: 600;
@@ -177,6 +183,15 @@
.ci-coverage {
float: right;
}
+
+ .stop-env-container {
+ color: $gl-text-color;
+ float: right;
+
+ a {
+ color: $gl-text-color;
+ }
+ }
}
.mr_source_commit,
@@ -188,6 +203,7 @@
padding-top: 2px;
padding-bottom: 2px;
list-style: none;
+
&:hover {
background: none;
}
@@ -211,6 +227,7 @@
padding-top: 20px;
padding-bottom: 10px;
}
+
svg {
width: 230px;
}
@@ -276,12 +293,6 @@
line-height: 31px;
}
-.builds {
- .table-holder {
- overflow-x: auto;
- }
-}
-
.panel-new-merge-request {
.panel-heading {
padding: 5px 10px;
@@ -369,7 +380,7 @@
}
.table-holder {
- .builds {
+ .ci-table {
th {
background-color: $white-light;
@@ -427,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 8c2ba3ed58c..13402acd8e1 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -50,7 +50,8 @@
}
}
-.issues-sortable-list, .merge_requests-sortable-list {
+.issues-sortable-list,
+.merge_requests-sortable-list {
.issuable-detail {
display: block;
margin-top: 7px;
@@ -59,6 +60,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..16ddef481bd 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -8,9 +8,10 @@
.diff-file .diff-content {
tr.line_holder:hover > td .line_note_link {
opacity: 1.0;
- filter: alpha(opacity=100);
+ filter: alpha(opacity = 100);
}
}
+
.diff-file,
.discussion {
.new-note {
@@ -23,7 +24,8 @@
display: none;
}
-.new-note, .note-edit-form {
+.new-note,
+.note-edit-form {
.note-form-actions {
margin-top: $gl-padding;
}
@@ -194,6 +196,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 d399f84a2ff..b90c91831f2 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -28,7 +28,8 @@ ul.notes {
}
}
- .note-created-ago, .note-updated-at {
+ .note-created-ago,
+ .note-updated-at {
white-space: nowrap;
}
@@ -147,9 +148,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;
}
@@ -345,6 +355,7 @@ ul.notes {
width: 32px;
// "hide" it by default
display: none;
+
&:hover {
background: $gl-info;
color: #fff;
@@ -448,7 +459,7 @@ ul.notes {
.discussion-next-btn {
svg {
margin: 0;
-
+
path {
fill: $gray-darkest;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 7843355f0ab..f88175365c6 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -20,13 +20,20 @@
margin: 4px;
}
- .table.builds {
+ .table.ci-table {
min-width: 1200px;
- .branch-commit {
- width: 33%;
+ .pipeline-id {
+ color: $black;
}
+ .branch-commit {
+ width: 30%;
+
+ .branch-name {
+ max-width: 195px;
+ }
+ }
}
}
@@ -44,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;
@@ -66,6 +80,10 @@
border-top-width: 1px;
}
+ .build.retried {
+ background-color: $gray-lightest;
+ }
+
.commit-link {
.ci-status {
@@ -81,6 +99,15 @@
}
}
+ .avatar {
+ margin-left: 0;
+ float: none;
+ }
+
+ .api {
+ color: $code-color;
+ }
+
.branch-commit {
.branch-name {
@@ -102,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 {
@@ -118,10 +144,6 @@
text-overflow: ellipsis;
}
- .avatar {
- margin-left: 0;
- }
-
.label {
margin-right: 4px;
}
@@ -137,17 +159,11 @@
.icon-container {
display: inline-block;
- text-align: right;
- width: 15px;
+ width: 10px;
- .fa {
- position: relative;
- right: 3px;
- }
-
- svg {
- position: relative;
- right: 1px;
+ &.commit-icon {
+ width: 15px;
+ text-align: center;
}
}
@@ -176,7 +192,7 @@
&::after {
content: '';
width: 8px;
- position: absolute;;
+ position: absolute;
right: -7px;
bottom: 8px;
border-bottom: 2px solid $border-color;
@@ -232,7 +248,8 @@
font-size: 14px;
}
- svg, .fa {
+ svg,
+ .fa {
margin-right: 0;
}
}
@@ -353,9 +370,6 @@
&:hover {
background-color: $gray-lighter;
- .dropdown-menu-toggle {
- background-color: transparent;
- }
}
&.playable {
@@ -385,6 +399,15 @@
}
}
+ .tooltip {
+ white-space: nowrap;
+
+ .tooltip-inner {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
.ci-status-text {
width: 135px;
white-space: nowrap;
@@ -402,6 +425,7 @@
}
.dropdown-menu-toggle {
+ background-color: transparent;
border: none;
width: auto;
padding: 0;
@@ -506,7 +530,8 @@
// Connect each build (except for first) with curved lines
&:not(:first-child) {
- &::after, &::before {
+ &::after,
+ &::before {
content: '';
top: -49px;
position: absolute;
@@ -532,10 +557,12 @@
// Connect second build to first build with smaller curved line
&:nth-child(2) {
- &::after, &::before {
+ &::after,
+ &::before {
height: 29px;
top: -9px;
}
+
.curve {
display: block;
}
@@ -546,7 +573,8 @@
.build {
// Remove right connecting horizontal line from first build in last stage
&:first-child {
- &::after, &::before {
+ &::after,
+ &::before {
border: none;
}
}
@@ -621,19 +649,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 c7eac5cf4b9..3f6fdaebc1d 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -243,6 +243,7 @@
.btn {
-webkit-flex-grow: 1;
flex-grow: 1;
+
&:first-child {
margin-left: 0;
}
@@ -252,7 +253,8 @@
}
table.u2f-registrations {
- th:not(:last-child), td:not(:last-child) {
+ th:not(:last-child),
+ td:not(:last-child) {
border-right: solid 1px transparent;
}
} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 530fb0c0d05..f6355941837 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -6,56 +6,79 @@
}
}
-.no-ssh-key-message, .project-limit-message {
+.no-ssh-key-message,
+.project-limit-message {
background-color: #f28d35;
margin-bottom: 0;
}
.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;
@@ -201,6 +224,7 @@
pointer-events: none;
}
}
+
.count {
@include btn-gray;
display: inline-block;
@@ -361,10 +385,13 @@ a.deploy-project-label {
margin: $gl-padding;
text-align: center;
width: 169px;
- &:hover, &.forked {
+
+ &:hover,
+ &.forked {
background-color: $row-hover;
border-color: $row-hover-border;
}
+
.no-avatar {
width: 100px;
height: 100px;
@@ -372,17 +399,20 @@ a.deploy-project-label {
border: 1px solid $gray-dark;
margin: 0 auto;
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;
@@ -644,6 +674,7 @@ pre.light-well {
.clone-options {
display: table-cell;
+
a.btn {
width: 100%;
}
@@ -705,7 +736,8 @@ pre.light-well {
.table-bordered {
border-radius: 1px;
- th:not(:last-child), td:not(:last-child) {
+ th:not(:last-child),
+ td:not(:last-child) {
border-right: solid 1px transparent;
}
}
@@ -728,7 +760,8 @@ pre.light-well {
}
}
-.project-refs-form .dropdown-menu, .dropdown-menu-projects {
+.project-refs-form .dropdown-menu,
+.dropdown-menu-projects {
width: 300px;
@media (min-width: $screen-sm-min) {
@@ -744,62 +777,6 @@ pre.light-well {
.dropdown-menu {
width: 300px;
}
-
- &.from .compare-dropdown-toggle {
- width: 237px;
- }
-
- &.to .compare-dropdown-toggle {
- width: 254px;
- }
-
- .dropdown-toggle-text {
- display: block;
- height: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
-}
-
-.compare-ellipsis {
- display: inline;
-}
-
-@media (max-width: $screen-xs-max) {
- .compare-form-group {
- .input-group {
- width: 100%;
-
- & > .compare-dropdown-toggle {
- width: 100%;
- }
- }
-
- .dropdown-menu {
- width: 100%;
- }
- }
-
- .compare-switch-container {
- text-align: center;
- padding: 0 0 $gl-padding;
-
- .commits-compare-switch {
- float: none;
- }
- }
-
- .compare-ellipsis {
- display: block;
- text-align: center;
- padding: 0 0 $gl-padding;
- }
-
- .commits-compare-btn {
- width: 100%;
- }
}
.clearable-input {
@@ -832,8 +809,36 @@ pre.light-well {
.form-control {
min-width: 100px;
}
+
.select2-choice {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
+
+.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/search.scss b/app/assets/stylesheets/pages/search.scss
index e77f9816d8a..6d472e8293f 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -65,7 +65,8 @@
.search-input-wrap {
width: 100%;
- .search-icon, .clear-icon {
+ .search-icon,
+ .clear-icon {
position: absolute;
right: 5px;
top: 0;
@@ -185,7 +186,8 @@
padding-right: $gl-padding + 15px;
}
- .btn-search, .btn-new {
+ .btn-search,
+ .btn-new {
width: 100%;
margin-top: 5px;
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index c05f3d5ff32..01426e28e92 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -65,6 +65,7 @@
.ci-status-icon-success {
color: $gl-success;
}
+
.ci-status-icon-failed {
color: $gl-danger;
}
@@ -73,10 +74,11 @@
.ci-status-icon-success_with_warning {
color: $gl-warning;
}
-
+
.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/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 41ad10f07bd..2b836fa1f4a 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;
@@ -22,17 +23,18 @@
border-bottom: 1px solid $table-border-gray;
border-top: 1px solid $table-border-gray;
- td, th {
+ td,
+ th {
line-height: 21px;
}
.last-commit {
@include str-truncated(506px);
-
+
@media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
@include str-truncated(450px);
}
-
+
}
.commit-history-link-spacer {
@@ -73,7 +75,8 @@
max-width: 320px;
vertical-align: middle;
- i, a {
+ i,
+ a {
color: $gl-dark-link-color;
}
@@ -168,4 +171,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/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index a30b6492572..8239b7e6879 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -1,7 +1,24 @@
-.wiki h1, .wiki h2, .wiki h3, .wiki h4, .wiki h5, .wiki h6 {margin-top: 17px; }
-.wiki h1 {font-size: 30px;}
-.wiki h2 {font-size: 22px;}
-.wiki h3 {font-size: 18px; font-weight: bold; }
+.wiki h1,
+.wiki h2,
+.wiki h3,
+.wiki h4,
+.wiki h5,
+.wiki h6 {
+ margin-top: 17px;
+}
+
+.wiki h1 {
+ font-size: 30px;
+}
+
+.wiki h2 {
+ font-size: 22px;
+}
+
+.wiki h3 {
+ font-size: 18px;
+ font-weight: bold;
+}
header,
nav,
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 b3455e04c29..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
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/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/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 18cd800c619..940a3ad20ba 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,6 +21,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
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],
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 3ec173abcdb..36d246d185b 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -2,8 +2,8 @@ class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
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/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 71eb56aed0b..a2b01ff43dc 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -72,10 +72,10 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
+ labels: true,
only: [:iid, :title, :confidential],
include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
})
end
end
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
index 76ae41319c4..67e3c9add81 100644
--- a/app/controllers/projects/boards/lists_controller.rb
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -76,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/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/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 7a7475a7345..ae060abee5c 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -1,6 +1,7 @@
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
@@ -27,9 +28,26 @@ class Projects::GroupLinksController < Projects::ApplicationController
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 96041b07647..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
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index a6626df4826..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
@@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
else
respond_to do |format|
- format.html { render 'new' }
+ format.html { render :new }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end
@@ -49,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
@@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
def destroy
@label.destroy
+ @labels = find_labels
respond_to do |format|
format.html do
@@ -80,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
@@ -119,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 869d96b86f4..2ee53f7ceda 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,15 +9,15 @@ 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, :assign_related_issues
+ :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]
@@ -33,14 +33,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authenticate_user!, only: [:assign_related_issues]
- before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
+ 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
@@ -170,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?
@@ -184,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
@@ -385,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)
@@ -403,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
@@ -449,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
@@ -483,7 +525,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars
@pipeline = @merge_request.pipeline
- @pipelines = [@pipeline].compact
end
def define_commit_vars
@@ -510,6 +551,15 @@ 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
@@ -525,10 +575,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
- @pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses.relevant if @pipeline
@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
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index f56b256984b..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
+ @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
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 62916270172..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
@@ -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
]
}
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/issuable_finder.rb b/app/finders/issuable_finder.rb
index 9f170428100..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?
@@ -274,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..95e62cdb02a
--- /dev/null
+++ b/app/finders/labels_finder.rb
@@ -0,0 +1,94 @@
+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)
+ return items if title.nil?
+ return items.none if title.blank?
+
+ items.where(title: title)
+ 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] || params[:name]
+ 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/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
index b7247ffa8b2..38c586ccd31 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -5,7 +5,7 @@ module BoardsHelper
{
endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id,
- disabled: !can?(current_user, :admin_list, @project),
+ disabled: "#{!can?(current_user, :admin_list, @project)}",
issue_link_base: namespace_project_issues_path(@project.namespace, @project)
}
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/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_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 692fadd505f..03b2db1bc91 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -124,6 +124,10 @@ module IssuablesHelper
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
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index b9f3d6c75c2..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,7 +137,10 @@ module LabelsHelper
end
def labels_filter_path
+ return group_labels_path(@group, :json) if @group
+
project = @target_project || @project
+
if project
namespace_project_labels_path(project.namespace, project, :json)
else
@@ -124,11 +149,24 @@ module LabelsHelper
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 249cb44e9d5..a6659ea2fd1 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -86,11 +86,15 @@ module MergeRequestsHelper
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
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 e667c9e4e2e..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}]", id: "project_project_feature_attributes_#{field}", 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/mailers/.gitkeep b/app/mailers/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/mailers/.gitkeep
+++ /dev/null
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 2444702104e..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
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5dbf66173de..bf5f92f8462 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,9 +1,10 @@
module Ci
class Build < CommitStatus
include TokenAuthenticatable
+ include AfterCommitQueue
- belongs_to :runner, class_name: 'Ci::Runner'
- belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+ belongs_to :runner
+ belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
serialize :options
@@ -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
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 957f6755b2e..adda3b8f40c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -7,19 +7,19 @@ module Ci
self.table_name = 'ci_commits'
- belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+ belongs_to :project, foreign_key: :gl_project_id
belongs_to :user
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
- has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
- has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
+ has_many :builds, foreign_key: :commit_id
+ has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
validates_presence_of :sha, unless: :importing?
validates_presence_of :ref, unless: :importing?
validates_presence_of :status, unless: :importing?
validate :valid_commit_sha, unless: :importing?
- after_save :keep_around_commits, unless: :importing?
+ after_create :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses
@@ -49,26 +49,25 @@ 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
- end
-
- before_transition do |pipeline|
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
after_transition [:created, :pending, :running] => :success do |pipeline|
@@ -76,7 +75,11 @@ module Ci
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
@@ -148,7 +151,7 @@ module Ci
def retryable?
builds.latest.any? do |build|
- build.failed? && build.retryable?
+ (build.failed? || build.canceled?) && build.retryable?
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 44cb19ece3b..123930273e0 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -6,9 +6,9 @@ module Ci
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
- has_many :builds, class_name: 'Ci::Build'
- has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
- has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
+ has_many :builds
+ has_many :runner_projects, dependent: :destroy
+ has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 4b44ffa886e..1f9baeca5b1 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -2,8 +2,8 @@ module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
- belongs_to :runner, class_name: 'Ci::Runner'
- belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+ belongs_to :runner
+ belongs_to :project, foreign_key: :gl_project_id
validates_uniqueness_of :runner_id, scope: :gl_project_id
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index a0b19b51a12..62889fe80d8 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,8 +4,8 @@ module Ci
acts_as_paranoid
- belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+ belongs_to :project, foreign_key: :gl_project_id
+ has_many :trigger_requests, dependent: :destroy
validates_presence_of :token
validates_uniqueness_of :token
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index fc674871743..2b807731d0d 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -2,9 +2,9 @@ module Ci
class TriggerRequest < ActiveRecord::Base
extend Ci::Model
- belongs_to :trigger, class_name: 'Ci::Trigger'
- belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
- has_many :builds, class_name: 'Ci::Build'
+ belongs_to :trigger
+ belongs_to :pipeline, foreign_key: :commit_id
+ has_many :builds
serialize :variables
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 6959223aed9..94d9e2b3208 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,7 +2,7 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
- belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+ belongs_to :project, foreign_key: :gl_project_id
validates_uniqueness_of :key, scope: :gl_project_id
validates :key,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7b554be4f9a..4cb3a69416e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
- belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+ belongs_to :project, foreign_key: :gl_project_id
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user
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 9f64f76721d..ef3e73a4072 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -5,6 +5,7 @@ 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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index c4b42ad82c7..17c3b526c97 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -145,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]).
@@ -230,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/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/deployment.rb b/app/models/deployment.rb
index 82b27b78229..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 :create_ref
+ after_create :create_ref
def commit
project.commit(sha)
@@ -34,13 +34,20 @@ class Deployment < ActiveRecord::Base
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!
@@ -77,9 +84,24 @@ class Deployment < ActiveRecord::Base
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', id.to_s)
+ 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 f0f3ee23223..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
@@ -48,7 +66,32 @@ class Environment < ActiveRecord::Base
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 0764cb8cabd..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
@@ -115,6 +116,10 @@ class Event < ActiveRecord::Base
action == LEFT
end
+ def expired?
+ action == EXPIRED
+ end
+
def destroyed?
action == DESTROYED
end
@@ -124,7 +129,7 @@ class Event < ActiveRecord::Base
end
def membership_changed?
- joined? || left?
+ joined? || left? || expired?
end
def created_project?
@@ -184,6 +189,8 @@ class Event < ActiveRecord::Base
'joined'
elsif left?
'left'
+ elsif expired?
+ 'removed due to membership expiration from'
elsif destroyed?
'destroyed'
elsif commented?
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/group.rb b/app/models/group.rb
index a2f88cca828..552e1154df6 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -6,7 +6,7 @@ class Group < Namespace
include AccessRequestable
include Referable
- has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember'
+ has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :group_members
has_many :users, through: :group_members
has_many :owners,
@@ -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
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 abd58e0454a..89158a50353 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -138,6 +138,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
@@ -207,7 +211,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?
@@ -274,4 +284,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 e8e12e2904e..149fd98ecb3 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -15,34 +15,49 @@ class Label < ActiveRecord::Base
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
+
+ label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
+ on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))).
+ join_sources
- def self.prioritized
- where.not(priority: nil).reorder(:priority, :title)
+ 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
@@ -77,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
#
@@ -84,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)
@@ -137,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/members/group_member.rb b/app/models/members/group_member.rb
index 1b54a85d064..204f34f0269 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -1,7 +1,7 @@
class GroupMember < Member
SOURCE_TYPE = 'Namespace'
- belongs_to :group, class_name: 'Group', foreign_key: 'source_id'
+ belongs_to :group, foreign_key: 'source_id'
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 125f26369d7..008fff0857c 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -3,7 +3,7 @@ class ProjectMember < Member
include Gitlab::ShellAdapter
- belongs_to :project, class_name: 'Project', foreign_key: 'source_id'
+ belongs_to :project, foreign_key: 'source_id'
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
@@ -121,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 a743bf313ae..4872f8b8649 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -6,8 +6,8 @@ class MergeRequest < ActiveRecord::Base
include Taskable
include Importable
- belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
- belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
+ belongs_to :target_project, class_name: "Project"
+ belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs, dependent: :destroy
@@ -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
@@ -322,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?
- end
-
- def closed_without_source_project?
- closed? && !source_project
+ closed? && source_project_missing?
end
- def forked_source_project_missing?
+ def source_project_missing?
return false unless for_fork?
return true unless source_project
@@ -344,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
@@ -658,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?
@@ -688,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
@@ -784,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
@@ -868,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 b8a10b7968e..dd65a9a8b86 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -299,8 +299,10 @@ class MergeRequestDiff < ActiveRecord::Base
end
def keep_around_commits
- repository.keep_around(start_commit_sha)
- repository.keep_around(head_commit_sha)
- repository.keep_around(base_commit_sha)
+ [repository, merge_request.source_project.repository].each do |repo|
+ repo.keep_around(start_commit_sha)
+ repo.keep_around(head_commit_sha)
+ repo.keep_around(base_commit_sha)
+ end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 758927edd5c..fbf7012972e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -32,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
@@ -63,11 +63,11 @@ class Project < ActiveRecord::Base
alias_attribute :title, :name
# Relations
- belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
+ belongs_to :creator, class_name: 'User'
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
belongs_to :namespace
- has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
+ has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
# Project services
@@ -76,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
@@ -106,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
@@ -115,7 +116,7 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
- has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember'
+ has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
has_many :users, through: :project_members
@@ -136,7 +137,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
- has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
+ has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
@@ -387,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?
@@ -490,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
@@ -663,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
@@ -718,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
@@ -728,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
@@ -829,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
@@ -1297,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
@@ -1308,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
@@ -1342,6 +1339,13 @@ 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
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 530f7d5a30e..b37ce1d3cf6 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -13,23 +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)
# 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)
@@ -57,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_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/repository.rb b/app/models/repository.rb
index 608c99eed46..4ae9c20726f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -109,6 +109,10 @@ 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
args = %W(
@@ -117,9 +121,8 @@ class Repository
)
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)
@@ -416,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
@@ -431,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
@@ -470,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.
@@ -719,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
diff --git a/app/models/service.rb b/app/models/service.rb
index 66c804f2b06..625fbc48302 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -196,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
@@ -218,7 +219,7 @@ class Service < ActiveRecord::Base
redmine
slack
teamcity
- )
+ ]
end
def self.create_from_template(project_id, template)
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/user.rb b/app/models/user.rb
index f367f4616fb..9e76df63d31 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -47,7 +47,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
- has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace"
+ has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id
# Profile
has_many :keys, dependent: :destroy
@@ -66,17 +66,17 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
- has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, class_name: 'ProjectMember'
+ has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
- has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
+ has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
- has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event"
+ has_many :events, dependent: :destroy, foreign_key: :author_id
has_many :subscriptions, dependent: :destroy
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
@@ -309,7 +309,7 @@ class User < ActiveRecord::Base
username
end
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, _target_project = nil)
"#{self.class.reference_prefix}#{username}"
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 97ff6233968..b65fb68cd88 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -19,6 +19,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 be4721d7a51..fbb3d4507d6 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -162,11 +162,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
@@ -183,13 +185,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/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index abc7aeece39..fe0d762ccd2 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -3,7 +3,7 @@ module Boards
class CreateService < BaseService
def execute(board)
List.transaction do
- label = project.labels.find(params[:label_id])
+ label = available_labels.find(params[:label_id])
position = next_position(board)
create_list(board, label, position)
@@ -12,6 +12,10 @@ module Boards
private
+ 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
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
index d8048f1c67e..939f9bfd068 100644
--- a/app/services/boards/lists/generate_service.rb
+++ b/app/services/boards/lists/generate_service.rb
@@ -19,8 +19,7 @@ module Boards
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
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/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/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/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
index f636e5fec4f..066efa1acc3 100644
--- a/app/services/merge_requests/assign_issues_service.rb
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
@assignable_issues ||= begin
if current_user == merge_request.author
closes_issues.select do |issue|
- !issue.assignee_id? && can?(current_user, :admin_issue, issue)
+ !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue)
end
else
[]
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/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/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/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/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 e4ae3dec8aa..5a81194a5f4 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -116,8 +116,10 @@ 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)
@@ -248,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/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/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index b760b42fde0..37bb6a3b0e0 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -75,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 a5e82e55cc1..73038164056 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -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
+ %table.table.ci-table.runner-builds
%thead
%tr
%th Build
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..05246303fb6
--- /dev/null
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -0,0 +1,6 @@
+%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
+ - if signin_enabled? && signup_enabled?
+ %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/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/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/_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/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 a9a384bd5f3..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 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/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/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/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 5590198a20e..d3987fc9c4f 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -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/_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/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/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index d8f16022407..c6d718a1cd1 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -26,7 +26,7 @@
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- %a.has-tooltip{ ":href" => "'/' + 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/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 966633f1f89..b1053028279 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
@@ -124,9 +123,9 @@
%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('arrow-right')
= ci_icon_for_status(build.status)
@@ -135,11 +134,5 @@
= 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 f3747ba2a21..36294c89fa8 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -5,7 +5,7 @@
.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/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 9089586a89d..7e83a88913a 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,5 +1,5 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- %span{class: 'hidden-xs hidden-sm'}
+ %span{class: 'hidden-xs hidden-sm download-button'}
.dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 9248adfde80..94632056b15 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -6,7 +6,7 @@
- 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))
@@ -27,7 +27,7 @@
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
- else
.light none
- .icon-container
+ .icon-container.commit-icon
= custom_icon("icon_commit")
- if commit_sha
@@ -35,8 +35,9 @@
- 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.')
+ = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
.label-container
- if build.tags.any?
@@ -47,8 +48,6 @@
%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
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 017d3ff6af2..55965172d3f 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,10 +1,10 @@
- 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
+ = 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
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 36eadbd2bf1..c6f359f5679 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -9,17 +9,15 @@
= ci_icon_for_status(status)
- else
= ci_status_with_icon(status)
- %td.branch-commit
+
+ %td
= 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"
+ %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?
@@ -29,6 +27,16 @@
- if pipeline.builds.any?(&:stuck?)
%span.label.label-warning stuck
+ %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)
@@ -36,16 +44,15 @@
- 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
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 288c06d9b67..d6916fb7f1a 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -56,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_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
index 5d0d5ba0262..f2d71fa6989 100644
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -1,5 +1,5 @@
- group_status = CommitStatus.where(id: subject).status
-%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+%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
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/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 76b68c544aa..7bde20c3286 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -10,7 +10,7 @@
= 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"
- .compare-ellipsis ...
+ .compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml
index 27d928c87a0..05fb37cdc0f 100644
--- a/app/views/projects/compare/_ref_dropdown.html.haml
+++ b/app/views/projects/compare/_ref_dropdown.html.haml
@@ -1,5 +1,5 @@
.dropdown-menu.dropdown-menu-selectable
- = dropdown_title "Select branch/tag"
- = dropdown_filter "Filter by branch/tag"
+ = dropdown_title "Select Git revision"
+ = dropdown_filter "Filter by Git revision"
= dropdown_content
= dropdown_loading
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 22c4a75d213..58a214bdbd1 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,28 +1,15 @@
-- if can?(current_user, :create_deployment, deployment) && deployment.deployable
- .pull-right
-
- - external_url = deployment.environment.external_url
- - if external_url
- = link_to external_url, target: '_blank', class: 'btn external-url' do
- = icon('external-link')
-
- - 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 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 ca0005abd0c..9238f232c7e 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -17,4 +17,6 @@
#{time_ago_with_tooltip(deployment.created_at)}
%td.hidden-xs
- = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
+ .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/edit.html.haml b/app/views/projects/edit.html.haml
index c8f84b96cb7..30473d14b9b 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -46,62 +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', data: { field: 'lfs_enabled' }
+ = 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 project
- = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
+ - 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
@@ -280,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/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index 251694e897c..b75d5df4150 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -28,4 +28,8 @@
#{time_ago_with_tooltip(last_deployment.created_at)}
%td.hidden-xs
- = render 'projects/deployments/actions', deployment: last_deployment
+ .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 ab801409722..8f555afcf11 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,14 +3,27 @@
= 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
.environments-container
- - if @environments.blank?
+ - if @all_environments.blank?
.blank-state.blank-state-no-icon
%h2.blank-state-title
You don't have any environments right now.
@@ -24,7 +37,7 @@
New environment
- else
.table-holder
- %table.table.builds.environments
+ %table.table.ci-table.environments
%tbody
%th Environment
%th Last Deployment
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 7a8d196cf4e..bcac73d3698 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -3,14 +3,16 @@
= 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
.deployments-container
- if @deployments.blank?
@@ -24,7 +26,7 @@
= link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
- %table.table.builds.environments
+ %table.table.ci-table.environments
%thead
%tr
%th ID
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 0a66d60accc..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,9 +1,10 @@
-- if subject.target_url
- = link_to subject.target_url do
+%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
- %span.ci-status-icon
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
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/_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 3a6fbbc7fbc..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.to_reference} #{@issue.title}", "Issues"
+- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
%h3.page-title
Edit Issue ##{@issue.iid}
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 09347ad5fff..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.to_reference} #{@issue.title}", "Issues"
+- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
@@ -53,7 +53,7 @@
.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_field(@issue, :title)
- if @issue.description.present?
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
deleted file mode 100644
index 71f7f354d72..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
- = icon('caret-down')
- .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_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index da6927879a4..9c6f562f7db 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -29,7 +29,11 @@
= link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- - if @pipeline
+ - 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
@@ -44,9 +48,11 @@
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
- - if @pipeline
+ - 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
@@ -59,5 +65,5 @@
:javascript
var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
- buildsLoaded: "#{@pipeline ? 'true' : 'false'}"
+ 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 47dd51639b5..f57abe73977 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests"
+- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
@@ -26,19 +26,19 @@
%ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
- - 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
@@ -47,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/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 7c3ac6652ee..03159f123f3 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests"
+- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
%h3.page-title
Edit Merge Request #{@merge_request.to_reference}
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index 0b05785430b..61020516bcf 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -3,4 +3,4 @@
Most recent commits displayed first
%ol#commits-list.list-unstyled
- = render "projects/commits/commits", project: @merge_request.project
+ = render "projects/commits/commits", project: @merge_request.source_project
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 5b7f83c344f..a82c846baa7 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -44,17 +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
- 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
- %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')}
- = icon('external-link', right: true)
+.js-success-icon.hidden
+ = ci_icon_for_status('success')
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/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 e62f810a521..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_field(@milestone, :title)
- %div
- - if @milestone.description.present?
- .description
- .wiki
- = preserve do
- = markdown_field(@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/new.html.haml b/app/views/projects/new.html.haml
index cc8cb134fb8..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
@@ -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 73fe6a715fa..ab719e38904 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -57,7 +57,7 @@
= 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
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 7d421c0e740..b10dd47709f 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,7 +1,7 @@
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
+ .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
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 2d1df095bfa..4bc49072f35 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -43,13 +43,14 @@
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.builds
+ %table.table.ci-table
%thead
- %th.col-xs-1.col-sm-1 Status
- %th.col-xs-2.col-sm-4 Pipeline
- %th.col-xs-2.col-sm-2 Stages
- %th.col-xs-2.col-sm-2
- %th.hidden-xs.col-sm-3
+ %th Status
+ %th Pipeline
+ %th Commit
+ %th Stages
+ %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/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 858af78f7bf..51b0939564e 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -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/show.html.haml b/app/views/projects/show.html.haml
index ea4deb6cb28..ba16c641462 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -12,72 +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"
-
- = 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?
@@ -86,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/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 6f593e8dff9..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_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/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 31620297be0..ed93857e6d4 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -29,8 +29,9 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- .filter-item.inline.reset-filters
- %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
+ - 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
@@ -77,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 c3f4e10c954..d410755cad1 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -23,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' }
@@ -85,20 +87,20 @@
.issuable-form-select-holder
- 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: "Filter by 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", show_menu_above: true } })
+ = 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) }
.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_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input"
+ = 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}" }
.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, show_menu_above: 'true' }
+ = 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 6d307611640..22b5a6aa11b 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -8,6 +8,7 @@
- classes = local_assigns.fetch(:classes, [])
- 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
@@ -23,7 +24,7 @@
= 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 }
+ = 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 ab3cc33d18f..f27a9002ec2 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -2,9 +2,10 @@
- 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: "Filter by milestone", toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
+= 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
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index f8059988038..7363ead09ff 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -107,7 +107,7 @@
= 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" } }
@@ -171,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 6ab6ae50389..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
@@ -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/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/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
index c541daba50e..c4cb4733482 100644
--- a/app/workers/clear_database_cache_worker.rb
+++ b/app/workers/clear_database_cache_worker.rb
@@ -1,6 +1,7 @@
# This worker clears all cache fields in the database, working in batches.
class ClearDatabaseCacheWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
BATCH_SIZE = 1000
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 174eabff9fd..a27585fd389 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -1,5 +1,6 @@
class ExpireBuildArtifactsWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
Rails.logger.info 'Scheduling removal of build artifacts'
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 916c2e633c1..eb403c134d1 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -1,11 +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
+ build = Ci::Build
+ .with_expired_artifacts
+ .reorder(nil)
+ .find_by(id: build_id)
- Rails.logger.info "Removing artifacts build #{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
index f44227d7086..357e4a9a1c3 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -1,7 +1,6 @@
class PipelineProcessWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 5dd443fea59..2aa6fff24da 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -1,6 +1,6 @@
class PipelineSuccessWorker
include Sidekiq::Worker
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 44a7f24e401..96c4152c674 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -1,7 +1,6 @@
class PipelineUpdateWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
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
index df4c4a6628b..0531630d13a 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -1,7 +1,6 @@
class TrendingProjectsWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :trending_projects
+ include CronjobQueue
def perform
Rails.logger.info('Refreshing trending projects')
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 962ffe0708d..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")
@@ -87,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"
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/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 83c3a42c19f..659ea51bc75 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -88,4 +88,6 @@ Rails.application.routes.draw do
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
root to: "root#index"
+
+ get '*unmatched_route', to: 'application#not_found'
end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 47a8a0a53d4..4838c9d91c6 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,8 +1,14 @@
require 'constraints/group_url_constrainer'
constraints(GroupUrlConstrainer.new) do
- scope(path: ':id', as: :group, controller: :groups) 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
@@ -22,5 +28,7 @@ resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
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/project.rb b/config/routes/project.rb
index f9d58f5d5b2..8142e231621 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -267,12 +267,14 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
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
@@ -317,7 +319,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
end
- resources :environments
+ resources :environments, except: [:destroy] do
+ member do
+ post :stop
+ end
+ end
resource :cycle_analytics, only: [:show]
@@ -407,7 +413,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
end
- resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
+ 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
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 54bbcb18f6a..0a9c924863d 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -1,8 +1,5 @@
require 'constraints/user_url_constrainer'
-get '/u/:username', to: redirect('/%{username}'),
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
-
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
passwords: :passwords,
@@ -23,7 +20,7 @@ constraints(UserUrlConstrainer.new) do
end
end
-scope(path: 'u/:username',
+scope(path: 'users/:username',
as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
controller: :users) do
@@ -33,5 +30,15 @@ scope(path: 'u/:username',
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/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 803cbca584d..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 },
]
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/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 a362fd8f228..02282b0f666 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161007133303) do
+ActiveRecord::Schema.define(version: 20161024042317) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -380,6 +380,7 @@ ActiveRecord::Schema.define(version: 20161007133303) do
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
@@ -404,6 +405,7 @@ ActiveRecord::Schema.define(version: 20161007133303) do
t.datetime "updated_at"
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
@@ -517,6 +519,17 @@ ActiveRecord::Schema.define(version: 20161007133303) 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"
@@ -525,13 +538,13 @@ ActiveRecord::Schema.define(version: 20161007133303) do
t.datetime "updated_at"
t.boolean "template", default: false
t.string "description"
- t.integer "priority"
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
@@ -830,6 +843,7 @@ ActiveRecord::Schema.define(version: 20161007133303) do
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
@@ -1210,6 +1224,9 @@ ActiveRecord::Schema.define(version: 20161007133303) do
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
diff --git a/doc/README.md b/doc/README.md
index 7e3d9b00900..c30bf328003 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -49,6 +49,7 @@
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
- [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.
diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md
index a2c358af095..b95c425842c 100644
--- a/doc/administration/integration/koding.md
+++ b/doc/administration/integration/koding.md
@@ -61,6 +61,7 @@ executing commands in the following snippet.
```bash
git clone https://github.com/koding/koding.git
cd koding
+docker-compose -f docker-compose-init.yml run init
docker-compose up
```
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/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/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/builds.md b/doc/api/builds.md
index e8a9e4743d3..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
@@ -64,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": ""
}
},
@@ -108,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": ""
}
}
@@ -132,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
@@ -212,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": ""
}
}
@@ -279,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/commits.md b/doc/api/commits.md
index 3e20beefb8a..e1ed99d98d3 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -288,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",
@@ -319,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
@@ -343,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"
@@ -370,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"
},
@@ -408,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",
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/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/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 f96bf7f6d63..b69db90e70d 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -465,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"
},
@@ -482,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": {
@@ -528,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"
},
@@ -552,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,
@@ -567,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"
}
@@ -1333,8 +1333,6 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `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
+| `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/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/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 a52b2d51d78..a50ba5432fe 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,
"bio": null,
@@ -81,7 +81,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,
"bio": null,
@@ -141,7 +141,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,
"bio": null,
@@ -172,7 +172,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,
"bio": null,
@@ -293,7 +293,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,
"bio": null,
@@ -643,7 +643,7 @@ Parameters:
| `id` | integer | yes | The ID of the user |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/user/:id/events
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/users/:id/events
```
Example response:
@@ -665,7 +665,7 @@ Example response:
"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"
},
@@ -682,7 +682,7 @@ Example response:
"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": {
@@ -728,7 +728,7 @@ Example response:
"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"
},
@@ -752,7 +752,7 @@ Example response:
"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,
@@ -767,7 +767,7 @@ Example response:
"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"
}
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 520c8b36a95..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].
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 08fbd9afa2f..ffc310ec8c7 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -13,6 +13,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [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/yaml/README.md b/doc/ci/yaml/README.md
index cdf5ecc7a84..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 of course implies that the underlying server which hosts the application
+is properly configured.
-This environment should be accessible under `https://branch-name.review.example.com/`.
+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.
@@ -1102,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/development/README.md b/doc/development/README.md
index 630fe64cee6..14d6f08e43a 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -16,7 +16,8 @@
- [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md)
-- [SQL guidelines](sql.md) for SQL guidelines
+- [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/doc_styleguide.md b/doc/development/doc_styleguide.md
index 882da2a6562..4cc581dd991 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
@@ -466,4 +466,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/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/install/installation.md b/doc/install/installation.md
index 1fa8678223a..c9acc9cdfb0 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -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.4
+ sudo -u git -H git checkout v0.8.5
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md
index a669bb28904..19d46135930 100644
--- a/doc/monitoring/performance/gitlab_configuration.md
+++ b/doc/monitoring/performance/gitlab_configuration.md
@@ -1 +1 @@
-This document was moved to [administration/monitoring/performance/gitlab_configuration](../administration/monitoring/performance/gitlab_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 93320b40174..0d4be02ff5f 100644
--- a/doc/monitoring/performance/grafana_configuration.md
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -1 +1 @@
-This document was moved to [administration/monitoring/performance/grafana_configuration](../administration/monitoring/performance/grafana_configuration.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 02647de1eb0..15fd275e916 100644
--- a/doc/monitoring/performance/influxdb_configuration.md
+++ b/doc/monitoring/performance/influxdb_configuration.md
@@ -1 +1 @@
-This document was moved to [administration/monitoring/performance/influxdb_configuration](../administration/monitoring/performance/influxdb_configuration.md).
+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 a989e323e04..e53f9701dc3 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -1 +1 @@
-This document was moved to [administration/monitoring/performance/influxdb_schema](../administration/monitoring/performance/influxdb_schema.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 ab3f3ac1664..ae88baa0c14 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -1 +1 @@
-This document was moved to [administration/monitoring/performance/introduction](../administration/monitoring/performance/introduction.md).
+This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/introduction.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 26baffdf792..fc0cd1b8af2 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -30,6 +30,10 @@ 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:
diff --git a/doc/university/README.md b/doc/university/README.md
index 8b3538d5616..f5a0dab39fe 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -64,21 +64,22 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
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
-1. [Getting Help](/getting-help/)
+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](/training)
+1. [GitLab Training Workshops](https://about.gitlab.com/training)
#### 1.8 GitLab Training Material
-1. [Git and GitLab Terminology](/glossary/)
+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)
@@ -209,7 +210,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
*Some content can only be accessed by GitLab team members*
-1. [Support Path](/support/)
+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.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 07743d050f7..cddfa7e3e01 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -72,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
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 00d63c1b3c6..8940d14559b 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -84,7 +84,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.8.4
+sudo -u git -H git checkout v0.8.5
sudo -u git -H make
```
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/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/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 5253825d507..60b7bec2ba7 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -87,20 +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.
-
-You need to make sure that your installation has HTTPS cloning enabled.
-HTTPS support is required by GitLab CI to clone all sources.
-
## Build triggers
[Build triggers][triggers] do not support the new permission model.
@@ -152,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
@@ -239,6 +254,12 @@ test:
This will make GitLab CI initialize (fetch) and update (checkout) all your
submodules recursively.
+If Git does not use the newly added relative URLs but still uses your old URLs,
+you might need to add `git submodule sync --recursive` to your `.gitlab-ci.yml`,
+prior to running `git submodule update --init --recursive`. This transfers the
+changes from your `.gitmodules` file into the `.git` folder, which is kept by
+runners between runs.
+
In case your environment or your Docker image doesn't have Git installed,
you have to either ask your Administrator or install the missing dependency
yourself:
@@ -286,7 +307,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/settings/import_export.md b/doc/user/project/settings/import_export.md
index 65ed9fae4ec..dfc762fe1d3 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -22,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/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/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/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/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/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 b8264f97687..244306e8464 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -21,7 +21,7 @@ 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
@@ -42,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
@@ -97,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
@@ -182,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/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/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/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/boards.rb b/lib/api/boards.rb
index 9b71d335128..4ac491edc1b 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -3,19 +3,28 @@ module API
class Boards < Grape::API
before { authenticate! }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get the project board
+ 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].to_i == board.id
+ if params[:board_id] == board.id
board
else
not_found!('Board')
@@ -27,31 +36,37 @@ module API
end
end
- # Get the lists of a project board
- # Does not include `backlog` and `done` lists
+ 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
- # Get a list of a project board
+ 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
- # Create a new board list
- #
- # Parameters:
- # id (required) - The ID of a project
- # label_id (required) - The ID of an existing label
- # Example Request:
- # POST /projects/:id/boards/:board_id/lists
+ 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
- required_attributes! [:label_id]
-
- unless user_project.labels.exists?(params[:label_id])
- render_api_error!({ error: "Label not found!" }, 400)
+ unless available_labels.exists?(params[:label_id])
+ render_api_error!({ error: 'Label not found!' }, 400)
end
authorize!(:admin_list, user_project)
@@ -68,21 +83,21 @@ module API
end
end
- # Moves a board list to a new position
- #
- # Parameters:
- # id (required) - The ID of a project
- # board_id (required) - The ID of a board
- # position (required) - The position of the list
- # Example Request:
- # PUT /projects/:id/boards/:board_id/lists/:list_id
+ 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].to_i })
+ { position: params[:position] })
if service.execute(list)
present list, with: Entities::List
@@ -91,14 +106,13 @@ module API
end
end
- # Delete a board list
- #
- # Parameters:
- # id (required) - The ID of a project
- # board_id (required) - The ID of a board
- # list_id (required) - The ID of a board list
- # Example Request:
- # DELETE /projects/:id/boards/:board_id/lists/:list_id
+ 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)
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 14ddc8c9a62..2f2cf769481 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -6,33 +6,42 @@ 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
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'
@@ -41,7 +50,6 @@ module API
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
@@ -65,79 +73,82 @@ module API
end
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 '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/helpers.rb b/lib/api/helpers.rb
index 67473f300c9..45120898b76 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -71,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)
@@ -118,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
@@ -197,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
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/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/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/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/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/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
index e008fd428b0..f3bd587c28b 100644
--- a/lib/banzai/filter/html_entity_filter.rb
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -5,7 +5,7 @@ module Banzai
# Text filter that escapes these HTML entities: & " < >
class HtmlEntityFilter < HTML::Pipeline::TextFilter
def call
- ERB::Util.html_escape(text)
+ ERB::Util.html_escape_once(text)
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/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 4fa8d05481f..f09d78be0ce 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -52,8 +52,8 @@ module Banzai
relative_url_root,
context[:project].path_with_namespace,
uri_type(file_path),
- ref,
- file_path
+ Addressable::URI.escape(ref),
+ Addressable::URI.escape(file_path)
].compact.join('/').squeeze('/').chomp('/')
uri
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/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/renderer.rb b/lib/banzai/renderer.rb
index 6924a293da8..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
#
@@ -141,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/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 2fd1fced65c..3e33c9399e2 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -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/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb
index 23920193743..91b70143f11 100644
--- a/lib/constraints/namespace_url_constrainer.rb
+++ b/lib/constraints/namespace_url_constrainer.rb
@@ -1,6 +1,9 @@
class NamespaceUrlConstrainer
def matches?(request)
- id = request.path.sub(/\A\/+/, '').split('/').first.sub(/.atom\z/, '')
+ 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)
@@ -10,4 +13,12 @@ class NamespaceUrlConstrainer
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/event_filter.rb b/lib/event_filter.rb
index 96e70e37e8f..21f6a9a762b 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -45,9 +45,16 @@ class EventFilter
when EventFilter.comments
actions = [Event::COMMENTED]
when EventFilter.team
- actions = [Event::JOINED, Event::LEFT]
+ actions = [Event::JOINED, Event::LEFT, Event::EXPIRED]
when EventFilter.all
- actions = [Event::PUSHED, Event::MERGED, Event::COMMENTED, Event::JOINED, Event::LEFT]
+ actions = [
+ Event::PUSHED,
+ Event::MERGED,
+ Event::COMMENTED,
+ Event::JOINED,
+ Event::LEFT,
+ Event::EXPIRED
+ ]
end
events.where(action: actions)
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index e4d996a3fb6..9b74364849e 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -113,17 +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
- @commit = @repo.commit(@options[:extended_sha1])
- end
- if @path.empty? && !@commit
- @id = @ref = extract_ref_without_atom(@id)
+ if @options[:extended_sha1].present?
+ @commit = @repo.commit(@options[:extended_sha1])
+ else
@commit = @repo.commit(@ref)
- request.format = :atom if @commit
+ 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
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index d0060fbaca1..9cec71a3222 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -47,8 +47,8 @@ module Gitlab
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)
+ @secret_token = SecureRandom.hex(16)
+ File.write(secret_file, @secret_token)
end
link_path = File.join(shell_path, '.gitlab_shell_secret')
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/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
new file mode 100644
index 00000000000..b1a6d5fe0f6
--- /dev/null
+++ b/lib/gitlab/ee_compat_check.rb
@@ -0,0 +1,261 @@
+# rubocop: disable Rails/Output
+module Gitlab
+ # Checks if a set of migrations requires downtime or not.
+ class EeCompatCheck
+ EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+
+ attr_reader :ce_branch, :check_dir, :ce_repo
+
+ def initialize(branch:, check_dir:, ce_repo: nil)
+ @ce_branch = branch
+ @check_dir = check_dir
+ @ce_repo = ce_repo || 'https://gitlab.com/gitlab-org/gitlab-ce.git'
+ end
+
+ def check
+ ensure_ee_repo
+ delete_patches
+
+ generate_patch(ce_branch, ce_patch_full_path)
+
+ Dir.chdir(check_dir) do
+ step("In the #{check_dir} directory")
+
+ step("Pulling latest master", %w[git pull --ff-only origin master])
+
+ status = catch(:halt_check) do
+ ce_branch_compat_check!
+
+ delete_ee_branch_locally
+
+ ee_branch_presence_check!
+
+ ee_branch_compat_check!
+ end
+
+ delete_ee_branch_locally
+ delete_patches
+
+ if status.nil?
+ true
+ else
+ false
+ end
+ end
+ end
+
+ private
+
+ def ensure_ee_repo
+ if Dir.exist?(check_dir)
+ step("#{check_dir} already exists")
+ else
+ cmd = %W[git clone --branch master --single-branch --depth 1 #{EE_REPO} #{check_dir}]
+ step("Cloning #{EE_REPO} into #{check_dir}", cmd)
+ end
+ end
+
+ def ce_branch_compat_check!
+ cmd = %W[git apply --check #{ce_patch_full_path}]
+ status = step("Checking if #{ce_patch_name} applies cleanly to EE/master", cmd)
+
+ if status.zero?
+ puts ce_applies_cleanly_msg(ce_branch)
+ throw(:halt_check)
+ end
+ end
+
+ def ee_branch_presence_check!
+ status = step("Fetching origin/#{ee_branch}", %W[git fetch origin #{ee_branch}])
+
+ unless status.zero?
+ puts
+ puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+
+ throw(:halt_check, :ko)
+ end
+ end
+
+ def ee_branch_compat_check!
+ step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD])
+
+ generate_patch(ee_branch, ee_patch_full_path)
+ cmd = %W[git apply --check #{ee_patch_full_path}]
+ status = step("Checking if #{ee_patch_name} applies cleanly to EE/master", cmd)
+
+ unless status.zero?
+ puts
+ puts ee_branch_doesnt_apply_cleanly_msg
+
+ throw(:halt_check, :ko)
+ end
+
+ puts
+ puts ee_applies_cleanly_msg
+ end
+
+ def generate_patch(branch, filepath)
+ FileUtils.rm(filepath, force: true)
+
+ depth = 0
+ loop do
+ depth += 10
+ step("Fetching origin/master", %W[git fetch origin master --depth=#{depth}])
+ status = step("Finding merge base with master", %W[git merge-base FETCH_HEAD #{branch}])
+
+ break if status.zero? || depth > 500
+ end
+
+ raise "#{branch} is too far behind master, please rebase it!" if depth > 500
+
+ step("Generating the patch against master")
+ output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
+ throw(:halt_check, :ko) unless status.zero?
+
+ File.write(filepath, output)
+ throw(:halt_check, :ko) unless File.exist?(filepath)
+ end
+
+ def delete_ee_branch_locally
+ command(%w[git checkout master])
+ step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}])
+ end
+
+ def delete_patches
+ step("Deleting #{ce_patch_full_path}")
+ FileUtils.rm(ce_patch_full_path, force: true)
+
+ step("Deleting #{ee_patch_full_path}")
+ FileUtils.rm(ee_patch_full_path, force: true)
+ end
+
+ def ce_patch_name
+ @ce_patch_name ||= "#{ce_branch}.patch"
+ end
+
+ def ce_patch_full_path
+ @ce_patch_full_path ||= File.expand_path(ce_patch_name, check_dir)
+ end
+
+ def ee_branch
+ @ee_branch ||= "#{ce_branch}-ee"
+ end
+
+ def ee_patch_name
+ @ee_patch_name ||= "#{ee_branch}.patch"
+ end
+
+ def ee_patch_full_path
+ @ee_patch_full_path ||= File.expand_path(ee_patch_name, check_dir)
+ end
+
+ def step(desc, cmd = nil)
+ puts "\n=> #{desc}\n"
+
+ if cmd
+ puts "\n$ #{cmd.join(' ')}"
+ command(cmd)
+ end
+ end
+
+ def command(cmd)
+ output, status = Gitlab::Popen.popen(cmd)
+ puts output
+
+ status
+ end
+
+ def ce_applies_cleanly_msg(ce_branch)
+ <<-MSG.strip_heredoc
+ =================================================================
+ 🎉 Congratulations!! 🎉
+
+ The #{ce_branch} branch applies cleanly to EE/master!
+
+ Much ❤️!!
+ =================================================================\n
+ MSG
+ end
+
+ def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+ <<-MSG.strip_heredoc
+ =================================================================
+ 💥 Oh no! 💥
+
+ The #{ce_branch} branch does not apply cleanly to the current
+ EE/master, and no #{ee_branch} branch was found 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.
+
+ There are different ways to create such branch:
+
+ 1. Create a new branch based on the CE branch and rebase it on top of EE/master
+
+ # In the EE repo
+ $ git fetch #{ce_repo} #{ce_branch}
+ $ git checkout -b #{ee_branch} FETCH_HEAD
+
+ # You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit
+ # before rebasing to limit the conflicts-resolving steps during the rebase
+ $ git fetch origin
+ $ git rebase origin/master
+
+ At this point you will likely have conflicts.
+ Solve them, and continue/finish the rebase.
+
+ You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE".
+
+ 2. Create a new branch from master and cherry-pick your CE commits
+
+ # In the EE repo
+ $ git fetch origin
+ $ git checkout -b #{ee_branch} FETCH_HEAD
+ $ git fetch #{ce_repo} #{ce_branch}
+ $ git cherry-pick SHA # Repeat for all the commits you want to pick
+
+ You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit.
+
+ Don't forget to push your branch to #{EE_REPO}:
+
+ # In the EE repo
+ $ git push origin #{ee_branch}
+
+ You can then retry this failed build, and hopefully it should pass.
+
+ Stay 💪 !
+ =================================================================\n
+ MSG
+ end
+
+ def ee_branch_doesnt_apply_cleanly_msg
+ <<-MSG.strip_heredoc
+ =================================================================
+ 💥 Oh no! 💥
+
+ The #{ce_branch} does not apply cleanly to the current
+ EE/master, and even though a #{ee_branch} branch exists in the EE
+ repository, it does not apply cleanly either to EE/master!
+
+ Please update the #{ee_branch}, push it again to #{EE_REPO}, and
+ retry this build.
+
+ Stay 💪 !
+ =================================================================\n
+ MSG
+ end
+
+ def ee_applies_cleanly_msg
+ <<-MSG.strip_heredoc
+ =================================================================
+ 🎉 Congratulations!! 🎉
+
+ The #{ee_branch} branch applies cleanly to EE/master!
+
+ Much ❤️!!
+ =================================================================\n
+ MSG
+ end
+ 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/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/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/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/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
index b9e4042220a..f755a404693 100644
--- a/lib/gitlab/import_export/attribute_cleaner.rb
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -1,7 +1,7 @@
module Gitlab
module ImportExport
class AttributeCleaner
- ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES
+ ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
def self.clean!(relation_hash:)
relation_hash.reject! do |key, _value|
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 5a109f24f9f..7cdba880a93 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -110,7 +110,7 @@ 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,
+ relation_hash: parsed_relation_hash(relation_hash),
members_mapper: members_mapper,
user: @user,
project_id: restored_project.id)
@@ -118,6 +118,10 @@ module Gitlab
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/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 9300f789e1b..dc630e76411 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -9,7 +9,10 @@ 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
@@ -19,9 +22,7 @@ module Gitlab
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
@@ -56,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
@@ -123,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?
@@ -171,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
@@ -184,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/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/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/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 a95a3455a4a..78ae187817a 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -29,5 +29,5 @@ namespace :cache do
task all: [:db, :redis]
end
- task clear: 'cache:clear:all'
+ task clear: 'cache:clear:redis'
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/ee_compat_check.rake b/lib/tasks/ee_compat_check.rake
new file mode 100644
index 00000000000..f494fa5c5c2
--- /dev/null
+++ b/lib/tasks/ee_compat_check.rake
@@ -0,0 +1,4 @@
+desc 'Checks if the branch would apply cleanly to EE'
+task ee_compat_check: :environment do
+ Rake::Task['gitlab:dev:ee_compat_check'].invoke
+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/dev.rake b/lib/tasks/gitlab/dev.rake
new file mode 100644
index 00000000000..5ee99dfc810
--- /dev/null
+++ b/lib/tasks/gitlab/dev.rake
@@ -0,0 +1,22 @@
+namespace :gitlab do
+ namespace :dev do
+ desc 'Checks if the branch would apply cleanly to EE'
+ task ee_compat_check: :environment do
+ return if defined?(Gitlab::License)
+ return unless ENV['CI']
+
+ success =
+ Gitlab::EeCompatCheck.new(
+ branch: ENV['CI_BUILD_REF_NAME'],
+ check_dir: File.expand_path('ee-compat-check', __dir__),
+ ce_repo: ENV['CI_BUILD_REPO']
+ ).check
+
+ if success
+ exit 0
+ else
+ exit 1
+ 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/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index a0870891cf4..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,74 +13,99 @@ 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 404' do
delete :leave, group_id: group
@@ -96,10 +115,7 @@ describe Groups::GroupMembersController do
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/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 84298f8bef4..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}/)
@@ -658,26 +662,97 @@ describe Projects::MergeRequestsController do
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
@@ -692,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
@@ -700,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
@@ -756,4 +911,34 @@ describe Projects::MergeRequestsController do
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 074f85157de..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 'adds no user to members' do
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: '',
+ access_level: Gitlab::Access::GUEST
- it { expect(response).to have_http_status(200) }
+ 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,19 +110,16 @@ 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 404' do
delete :leave, namespace_id: project.namespace,
project_id: project
@@ -145,10 +130,7 @@ describe Projects::ProjectMembersController do
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_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index da0fdce39db..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
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 41d263a46a4..2d762fdaa04 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -116,116 +116,126 @@ describe SnippetsController do
end
end
- describe 'GET #raw' do
- let(:user) { create(:user) }
+ %w(raw download).each do |action|
+ describe "GET #{action}" do
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- context 'when the personal snippet is private' do
- let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ it 'responds with status 404' do
+ get action, id: other_personal_snippet.to_param
- context 'when signed in user is not the author' do
- let(:other_author) { create(:author) }
- let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+ expect(response).to have_http_status(404)
+ end
+ end
- it 'responds with status 404' do
- get :raw, id: other_personal_snippet.to_param
+ context 'when signed in user is the author' do
+ before { get action, id: personal_snippet.to_param }
- expect(response).to have_http_status(404)
- end
- end
+ it 'responds with status 200' do
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
- context 'when signed in user is the author' do
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ it 'has expected headers' do
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ if action == :download
+ expect(response.header['Content-Disposition']).to match(/attachment/)
+ elsif action == :raw
+ expect(response.header['Content-Disposition']).to match(/inline/)
+ end
+ end
end
end
- end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get :raw, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get action, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to redirect_to(new_user_session_path)
+ end
end
end
- end
- context 'when the personal snippet is internal' do
- let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
end
- end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get :raw, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get action, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to redirect_to(new_user_session_path)
+ end
end
end
- end
- context 'when the personal snippet is public' do
- let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
end
- end
- context 'when not signed in' do
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
end
end
- end
- context 'when the personal snippet does not exist' do
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 404' do
- get :raw, id: 'doesntexist'
+ it 'responds with status 404' do
+ get action, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
- end
- context 'when not signed in' do
- it 'responds with status 404' do
- get :raw, id: 'doesntexist'
+ context 'when not signed in' do
+ it 'responds with status 404' do
+ get action, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
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 1ddb305a8af..c21927640d1 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -8,5 +8,6 @@ FactoryGirl.define do
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 719ef17f57e..4065e2defbc 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -45,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|
@@ -55,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
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 0fb1608a0a3..c533ce1d87f 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -624,6 +624,10 @@ describe 'Issue Boards', feature: true, js: true do
it 'does not show create new list' do
expect(page).not_to have_selector('.js-new-board-list')
end
+
+ it 'does not allow dragging' do
+ expect(page).not_to have_selector('.user-can-drag')
+ end
end
context 'as guest user' do
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
index 33dfd0d5b62..43eb4000e58 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/compare_spec.rb
@@ -44,7 +44,7 @@ describe "Compare", js: true do
def select_using_dropdown(dropdown_type, selection)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
- dropdown.fill_in("Filter by branch/tag", with: selection)
- click_link selection
+ 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/environments_spec.rb b/spec/features/environments_spec.rb
index 68ea4eeae31..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')
@@ -44,7 +61,7 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' 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
@@ -65,20 +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
@@ -127,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') }
@@ -140,16 +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
@@ -196,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/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 c54ec2563ad..13bfe90302c 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -11,67 +11,99 @@ feature 'Group', feature: true do
end
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'
+ describe 'create a group' do
+ before { visit new_group_path }
- click_button 'Create group'
+ 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
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_namespace_error_message
+ end
end
- end
-
- describe 'creating a group with .atom at end of group path' do
- it 'renders new group form with validation errors' do
- visit new_group_path
- fill_in 'Group path', with: 'atom_group.atom'
- click_button 'Create group'
+ 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
+ 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 'creating a group with .git at end of group path' do
- it 'renders new group form with validation errors' do
- visit new_group_path
- fill_in 'Group path', with: 'git_group.git'
- click_button 'Create group'
+ describe 'group edit' do
+ let(:group) { create(:group) }
+ let(:path) { edit_group_path(group) }
+ let(:new_name) { 'new-name' }
+
+ before { visit path }
+
+ it 'saves new settings' do
+ fill_in 'group_name', with: new_name
+ click_button 'Save group'
+
+ expect(page).to have_content 'successfully updated'
+ expect(find('#group_name').value).to eq(new_name)
- expect(current_path).to eq(groups_path)
- expect(page).to have_namespace_error_message
+ 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/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/login_spec.rb b/spec/features/login_spec.rb
index 2523b4b7898..76bcfbe523a 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
@@ -215,4 +215,69 @@ feature 'Login', feature: true do
end
end
end
+
+ describe 'UI tabs and panes' do
+ context 'when no defaults are changed' do
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness
+ end
+ end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting(signup_enabled: false)
+ end
+
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness
+ end
+ end
+
+ context 'when ldap is enabled' do
+ before do
+ visit new_user_session_path
+ allow(page).to receive(:form_based_providers).and_return([:ldapmain])
+ allow(page).to receive(:ldap_enabled).and_return(true)
+ end
+
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness(false)
+ end
+ end
+
+ context 'when crowd is enabled' do
+ before do
+ visit new_user_session_path
+ allow(page).to receive(:form_based_providers).and_return([:crowd])
+ allow(page).to receive(:crowd_enabled?).and_return(true)
+ end
+
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness(false)
+ end
+ end
+
+ def ensure_tab_pane_correctness(visit_path = true)
+ if visit_path
+ visit new_user_session_path
+ end
+
+ ensure_tab_pane_counts
+ ensure_one_active_tab
+ ensure_one_active_pane
+ end
+
+ def ensure_tab_pane_counts
+ tabs_count = page.all('[role="tab"]').size
+ expect(page).to have_selector('[role="tabpanel"]', count: tabs_count)
+ end
+
+ def ensure_one_active_tab
+ expect(page).to have_selector('.nav-tabs > li.active', count: 1)
+ end
+
+ def ensure_one_active_pane
+ expect(page).to have_selector('.tab-pane.active', count: 1)
+ 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/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
index bc2b0ff3e2c..c3c3ab33872 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -101,7 +101,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
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 cb3cea3fd51..7b8af555f0e 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -20,7 +20,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
login_with(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
-
+
after do
wait_for_ajax
end
@@ -34,7 +34,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
expect(page).to have_content 'Your commands have been executed!'
expect(merge_request.reload.work_in_progress?).to eq true
- end
+ end
it 'removes the WIP: prefix from the title' do
merge_request.title = merge_request.wip_title
@@ -45,7 +45,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
expect(page).to have_content 'Your commands have been executed!'
expect(merge_request.reload.work_in_progress?).to eq false
- end
+ end
end
context 'when the current user cannot toggle the WIP prefix' do
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/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 9b487e350f2..1d4484a9edd 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/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/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index f32834801a0..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 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).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 cd79c4f512d..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)
@@ -26,7 +27,26 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
- preview_template(template_content)
+ preview_template
+ 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
@@ -37,7 +57,7 @@ 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
@@ -75,7 +95,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal'
wait_for_ajax
- preview_template(template_content)
+ preview_template
save_changes
end
end
@@ -102,25 +122,31 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do
select_template 'feature-proposal'
wait_for_ajax
- preview_template(template_content)
+ preview_template
save_changes
end
end
end
end
- def preview_template(expected_content)
+ def preview_template(expected_content = template_content)
click_link 'Preview'
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/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/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/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 6498b7317b4..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,11 +32,12 @@ 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
@@ -49,6 +51,40 @@ feature 'Users', feature: true do
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)
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
new file mode 100644
index 00000000000..10cfb66ec1c
--- /dev/null
+++ b/spec/finders/labels_finder_spec.rb
@@ -0,0 +1,101 @@
+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
+
+ it 'returns labels available if nil title is supplied' do
+ group_2.add_developer(user)
+ # params[:title] will return `nil` regardless whether it is specified
+ finder = described_class.new(user, title: nil)
+
+ 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
+
+ it 'returns label with title alias' do
+ finder = described_class.new(user, name: 'Group Label 2')
+
+ expect(finder.execute).to eq [group_label_2]
+ end
+
+ it 'returns no labels if empty title is supplied' do
+ finder = described_class.new(user, title: [])
+
+ expect(finder.execute).to be_empty
+ end
+
+ it 'returns no labels if blank title is supplied' do
+ finder = described_class.new(user, title: '')
+
+ expect(finder.execute).to be_empty
+ end
+
+ it 'returns no labels if empty name is supplied' do
+ finder = described_class.new(user, name: [])
+
+ expect(finder.execute).to be_empty
+ end
+ end
+ end
+end
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/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/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/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..da9259edd78
--- /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 select the correct input elements', 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(4);
+ });
+
+ 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/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/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
index 4c68ce6d6e4..f9e6bd609f0 100644
--- a/spec/lib/banzai/filter/html_entity_filter_spec.rb
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -11,4 +11,9 @@ describe Banzai::Filter::HtmlEntityFilter, lib: true do
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/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 6b58f3e43ee..2bfa51deb20 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -50,14 +50,6 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
end
end
- shared_examples :relative_to_requested do
- it 'rebuilds URL relative to the requested path' do
- doc = filter(link('users.md'))
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/blob/#{ref}/doc/api/users.md"
- end
- end
-
context 'with a project_wiki' do
let(:project_wiki) { double('ProjectWiki') }
include_examples :preserve_unchanged
@@ -188,12 +180,38 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
context 'when requested path is a file in the repo' do
let(:requested_path) { 'doc/api/README.md' }
- include_examples :relative_to_requested
+ it 'rebuilds URL relative to the containing directory' do
+ doc = filter(link('users.md'))
+ expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/doc/api/users.md"
+ end
end
context 'when requested path is a directory in the repo' do
- let(:requested_path) { 'doc/api' }
- include_examples :relative_to_requested
+ let(:requested_path) { 'doc/api/' }
+ it 'rebuilds URL relative to the directory' do
+ doc = filter(link('users.md'))
+ expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/doc/api/users.md"
+ end
+ end
+
+ context 'when ref name contains percent sign' do
+ let(:ref) { '100%branch' }
+ let(:commit) { project.commit('1b12f15a11fc6e62177bef08f47bc7b5ce50b141') }
+ let(:requested_path) { 'foo/bar/' }
+ it 'correctly escapes the ref' do
+ doc = filter(link('.gitkeep'))
+ expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/foo/bar/.gitkeep"
+ end
+ end
+
+ context 'when requested path is a directory with space in the repo' do
+ let(:ref) { 'master' }
+ let(:commit) { project.commit('38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e') }
+ let(:requested_path) { 'with space/' }
+ it 'does not escape the space twice' do
+ doc = filter(link('README.md'))
+ expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/with%20space/README.md"
+ end
end
end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 90da78a67dd..6bcda87c999 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -24,7 +24,7 @@ describe Banzai::ObjectRenderer do
with(an_instance_of(Array)).
and_call_original
- expect(object).to receive(:redacted_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)
@@ -92,10 +92,10 @@ describe Banzai::ObjectRenderer do
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
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/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/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb
index a5feaacb8ee..7814711fe27 100644
--- a/spec/lib/constraints/namespace_url_constrainer_spec.rb
+++ b/spec/lib/constraints/namespace_url_constrainer_spec.rb
@@ -17,6 +17,16 @@ describe NamespaceUrlConstrainer, lib: true do
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)
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/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 de68e32e5b4..a5aa387f4f7 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/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/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5d5836e9bee..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
@@ -125,6 +126,7 @@ project:
- 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/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/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/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 c8207e58e90..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
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/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 15cd3a7ed70..2e3702f7520 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -64,7 +64,7 @@ describe CacheMarkdownField do
let(:html) { "<p><code>Foo</code></p>" }
let(:updated_markdown) { "`Bar`" }
- let(:updated_html) { "<p><code>Bar</code></p>" }
+ let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" }
subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
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/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 733b79079ed..aca49be2942 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -40,6 +40,33 @@ describe Event, models: true do
end
end
+ 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
subject { Event.new(project: target.project, target: target) }
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/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 3b8b743af2d..60d30eb7418 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/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/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index d85a1c1e3b2..f6b2ec5ae31 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe ProjectMember, models: true do
describe 'associations' do
- it { is_expected.to belong_to(:project).class_name('Project').with_foreign_key(:source_id) }
+ it { is_expected.to belong_to(:project).with_foreign_key(:source_id) }
end
describe 'validations' do
@@ -54,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)
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_spec.rb b/spec/models/merge_request_spec.rb
index 5884b4cff8c..1067ff7bb4d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -6,8 +6,8 @@ describe MergeRequest, models: true do
subject { create(:merge_request) }
describe 'associations' do
- it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
- it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
+ it { is_expected.to belong_to(:target_project).class_name('Project') }
+ it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
end
@@ -334,7 +334,7 @@ describe MergeRequest, models: true do
wip_title = "WIP: #{subject.title}"
expect(subject.wip_title).to eq wip_title
- end
+ end
it "does not add the WIP: prefix multiple times" do
wip_title = "WIP: #{subject.title}"
@@ -640,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
@@ -1155,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')
@@ -1172,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) }
@@ -1187,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
@@ -1207,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
@@ -1250,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
@@ -1294,7 +1286,8 @@ describe MergeRequest, models: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
- let(:merge_request) do
+
+ let!(:merge_request) do
create(:closed_merge_request,
source_project: fork_project,
target_project: project)
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 8d554a01be5..a55d43ab2f9 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_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 26dd95bdfec..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
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_spec.rb b/spec/models/project_spec.rb
index 308a00db9cd..f4dda1ee558 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -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,6 +67,14 @@ 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, :public) }
let(:requester) { create(:user) }
@@ -228,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) }
@@ -389,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
@@ -552,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
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 4b80efbe12b..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 }
@@ -1123,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/user_spec.rb b/spec/models/user_spec.rb
index 65b2896930a..10c39b90212 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -15,11 +15,11 @@ describe User, models: true do
describe 'associations' do
it { is_expected.to have_one(:namespace) }
- it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) }
+ it { is_expected.to have_many(:snippets).dependent(:destroy) }
it { is_expected.to have_many(:project_members).dependent(:destroy) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
- it { is_expected.to have_many(:events).class_name('Event').dependent(:destroy) }
+ it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:recent_events).class_name('Event') }
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index f4b04445c6c..4f5c09a3029 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -106,9 +106,20 @@ describe API::API, api: true do
describe "POST /projects/:id/board/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
- it 'creates a new issue board list' do
- post api(base_url, user),
- label_id: ux_label.id
+ 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)
@@ -116,15 +127,13 @@ describe API::API, api: true do
end
it 'returns 400 when creating a new list if label_id is invalid' do
- post api(base_url, user),
- label_id: 23423
+ 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
+ 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
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/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 66fa0c0c01f..a6e8550fac3 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -72,6 +72,17 @@ 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 "Create a commit with multiple files and actions" 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/members_spec.rb b/spec/requests/api/members_spec.rb
index d22e0595788..493c0a893d1 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -328,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/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/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/users_spec.rb b/spec/requests/api/users_spec.rb
index f83f4d2c9b1..d48752473f3 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -846,7 +846,7 @@ describe API::API, api: true do
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)
@@ -873,7 +873,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 }
@@ -914,7 +914,7 @@ describe API::API, api: true do
end
end
- describe 'GET /user/:id/events' do
+ 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) }
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 5a1ed7d4a25..27f0fd22ae6 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -412,9 +412,10 @@ describe 'Git HTTP requests', lib: true do
context "when the params are anything else" do
let(:params) { { service: 'git-implode-pack' } }
+ before { get path, params }
- it "fails to find a route" do
- expect { get(path, params) }.to raise_error(ActionController::RoutingError)
+ it "redirects to the sign-in page" do
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 0ee1c811dfb..c18a2d55e43 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -15,27 +15,27 @@ describe UsersController, "routing" do
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
@@ -270,6 +270,12 @@ describe "Groups", "routing" do
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
describe HealthCheckController, 'routing' do
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index e7806add916..a7e9efcf93f 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -9,6 +9,10 @@ describe Boards::Lists::CreateService, services: true do
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(board)
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
index 8b2f5e81338..ed0337662af 100644
--- a/spec/services/boards/lists/generate_service_spec.rb
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -8,6 +8,10 @@ describe Boards::Lists::GenerateService, services: true do
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(board) }.to change(board.lists, :count).by(2)
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/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/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 8e3e12114f2..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.
@@ -484,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/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 93bf0f64963..f0ded06b785 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -23,14 +23,15 @@ describe Issues::MoveService, services: true do
old_project.team << [user, :reporter]
new_project.team << [user, :reporter]
- ['label1', 'label2'].each do |label|
+ labels = Array.new(2) { |x| "label%d" % (x + 1) }
+
+ labels.each do |label|
old_issue.labels << create(:label,
project_id: old_project.id,
title: label)
- end
- new_project.labels << create(:label, title: 'label1')
- new_project.labels << create(:label, title: 'label2')
+ new_project.labels << create(:label, title: label)
+ end
end
end
@@ -207,10 +208,10 @@ describe Issues::MoveService, services: true do
end
end
- describe 'rewritting references' do
+ describe 'rewriting references' do
include_context 'issue move executed'
- context 'issue reference' do
+ context 'issue references' do
let(:another_issue) { create(:issue, project: old_project) }
let(:description) { "Some description #{another_issue.to_reference}" }
@@ -219,6 +220,16 @@ describe Issues::MoveService, services: true do
.to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}"
end
end
+
+ context "user references" do
+ let(:another_issue) { create(:issue, project: old_project) }
+ let(:description) { "Some description #{user.to_reference}" }
+
+ it "doesn't throw any errors for issues containing user references" do
+ expect(new_issue.description)
+ .to eq "Some description #{user.to_reference}"
+ end
+ end
end
context 'moving to same project' do
@@ -277,5 +288,25 @@ describe Issues::MoveService, services: true do
it { expect { move }.to raise_error(StandardError, /permissions/) }
end
end
+
+ context 'movable issue with no assigned labels' do
+ before do
+ old_project.team << [user, :reporter]
+ new_project.team << [user, :reporter]
+
+ labels = Array.new(2) { |x| "label%d" % (x + 1) }
+
+ labels.each do |label|
+ new_project.labels << create(:label, title: label)
+ end
+ end
+
+ include_context 'issue move executed'
+
+ it 'does not assign labels to new issue' do
+ expected_label_titles = new_issue.reload.labels.map(&:title)
+ expect(expected_label_titles.size).to eq 0
+ end
+ end
end
end
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/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index 7aeb95a15ea..5034b6ef33f 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -46,4 +46,16 @@ describe MergeRequests::AssignIssuesService, services: true do
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/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ee53e110aee..f93d7732a9a 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -74,6 +74,18 @@ describe MergeRequests::MergeService, services: true do
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
@@ -120,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/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 5b4e4908add..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 }
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/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/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/mailers/shared/notify.rb b/spec/support/notify_shared_examples.rb
index 3956d05060b..3956d05060b 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/support/notify_shared_examples.rb
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 d56274d0979..c79975d8667 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -17,6 +17,7 @@ module TestEnv
'markdown' => '0ed8c6c',
'lfs' => 'be93687',
'master' => 'b83d6e3',
+ 'merge-test' => '5937ac0',
"'test'" => 'e56497b',
'orphaned-branch' => '45127a9',
'binary-encoding' => '7b1cf43',
@@ -26,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',
@@ -97,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/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/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
new file mode 100644
index 00000000000..6f70b3daf8e
--- /dev/null
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/show/_commits.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:target_project) { create(:project) }
+
+ let(:source_project) do
+ create(:project, forked_from_project: target_project)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, :simple,
+ source_project: source_project,
+ target_project: target_project,
+ author: user)
+ end
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+
+ assign(:merge_request, merge_request)
+ assign(:commits, merge_request.commits)
+ end
+
+ it 'shows commits from source project' do
+ render
+
+ commit = source_project.commit(merge_request.source_branch)
+ href = namespace_project_commit_path(
+ source_project.namespace,
+ source_project,
+ commit)
+
+ expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href)
+ end
+end
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 86980f59cd8..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::Test::ControllerHelpers
-
- 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/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/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_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
index 2b140f2ba28..d202b3de77e 100644
--- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -6,28 +6,48 @@ describe ExpireBuildInstanceArtifactsWorker do
let(:worker) { described_class.new }
describe '#perform' do
- before { build }
-
- subject! { worker.perform(build.id) }
+ before do
+ worker.perform(build.id)
+ end
context 'with expired artifacts' do
- let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
+ let(:artifacts_expiry) { { artifacts_expire_at: Time.now - 7.days } }
- it 'does expire' do
- expect(build.reload.artifacts_expired?).to be_truthy
- end
+ context 'when associated project is valid' do
+ let(:build) do
+ create(:ci_build, :artifacts, artifacts_expiry)
+ end
- it 'does remove files' do
- expect(build.reload.artifacts_file.exists?).to be_falsey
+ 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
- it 'does nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).to be_nil
+ 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) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
+ 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
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/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index ffeaafe654a..984acdade36 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -92,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/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