summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-10-18 09:31:30 +0000
committerLin Jen-Shin <godfat@godfat.org>2016-10-18 09:31:30 +0000
commit68f964ad14a007a99df3d8d7f76d125fbe7f67c9 (patch)
tree26c0938530c3b761678cda2607427f6de3eba7c8
parenta04e9f9b61d93ca872fe108d83f519ba6151631e (diff)
parent4e6af0c3fa335d138343dce3e0216303a9b1cd79 (diff)
downloadgitlab-ce-68f964ad14a007a99df3d8d7f76d125fbe7f67c9.tar.gz
Merge remote-tracking branch 'upstream/master' into show-commit-status-from-source-project
* upstream/master: (409 commits) Update endpoint to username validator change border color to variable Add todo for deprecated user routes and more information about deprecation to changelog Provide better error message to the user Apply better hierarchy to markdown headers and issue/mr titles Swapped button text manipulation outcomes for the toggle query Fixed find file keyboard navigation Update CHANGELOG for 8.12.7 Added download-button class and applied button margin Enable activerecord_sane_schema_dumper for test Updated logo from @luke Fix broken specs on MySQL after https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6896 Fix Test Env (proper error handling when gitlab-shell is not clonned) Fix randomly crashing spinach test for merge request [Great spinach fix] Replace gsub with delete Remove carriage returns from commit description as summary is on a newline and will always include carriage returns Convert due_date_select.js filetype to es6. Stop directly parsing due_date with Date.parse, prefer parsing implicitly. Improve spec for pipeline metrics worker Add Pipeline metrics worker ...
-rw-r--r--.gitattributes2
-rw-r--r--.gitlab-ci.yml43
-rw-r--r--.gitlab/merge_request_templates/Documentation.md2
-rw-r--r--.scss-lint.yml4
-rw-r--r--.vagrant_enabled0
-rw-r--r--CHANGELOG.md (renamed from CHANGELOG)459
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile12
-rw-r--r--Gemfile.lock20
-rw-r--r--app/assets/javascripts/api.js7
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es63
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es610
-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/dispatcher.js.es6 (renamed from app/assets/javascripts/dispatcher.js)23
-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.es6167
-rw-r--r--app/assets/javascripts/groups.js13
-rw-r--r--app/assets/javascripts/labels_select.js4
-rw-r--r--app/assets/javascripts/member_expiration_date.js8
-rw-r--r--app/assets/javascripts/members.js.es636
-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.js41
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es6 (renamed from app/assets/javascripts/merge_request_widget.js)76
-rw-r--r--app/assets/javascripts/milestone_select.js2
-rw-r--r--app/assets/javascripts/pipeline.js.es624
-rw-r--r--app/assets/javascripts/pipelines.js.es640
-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.js4
-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.js11
-rw-r--r--app/assets/stylesheets/behaviors.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss9
-rw-r--r--app/assets/stylesheets/framework/callout.scss5
-rw-r--r--app/assets/stylesheets/framework/common.scss39
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss18
-rw-r--r--app/assets/stylesheets/framework/forms.scss13
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/framework/lists.scss13
-rw-r--r--app/assets/stylesheets/framework/logo.scss85
-rw-r--r--app/assets/stylesheets/framework/mixins.scss8
-rw-r--r--app/assets/stylesheets/framework/nav.scss1
-rw-r--r--app/assets/stylesheets/framework/panels.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss8
-rw-r--r--app/assets/stylesheets/framework/timeline.scss1
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss10
-rw-r--r--app/assets/stylesheets/framework/typography.scss40
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/highlight/dark.scss128
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss118
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss136
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss136
-rw-r--r--app/assets/stylesheets/highlight/white.scss2
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss2
-rw-r--r--app/assets/stylesheets/notify.scss10
-rw-r--r--app/assets/stylesheets/pages/admin.scss12
-rw-r--r--app/assets/stylesheets/pages/builds.scss8
-rw-r--r--app/assets/stylesheets/pages/commit.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss1
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss37
-rw-r--r--app/assets/stylesheets/pages/editor.scss5
-rw-r--r--app/assets/stylesheets/pages/environments.scss4
-rw-r--r--app/assets/stylesheets/pages/events.scss8
-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.scss1
-rw-r--r--app/assets/stylesheets/pages/labels.scss1
-rw-r--r--app/assets/stylesheets/pages/lint.scss1
-rw-r--r--app/assets/stylesheets/pages/login.scss176
-rw-r--r--app/assets/stylesheets/pages/members.scss98
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss49
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss46
-rw-r--r--app/assets/stylesheets/pages/milestone.scss1
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss10
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss189
-rw-r--r--app/assets/stylesheets/pages/profile.scss1
-rw-r--r--app/assets/stylesheets/pages/projects.scss18
-rw-r--r--app/assets/stylesheets/pages/runners.scss1
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss5
-rw-r--r--app/assets/stylesheets/pages/xterm.scss290
-rw-r--r--app/controllers/application_controller.rb11
-rw-r--r--app/controllers/explore/projects_controller.rb3
-rw-r--r--app/controllers/namespaces_controller.rb25
-rw-r--r--app/controllers/projects/board_lists_controller.rb65
-rw-r--r--app/controllers/projects/boards/issues_controller.rb9
-rw-r--r--app/controllers/projects/boards/lists_controller.rb18
-rw-r--r--app/controllers/projects/boards_controller.rb28
-rw-r--r--app/controllers/projects/graphs_controller.rb6
-rw-r--r--app/controllers/projects/group_links_controller.rb20
-rw-r--r--app/controllers/projects/merge_requests_controller.rb67
-rw-r--r--app/controllers/projects/project_members_controller.rb36
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb14
-rw-r--r--app/controllers/users_controller.rb6
-rw-r--r--app/finders/trending_projects_finder.rb16
-rw-r--r--app/helpers/boards_helper.rb12
-rw-r--r--app/helpers/button_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb17
-rw-r--r--app/models/ci/build.rb22
-rw-r--r--app/models/ci/pipeline.rb33
-rw-r--r--app/models/commit_status.rb16
-rw-r--r--app/models/compare.rb21
-rw-r--r--app/models/cycle_analytics.rb14
-rw-r--r--app/models/deployment.rb13
-rw-r--r--app/models/environment.rb15
-rw-r--r--app/models/event.rb8
-rw-r--r--app/models/merge_request.rb17
-rw-r--r--app/models/merge_request_diff.rb21
-rw-r--r--app/models/project.rb33
-rw-r--r--app/models/project_group_link.rb2
-rw-r--r--app/models/repository.rb11
-rw-r--r--app/models/trending_project.rb35
-rw-r--r--app/models/user.rb5
-rw-r--r--app/policies/project_policy.rb3
-rw-r--r--app/services/boards/base_service.rb5
-rw-r--r--app/services/boards/create_service.rb17
-rw-r--r--app/services/boards/issues/create_service.rb17
-rw-r--r--app/services/boards/issues/list_service.rb6
-rw-r--r--app/services/boards/issues/move_service.rb8
-rw-r--r--app/services/boards/list_service.rb14
-rw-r--r--app/services/boards/lists/create_service.rb12
-rw-r--r--app/services/boards/lists/destroy_service.rb6
-rw-r--r--app/services/boards/lists/generate_service.rb10
-rw-r--r--app/services/boards/lists/list_service.rb9
-rw-r--r--app/services/boards/lists/move_service.rb5
-rw-r--r--app/services/compare_service.rb7
-rw-r--r--app/services/create_deployment_service.rb34
-rw-r--r--app/services/git_push_service.rb15
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb4
-rw-r--r--app/services/merge_requests/assign_issues_service.rb35
-rw-r--r--app/services/merge_requests/base_service.rb27
-rw-r--r--app/services/merge_requests/build_service.rb23
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb9
-rw-r--r--app/services/merge_requests/resolve_service.rb24
-rw-r--r--app/services/merge_requests/update_service.rb5
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/slash_commands/interpret_service.rb21
-rw-r--r--app/services/todo_service.rb6
-rw-r--r--app/validators/namespace_validator.rb1
-rw-r--r--app/views/admin/appearances/preview.html.haml17
-rw-r--r--app/views/admin/application_settings/_form.html.haml6
-rw-r--r--app/views/admin/runners/show.html.haml2
-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.haml12
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml10
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml10
-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.haml37
-rw-r--r--app/views/devise/shared/_signup_box.html.haml37
-rw-r--r--app/views/devise/shared/_tab_single.html.haml3
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml10
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml5
-rw-r--r--app/views/devise/unlocks/new.html.haml10
-rw-r--r--app/views/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/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/_project.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml4
-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/index.html.haml16
-rw-r--r--app/views/projects/boards/show.html.haml5
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/builds/_table.html.haml2
-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_pipeline.html.haml6
-rw-r--r--app/views/projects/commit/_pipeline.html.haml81
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml2
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml20
-rw-r--r--app/views/projects/commit/_pipelines_list.haml2
-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/_ref_dropdown.html.haml4
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/environments/edit.html.haml11
-rw-r--r--app/views/projects/environments/index.html.haml2
-rw-r--r--app/views/projects/environments/new.html.haml11
-rw-r--r--app/views/projects/environments/show.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml6
-rw-r--r--app/views/projects/group_links/update.js.haml3
-rw-r--r--app/views/projects/issues/_head.html.haml2
-rw-r--r--app/views/projects/labels/edit.html.haml11
-rw-r--r--app/views/projects/labels/new.html.haml11
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml13
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml50
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml29
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml16
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml12
-rw-r--r--app/views/projects/merge_requests/conflicts/_inline_view.html.haml28
-rw-r--r--app/views/projects/merge_requests/conflicts/_parallel_view.html.haml27
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml31
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml13
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml15
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml10
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml10
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml16
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml1
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml3
-rw-r--r--app/views/projects/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/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.haml2
-rw-r--r--app/views/projects/pipelines/show.html.haml13
-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/shared/icons/_illustration_no_commits.svg1
-rw-r--r--app/views/shared/issuable/_form.html.haml8
-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.haml2
-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/snippets/show.html.haml1
-rw-r--r--app/views/u2f/_authenticate.html.haml2
-rw-r--r--app/workers/build_coverage_worker.rb9
-rw-r--r--app/workers/build_finished_worker.rb10
-rw-r--r--app/workers/build_hooks_worker.rb9
-rw-r--r--app/workers/build_success_worker.rb27
-rw-r--r--app/workers/pipeline_hooks_worker.rb9
-rw-r--r--app/workers/pipeline_metrics_worker.rb30
-rw-r--r--app/workers/pipeline_process_worker.rb (renamed from app/workers/process_pipeline_worker.rb)2
-rw-r--r--app/workers/pipeline_success_worker.rb12
-rw-r--r--app/workers/pipeline_update_worker.rb (renamed from app/workers/update_pipeline_worker.rb)2
-rw-r--r--app/workers/trending_projects_worker.rb11
-rw-r--r--app/workers/update_merge_requests_worker.rb16
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/1_settings.rb4
-rw-r--r--config/initializers/metrics.rb1
-rw-r--r--config/routes.rb4
-rw-r--r--config/routes/group.rb8
-rw-r--r--config/routes/project.rb9
-rw-r--r--config/routes/snippets.rb1
-rw-r--r--config/routes/user.rb15
-rw-r--r--db/migrate/20161007133303_precalculate_trending_projects.rb38
-rw-r--r--db/schema.rb1183
-rw-r--r--doc/README.md12
-rw-r--r--doc/administration/high_availability/gitlab.md6
-rw-r--r--doc/administration/monitoring/performance/gitlab_configuration.md40
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md111
-rw-r--r--doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.pngbin0 -> 14368 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_dashboard_import.pngbin0 -> 18267 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_data_source_configuration.pngbin0 -> 26060 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_data_source_empty.pngbin0 -> 21821 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_save_icon.pngbin0 -> 9107 bytes
-rw-r--r--doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.pngbin0 -> 61357 bytes
-rw-r--r--doc/administration/monitoring/performance/img/request_profile_result.pngbin0 -> 9720 bytes
-rw-r--r--doc/administration/monitoring/performance/img/request_profiling_token.pngbin0 -> 30076 bytes
-rw-r--r--doc/administration/monitoring/performance/influxdb_configuration.md193
-rw-r--r--doc/administration/monitoring/performance/influxdb_schema.md97
-rw-r--r--doc/administration/monitoring/performance/introduction.md65
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md16
-rw-r--r--doc/administration/operations.md6
-rw-r--r--doc/administration/operations/cleaning_up_redis_sessions.md52
-rw-r--r--doc/administration/operations/moving_repositories.md180
-rw-r--r--doc/administration/operations/sidekiq_memory_killer.md40
-rw-r--r--doc/administration/operations/unicorn.md86
-rw-r--r--doc/administration/reply_by_email.md302
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md324
-rw-r--r--doc/administration/restart_gitlab.md2
-rw-r--r--doc/administration/troubleshooting/debug.md4
-rw-r--r--doc/api/README.md18
-rw-r--r--doc/api/award_emoji.md18
-rw-r--r--doc/api/builds.md8
-rw-r--r--doc/api/commits.md8
-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.md12
-rw-r--r--doc/api/templates/gitignores.md579
-rw-r--r--doc/api/templates/gitlab_ci_ymls.md120
-rw-r--r--doc/api/templates/licenses.md (renamed from doc/api/licenses.md)8
-rw-r--r--doc/api/todos.md18
-rw-r--r--doc/api/users.md160
-rw-r--r--doc/api/version.md23
-rw-r--r--doc/ci/examples/README.md2
-rw-r--r--doc/ci/examples/test-phoenix-application.md56
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/variables/README.md34
-rw-r--r--doc/ci/yaml/README.md9
-rw-r--r--doc/development/doc_styleguide.md33
-rw-r--r--doc/development/frontend.md11
-rw-r--r--doc/development/performance.md5
-rw-r--r--doc/gitlab-basics/README.md6
-rw-r--r--doc/gitlab-basics/add-file.md28
-rw-r--r--doc/gitlab-basics/add-merge-request.md49
-rw-r--r--doc/gitlab-basics/basicsimages/add_new_merge_request.pngbin9003 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/add_sshkey.pngbin1394 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_info.pngbin7572 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_name.pngbin2137 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branches.pngbin3548 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/button-create-mr.pngbin5927 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/click-on-new-group.pngbin1957 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_changes.pngbin4941 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_message.pngbin5103 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commits.pngbin4112 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/compare_branches.pngbin1520 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_file.pngbin2451 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_group.pngbin3184 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/edit_file.pngbin2221 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_located.pngbin3078 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_name.pngbin2412 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_file.pngbin8426 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_group.pngbin5897 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/fork.pngbin896 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/group_info.pngbin15479 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/groups.pngbin4752 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/https.pngbin2822 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/image_file.pngbin2796 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issue_title.pngbin8311 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issues.pngbin4153 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/key.pngbin1177 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/merge_requests.pngbin4213 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_issue.pngbin2974 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_merge_request.pngbin3162 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_project.pngbin2234 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/newbranch.pngbin1244 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/paste_sshkey.pngbin7699 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/profile_settings.pngbin1101 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/project_info.pngbin21041 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group.pngbin6034 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group2.pngbin5040 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_branch.pngbin11207 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_project.pngbin16176 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/settings.pngbin4149 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/shh_keys.pngbin4782 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/submit_new_issue.pngbin8644 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/title_description_mr.pngbin11919 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/white_space.pngbin2192 -> 0 bytes
-rw-r--r--doc/gitlab-basics/command-line-commands.md26
-rw-r--r--doc/gitlab-basics/create-branch.md43
-rw-r--r--doc/gitlab-basics/create-group.md51
-rw-r--r--doc/gitlab-basics/create-issue.md33
-rw-r--r--doc/gitlab-basics/create-project.md27
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md38
-rw-r--r--doc/gitlab-basics/fork-project.md19
-rw-r--r--doc/gitlab-basics/img/create_new_group_info.pngbin0 -> 53103 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_group_sidebar.pngbin0 -> 5396 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin0 -> 10050 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_from_group.pngbin0 -> 6545 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin0 -> 49451 bytes
-rw-r--r--doc/gitlab-basics/img/fork_choose_namespace.pngbin0 -> 39253 bytes
-rw-r--r--doc/gitlab-basics/img/fork_new.pngbin0 -> 25540 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_new.pngbin0 -> 3596 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_page.pngbin0 -> 91432 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_select_branch.pngbin0 -> 50707 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_button.pngbin0 -> 3070 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_page.pngbin0 -> 53268 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings.pngbin0 -> 5975 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys.pngbin0 -> 42977 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.pngbin0 -> 37486 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.pngbin0 -> 18498 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_title.pngbin0 -> 2362 bytes
-rw-r--r--doc/gitlab-basics/img/project_clone_url.pngbin0 -> 40490 bytes
-rw-r--r--doc/gitlab-basics/img/project_navbar.pngbin0 -> 5745 bytes
-rw-r--r--doc/gitlab-basics/img/public_file_link.png (renamed from doc/gitlab-basics/basicsimages/public_file_link.png)bin3023 -> 3023 bytes
-rw-r--r--doc/gitlab-basics/img/select_group_dropdown.pngbin0 -> 8038 bytes
-rw-r--r--doc/incoming_email/README.md303
-rw-r--r--doc/incoming_email/postfix.md322
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/monitoring/health_check.md67
-rw-r--r--doc/monitoring/performance/gitlab_configuration.md41
-rw-r--r--doc/monitoring/performance/grafana_configuration.md112
-rw-r--r--doc/monitoring/performance/influxdb_configuration.md194
-rw-r--r--doc/monitoring/performance/influxdb_schema.md98
-rw-r--r--doc/monitoring/performance/introduction.md66
-rw-r--r--doc/operations/README.md6
-rw-r--r--doc/operations/cleaning_up_redis_sessions.md53
-rw-r--r--doc/operations/moving_repositories.md181
-rw-r--r--doc/operations/sidekiq_memory_killer.md41
-rw-r--r--doc/operations/unicorn.md87
-rw-r--r--doc/raketasks/backup_hrz.pngbin8907 -> 31784 bytes
-rw-r--r--doc/raketasks/user_management.md15
-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/update/8.12-to-8.13.md2
-rw-r--r--doc/user/admin_area/monitoring/health_check.md66
-rw-r--r--doc/user/admin_area/monitoring/img/health_check_token.png (renamed from doc/monitoring/img/health_check_token.png)bin6630 -> 6630 bytes
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/project/cycle_analytics.md22
-rw-r--r--doc/user/project/git_attributes.md22
-rw-r--r--doc/user/project/merge_requests/img/versions_compare.png (renamed from doc/user/project/merge_requests/img/versions-compare.png)bin68722 -> 68722 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_dropdown.png (renamed from doc/user/project/merge_requests/img/versions-dropdown.png)bin60587 -> 60587 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_system_note.pngbin0 -> 18731 bytes
-rw-r--r--doc/user/project/merge_requests/merge_when_build_succeeds.md20
-rw-r--r--doc/user/project/merge_requests/versions.md30
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md55
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_from_issue.pngbin0 -> 4728 bytes
-rw-r--r--doc/user/project/repository/web_editor.md4
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/importing/migrating_from_svn.md108
-rw-r--r--docker/README.md4
-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/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/merge_requests.rb10
-rw-r--r--features/steps/project/team_management.rb17
-rw-r--r--features/steps/shared/note.rb6
-rw-r--r--features/steps/shared/project.rb4
-rw-r--r--features/support/db_cleaner.rb2
-rw-r--r--features/support/rerun.rb2
-rw-r--r--lib/api/api.rb14
-rw-r--r--lib/api/boards.rb105
-rw-r--r--lib/api/license_templates.rb58
-rw-r--r--lib/api/projects.rb6
-rw-r--r--lib/api/system_hooks.rb60
-rw-r--r--lib/api/templates.rb124
-rw-r--r--lib/api/todos.rb45
-rw-r--r--lib/api/users.rb20
-rw-r--r--lib/api/variables.rb89
-rw-r--r--lib/api/version.rb12
-rw-r--r--lib/banzai/filter/emoji_filter.rb53
-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/extracts_path.rb40
-rw-r--r--lib/gitlab/backend/shell.rb4
-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/emoji.rb10
-rw-r--r--lib/tasks/ce_to_ee_merge_check.rake4
-rw-r--r--lib/tasks/gitlab/check.rake6
-rw-r--r--lib/tasks/gitlab/dev.rake107
-rw-r--r--lib/tasks/gitlab/users.rake11
-rwxr-xr-xscripts/lint-doc.sh6
-rw-r--r--spec/controllers/namespaces_controller_spec.rb118
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb66
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb61
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb84
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb16
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb41
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb44
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb251
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb14
-rw-r--r--spec/factories/boards.rb5
-rw-r--r--spec/factories/projects.rb8
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb21
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb7
-rw-r--r--spec/features/compare_spec.rb4
-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/login_spec.rb2
-rw-r--r--spec/features/merge_requests/assign_issues_spec.rb51
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb137
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb2
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb57
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb6
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb26
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb42
-rw-r--r--spec/features/projects/guest_navigation_menu_spec.rb28
-rw-r--r--spec/features/projects/issuable_templates_spec.rb40
-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/security/project/private_access_spec.rb2
-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/trending_projects_finder_spec.rb48
-rw-r--r--spec/fixtures/api/schemas/board.json11
-rw-r--r--spec/fixtures/api/schemas/boards.json4
-rw-r--r--spec/fixtures/api/schemas/conflicts.json137
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es62
-rw-r--r--spec/javascripts/boards/issue_spec.js.es62
-rw-r--r--spec/javascripts/boards/list_spec.js.es62
-rw-r--r--spec/javascripts/boards/mock_data.js.es68
-rw-r--r--spec/javascripts/fixtures/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/emoji_filter_spec.rb85
-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/extracts_path_spec.rb87
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb11
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb27
-rw-r--r--spec/models/commit_spec.rb8
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb2
-rw-r--r--spec/models/cycle_analytics/code_spec.rb88
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb2
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb2
-rw-r--r--spec/models/cycle_analytics/review_spec.rb4
-rw-r--r--spec/models/cycle_analytics/test_spec.rb6
-rw-r--r--spec/models/deploy_key_spec.rb3
-rw-r--r--spec/models/deployment_spec.rb9
-rw-r--r--spec/models/environment_spec.rb17
-rw-r--r--spec/models/event_spec.rb109
-rw-r--r--spec/models/key_spec.rb3
-rw-r--r--spec/models/label_link_spec.rb3
-rw-r--r--spec/models/merge_request_diff_spec.rb62
-rw-r--r--spec/models/merge_request_spec.rb16
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_spec.rb58
-rw-r--r--spec/models/repository_spec.rb52
-rw-r--r--spec/models/trending_project_spec.rb56
-rw-r--r--spec/policies/project_policy_spec.rb5
-rw-r--r--spec/requests/api/commits_spec.rb12
-rw-r--r--spec/requests/api/license_templates_spec.rb136
-rw-r--r--spec/requests/api/project_hooks_spec.rb5
-rw-r--r--spec/requests/api/projects_spec.rb42
-rw-r--r--spec/requests/api/repositories_spec.rb8
-rw-r--r--spec/requests/api/templates_spec.rb204
-rw-r--r--spec/requests/api/users_spec.rb104
-rw-r--r--spec/requests/api/version_spec.rb27
-rw-r--r--spec/requests/git_http_spec.rb4
-rw-r--r--spec/routing/project_routing_spec.rb2
-rw-r--r--spec/routing/routing_spec.rb22
-rw-r--r--spec/services/boards/create_service_spec.rb22
-rw-r--r--spec/services/boards/issues/create_service_spec.rb12
-rw-r--r--spec/services/boards/issues/list_service_spec.rb33
-rw-r--r--spec/services/boards/issues/move_service_spec.rb36
-rw-r--r--spec/services/boards/list_service_spec.rb37
-rw-r--r--spec/services/boards/lists/create_service_spec.rb14
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb12
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb12
-rw-r--r--spec/services/boards/lists/list_service_spec.rb16
-rw-r--r--spec/services/boards/lists/move_service_spec.rb8
-rw-r--r--spec/services/compare_service_spec.rb21
-rw-r--r--spec/services/create_deployment_service_spec.rb17
-rw-r--r--spec/services/git_push_service_spec.rb4
-rw-r--r--spec/services/merge_requests/assign_issues_service_spec.rb49
-rw-r--r--spec/services/merge_requests/build_service_spec.rb20
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb93
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb7
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb141
-rw-r--r--spec/services/merge_requests/update_service_spec.rb18
-rw-r--r--spec/services/notification_service_spec.rb57
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb43
-rw-r--r--spec/services/system_note_service_spec.rb8
-rw-r--r--spec/services/todo_service_spec.rb22
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/test_env.rb13
-rw-r--r--spec/tasks/gitlab/users_rake_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/emails_on_push_worker_spec.rb4
-rw-r--r--spec/workers/pipeline_hooks_worker_spec.rb23
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb46
-rw-r--r--spec/workers/pipeline_proccess_worker_spec.rb (renamed from spec/workers/process_pipeline_worker_spec.rb)2
-rw-r--r--spec/workers/pipeline_success_worker_spec.rb24
-rw-r--r--spec/workers/pipeline_update_worker_spec.rb (renamed from spec/workers/update_pipeline_worker_spec.rb)2
-rw-r--r--spec/workers/post_receive_spec.rb8
-rw-r--r--spec/workers/trending_projects_worker_spec.rb11
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb38
611 files changed, 12521 insertions, 6189 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 cb6f691058e..76117a48730 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -19,12 +19,10 @@ variables:
before_script:
- source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml
- - mkdir -p tmp/tests
- - mount -t tmpfs tmpfs tmp/tests || echo "tmpfs mount failed, falling back to disc"
- 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
@@ -101,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:
@@ -212,6 +210,13 @@ rake brakeman: *exec
rake flay: *exec
license_finder: *exec
rake downtime_check: *exec
+rake ce_to_ee_merge_check:
+ <<: *exec
+ only:
+ - branches
+ except:
+ - tags
+ allow_failure: yes
rake db:migrate:reset:
stage: test
@@ -257,6 +262,12 @@ lint-doc:
script:
- scripts/lint-doc.sh
+bundler:check:
+ stage: test
+ <<: *ruby-static-analysis
+ script:
+ - bundle check
+
bundler:audit:
stage: test
<<: *ruby-static-analysis
@@ -295,6 +306,17 @@ coverage:
- coverage/index.html
- coverage/assets/
+# Trigger docs build
+trigger_docs:
+ stage: post-test
+ before_script: []
+ cache: {}
+ artifacts: {}
+ script:
+ - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master https://gitlab.com/api/v3/projects/38069/trigger/builds"
+ only:
+ - master
+
# Notify slack in the end
notify:slack:
@@ -327,3 +349,16 @@ pages:
- public
only:
- master
+
+# Insurance in case a gem needed by one of our releases gets yanked from
+# rubygems.org in the future.
+cache gems:
+ only:
+ - tags
+ variables:
+ SETUP_DB: "false"
+ script:
+ - bundle package --all --all-platforms
+ artifacts:
+ paths:
+ - vendor/cache
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index d2a1eb56423..9b541aadad1 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -1,4 +1,4 @@
-See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html.
+See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html
## What does this MR do?
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 71df6be6a15..5093702519b 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -61,7 +61,7 @@ linters:
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
- enabled: false
+ enabled: true
# Reports when you have an empty rule set.
EmptyRule:
@@ -219,7 +219,7 @@ linters:
# Property values, @extend, @include, and @import directives, and variable
# declarations should always end with a semicolon.
TrailingSemicolon:
- enabled: false
+ enabled: true
# Reports lines containing trailing whitespace.
TrailingWhitespace:
diff --git a/.vagrant_enabled b/.vagrant_enabled
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/.vagrant_enabled
+++ /dev/null
diff --git a/CHANGELOG b/CHANGELOG.md
index da4e66762e4..6c08cf09bad 100644
--- a/CHANGELOG
+++ b/CHANGELOG.md
@@ -1,30 +1,50 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.13.0 (unreleased)
+## 8.13.0 (2016-10-22)
+
+ - 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
- - Improve issue load time performance by avoiding ORDER BY in find_by call
- - Use gitlab-shell v3.6.2 (GIT TRACE logging)
+ - Use gitlab-shell v3.6.6
+ - Ability to resolve merge request conflicts with editor !6374
- Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
+ - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
+ - Updating verbiage on git basics to be more intuitive
+ - 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
+ - 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)
+ - 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)
- 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
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment
+ - Allow browsing branches that end with '.atom'
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
+ - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
- Add more tests for calendar contribution (ClemMakesApps)
- Update Gitlab Shell to fix some problems with moving projects between storages
- Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
+ - API: Ability to retrieve version information (Robert Schilling)
- Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
+ - Unicode emoji are now converted to images
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
@@ -35,15 +55,19 @@ v 8.13.0 (unreleased)
- Add new issue button to each list on Issues Board
- 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)
- 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)
- Added tooltip to fork count on project show page. (Justin DiPierro)
- Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend`
@@ -53,22 +77,23 @@ 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)
- Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view
+ - API: New /users/:id/events endpoint
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
+ - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- Fix deploy status responsiveness error !6633
- Make searching for commits case insensitive
@@ -82,16 +107,47 @@ v 8.13.0 (unreleased)
- Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone)
+ - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Retouch environments list and deployments list
+ - Add multiple command support for all label related slash commands !6780 (barthc)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
+ - 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
+ - API: all unknown routing will be handled with 404 Not Found
+ - Add docs for request profiling
+ - Make guests unable to view MRs on private projects
+
+## 8.12.7
+
+ - Prevent running `GfmAutocomplete` setup for each diff note. !6569
+ - Fix long commit messages overflow viewport in file tree. !6573
+ - Use `gitlab-markup` gem instead of `github-markup` to fix `.rst` file rendering. !6659
+ - Prevent flash alert text from being obscured when container is fluid. !6694
+ - Fix due date being displayed as `NaN` in Safari. !6797
+ - Fix JS bug with select2 because of missing `data-field` attribute in select box. !6812
+ - Do not alter `force_remove_source_branch` options on MergeRequest unless specified. !6817
+ - Fix GFM autocomplete setup being called several times. !6840
+ - Handle case where deployment ref no longer exists. !6855
+
+## 8.12.6
+
+ - Update mailroom to 0.8.1 in Gemfile.lock !6814
+
+## 8.12.5
-v 8.12.5 (unreleased)
+ - Switch from request to env in ::API::Helpers. !6615
+ - Update the mail_room gem to 0.8.1 to fix a race condition with the mailbox watching thread. !6714
+ - Improve issue load time performance by avoiding ORDER BY in find_by call. !6724
+ - Add a new gitlab:users:clear_all_authentication_tokens task. !6745
+ - Don't send Private-Token (API authentication) headers to Sentry
+ - Share projects via the API only with groups the authenticated user can access
+
+## 8.12.4
-v 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
@@ -104,12 +160,14 @@ v 8.12.4
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
- - Change user & group landing page routing from /u/:username to /:username
+ - Improve the way merge request versions are compared with each other
+
+## 8.12.3
-v 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
@@ -124,13 +182,15 @@ v 8.12.2
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path
- Refactor remnants of CoffeeScript destructured opts and super !6261
- - Prevent running GfmAutocomplete setup for each diff note !6569
-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
- Bump fog-aws to v0.11.0 to support ap-south-1 region
@@ -226,6 +286,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)
@@ -318,19 +379,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.8
+## 8.11.9
+
+ - Don't send Private-Token (API authentication) headers to Sentry
+ - Share projects via the API only with groups the authenticated user can access
+
+## 8.11.8
+
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
- API: disable rails session auth for non-GET/HEAD requests
- Escape HTML nodes in builds commands in CI linter
-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.
- - Updating verbiage on git basics to be more intuitive
+ - 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
@@ -340,7 +409,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
@@ -353,7 +423,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
@@ -370,7 +441,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
@@ -381,17 +453,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)
@@ -489,7 +564,6 @@ v 8.11.0
- Add pipeline events hook
- Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
- - Clarify documentation for Runners API (Gennady Trafimenkov)
- Make branches sortable without push permission !5462 (winniehell)
- Check for Ci::Build artifacts at database level on pipeline partial
- Convert image diff background image to CSS (ClemMakesApps)
@@ -545,44 +619,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.11
+## 8.10.12
+
+ - Don't send Private-Token (API authentication) headers to Sentry
+ - Share projects via the API only with groups the authenticated user can access
+
+## 8.10.11
+
- Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests
- API: disable rails session auth for non-GET/HEAD requests
- Escape HTML nodes in builds commands in CI linter
-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
@@ -593,7 +681,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
@@ -614,7 +703,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
@@ -623,7 +713,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)
@@ -787,26 +878,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
@@ -815,7 +912,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
@@ -830,7 +928,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
@@ -844,7 +943,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
@@ -861,12 +961,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
@@ -910,7 +1012,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
@@ -1065,21 +1168,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
@@ -1092,12 +1200,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
@@ -1116,7 +1226,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
@@ -1127,13 +1238,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
@@ -1209,34 +1322,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
@@ -1245,20 +1364,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
@@ -1268,7 +1390,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
@@ -1384,12 +1507,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
@@ -1402,12 +1527,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
@@ -1415,7 +1542,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
@@ -1424,11 +1552,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
@@ -1441,7 +1571,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
@@ -1463,7 +1594,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
@@ -1478,7 +1610,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)
@@ -1543,11 +1676,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
@@ -1558,41 +1693,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
@@ -1609,7 +1754,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
@@ -1629,7 +1775,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)
@@ -1708,11 +1855,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
@@ -1723,28 +1872,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
@@ -1755,7 +1911,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
@@ -1768,14 +1925,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
@@ -1848,11 +2007,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
@@ -1861,22 +2022,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
@@ -1892,17 +2059,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)
@@ -1970,11 +2140,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
@@ -1982,10 +2154,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
@@ -1994,7 +2168,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)
@@ -2004,11 +2179,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
@@ -2068,19 +2245,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
@@ -2090,10 +2270,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)
@@ -2178,11 +2360,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
@@ -2191,13 +2375,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
@@ -2211,10 +2397,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)
@@ -2279,5 +2467,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_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 0f44168a4d5..4f2c1d15f6d 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.6.4
+3.6.6
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 426b824901e..05166b6a828 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,7 +51,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.6.7'
+gem 'gitlab_git', '~> 10.6.8'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -101,7 +101,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
-gem 'github-markup', '~> 1.4'
+gem 'gitlab-markup', '~> 1.5.0'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6'
@@ -225,7 +225,7 @@ gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0'
-gem 'request_store', '~> 1.3.0'
+gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
@@ -308,6 +308,8 @@ group :development, :test do
gem 'license_finder', '~> 2.1.0', require: false
gem 'knapsack', '~> 1.11.0'
+
+ gem 'activerecord_sane_schema_dumper', '0.2'
end
group :test do
@@ -324,7 +326,7 @@ gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
-gem 'mail_room', '~> 0.8'
+gem 'mail_room', '~> 0.8.1'
gem 'email_reply_parser', '~> 0.5.8'
@@ -341,7 +343,7 @@ gem 'oauth2', '~> 1.2.0'
gem 'paranoia', '~> 2.0'
# Health check
-gem 'health_check', '~> 2.1.0'
+gem 'health_check', '~> 2.2.0'
# System information
gem 'vmstat', '~> 2.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index b98c3acf948..a9892d1c130 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -38,6 +38,8 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3)
railties (>= 4.0, < 5.1)
+ activerecord_sane_schema_dumper (0.2)
+ rails (>= 4, < 5)
activesupport (4.2.7.1)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
@@ -280,7 +282,8 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.6.7)
+ gitlab-markup (1.5.0)
+ gitlab_git (10.6.8)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -334,7 +337,7 @@ GEM
thor
tilt
hashie (3.4.4)
- health_check (2.1.0)
+ health_check (2.2.1)
rails (>= 4.0)
hipchat (1.5.2)
httparty
@@ -399,7 +402,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.8.0)
+ mail_room (0.8.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
@@ -805,6 +808,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
+ activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
@@ -861,9 +865,9 @@ DEPENDENCIES
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
github-linguist (~> 4.7.0)
- github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.6.7)
+ gitlab-markup (~> 1.5.0)
+ gitlab_git (~> 10.6.8)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
@@ -872,7 +876,7 @@ DEPENDENCIES
grape-entity (~> 0.4.2)
haml_lint (~> 0.18.2)
hamlit (~> 2.6.1)
- health_check (~> 2.1.0)
+ health_check (~> 2.2.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
@@ -889,7 +893,7 @@ DEPENDENCIES
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
loofah (~> 2.0.3)
- mail_room (~> 0.8)
+ mail_room (~> 0.8.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
@@ -934,7 +938,7 @@ DEPENDENCIES
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-rails (~> 4.0.0)
- request_store (~> 1.3.0)
+ request_store (~> 1.3)
rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 2.0)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 599331df3f5..56ec1489f89 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -6,11 +6,10 @@
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/:namespace_path/:project_path/labels",
- licensePath: "/api/:version/licenses/:key",
- gitignorePath: "/api/:version/gitignores/:key",
- gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
+ licensePath: "/api/:version/templates/licenses/:key",
+ gitignorePath: "/api/:version/templates/gitignores/:key",
+ gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
-
group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath)
.replace(':id', group_id);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 91c12570e09..d4f8f4b9420 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -28,12 +28,13 @@ $(() => {
state: Store.state,
loading: true,
endpoint: $boardApp.dataset.endpoint,
+ boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase
},
init: Store.create.bind(Store),
created () {
- gl.boardService = new BoardService(this.endpoint);
+ gl.boardService = new BoardService(this.endpoint, this.boardId);
},
ready () {
Store.disabled = this.disabled;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index 2b825c3949f..b9c91cbf31e 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,15 +1,15 @@
class BoardService {
- constructor (root) {
+ constructor (root, boardId) {
Vue.http.options.root = root;
- this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
+ this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
- url: `${root}/lists/generate.json`
+ url: `${root}/${boardId}/lists/generate.json`
}
});
- this.issue = Vue.resource(`${root}/issues{/id}`, {});
- this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
+ this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
+ this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index f336bfc36d6..97462a5959c 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -15,18 +15,17 @@
this.hideSidebar = bind(this.hideSidebar, this);
this.toggleSidebar = bind(this.toggleSidebar, this);
this.updateDropdown = bind(this.updateDropdown, this);
+ this.$document = $(document);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
- $('.js-build-sidebar').niceScroll();
+ this.initSidebar();
this.populateJobs(this.build_stage);
this.updateStageDropdownText(this.build_stage);
- this.hideSidebar();
- $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
$(window).off('resize.build').on('resize.build', this.hideSidebar);
- $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
$('#js-build-scroll > a').off('click').on('click', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
@@ -62,6 +61,21 @@
}
}
+ Build.prototype.initSidebar = function() {
+ this.$sidebar = $('.js-build-sidebar');
+ this.sidebarTranslationLimits = {
+ min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
+ }
+ this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
+ this.$sidebar.css({
+ top: this.sidebarTranslationLimits.max
+ });
+ this.$sidebar.niceScroll();
+ this.hideSidebar();
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+ this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
+ };
+
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
@@ -129,15 +143,23 @@
Build.prototype.toggleSidebar = function() {
if (this.shouldHideSidebar()) {
- return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed');
+ return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed');
}
};
+ Build.prototype.translateSidebar = function(e) {
+ var newPosition = this.sidebarTranslationLimits.max - document.body.scrollTop;
+ if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
+ this.$sidebar.css({
+ top: newPosition
+ });
+ };
+
Build.prototype.hideSidebar = function() {
if (this.shouldHideSidebar()) {
- return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
} else {
- return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
}
};
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/dispatcher.js b/app/assets/javascripts/dispatcher.js.es6
index 8d99b12102d..73691f40c74 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,7 +21,11 @@
path = page.split(':');
shortcut_handler = null;
switch (page) {
+ case 'sessions:new':
+ new UsernameValidator();
+ break;
case 'projects:boards:show':
+ case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:merge_requests:index':
@@ -45,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':
@@ -96,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();
@@ -126,6 +128,9 @@
new TreeView();
}
break;
+ case 'projects:pipelines:show':
+ new gl.Pipelines();
+ break;
case 'groups:activity':
new Activities();
break;
@@ -136,12 +141,12 @@
break;
case 'groups:group_members:index':
new gl.MemberExpirationDate();
- new GroupMembers();
+ new gl.Members();
new UsersSelect();
break;
case 'projects:project_members:index':
new gl.MemberExpirationDate();
- new ProjectMembers();
+ new gl.Members();
new UsersSelect();
break;
case 'groups:new':
@@ -287,6 +292,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..8657e7b4abf
--- /dev/null
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -0,0 +1,167 @@
+((global) => {
+ /*
+ * This class overrides the browser's validation error bubbles, displaying custom
+ * error messages for invalid fields instead. To begin validating any form, add the
+ * class `show-gl-field-errors` to the form element, and ensure error messages are
+ * declared in each inputs' title attribute.
+ *
+ * Example:
+ *
+ * <form class='show-gl-field-errors'>
+ * <input type='text' name='username' title='Username is required.'/>
+ *</form>
+ *
+ * */
+
+ const errorMessageClass = 'gl-field-error';
+ const inputErrorClass = 'gl-field-error-outline';
+
+ class GlFieldError {
+ constructor({ input, formErrors }) {
+ this.inputElement = $(input);
+ this.inputDomElement = this.inputElement.get(0);
+ this.form = formErrors;
+ this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
+ this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${ this.errorMessage }</p>`);
+
+ this.state = {
+ valid: false,
+ empty: true
+ };
+
+ this.initFieldValidation();
+ }
+
+ initFieldValidation() {
+ // hidden when injected into DOM
+ this.inputElement.after(this.fieldErrorElement);
+ this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
+ this.scopedSiblings = this.safelySelectSiblings();
+ }
+
+ safelySelectSiblings() {
+ // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled with input validity
+ const ignoreSelector = '.validation-ignore';
+ const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreSelector})`);
+ const parentContainer = this.inputElement.parent('.form-group');
+
+ // Only select siblings when they're scoped within a form-group with one input
+ const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
+
+ return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
+ }
+
+ renderValidity() {
+ this.renderClear();
+
+ if (this.state.valid) {
+ return this.renderValid();
+ }
+
+ if (this.state.empty) {
+ return this.renderEmpty();
+ }
+
+ if (!this.state.valid) {
+ return this.renderInvalid();
+ }
+
+ }
+
+ handleInvalidSubmit(event) {
+ event.preventDefault();
+ const currentValue = this.accessCurrentValue();
+ this.state.valid = false;
+ this.state.empty = currentValue === '';
+
+ this.renderValidity();
+ this.form.focusOnFirstInvalid.apply(this.form);
+ // For UX, wait til after first invalid submission to check each keyup
+ this.inputElement.off('keyup.field_validator')
+ .on('keyup.field_validator', this.updateValidity.bind(this));
+
+ }
+
+ /* Get or set current input value */
+ accessCurrentValue(newVal) {
+ return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
+ }
+
+ getInputValidity() {
+ return this.inputDomElement.validity.valid;
+ }
+
+ updateValidity() {
+ const inputVal = this.accessCurrentValue();
+ this.state.empty = !inputVal.length;
+ this.state.valid = this.getInputValidity();
+ this.renderValidity();
+ }
+
+ renderValid() {
+ return this.renderClear();
+ }
+
+ renderEmpty() {
+ return this.renderInvalid();
+ }
+
+ renderInvalid() {
+ this.inputElement.addClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ return this.fieldErrorElement.show();
+ }
+
+ renderClear() {
+ const inputVal = this.accessCurrentValue();
+ if (!inputVal.split(' ').length) {
+ const trimmedInput = inputVal.trim();
+ this.accessCurrentValue(trimmedInput);
+ }
+ this.inputElement.removeClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ this.fieldErrorElement.hide();
+ }
+ }
+
+ const customValidationFlag = 'no-gl-field-errors';
+
+ class GlFieldErrors {
+ constructor(form) {
+ this.form = $(form);
+ this.state = {
+ inputs: [],
+ valid: false
+ };
+ this.initValidators();
+ }
+
+ initValidators () {
+ // select all non-hidden inputs in form
+ this.state.inputs = this.form.find(':input:not([type=hidden])').toArray()
+ .filter((input) => !input.classList.contains(customValidationFlag))
+ .map((input) => new GlFieldError({ input, formErrors: this }));
+
+ this.form.on('submit', this.catchInvalidFormSubmit);
+ }
+
+ /* Neccessary to prevent intercept and override invalid form submit
+ * because Safari & iOS quietly allow form submission when form is invalid
+ * and prevents disabling of invalid submit button by application.js */
+
+ catchInvalidFormSubmit (event) {
+ if (!event.currentTarget.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ focusOnFirstInvalid () {
+ const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
+ firstInvalid.inputElement.focus();
+ }
+ }
+
+ global.GlFieldErrors = GlFieldErrors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js
deleted file mode 100644
index 4382dd6860f..00000000000
--- a/app/assets/javascripts/groups.js
+++ /dev/null
@@ -1,13 +0,0 @@
-(function() {
- this.GroupMembers = (function() {
- function GroupMembers() {
- $('li.group_member').bind('ajax:success', function() {
- return $(this).fadeOut();
- });
- }
-
- return GroupMembers;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index e356872624a..f1e719937c7 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -292,7 +292,7 @@
return;
}
- if (page === 'projects:boards:show') {
+ if ($('html').hasClass('issue-boards-page')) {
return;
}
if ($dropdown.hasClass('js-multiselect')) {
@@ -334,7 +334,7 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
- if (page === 'projects:boards:show') {
+ if ($('html').hasClass('issue-boards-page')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
}
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..a0cd20f21e8
--- /dev/null
+++ b/app/assets/javascripts/members.js.es6
@@ -0,0 +1,36 @@
+((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);
+ }
+
+ 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..fd21aa1fefa 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() {
@@ -380,6 +381,46 @@
// 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 tabsWidth = $tabs.outerWidth(),
+ $diffTabs = $('#diff-notes-app'),
+ offsetTop = $tabs.offset().top - ($('.navbar-fixed-top').height() + $('.layout-nav').height());
+
+ $tabs.off('affix.bs.affix affix-top.bs.affix')
+ .affix({
+ offset: {
+ top: offsetTop
+ }
+ }).on('affix.bs.affix', function () {
+ $tabs.css({
+ left: $tabs.offset().left,
+ width: tabsWidth
+ });
+ $diffTabs.css({
+ marginTop: $tabs.height()
+ });
+ }).on('affix-top.bs.affix', function () {
+ $tabs.css({
+ left: '',
+ width: ''
+ });
+ $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..fcadc4bc515 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,7 +1,26 @@
-(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>
+ </div>
+ </div>`;
+
+ global.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
@@ -10,17 +29,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 +66,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 +74,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 +94,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 +150,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 +183,41 @@
})(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.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 +258,4 @@
})();
-}).call(this);
+ })(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 26cc6eb0e96..cee42633c79 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -110,7 +110,7 @@
e.preventDefault();
return;
}
- if (page === 'projects:boards:show') {
+ if ($('html').hasClass('issue-boards-page')) {
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
deleted file mode 100644
index 8813bb5dfef..00000000000
--- a/app/assets/javascripts/pipeline.js.es6
+++ /dev/null
@@ -1,24 +0,0 @@
-(function() {
- function toggleGraph() {
- const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
- const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
- const $btnText = $(this).find('.toggle-btn-text');
- const $icon = $(this).find('.fa');
-
- $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
-
- const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
- const expandIcon = 'fa-caret-down';
- const hideIcon = 'fa-caret-up';
-
- if(graphCollapsed) {
- $btnText.text('Expand');
- $icon.removeClass(hideIcon).addClass(expandIcon);
- } else {
- $btnText.text('Hide');
- $icon.removeClass(expandIcon).addClass(hideIcon);
- }
- }
-
- $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
-})();
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
new file mode 100644
index 00000000000..a7624de6089
--- /dev/null
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -0,0 +1,40 @@
+((global) => {
+
+ class Pipelines {
+ constructor() {
+ $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
+ this.addMarginToBuildColumns();
+ }
+
+ toggleGraph() {
+ const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
+ const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
+ const $btnText = $(this).find('.toggle-btn-text');
+ const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+
+ $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+
+
+ graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
+ }
+
+ addMarginToBuildColumns() {
+ const $secondChildBuildNode = $('.build:nth-child(2)');
+ if ($secondChildBuildNode.length) {
+ const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
+ const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
+ const $previousColumn = $multiBuildColumn.prev('.stage-column');
+ $multiBuildColumn.addClass('left-margin');
+ $firstChildBuildNode.addClass('left-connector');
+ $previousColumn.each(function() {
+ $this = $(this);
+ if ($('.build', $this).length === 1) $this.addClass('no-margin');
+ });
+ }
+ $('.pipeline-graph').removeClass('hidden');
+ }
+ }
+
+ global.Pipelines = Pipelines;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 8e38ccf7e44..b8347367717 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -7,6 +7,7 @@
function ProjectFindFile(element1, options) {
this.element = element1;
this.options = options;
+ this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this);
@@ -154,6 +155,14 @@
return location.href = this.options.treeUrl;
};
+ ProjectFindFile.prototype.goToBlob = function() {
+ var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
+
+ if ($link.length) {
+ $link.get(0).click();
+ }
+ };
+
return ProjectFindFile;
})();
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
deleted file mode 100644
index 78f7b48bc7d..00000000000
--- a/app/assets/javascripts/project_members.js
+++ /dev/null
@@ -1,10 +0,0 @@
-(function() {
- this.ProjectMembers = (function() {
- function ProjectMembers() {
- $('li.project_member').bind('ajax:success', function() {
- return $(this).fadeOut();
- });
- }
- return ProjectMembers;
- })();
-}).call(this);
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index a787b11f2a9..3cf41505814 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -4,7 +4,9 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
- this.$selects = $('.features select');
+ this.$selects = $('.features select').filter(function () {
+ return $(this).data('field');
+ });
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
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 bcabda3ceb2..3020b7cc239 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -160,7 +160,7 @@
selectedId = user.id;
return;
}
- if (page === 'projects:boards:show') {
+ if ($('html').hasClass('issue-boards-page')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
@@ -261,10 +261,11 @@
}
}
if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
+ var trimmed = query.term.trim();
emailUser = {
name: "Invite \"" + query.term + "\"",
- username: query.term,
- id: query.term
+ username: trimmed,
+ id: trimmed
};
data.results.unshift(emailUser);
}
@@ -324,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/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 8002e56724b..df2e2ea8d2c 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;
}
@@ -203,6 +204,7 @@
}
}
}
+
&.user-cover-block {
padding: 24px 0 0;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index a7c8d782e9b..e6656c2d69a 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;
}
@@ -240,6 +241,7 @@
width: 100%;
margin: 0;
margin-bottom: 15px;
+
&.btn {
padding: 6px 0;
}
@@ -321,6 +323,7 @@
.btn-build {
margin-left: 10px;
+
i {
color: $gl-icon-color;
}
@@ -328,6 +331,7 @@
.clone-dropdown-btn a {
color: $dropdown-link-color;
+
&:hover {
text-decoration: none;
}
@@ -337,6 +341,7 @@
background-color: $background-color !important;
border: 1px solid lightgrey;
cursor: default;
+
&:active {
-moz-box-shadow: inset 0 0 0 white;
-webkit-box-shadow: inset 0 0 0 white;
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index da7bab74a32..f3b6ad88ad6 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -13,10 +13,12 @@
color: $text-color;
background: $background-color;
}
+
.bs-callout h4 {
margin-top: 0;
margin-bottom: 5px;
}
+
.bs-callout p:last-child {
margin-bottom: 0;
}
@@ -27,16 +29,19 @@
border-color: #eed3d7;
color: #b94a48;
}
+
.bs-callout-warning {
background-color: #faf8f0;
border-color: #faebcc;
color: #8a6d3b;
}
+
.bs-callout-info {
background-color: #f4f8fa;
border-color: #bce8f1;
color: #34789a;
}
+
.bs-callout-success {
background-color: #dff0d8;
border-color: #5ca64d;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 5957dce89bc..81e4e264560 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,31 +1,31 @@
/** COLORS **/
.cgray { color: $gl-gray; }
-.clgray { color: #bbb }
+.clgray { color: #bbb; }
.cred { color: $gl-text-red; }
.cgreen { color: $gl-text-green; }
-.cdark { color: #444 }
+.cdark { color: #444; }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
-.prepend-top-10 { margin-top: 10px }
+.prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top: 20px }
-.prepend-left-5 { margin-left: 5px }
-.prepend-left-10 { margin-left: 10px }
+.prepend-top-20 { margin-top: 20px; }
+.prepend-left-5 { margin-left: 5px; }
+.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
-.prepend-left-20 { margin-left: 20px }
-.append-right-5 { margin-right: 5px }
-.append-right-10 { margin-right: 10px }
+.prepend-left-20 { margin-left: 20px; }
+.append-right-5 { margin-right: 5px; }
+.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
-.append-right-20 { margin-right: 20px }
-.append-bottom-0 { margin-bottom: 0 }
-.append-bottom-10 { margin-bottom: 10px }
-.append-bottom-15 { margin-bottom: 15px }
-.append-bottom-20 { margin-bottom: 20px }
+.append-right-20 { margin-right: 20px; }
+.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-10 { margin-bottom: 10px; }
+.append-bottom-15 { margin-bottom: 15px; }
+.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
-.inline { display: inline-block }
-.center { text-align: center }
+.inline { display: inline-block; }
+.center { text-align: center; }
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: #999; }
@@ -97,6 +97,7 @@ span.update-author {
color: #999;
font-weight: normal;
font-style: italic;
+
strong {
font-weight: bold;
font-style: normal;
@@ -128,7 +129,7 @@ p.time {
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
- img { max-width: 100% }
+ img { max-width: 100%; }
.note-title {
li {
border-bottom: none !important;
@@ -172,6 +173,7 @@ li.note {
@extend .col-md-6;
text-align: left;
margin-top: 40px;
+
pre {
background: white;
border: none;
@@ -197,6 +199,7 @@ li.note {
background: #c67;
color: #fff;
font-weight: bold;
+
a {
color: #fff;
text-decoration: underline;
@@ -227,6 +230,7 @@ li.note {
&.milestone-closed {
background: $gray-light;
}
+
.progress {
margin-bottom: 0;
margin-top: 4px;
@@ -286,6 +290,7 @@ table {
.footer-links {
margin-bottom: 20px;
+
a {
margin-right: 15px;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index baa95711329..a839371a6f2 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;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 81520500594..13c1bbf0359 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;
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..fe834f4e2f6 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -62,7 +62,7 @@
}
i {
- color: $white-light
+ color: $white-light;
}
path,
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 9823abdde1f..3a4fdd0da22 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -168,6 +168,7 @@ header {
a {
color: $gl-text-color;
+
&:hover {
text-decoration: underline;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index efc348214c2..4b2627c1b87 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -60,6 +60,7 @@
padding-top: 1px;
margin: 0;
color: $gray-dark;
+
img {
position: relative;
top: 3px;
@@ -128,6 +129,10 @@ ul.content-list {
color: $gl-dark-link-color;
}
+ .member-group-link {
+ color: $blue-normal;
+ }
+
.description {
p {
@include str-truncated;
@@ -168,6 +173,14 @@ ul.content-list {
}
}
+ .member-controls {
+ float: none;
+
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ }
+ }
+
// When dragging a list item
&.ui-sortable-helper {
border-bottom: none;
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
index 3ee3fb4cee5..a90e45bb5f4 100644
--- a/app/assets/stylesheets/framework/logo.scss
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -1,15 +1,3 @@
-@mixin unique-keyframes {
- $animation-name: unique-id();
- @include webkit-prefix(animation-name, $animation-name);
-
- @-webkit-keyframes #{$animation-name} {
- @content;
- }
- @keyframes #{$animation-name} {
- @content;
- }
-}
-
@mixin tanuki-logo-colors($path-color) {
fill: $path-color;
transition: all 0.8s;
@@ -20,28 +8,6 @@
}
}
-@mixin tanuki-second-highlight-animations($tanuki-color) {
- @include unique-keyframes {
- 10%, 80% {
- fill: #{$tanuki-color}
- }
- 20%, 90% {
- fill: lighten($tanuki-color, 25%);
- }
- }
-}
-
-@mixin tanuki-forth-highlight-animations($tanuki-color) {
- @include unique-keyframes {
- 30%, 60% {
- fill: #{$tanuki-color};
- }
- 40%, 70% {
- fill: lighten($tanuki-color, 25%);
- }
- }
-}
-
.tanuki-logo {
.tanuki-left-ear,
@@ -67,10 +33,11 @@
}
.tanuki-left-cheek {
- @include unique-keyframes {
+ @include include-keyframes(animate-tanuki-left-cheek) {
0%, 10%, 100% {
fill: lighten($tanuki-yellow, 25%);
}
+
90% {
fill: $tanuki-yellow;
}
@@ -78,18 +45,35 @@
}
.tanuki-left-eye {
- @include tanuki-second-highlight-animations($tanuki-orange);
+ @include include-keyframes(animate-tanuki-left-eye) {
+ 10%, 80% {
+ fill: $tanuki-orange;
+ }
+
+ 20%, 90% {
+ fill: lighten($tanuki-orange, 25%);
+ }
+ }
}
.tanuki-left-ear {
- @include tanuki-second-highlight-animations($tanuki-red);
+ @include include-keyframes(animate-tanuki-left-ear) {
+ 10%, 80% {
+ fill: $tanuki-red;
+ }
+
+ 20%, 90% {
+ fill: lighten($tanuki-red, 25%);
+ }
+ }
}
.tanuki-nose {
- @include unique-keyframes {
+ @include include-keyframes(animate-tanuki-nose) {
20%, 70% {
fill: $tanuki-red;
}
+
30%, 80% {
fill: lighten($tanuki-red, 25%);
}
@@ -97,22 +81,39 @@
}
.tanuki-right-eye {
- @include tanuki-forth-highlight-animations($tanuki-orange);
+ @include include-keyframes(animate-tanuki-right-eye) {
+ 30%, 60% {
+ fill: $tanuki-orange;
+ }
+
+ 40%, 70% {
+ fill: lighten($tanuki-orange, 25%);
+ }
+ }
}
.tanuki-right-ear {
- @include tanuki-forth-highlight-animations($tanuki-red);
+ @include include-keyframes(animate-tanuki-right-ear) {
+ 30%, 60% {
+ fill: $tanuki-red;
+ }
+
+ 40%, 70% {
+ fill: lighten($tanuki-red, 25%);
+ }
+ }
}
.tanuki-right-cheek {
- @include unique-keyframes {
+ @include include-keyframes(animate-tanuki-right-cheek) {
40% {
fill: $tanuki-yellow;
}
+
60% {
fill: lighten($tanuki-yellow, 25%);
}
}
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/framework/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/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea43f4afc37..899db045b74 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -210,6 +210,7 @@
@media (max-width: $screen-xs-max) {
padding-bottom: 0;
width: 100%;
+
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
margin: 0 0 10px;
display: block;
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index c6f30e144fd..5ba0486177f 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -13,6 +13,11 @@
.dropdown-menu-toggle {
line-height: 20px;
}
+
+ .badge {
+ margin-top: -2px;
+ margin-left: 5px;
+ }
}
.panel-body {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 79cd26714a3..e0708c65695 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -93,7 +93,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 +101,7 @@
}
.select2-search-choice {
- margin: 8px 0 0 8px;
+ margin: 5px 0 0 8px;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
@@ -137,6 +137,7 @@
.select2-results {
max-height: 350px;
+
.select2-highlighted {
background: $gl-primary;
}
@@ -212,9 +213,11 @@
.group-image {
float: left;
}
+
.group-name {
font-weight: bold;
}
+
.group-path {
color: #999;
}
@@ -239,6 +242,7 @@
color: #aaa;
font-weight: normal;
}
+
.namespace-path {
margin-left: 10px;
font-weight: bolder;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 0b0bd80c326..eb63a9f214b 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -48,6 +48,7 @@
&:before {
background: none;
}
+
.timeline-entry .timeline-entry-inner {
.timeline-icon {
display: none;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index e3154657c54..f4106641269 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -48,31 +48,40 @@
.clearfix {
@include clearfix();
}
+
.center-block {
@include center-block();
}
+
.pull-right {
float: right !important;
}
+
.pull-left {
float: left !important;
}
+
.hide {
display: none;
}
+
.show {
display: block !important;
}
+
.invisible {
visibility: hidden;
}
+
.text-hide {
@include text-hide();
}
+
.hidden {
display: none !important;
visibility: hidden !important;
}
+
.affix {
position: fixed;
}
@@ -146,6 +155,7 @@
padding: 6px 15px;
font-size: 13px;
font-weight: normal;
+
a {
color: #777;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index d099a884f54..55de9053be5 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -45,40 +45,38 @@
}
h1 {
- font-size: 2em;
+ font-size: 1.75em;
font-weight: 600;
- margin: 1em 0 10px;
+ margin: 16px 0 10px;
padding: 0 0 0.3em;
- border-bottom: 1px solid $btn-default-border;
+ border-bottom: 1px solid $white-dark;
color: $gl-gray-dark;
}
h2 {
- font-size: 1.6em;
+ font-size: 1.5em;
font-weight: 600;
- margin: 1em 0 10px;
- padding-bottom: 0.3em;
- border-bottom: 1px solid $btn-default-border;
+ margin: 16px 0 10px;
color: $gl-gray-dark;
}
h3 {
- margin: 1em 0 10px;
- font-size: 1.4em;
+ margin: 16px 0 10px;
+ font-size: 1.3em;
}
h4 {
- margin: 1em 0 10px;
- font-size: 1.25em;
+ margin: 16px 0 10px;
+ font-size: 1.2em;
}
h5 {
- margin: 1em 0 10px;
+ margin: 16px 0 10px;
font-size: 1em;
}
h6 {
- margin: 1em 0 10px;
+ margin: 16px 0 10px;
font-size: 0.95em;
}
@@ -87,7 +85,12 @@
font-size: inherit;
padding: 8px 21px;
margin: 12px 0;
- border-left: 3px solid #e7e9ed;
+ border-left: 3px solid $white-dark;
+ }
+
+ blockquote:dir(rtl) {
+ border-left: 0;
+ border-right: 3px solid $white-dark;
}
blockquote p {
@@ -106,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;
@@ -128,6 +136,10 @@
margin: 3px 0 3px 28px !important;
}
+ ul:dir(rtl), ol:dir(rtl) {
+ margin: 3px 28px 3px 0 !important;
+ }
+
li {
line-height: 1.6em;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 14ec310de2d..7690d65de8e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -17,8 +17,10 @@ $white-normal: #ededed;
$white-dark: #ececec;
$gray-light: #fafafa;
+$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
$gray-dark: #ededed;
+$gray-darker: #eee;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
@@ -33,6 +35,8 @@ $blue-medium-light: #3498cb;
$blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4;
+$blue-light-transparent: rgba(44, 159, 216, 0.05);
+
$orange-light: #fc8a51;
$orange-normal: #e75e40;
$orange-dark: #ce5237;
@@ -52,6 +56,7 @@ $border-gray-light: #dcdcdc;
$border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf;
+$border-green-extra-light: #9adb84;
$border-green-light: #2faa60;
$border-green-normal: #2ca05b;
$border-green-dark: #279654;
@@ -91,6 +96,7 @@ $table-text-gray: #8f8f8f;
$gl-font-size: 15px;
$gl-title-color: #333;
$gl-text-color: #5c5c5c;
+$gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 16ffbe57a99..a3acee299e3 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -55,68 +55,68 @@
color: #000 !important;
}
- .hll { background-color: #373b41 }
- .c { color: #969896 } /* Comment */
- .err { color: #c66 } /* Error */
- .k { color: #b294bb } /* Keyword */
- .l { color: #de935f } /* Literal */
- .n { color: #c5c8c6 } /* Name */
- .o { color: #8abeb7 } /* Operator */
- .p { color: #c5c8c6 } /* Punctuation */
- .cm { color: #969896 } /* Comment.Multiline */
- .cp { color: #969896 } /* Comment.Preproc */
- .c1 { color: #969896 } /* Comment.Single */
- .cs { color: #969896 } /* Comment.Special */
- .gd { color: #c66 } /* Generic.Deleted */
- .ge { font-style: italic } /* Generic.Emph */
- .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
- .gi { color: #b5bd68 } /* Generic.Inserted */
- .gp { color: #969896; font-weight: bold } /* Generic.Prompt */
- .gs { font-weight: bold } /* Generic.Strong */
- .gu { color: #8abeb7; font-weight: bold } /* Generic.Subheading */
- .kc { color: #b294bb } /* Keyword.Constant */
- .kd { color: #b294bb } /* Keyword.Declaration */
- .kn { color: #8abeb7 } /* Keyword.Namespace */
- .kp { color: #b294bb } /* Keyword.Pseudo */
- .kr { color: #b294bb } /* Keyword.Reserved */
- .kt { color: #f0c674 } /* Keyword.Type */
- .ld { color: #b5bd68 } /* Literal.Date */
- .m { color: #de935f } /* Literal.Number */
- .s { color: #b5bd68 } /* Literal.String */
- .na { color: #81a2be } /* Name.Attribute */
- .nb { color: #c5c8c6 } /* Name.Builtin */
- .nc { color: #f0c674 } /* Name.Class */
- .no { color: #c66 } /* Name.Constant */
- .nd { color: #8abeb7 } /* Name.Decorator */
- .ni { color: #c5c8c6 } /* Name.Entity */
- .ne { color: #c66 } /* Name.Exception */
- .nf { color: #81a2be } /* Name.Function */
- .nl { color: #c5c8c6 } /* Name.Label */
- .nn { color: #f0c674 } /* Name.Namespace */
- .nx { color: #81a2be } /* Name.Other */
- .py { color: #c5c8c6 } /* Name.Property */
- .nt { color: #8abeb7 } /* Name.Tag */
- .nv { color: #c66 } /* Name.Variable */
- .ow { color: #8abeb7 } /* Operator.Word */
- .w { color: #c5c8c6 } /* Text.Whitespace */
- .mf { color: #de935f } /* Literal.Number.Float */
- .mh { color: #de935f } /* Literal.Number.Hex */
- .mi { color: #de935f } /* Literal.Number.Integer */
- .mo { color: #de935f } /* Literal.Number.Oct */
- .sb { color: #b5bd68 } /* Literal.String.Backtick */
- .sc { color: #c5c8c6 } /* Literal.String.Char */
- .sd { color: #969896 } /* Literal.String.Doc */
- .s2 { color: #b5bd68 } /* Literal.String.Double */
- .se { color: #de935f } /* Literal.String.Escape */
- .sh { color: #b5bd68 } /* Literal.String.Heredoc */
- .si { color: #de935f } /* Literal.String.Interpol */
- .sx { color: #b5bd68 } /* Literal.String.Other */
- .sr { color: #b5bd68 } /* Literal.String.Regex */
- .s1 { color: #b5bd68 } /* Literal.String.Single */
- .ss { color: #b5bd68 } /* Literal.String.Symbol */
- .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
- .vc { color: #c66 } /* Name.Variable.Class */
- .vg { color: #c66 } /* Name.Variable.Global */
- .vi { color: #c66 } /* Name.Variable.Instance */
- .il { color: #de935f } /* Literal.Number.Integer.Long */
+ .hll { background-color: #373b41; }
+ .c { color: #969896; } /* Comment */
+ .err { color: #c66; } /* Error */
+ .k { color: #b294bb; } /* Keyword */
+ .l { color: #de935f; } /* Literal */
+ .n { color: #c5c8c6; } /* Name */
+ .o { color: #8abeb7; } /* Operator */
+ .p { color: #c5c8c6; } /* Punctuation */
+ .cm { color: #969896; } /* Comment.Multiline */
+ .cp { color: #969896; } /* Comment.Preproc */
+ .c1 { color: #969896; } /* Comment.Single */
+ .cs { color: #969896; } /* Comment.Special */
+ .gd { color: #c66; } /* Generic.Deleted */
+ .ge { font-style: italic; } /* Generic.Emph */
+ .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */
+ .gi { color: #b5bd68; } /* Generic.Inserted */
+ .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */
+ .gs { font-weight: bold; } /* Generic.Strong */
+ .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */
+ .kc { color: #b294bb; } /* Keyword.Constant */
+ .kd { color: #b294bb; } /* Keyword.Declaration */
+ .kn { color: #8abeb7; } /* Keyword.Namespace */
+ .kp { color: #b294bb; } /* Keyword.Pseudo */
+ .kr { color: #b294bb; } /* Keyword.Reserved */
+ .kt { color: #f0c674; } /* Keyword.Type */
+ .ld { color: #b5bd68; } /* Literal.Date */
+ .m { color: #de935f; } /* Literal.Number */
+ .s { color: #b5bd68; } /* Literal.String */
+ .na { color: #81a2be; } /* Name.Attribute */
+ .nb { color: #c5c8c6; } /* Name.Builtin */
+ .nc { color: #f0c674; } /* Name.Class */
+ .no { color: #c66; } /* Name.Constant */
+ .nd { color: #8abeb7; } /* Name.Decorator */
+ .ni { color: #c5c8c6; } /* Name.Entity */
+ .ne { color: #c66; } /* Name.Exception */
+ .nf { color: #81a2be; } /* Name.Function */
+ .nl { color: #c5c8c6; } /* Name.Label */
+ .nn { color: #f0c674; } /* Name.Namespace */
+ .nx { color: #81a2be; } /* Name.Other */
+ .py { color: #c5c8c6; } /* Name.Property */
+ .nt { color: #8abeb7; } /* Name.Tag */
+ .nv { color: #c66; } /* Name.Variable */
+ .ow { color: #8abeb7; } /* Operator.Word */
+ .w { color: #c5c8c6; } /* Text.Whitespace */
+ .mf { color: #de935f; } /* Literal.Number.Float */
+ .mh { color: #de935f; } /* Literal.Number.Hex */
+ .mi { color: #de935f; } /* Literal.Number.Integer */
+ .mo { color: #de935f; } /* Literal.Number.Oct */
+ .sb { color: #b5bd68; } /* Literal.String.Backtick */
+ .sc { color: #c5c8c6; } /* Literal.String.Char */
+ .sd { color: #969896; } /* Literal.String.Doc */
+ .s2 { color: #b5bd68; } /* Literal.String.Double */
+ .se { color: #de935f; } /* Literal.String.Escape */
+ .sh { color: #b5bd68; } /* Literal.String.Heredoc */
+ .si { color: #de935f; } /* Literal.String.Interpol */
+ .sx { color: #b5bd68; } /* Literal.String.Other */
+ .sr { color: #b5bd68; } /* Literal.String.Regex */
+ .s1 { color: #b5bd68; } /* Literal.String.Single */
+ .ss { color: #b5bd68; } /* Literal.String.Symbol */
+ .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */
+ .vc { color: #c66; } /* Name.Variable.Class */
+ .vg { color: #c66; } /* Name.Variable.Global */
+ .vi { color: #c66; } /* Name.Variable.Instance */
+ .il { color: #de935f; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 7de920e074b..e9228c94db9 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -55,65 +55,65 @@
color: #000 !important;
}
- .hll { background-color: #49483e }
- .c { color: #75715e } /* Comment */
- .err { color: #960050; background-color: #1e0010 } /* Error */
- .k { color: #66d9ef } /* Keyword */
- .l { color: #ae81ff } /* Literal */
- .n { color: #f8f8f2 } /* Name */
- .o { color: #f92672 } /* Operator */
- .p { color: #f8f8f2 } /* Punctuation */
- .cm { color: #75715e } /* Comment.Multiline */
- .cp { color: #75715e } /* Comment.Preproc */
- .c1 { color: #75715e } /* Comment.Single */
- .cs { color: #75715e } /* Comment.Special */
- .ge { font-style: italic } /* Generic.Emph */
- .gs { font-weight: bold } /* Generic.Strong */
- .kc { color: #66d9ef } /* Keyword.Constant */
- .kd { color: #66d9ef } /* Keyword.Declaration */
- .kn { color: #f92672 } /* Keyword.Namespace */
- .kp { color: #66d9ef } /* Keyword.Pseudo */
- .kr { color: #66d9ef } /* Keyword.Reserved */
- .kt { color: #66d9ef } /* Keyword.Type */
- .ld { color: #e6db74 } /* Literal.Date */
- .m { color: #ae81ff } /* Literal.Number */
- .s { color: #e6db74 } /* Literal.String */
- .na { color: #a6e22e } /* Name.Attribute */
- .nb { color: #f8f8f2 } /* Name.Builtin */
- .nc { color: #a6e22e } /* Name.Class */
- .no { color: #66d9ef } /* Name.Constant */
- .nd { color: #a6e22e } /* Name.Decorator */
- .ni { color: #f8f8f2 } /* Name.Entity */
- .ne { color: #a6e22e } /* Name.Exception */
- .nf { color: #a6e22e } /* Name.Function */
- .nl { color: #f8f8f2 } /* Name.Label */
- .nn { color: #f8f8f2 } /* Name.Namespace */
- .nx { color: #a6e22e } /* Name.Other */
- .py { color: #f8f8f2 } /* Name.Property */
- .nt { color: #f92672 } /* Name.Tag */
- .nv { color: #f8f8f2 } /* Name.Variable */
- .ow { color: #f92672 } /* Operator.Word */
- .w { color: #f8f8f2 } /* Text.Whitespace */
- .mf { color: #ae81ff } /* Literal.Number.Float */
- .mh { color: #ae81ff } /* Literal.Number.Hex */
- .mi { color: #ae81ff } /* Literal.Number.Integer */
- .mo { color: #ae81ff } /* Literal.Number.Oct */
- .sb { color: #e6db74 } /* Literal.String.Backtick */
- .sc { color: #e6db74 } /* Literal.String.Char */
- .sd { color: #e6db74 } /* Literal.String.Doc */
- .s2 { color: #e6db74 } /* Literal.String.Double */
- .se { color: #ae81ff } /* Literal.String.Escape */
- .sh { color: #e6db74 } /* Literal.String.Heredoc */
- .si { color: #e6db74 } /* Literal.String.Interpol */
- .sx { color: #e6db74 } /* Literal.String.Other */
- .sr { color: #e6db74 } /* Literal.String.Regex */
- .s1 { color: #e6db74 } /* Literal.String.Single */
- .ss { color: #e6db74 } /* Literal.String.Symbol */
- .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
- .vc { color: #f8f8f2 } /* Name.Variable.Class */
- .vg { color: #f8f8f2 } /* Name.Variable.Global */
- .vi { color: #f8f8f2 } /* Name.Variable.Instance */
- .il { color: #ae81ff } /* Literal.Number.Integer.Long */
+ .hll { background-color: #49483e; }
+ .c { color: #75715e; } /* Comment */
+ .err { color: #960050; background-color: #1e0010; } /* Error */
+ .k { color: #66d9ef; } /* Keyword */
+ .l { color: #ae81ff; } /* Literal */
+ .n { color: #f8f8f2; } /* Name */
+ .o { color: #f92672; } /* Operator */
+ .p { color: #f8f8f2; } /* Punctuation */
+ .cm { color: #75715e; } /* Comment.Multiline */
+ .cp { color: #75715e; } /* Comment.Preproc */
+ .c1 { color: #75715e; } /* Comment.Single */
+ .cs { color: #75715e; } /* Comment.Special */
+ .ge { font-style: italic; } /* Generic.Emph */
+ .gs { font-weight: bold; } /* Generic.Strong */
+ .kc { color: #66d9ef; } /* Keyword.Constant */
+ .kd { color: #66d9ef; } /* Keyword.Declaration */
+ .kn { color: #f92672; } /* Keyword.Namespace */
+ .kp { color: #66d9ef; } /* Keyword.Pseudo */
+ .kr { color: #66d9ef; } /* Keyword.Reserved */
+ .kt { color: #66d9ef; } /* Keyword.Type */
+ .ld { color: #e6db74; } /* Literal.Date */
+ .m { color: #ae81ff; } /* Literal.Number */
+ .s { color: #e6db74; } /* Literal.String */
+ .na { color: #a6e22e; } /* Name.Attribute */
+ .nb { color: #f8f8f2; } /* Name.Builtin */
+ .nc { color: #a6e22e; } /* Name.Class */
+ .no { color: #66d9ef; } /* Name.Constant */
+ .nd { color: #a6e22e; } /* Name.Decorator */
+ .ni { color: #f8f8f2; } /* Name.Entity */
+ .ne { color: #a6e22e; } /* Name.Exception */
+ .nf { color: #a6e22e; } /* Name.Function */
+ .nl { color: #f8f8f2; } /* Name.Label */
+ .nn { color: #f8f8f2; } /* Name.Namespace */
+ .nx { color: #a6e22e; } /* Name.Other */
+ .py { color: #f8f8f2; } /* Name.Property */
+ .nt { color: #f92672; } /* Name.Tag */
+ .nv { color: #f8f8f2; } /* Name.Variable */
+ .ow { color: #f92672; } /* Operator.Word */
+ .w { color: #f8f8f2; } /* Text.Whitespace */
+ .mf { color: #ae81ff; } /* Literal.Number.Float */
+ .mh { color: #ae81ff; } /* Literal.Number.Hex */
+ .mi { color: #ae81ff; } /* Literal.Number.Integer */
+ .mo { color: #ae81ff; } /* Literal.Number.Oct */
+ .sb { color: #e6db74; } /* Literal.String.Backtick */
+ .sc { color: #e6db74; } /* Literal.String.Char */
+ .sd { color: #e6db74; } /* Literal.String.Doc */
+ .s2 { color: #e6db74; } /* Literal.String.Double */
+ .se { color: #ae81ff; } /* Literal.String.Escape */
+ .sh { color: #e6db74; } /* Literal.String.Heredoc */
+ .si { color: #e6db74; } /* Literal.String.Interpol */
+ .sx { color: #e6db74; } /* Literal.String.Other */
+ .sr { color: #e6db74; } /* Literal.String.Regex */
+ .s1 { color: #e6db74; } /* Literal.String.Single */
+ .ss { color: #e6db74; } /* Literal.String.Symbol */
+ .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */
+ .vc { color: #f8f8f2; } /* Name.Variable.Class */
+ .vg { color: #f8f8f2; } /* Name.Variable.Global */
+ .vi { color: #f8f8f2; } /* Name.Variable.Instance */
+ .il { color: #ae81ff; } /* Literal.Number.Integer.Long */
.gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */
.gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */
.gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index b11499c71ee..c3c7773b9e2 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -72,72 +72,72 @@
green #859900 operators, other keywords
*/
- .c { color: #586e75 } /* Comment */
- .err { color: #93a1a1 } /* Error */
- .g { color: #93a1a1 } /* Generic */
- .k { color: #859900 } /* Keyword */
- .l { color: #93a1a1 } /* Literal */
- .n { color: #93a1a1 } /* Name */
- .o { color: #859900 } /* Operator */
- .x { color: #cb4b16 } /* Other */
- .p { color: #93a1a1 } /* Punctuation */
- .cm { color: #586e75 } /* Comment.Multiline */
- .cp { color: #859900 } /* Comment.Preproc */
- .c1 { color: #586e75 } /* Comment.Single */
- .cs { color: #859900 } /* Comment.Special */
- .gd { color: #2aa198 } /* Generic.Deleted */
- .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */
- .gr { color: #dc322f } /* Generic.Error */
- .gh { color: #cb4b16 } /* Generic.Heading */
- .gi { color: #859900 } /* Generic.Inserted */
- .go { color: #93a1a1 } /* Generic.Output */
- .gp { color: #93a1a1 } /* Generic.Prompt */
- .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */
- .gu { color: #cb4b16 } /* Generic.Subheading */
- .gt { color: #93a1a1 } /* Generic.Traceback */
- .kc { color: #cb4b16 } /* Keyword.Constant */
- .kd { color: #268bd2 } /* Keyword.Declaration */
- .kn { color: #859900 } /* Keyword.Namespace */
- .kp { color: #859900 } /* Keyword.Pseudo */
- .kr { color: #268bd2 } /* Keyword.Reserved */
- .kt { color: #dc322f } /* Keyword.Type */
- .ld { color: #93a1a1 } /* Literal.Date */
- .m { color: #2aa198 } /* Literal.Number */
- .s { color: #2aa198 } /* Literal.String */
- .na { color: #93a1a1 } /* Name.Attribute */
- .nb { color: #b58900 } /* Name.Builtin */
- .nc { color: #268bd2 } /* Name.Class */
- .no { color: #cb4b16 } /* Name.Constant */
- .nd { color: #268bd2 } /* Name.Decorator */
- .ni { color: #cb4b16 } /* Name.Entity */
- .ne { color: #cb4b16 } /* Name.Exception */
- .nf { color: #268bd2 } /* Name.Function */
- .nl { color: #93a1a1 } /* Name.Label */
- .nn { color: #93a1a1 } /* Name.Namespace */
- .nx { color: #93a1a1 } /* Name.Other */
- .py { color: #93a1a1 } /* Name.Property */
- .nt { color: #268bd2 } /* Name.Tag */
- .nv { color: #268bd2 } /* Name.Variable */
- .ow { color: #859900 } /* Operator.Word */
- .w { color: #93a1a1 } /* Text.Whitespace */
- .mf { color: #2aa198 } /* Literal.Number.Float */
- .mh { color: #2aa198 } /* Literal.Number.Hex */
- .mi { color: #2aa198 } /* Literal.Number.Integer */
- .mo { color: #2aa198 } /* Literal.Number.Oct */
- .sb { color: #586e75 } /* Literal.String.Backtick */
- .sc { color: #2aa198 } /* Literal.String.Char */
- .sd { color: #93a1a1 } /* Literal.String.Doc */
- .s2 { color: #2aa198 } /* Literal.String.Double */
- .se { color: #cb4b16 } /* Literal.String.Escape */
- .sh { color: #93a1a1 } /* Literal.String.Heredoc */
- .si { color: #2aa198 } /* Literal.String.Interpol */
- .sx { color: #2aa198 } /* Literal.String.Other */
- .sr { color: #dc322f } /* Literal.String.Regex */
- .s1 { color: #2aa198 } /* Literal.String.Single */
- .ss { color: #2aa198 } /* Literal.String.Symbol */
- .bp { color: #268bd2 } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2 } /* Name.Variable.Class */
- .vg { color: #268bd2 } /* Name.Variable.Global */
- .vi { color: #268bd2 } /* Name.Variable.Instance */
- .il { color: #2aa198 } /* Literal.Number.Integer.Long */
+ .c { color: #586e75; } /* Comment */
+ .err { color: #93a1a1; } /* Error */
+ .g { color: #93a1a1; } /* Generic */
+ .k { color: #859900; } /* Keyword */
+ .l { color: #93a1a1; } /* Literal */
+ .n { color: #93a1a1; } /* Name */
+ .o { color: #859900; } /* Operator */
+ .x { color: #cb4b16; } /* Other */
+ .p { color: #93a1a1; } /* Punctuation */
+ .cm { color: #586e75; } /* Comment.Multiline */
+ .cp { color: #859900; } /* Comment.Preproc */
+ .c1 { color: #586e75; } /* Comment.Single */
+ .cs { color: #859900; } /* Comment.Special */
+ .gd { color: #2aa198; } /* Generic.Deleted */
+ .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */
+ .gr { color: #dc322f; } /* Generic.Error */
+ .gh { color: #cb4b16; } /* Generic.Heading */
+ .gi { color: #859900; } /* Generic.Inserted */
+ .go { color: #93a1a1; } /* Generic.Output */
+ .gp { color: #93a1a1; } /* Generic.Prompt */
+ .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */
+ .gu { color: #cb4b16; } /* Generic.Subheading */
+ .gt { color: #93a1a1; } /* Generic.Traceback */
+ .kc { color: #cb4b16; } /* Keyword.Constant */
+ .kd { color: #268bd2; } /* Keyword.Declaration */
+ .kn { color: #859900; } /* Keyword.Namespace */
+ .kp { color: #859900; } /* Keyword.Pseudo */
+ .kr { color: #268bd2; } /* Keyword.Reserved */
+ .kt { color: #dc322f; } /* Keyword.Type */
+ .ld { color: #93a1a1; } /* Literal.Date */
+ .m { color: #2aa198; } /* Literal.Number */
+ .s { color: #2aa198; } /* Literal.String */
+ .na { color: #93a1a1; } /* Name.Attribute */
+ .nb { color: #b58900; } /* Name.Builtin */
+ .nc { color: #268bd2; } /* Name.Class */
+ .no { color: #cb4b16; } /* Name.Constant */
+ .nd { color: #268bd2; } /* Name.Decorator */
+ .ni { color: #cb4b16; } /* Name.Entity */
+ .ne { color: #cb4b16; } /* Name.Exception */
+ .nf { color: #268bd2; } /* Name.Function */
+ .nl { color: #93a1a1; } /* Name.Label */
+ .nn { color: #93a1a1; } /* Name.Namespace */
+ .nx { color: #93a1a1; } /* Name.Other */
+ .py { color: #93a1a1; } /* Name.Property */
+ .nt { color: #268bd2; } /* Name.Tag */
+ .nv { color: #268bd2; } /* Name.Variable */
+ .ow { color: #859900; } /* Operator.Word */
+ .w { color: #93a1a1; } /* Text.Whitespace */
+ .mf { color: #2aa198; } /* Literal.Number.Float */
+ .mh { color: #2aa198; } /* Literal.Number.Hex */
+ .mi { color: #2aa198; } /* Literal.Number.Integer */
+ .mo { color: #2aa198; } /* Literal.Number.Oct */
+ .sb { color: #586e75; } /* Literal.String.Backtick */
+ .sc { color: #2aa198; } /* Literal.String.Char */
+ .sd { color: #93a1a1; } /* Literal.String.Doc */
+ .s2 { color: #2aa198; } /* Literal.String.Double */
+ .se { color: #cb4b16; } /* Literal.String.Escape */
+ .sh { color: #93a1a1; } /* Literal.String.Heredoc */
+ .si { color: #2aa198; } /* Literal.String.Interpol */
+ .sx { color: #2aa198; } /* Literal.String.Other */
+ .sr { color: #dc322f; } /* Literal.String.Regex */
+ .s1 { color: #2aa198; } /* Literal.String.Single */
+ .ss { color: #2aa198; } /* Literal.String.Symbol */
+ .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
+ .vc { color: #268bd2; } /* Name.Variable.Class */
+ .vg { color: #268bd2; } /* Name.Variable.Global */
+ .vi { color: #268bd2; } /* Name.Variable.Instance */
+ .il { color: #2aa198; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 657bb5e3cd9..5956a28cafe 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -78,72 +78,72 @@
green #859900 operators, other keywords
*/
- .c { color: #93a1a1 } /* Comment */
- .err { color: #586e75 } /* Error */
- .g { color: #586e75 } /* Generic */
- .k { color: #859900 } /* Keyword */
- .l { color: #586e75 } /* Literal */
- .n { color: #586e75 } /* Name */
- .o { color: #859900 } /* Operator */
- .x { color: #cb4b16 } /* Other */
- .p { color: #586e75 } /* Punctuation */
- .cm { color: #93a1a1 } /* Comment.Multiline */
- .cp { color: #859900 } /* Comment.Preproc */
- .c1 { color: #93a1a1 } /* Comment.Single */
- .cs { color: #859900 } /* Comment.Special */
- .gd { color: #2aa198 } /* Generic.Deleted */
- .ge { color: #586e75; font-style: italic } /* Generic.Emph */
- .gr { color: #dc322f } /* Generic.Error */
- .gh { color: #cb4b16 } /* Generic.Heading */
- .gi { color: #859900 } /* Generic.Inserted */
- .go { color: #586e75 } /* Generic.Output */
- .gp { color: #586e75 } /* Generic.Prompt */
- .gs { color: #586e75; font-weight: bold } /* Generic.Strong */
- .gu { color: #cb4b16 } /* Generic.Subheading */
- .gt { color: #586e75 } /* Generic.Traceback */
- .kc { color: #cb4b16 } /* Keyword.Constant */
- .kd { color: #268bd2 } /* Keyword.Declaration */
- .kn { color: #859900 } /* Keyword.Namespace */
- .kp { color: #859900 } /* Keyword.Pseudo */
- .kr { color: #268bd2 } /* Keyword.Reserved */
- .kt { color: #dc322f } /* Keyword.Type */
- .ld { color: #586e75 } /* Literal.Date */
- .m { color: #2aa198 } /* Literal.Number */
- .s { color: #2aa198 } /* Literal.String */
- .na { color: #586e75 } /* Name.Attribute */
- .nb { color: #b58900 } /* Name.Builtin */
- .nc { color: #268bd2 } /* Name.Class */
- .no { color: #cb4b16 } /* Name.Constant */
- .nd { color: #268bd2 } /* Name.Decorator */
- .ni { color: #cb4b16 } /* Name.Entity */
- .ne { color: #cb4b16 } /* Name.Exception */
- .nf { color: #268bd2 } /* Name.Function */
- .nl { color: #586e75 } /* Name.Label */
- .nn { color: #586e75 } /* Name.Namespace */
- .nx { color: #586e75 } /* Name.Other */
- .py { color: #586e75 } /* Name.Property */
- .nt { color: #268bd2 } /* Name.Tag */
- .nv { color: #268bd2 } /* Name.Variable */
- .ow { color: #859900 } /* Operator.Word */
- .w { color: #586e75 } /* Text.Whitespace */
- .mf { color: #2aa198 } /* Literal.Number.Float */
- .mh { color: #2aa198 } /* Literal.Number.Hex */
- .mi { color: #2aa198 } /* Literal.Number.Integer */
- .mo { color: #2aa198 } /* Literal.Number.Oct */
- .sb { color: #93a1a1 } /* Literal.String.Backtick */
- .sc { color: #2aa198 } /* Literal.String.Char */
- .sd { color: #586e75 } /* Literal.String.Doc */
- .s2 { color: #2aa198 } /* Literal.String.Double */
- .se { color: #cb4b16 } /* Literal.String.Escape */
- .sh { color: #586e75 } /* Literal.String.Heredoc */
- .si { color: #2aa198 } /* Literal.String.Interpol */
- .sx { color: #2aa198 } /* Literal.String.Other */
- .sr { color: #dc322f } /* Literal.String.Regex */
- .s1 { color: #2aa198 } /* Literal.String.Single */
- .ss { color: #2aa198 } /* Literal.String.Symbol */
- .bp { color: #268bd2 } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2 } /* Name.Variable.Class */
- .vg { color: #268bd2 } /* Name.Variable.Global */
- .vi { color: #268bd2 } /* Name.Variable.Instance */
- .il { color: #2aa198 } /* Literal.Number.Integer.Long */
+ .c { color: #93a1a1; } /* Comment */
+ .err { color: #586e75; } /* Error */
+ .g { color: #586e75; } /* Generic */
+ .k { color: #859900; } /* Keyword */
+ .l { color: #586e75; } /* Literal */
+ .n { color: #586e75; } /* Name */
+ .o { color: #859900; } /* Operator */
+ .x { color: #cb4b16; } /* Other */
+ .p { color: #586e75; } /* Punctuation */
+ .cm { color: #93a1a1; } /* Comment.Multiline */
+ .cp { color: #859900; } /* Comment.Preproc */
+ .c1 { color: #93a1a1; } /* Comment.Single */
+ .cs { color: #859900; } /* Comment.Special */
+ .gd { color: #2aa198; } /* Generic.Deleted */
+ .ge { color: #586e75; font-style: italic; } /* Generic.Emph */
+ .gr { color: #dc322f; } /* Generic.Error */
+ .gh { color: #cb4b16; } /* Generic.Heading */
+ .gi { color: #859900; } /* Generic.Inserted */
+ .go { color: #586e75; } /* Generic.Output */
+ .gp { color: #586e75; } /* Generic.Prompt */
+ .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */
+ .gu { color: #cb4b16; } /* Generic.Subheading */
+ .gt { color: #586e75; } /* Generic.Traceback */
+ .kc { color: #cb4b16; } /* Keyword.Constant */
+ .kd { color: #268bd2; } /* Keyword.Declaration */
+ .kn { color: #859900; } /* Keyword.Namespace */
+ .kp { color: #859900; } /* Keyword.Pseudo */
+ .kr { color: #268bd2; } /* Keyword.Reserved */
+ .kt { color: #dc322f; } /* Keyword.Type */
+ .ld { color: #586e75; } /* Literal.Date */
+ .m { color: #2aa198; } /* Literal.Number */
+ .s { color: #2aa198; } /* Literal.String */
+ .na { color: #586e75; } /* Name.Attribute */
+ .nb { color: #b58900; } /* Name.Builtin */
+ .nc { color: #268bd2; } /* Name.Class */
+ .no { color: #cb4b16; } /* Name.Constant */
+ .nd { color: #268bd2; } /* Name.Decorator */
+ .ni { color: #cb4b16; } /* Name.Entity */
+ .ne { color: #cb4b16; } /* Name.Exception */
+ .nf { color: #268bd2; } /* Name.Function */
+ .nl { color: #586e75; } /* Name.Label */
+ .nn { color: #586e75; } /* Name.Namespace */
+ .nx { color: #586e75; } /* Name.Other */
+ .py { color: #586e75; } /* Name.Property */
+ .nt { color: #268bd2; } /* Name.Tag */
+ .nv { color: #268bd2; } /* Name.Variable */
+ .ow { color: #859900; } /* Operator.Word */
+ .w { color: #586e75; } /* Text.Whitespace */
+ .mf { color: #2aa198; } /* Literal.Number.Float */
+ .mh { color: #2aa198; } /* Literal.Number.Hex */
+ .mi { color: #2aa198; } /* Literal.Number.Integer */
+ .mo { color: #2aa198; } /* Literal.Number.Oct */
+ .sb { color: #93a1a1; } /* Literal.String.Backtick */
+ .sc { color: #2aa198; } /* Literal.String.Char */
+ .sd { color: #586e75; } /* Literal.String.Doc */
+ .s2 { color: #2aa198; } /* Literal.String.Double */
+ .se { color: #cb4b16; } /* Literal.String.Escape */
+ .sh { color: #586e75; } /* Literal.String.Heredoc */
+ .si { color: #2aa198; } /* Literal.String.Interpol */
+ .sx { color: #2aa198; } /* Literal.String.Other */
+ .sr { color: #dc322f; } /* Literal.String.Regex */
+ .s1 { color: #2aa198; } /* Literal.String.Single */
+ .ss { color: #2aa198; } /* Literal.String.Symbol */
+ .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
+ .vc { color: #268bd2; } /* Name.Variable.Class */
+ .vg { color: #268bd2; } /* Name.Variable.Global */
+ .vi { color: #268bd2; } /* Name.Variable.Instance */
+ .il { color: #2aa198; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 36a80a916b2..6f31a5235c0 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -86,7 +86,7 @@
background-color: #fafe3d !important;
}
- .hll { background-color: #f8f8f8 }
+ .hll { background-color: #f8f8f8; }
.c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 5bfe9bcb443..8d1a6020ca4 100644
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -78,7 +78,7 @@ span.highlight_word {
background-color: #fafe3d !important;
}
-.hll { background-color: #f8f8f8 }
+.hll { background-color: #f8f8f8; }
.c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index fc12964872d..ced8c4a9907 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -2,22 +2,28 @@ img {
max-width: 100%;
height: auto;
}
+
p.details {
font-style: italic;
- color: #777
+ color: #777;
}
+
.footer > p {
font-size: small;
- color: #777
+ color: #777;
}
+
pre.commit-message {
white-space: pre-wrap;
}
+
.file-stats > a {
text-decoration: none;
+
> .new-file {
color: #090;
}
+
> .deleted-file {
color: #b00;
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 8f71381f5c4..140d589024b 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -22,7 +22,7 @@
.admin-filter form {
.select2-container {
- width: 100%
+ width: 100%;
}
.controls {
@@ -31,7 +31,7 @@
.form-actions {
padding-left: 130px;
- background: #fff
+ background: #fff;
}
.visibility-levels {
@@ -106,26 +106,33 @@
.table {
table-layout: fixed;
}
+
.subheading {
padding-bottom: $gl-padding;
}
+
.message {
word-wrap: break-word;
}
+
.btn {
white-space: normal;
padding: $gl-btn-padding;
}
+
th {
width: 15%;
+
&.wide {
width: 55%;
}
}
+
@media (max-width: $screen-sm-max) {
th {
width: 100%;
}
+
td {
width: 100%;
float: left;
@@ -137,6 +144,7 @@
margin-left: $btn-side-margin;
margin-top: 3px;
}
+
span {
font-size: 19px;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 194a39a8377..2fbf0cf34bf 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;
}
@@ -218,6 +219,7 @@
.build-detail-row {
margin-bottom: 5px;
+
&:last-of-type {
margin-bottom: 0;
}
@@ -233,3 +235,9 @@
right: 0;
margin-top: -17px;
}
+
+@media (min-width: $screen-md-min) {
+ .sub-nav.build {
+ width: calc(100% + #{$gutter_width});
+ }
+}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 53ec0002afe..264e7e01a34 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -51,6 +51,7 @@
margin-left: 4px;
}
}
+
.commit-committer-link,
.commit-author-link {
color: $gl-gray;
@@ -108,21 +109,25 @@
line-height: 20px;
}
}
+
.new-file {
a {
color: $gl-text-green;
}
}
+
.renamed-file {
a {
color: $gl-text-orange;
}
}
+
.deleted-file {
a {
color: $gl-text-red;
}
}
+
.edit-file {
a {
color: $gl-text-color;
@@ -158,6 +163,7 @@
position: absolute;
z-index: 1;
}
+
> textarea {
background-color: rgba(0, 0, 0, 0.0);
font-family: inherit;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index dc57a837155..2b5621e20d6 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -161,6 +161,7 @@
.branch-commit {
color: $gl-gray;
+
.commit-id, .commit-row-message {
color: $gl-gray;
}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 42928ee279c..76225ed8d06 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -5,6 +5,7 @@
background: $background-color;
border-top-left-radius: 0;
}
+
border-top-left-radius: 0;
}
}
@@ -17,6 +18,7 @@
float: left;
@extend .col-md-2;
}
+
.btn {
margin-left: 5px;
float: left;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 4d9c73c6840..2357671c2ae 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -20,9 +20,11 @@
.detail-page-description {
.title {
- margin: 0;
- font-size: 23px;
+ margin: 0 0 16px;
+ font-size: 2em;
color: $gl-gray-dark;
+ padding: 0 0 0.3em;
+ border-bottom: 1px solid $white-dark;
}
.description {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index b8ef76cc74e..bdc82a8f0f5 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -33,6 +33,19 @@
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;
@@ -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,14 @@
margin: auto;
position: relative;
}
+
.frame.added, .frame.deleted {
position: absolute;
display: block;
top: 0;
left: 0;
}
+
.controls {
display: block;
height: 14px;
@@ -311,6 +342,7 @@
}
//.view.onion-skin
}
+
.view-modes {
padding: 10px;
text-align: center;
@@ -328,19 +360,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..029dabd2138 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,16 +71,20 @@
.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;
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3f19e920166..820cc0fc991 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -52,13 +52,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/events.scss b/app/assets/stylesheets/pages/events.scss
index 789d6237df8..5d9a76dac05 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -78,6 +78,7 @@
margin-bottom: 0;
}
}
+
.event-note-icon {
color: #777;
float: left;
@@ -86,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;
}
@@ -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..623da67a239 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -37,6 +37,7 @@ ul.related-merge-requests > li {
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
+
.merge-request-id {
flex-shrink: 0;
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 701c29a3986..9bac6d46355 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;
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..4c0c0839fe2 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -17,6 +17,7 @@
line-height: 1.5;
p {
+ font-size: 18px;
color: #888;
}
@@ -36,10 +37,14 @@
}
}
- .login-box {
- background: #fafafa;
- border-radius: 10px;
- box-shadow: 0 0 2px #ccc;
+ p {
+ font-size: 13px;
+ }
+
+ .login-box, .omniauth-container {
+ box-shadow: 0 0 0 1px $border-color;
+ border-bottom-right-radius: 2px;
+ border-bottom-left-radius: 2px;
padding: 15px;
.login-heading h3 {
@@ -58,42 +63,127 @@
a.forgot {
float: right;
- padding-top: 6px
+ padding-top: 6px;
}
.nav .active a {
background: transparent;
}
- }
- .form-control {
- font-size: 14px;
- padding: 10px 8px;
- width: 100%;
- height: auto;
-
- &.top {
- border-radius: 5px 5px 0 0;
- margin-bottom: 0;
+ // 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;
+ }
+
}
+ }
- &.bottom {
- border-radius: 0 0 5px 5px;
- border-top: 0;
- margin-bottom: 20px;
+ .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: 2px;
+ border-top-left-radius: 2px;
+
+ li {
+ flex: 1;
+ text-align: center;
+
+ &:last-of-type {
+ border-left: 1px solid $border-color;
+ }
+
+ &:not(.active) {
+ background-color: $gray-light;
+ }
+
+ a {
+ width: 100%;
+ font-size: 18px;
- &.middle {
- border-top: 0;
- margin-bottom: 0;
- border-radius: 0;
+ &: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;
+ }
+ }
+ }
}
+ }
+
+ .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,14 +191,6 @@
color: #a00;
}
}
-
- .remember-me {
- margin-top: -10px;
-
- label {
- font-weight: normal;
- }
- }
}
@media (max-width: $screen-xs-max) {
@@ -127,3 +209,35 @@
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; // height of footer + bottom padding of email confirmation link
+ }
+}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
new file mode 100644
index 00000000000..756efa9c7fa
--- /dev/null
+++ b/app/assets/stylesheets/pages/members.scss
@@ -0,0 +1,98 @@
+.project-members-title {
+ padding-bottom: 10px;
+ border-bottom: 1px solid $border-color;
+}
+
+.member {
+ .list-item-name {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ width: 50%;
+ }
+
+ strong {
+ font-weight: 600;
+ }
+ }
+
+ .controls {
+ @media (min-width: $screen-sm-min) {
+ display: -webkit-flex;
+ display: flex;
+ width: 400px;
+ max-width: 50%;
+ }
+ }
+
+ .form-horizontal {
+ margin-top: 5px;
+
+ @media (min-width: $screen-sm-min) {
+ display: -webkit-flex;
+ display: flex;
+ width: 100%;
+ margin-top: 3px;
+ }
+ }
+
+ .btn-remove {
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: auto;
+ }
+ }
+}
+
+.member-form-control {
+ @media (max-width: $screen-xs-max) {
+ padding: 5px 0;
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ width: 50%;
+ }
+}
+
+.member-access-text {
+ margin-left: auto;
+ line-height: 43px;
+}
+
+.member.existing-title {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+}
+
+.member-search-form {
+ position: relative;
+
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ }
+
+ .form-control {
+ width: 100%;
+ padding-right: 35px;
+
+ @media (min-width: $screen-sm-min) {
+ width: 350px;
+ }
+ }
+}
+
+.member-search-btn {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 35px;
+ padding-left: 10px;
+ padding-right: 10px;
+ color: $gray-darkest;
+ background: transparent;
+ border: 0;
+ outline: 0;
+}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 5ec660799e3..eed2b0ab7cc 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -131,6 +131,7 @@ $colors: (
}
}
}
+
&.head {
background-color: map-get($colors, #{$color}_header_head_neutral);
border-color: map-get($colors, #{$color}_header_head_neutral);
@@ -174,6 +175,7 @@ $colors: (
background-color: map-get($colors, #{$color}_line_not_chosen);
}
}
+
&.head {
background-color: map-get($colors, #{$color}_line_head_neutral);
@@ -235,4 +237,51 @@ $colors: (
.btn-success .fa-spinner {
color: #fff;
}
+
+ .editor-wrap {
+ &.is-loading {
+ .editor {
+ display: none;
+ }
+
+ .loading {
+ display: block;
+ }
+ }
+
+ &.saved {
+ .editor {
+ border-top: solid 2px $border-green-extra-light;
+ }
+ }
+
+ .editor {
+ pre {
+ height: 350px;
+ border: none;
+ border-radius: 0;
+ margin-bottom: 0;
+ }
+ }
+
+ .loading {
+ display: none;
+ }
+ }
+
+ .discard-changes-alert {
+ background-color: $background-color;
+ text-align: right;
+ padding: $gl-padding-top $gl-padding;
+ color: $gl-text-color;
+
+ .discard-actions {
+ display: inline-block;
+ margin-left: 10px;
+ }
+ }
+
+ .resolve-conflicts-form {
+ padding-top: $gl-padding;
+ }
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 043f3f3afe1..101472278e2 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;
@@ -188,6 +194,7 @@
padding-top: 2px;
padding-bottom: 2px;
list-style: none;
+
&:hover {
background: none;
}
@@ -204,6 +211,19 @@
word-break: break-all;
}
+.commits-empty {
+ text-align: center;
+
+ h4 {
+ padding-top: 20px;
+ padding-bottom: 10px;
+ }
+
+ svg {
+ width: 230px;
+ }
+}
+
.mr-list {
.merge-request {
padding: 10px 15px;
@@ -264,12 +284,6 @@
line-height: 31px;
}
-.builds {
- .table-holder {
- overflow-x: auto;
- }
-}
-
.panel-new-merge-request {
.panel-heading {
padding: 5px 10px;
@@ -357,7 +371,7 @@
}
.table-holder {
- .builds {
+ .ci-table {
th {
background-color: $white-light;
@@ -389,8 +403,12 @@
padding: 16px;
}
+ .content-block {
+ border-top: 1px solid $border-color;
+ padding: $gl-padding-top $gl-padding;
+ }
+
.comments-disabled-notif {
- padding: 10px 16px;
.btn {
margin-left: 5px;
}
@@ -401,10 +419,6 @@
margin: 0 7px;
}
- .comments-disabled-notif {
- border-top: 1px solid $border-color;
- }
-
.dropdown-title {
color: $gl-text-color;
}
@@ -415,9 +429,11 @@
}
}
-.merge-request-details {
+.merge-request-tabs {
+ background-color: #fff;
- .title {
- margin-bottom: 20px;
+ &.affix {
+ top: 100px;
+ z-index: 9;
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 8c2ba3ed58c..dd6d1783667 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -59,6 +59,7 @@
color: $gl-placeholder-color;
margin-right: 5px;
}
+
.avatar {
float: none;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index bd875b9823f..17f28959414 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -11,6 +11,7 @@
filter: alpha(opacity=100);
}
}
+
.diff-file,
.discussion {
.new-note {
@@ -194,6 +195,7 @@
min-height: 140px;
max-height: 500px;
}
+
.note-form-actions {
background: transparent;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index d399f84a2ff..fffcdc812a7 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -147,9 +147,18 @@ ul.notes {
// Diff code in discussion view
.discussion-body .diff-file {
+ .file-title {
+ cursor: default;
+
+ &:hover {
+ background-color: $gray-light;
+ }
+ }
+
.diff-header > span {
margin-right: 10px;
}
+
.line_content {
white-space: pre-wrap;
}
@@ -345,6 +354,7 @@ ul.notes {
width: 32px;
// "hide" it by default
display: none;
+
&:hover {
background: $gl-info;
color: #fff;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index a2779704eff..7b71876b822 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -20,7 +20,7 @@
margin: 4px;
}
- .table.builds {
+ .table.ci-table {
min-width: 1200px;
.branch-commit {
@@ -44,13 +44,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;
@@ -176,7 +183,7 @@
&::after {
content: '';
width: 8px;
- position: absolute;;
+ position: absolute;
right: -7px;
bottom: 8px;
border-bottom: 2px solid $border-color;
@@ -303,16 +310,41 @@
.stage-column {
display: inline-block;
vertical-align: top;
- margin-right: 65px;
+
+ &:not(:last-child) {
+ margin-right: 44px;
+ }
+
+ &.left-margin {
+ &:not(:first-child) {
+ margin-left: 44px;
+
+ .left-connector {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -48px;
+ border-top: 2px solid $border-color;
+ width: 48px;
+ height: 1px;
+ }
+ }
+ }
+ }
+
+ &.no-margin {
+ margin: 0;
+ }
li {
list-style: none;
}
.stage-name {
- margin-bottom: 15px;
+ margin: 0 0 15px 10px;
font-weight: bold;
- width: 150px;
+ width: 176px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -321,17 +353,24 @@
.build {
border: 1px solid $border-color;
position: relative;
- padding: 6px 10px;
+ padding: 7px 10px 8px;
border-radius: 30px;
- width: 150px;
+ width: 186px;
margin-bottom: 10px;
+ &:hover {
+ background-color: $gray-lighter;
+
+ .dropdown-menu-toggle {
+ background-color: transparent;
+ }
+ }
+
&.playable {
- background-color: $gray-light;
svg {
- height: 12px;
- width: 12px;
+ height: 13px;
+ width: 20px;
position: relative;
top: 1px;
@@ -342,10 +381,20 @@
}
.build-content {
- width: 130px;
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+ width: 164px;
+
+ .ci-status-icon {
+ svg {
+ height: 20px;
+ width: 20px;
+ }
+ }
.ci-status-text {
- width: 110px;
+ width: 135px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -356,44 +405,56 @@
}
a {
- color: $layout-link-gray;
+ color: $gl-text-color-light;
text-decoration: none;
-
- &:hover {
- .ci-status-text {
- text-decoration: underline;
- }
- }
}
.dropdown-menu-toggle {
border: none;
width: auto;
padding: 0;
- color: $layout-link-gray;
+ color: $gl-text-color-light;
+ flex-grow: 1;
.ci-status-text {
- width: 80px;
+ max-width: 112px;
+ width: auto;
}
}
.grouped-pipeline-dropdown {
padding: 8px 0;
- width: 200px;
+ width: 186px;
left: auto;
- right: -214px;
+ right: -197px;
top: -9px;
- max-height: 245px;
- overflow-y: scroll;
- a:hover {
- .ci-status-text {
- text-decoration: none;
+ ul {
+ max-height: 245px;
+ overflow: auto;
+ }
+
+ a {
+ color: $gl-text-color;
+ padding: 7px 8px 8px;
+
+ &:hover {
+ background-color: $blue-light-transparent;
+ border-radius: 3px;
+
+ .ci-status-text {
+ text-decoration: none;
+ }
}
}
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+
.ci-status-text {
- width: 145px;
+ width: 112px;
}
.arrow {
@@ -426,9 +487,10 @@
}
.badge {
- background-color: $gray-dark;
- color: $layout-link-gray;
+ background-color: $gray-darker;
+ color: $gl-text-color-light;
font-weight: normal;
+ margin-left: $btn-xs-side-margin;
}
}
@@ -442,10 +504,10 @@
&::after {
content: '';
position: absolute;
- top: 50%;
- right: -69px;
+ top: 48%;
+ right: -48px;
border-top: 2px solid $border-color;
- width: 69px;
+ width: 48px;
height: 1px;
}
}
@@ -454,25 +516,25 @@
&:not(:first-child) {
&::after, &::before {
content: '';
- top: -47px;
+ top: -49px;
position: absolute;
border-bottom: 2px solid $border-color;
- width: 20px;
- height: 65px;
+ width: 25px;
+ height: 69px;
}
// Right connecting curves
&::after {
- right: -20px;
+ right: -25px;
border-right: 2px solid $border-color;
- border-radius: 0 0 15px;
+ border-radius: 0 0 20px;
}
// Left connecting curves
&::before {
- left: -20px;
+ left: -25px;
border-left: 2px solid $border-color;
- border-radius: 0 0 0 15px;
+ border-radius: 0 0 0 20px;
}
}
@@ -480,8 +542,9 @@
&:nth-child(2) {
&::after, &::before {
height: 29px;
- top: -10px;
+ top: -9px;
}
+
.curve {
display: block;
}
@@ -538,20 +601,20 @@
width: 21px;
height: 25px;
position: absolute;
- top: -29px;
+ top: -32px;
border-top: 2px solid $border-color;
}
&::after {
- left: -39px;
+ left: -44px;
border-right: 2px solid $border-color;
- border-radius: 0 15px;
+ border-radius: 0 20px;
}
&::before {
- right: -39px;
+ right: -44px;
border-left: 2px solid $border-color;
- border-radius: 15px 0 0;
+ border-radius: 20px 0 0;
}
}
}
@@ -567,19 +630,31 @@
}
}
-.pipelines.tab-pane {
+.tab-pane {
- .content-list.pipelines {
- overflow: auto;
- }
+ &.pipelines {
- .stage {
- max-width: 100px;
- width: 100px;
+ .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..ed80d2beec2 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;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 530fb0c0d05..d30f02340b9 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -17,34 +17,43 @@
&.features .control-label {
font-weight: normal;
}
+
.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;
}
@@ -56,6 +65,7 @@
border-radius: 3px;
border: 1px solid #e5e5e5;
}
+
&+ .select2 a {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
@@ -201,6 +211,7 @@
pointer-events: none;
}
}
+
.count {
@include btn-gray;
display: inline-block;
@@ -361,10 +372,12 @@ a.deploy-project-label {
margin: $gl-padding;
text-align: center;
width: 169px;
+
&:hover, &.forked {
background-color: $row-hover;
border-color: $row-hover-border;
}
+
.no-avatar {
width: 100px;
height: 100px;
@@ -372,17 +385,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 +660,7 @@ pre.light-well {
.clone-options {
display: table-cell;
+
a.btn {
width: 100%;
}
@@ -832,6 +849,7 @@ pre.light-well {
.form-control {
min-width: 100px;
}
+
.select2-choice {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index eec22c5dc96..7b3878c91df 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -6,6 +6,7 @@
&.runner-state-shared {
background: #32b186;
}
+
&.runner-state-specific {
background: #3498db;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index c05f3d5ff32..f1d53c7b8bc 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;
}
@@ -77,6 +78,7 @@
.ci-status-icon-running {
color: $blue-normal;
}
+
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found,
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 41ad10f07bd..6ea7a2b5498 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -5,6 +5,7 @@
.file-finder {
width: 50%;
+
.file-finder-input {
width: 95%;
display: inline-block;
@@ -168,4 +169,8 @@
margin-top: 11px;
position: relative;
z-index: 2;
+
+ .download-button {
+ margin-left: $btn-side-margin;
+ }
}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index c9846103762..3fa7fa3d7e3 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -23,15 +23,19 @@
.term-bold {
font-weight: bold;
}
+
.term-italic {
font-style: italic;
}
+
.term-conceal {
visibility: hidden;
}
+
.term-underline {
text-decoration: underline;
}
+
.term-cross {
text-decoration: line-through;
}
@@ -39,48 +43,63 @@
.term-fg-black {
color: $black;
}
+
.term-fg-red {
color: $red;
}
+
.term-fg-green {
color: $green;
}
+
.term-fg-yellow {
color: $yellow;
}
+
.term-fg-blue {
color: $blue;
}
+
.term-fg-magenta {
color: $magenta;
}
+
.term-fg-cyan {
color: $cyan;
}
+
.term-fg-white {
color: $white;
}
+
.term-fg-l-black {
color: $l-black;
}
+
.term-fg-l-red {
color: $l-red;
}
+
.term-fg-l-green {
color: $l-green;
}
+
.term-fg-l-yellow {
color: $l-yellow;
}
+
.term-fg-l-blue {
color: $l-blue;
}
+
.term-fg-l-magenta {
color: $l-magenta;
}
+
.term-fg-l-cyan {
color: $l-cyan;
}
+
.term-fg-l-white {
color: $l-white;
}
@@ -88,818 +107,1087 @@
.term-bg-black {
background-color: $black;
}
+
.term-bg-red {
background-color: $red;
}
+
.term-bg-green {
background-color: $green;
}
+
.term-bg-yellow {
background-color: $yellow;
}
+
.term-bg-blue {
background-color: $blue;
}
+
.term-bg-magenta {
background-color: $magenta;
}
+
.term-bg-cyan {
background-color: $cyan;
}
+
.term-bg-white {
background-color: $white;
}
+
.term-bg-l-black {
background-color: $l-black;
}
+
.term-bg-l-red {
background-color: $l-red;
}
+
.term-bg-l-green {
background-color: $l-green;
}
+
.term-bg-l-yellow {
background-color: $l-yellow;
}
+
.term-bg-l-blue {
background-color: $l-blue;
}
+
.term-bg-l-magenta {
background-color: $l-magenta;
}
+
.term-bg-l-cyan {
background-color: $l-cyan;
}
+
.term-bg-l-white {
background-color: $l-white;
}
-
.xterm-fg-0 {
color: #000;
}
+
.xterm-fg-1 {
color: #800000;
}
+
.xterm-fg-2 {
color: #008000;
}
+
.xterm-fg-3 {
color: #808000;
}
+
.xterm-fg-4 {
color: #000080;
}
+
.xterm-fg-5 {
color: #800080;
}
+
.xterm-fg-6 {
color: #008080;
}
+
.xterm-fg-7 {
color: #c0c0c0;
}
+
.xterm-fg-8 {
color: #808080;
}
+
.xterm-fg-9 {
color: #f00;
}
+
.xterm-fg-10 {
color: #0f0;
}
+
.xterm-fg-11 {
color: #ff0;
}
+
.xterm-fg-12 {
color: #00f;
}
+
.xterm-fg-13 {
color: #f0f;
}
+
.xterm-fg-14 {
color: #0ff;
}
+
.xterm-fg-15 {
color: #fff;
}
+
.xterm-fg-16 {
color: #000;
}
+
.xterm-fg-17 {
color: #00005f;
}
+
.xterm-fg-18 {
color: #000087;
}
+
.xterm-fg-19 {
color: #0000af;
}
+
.xterm-fg-20 {
color: #0000d7;
}
+
.xterm-fg-21 {
color: #00f;
}
+
.xterm-fg-22 {
color: #005f00;
}
+
.xterm-fg-23 {
color: #005f5f;
}
+
.xterm-fg-24 {
color: #005f87;
}
+
.xterm-fg-25 {
color: #005faf;
}
+
.xterm-fg-26 {
color: #005fd7;
}
+
.xterm-fg-27 {
color: #005fff;
}
+
.xterm-fg-28 {
color: #008700;
}
+
.xterm-fg-29 {
color: #00875f;
}
+
.xterm-fg-30 {
color: #008787;
}
+
.xterm-fg-31 {
color: #0087af;
}
+
.xterm-fg-32 {
color: #0087d7;
}
+
.xterm-fg-33 {
color: #0087ff;
}
+
.xterm-fg-34 {
color: #00af00;
}
+
.xterm-fg-35 {
color: #00af5f;
}
+
.xterm-fg-36 {
color: #00af87;
}
+
.xterm-fg-37 {
color: #00afaf;
}
+
.xterm-fg-38 {
color: #00afd7;
}
+
.xterm-fg-39 {
color: #00afff;
}
+
.xterm-fg-40 {
color: #00d700;
}
+
.xterm-fg-41 {
color: #00d75f;
}
+
.xterm-fg-42 {
color: #00d787;
}
+
.xterm-fg-43 {
color: #00d7af;
}
+
.xterm-fg-44 {
color: #00d7d7;
}
+
.xterm-fg-45 {
color: #00d7ff;
}
+
.xterm-fg-46 {
color: #0f0;
}
+
.xterm-fg-47 {
color: #00ff5f;
}
+
.xterm-fg-48 {
color: #00ff87;
}
+
.xterm-fg-49 {
color: #00ffaf;
}
+
.xterm-fg-50 {
color: #00ffd7;
}
+
.xterm-fg-51 {
color: #0ff;
}
+
.xterm-fg-52 {
color: #5f0000;
}
+
.xterm-fg-53 {
color: #5f005f;
}
+
.xterm-fg-54 {
color: #5f0087;
}
+
.xterm-fg-55 {
color: #5f00af;
}
+
.xterm-fg-56 {
color: #5f00d7;
}
+
.xterm-fg-57 {
color: #5f00ff;
}
+
.xterm-fg-58 {
color: #5f5f00;
}
+
.xterm-fg-59 {
color: #5f5f5f;
}
+
.xterm-fg-60 {
color: #5f5f87;
}
+
.xterm-fg-61 {
color: #5f5faf;
}
+
.xterm-fg-62 {
color: #5f5fd7;
}
+
.xterm-fg-63 {
color: #5f5fff;
}
+
.xterm-fg-64 {
color: #5f8700;
}
+
.xterm-fg-65 {
color: #5f875f;
}
+
.xterm-fg-66 {
color: #5f8787;
}
+
.xterm-fg-67 {
color: #5f87af;
}
+
.xterm-fg-68 {
color: #5f87d7;
}
+
.xterm-fg-69 {
color: #5f87ff;
}
+
.xterm-fg-70 {
color: #5faf00;
}
+
.xterm-fg-71 {
color: #5faf5f;
}
+
.xterm-fg-72 {
color: #5faf87;
}
+
.xterm-fg-73 {
color: #5fafaf;
}
+
.xterm-fg-74 {
color: #5fafd7;
}
+
.xterm-fg-75 {
color: #5fafff;
}
+
.xterm-fg-76 {
color: #5fd700;
}
+
.xterm-fg-77 {
color: #5fd75f;
}
+
.xterm-fg-78 {
color: #5fd787;
}
+
.xterm-fg-79 {
color: #5fd7af;
}
+
.xterm-fg-80 {
color: #5fd7d7;
}
+
.xterm-fg-81 {
color: #5fd7ff;
}
+
.xterm-fg-82 {
color: #5fff00;
}
+
.xterm-fg-83 {
color: #5fff5f;
}
+
.xterm-fg-84 {
color: #5fff87;
}
+
.xterm-fg-85 {
color: #5fffaf;
}
+
.xterm-fg-86 {
color: #5fffd7;
}
+
.xterm-fg-87 {
color: #5fffff;
}
+
.xterm-fg-88 {
color: #870000;
}
+
.xterm-fg-89 {
color: #87005f;
}
+
.xterm-fg-90 {
color: #870087;
}
+
.xterm-fg-91 {
color: #8700af;
}
+
.xterm-fg-92 {
color: #8700d7;
}
+
.xterm-fg-93 {
color: #8700ff;
}
+
.xterm-fg-94 {
color: #875f00;
}
+
.xterm-fg-95 {
color: #875f5f;
}
+
.xterm-fg-96 {
color: #875f87;
}
+
.xterm-fg-97 {
color: #875faf;
}
+
.xterm-fg-98 {
color: #875fd7;
}
+
.xterm-fg-99 {
color: #875fff;
}
+
.xterm-fg-100 {
color: #878700;
}
+
.xterm-fg-101 {
color: #87875f;
}
+
.xterm-fg-102 {
color: #878787;
}
+
.xterm-fg-103 {
color: #8787af;
}
+
.xterm-fg-104 {
color: #8787d7;
}
+
.xterm-fg-105 {
color: #8787ff;
}
+
.xterm-fg-106 {
color: #87af00;
}
+
.xterm-fg-107 {
color: #87af5f;
}
+
.xterm-fg-108 {
color: #87af87;
}
+
.xterm-fg-109 {
color: #87afaf;
}
+
.xterm-fg-110 {
color: #87afd7;
}
+
.xterm-fg-111 {
color: #87afff;
}
+
.xterm-fg-112 {
color: #87d700;
}
+
.xterm-fg-113 {
color: #87d75f;
}
+
.xterm-fg-114 {
color: #87d787;
}
+
.xterm-fg-115 {
color: #87d7af;
}
+
.xterm-fg-116 {
color: #87d7d7;
}
+
.xterm-fg-117 {
color: #87d7ff;
}
+
.xterm-fg-118 {
color: #87ff00;
}
+
.xterm-fg-119 {
color: #87ff5f;
}
+
.xterm-fg-120 {
color: #87ff87;
}
+
.xterm-fg-121 {
color: #87ffaf;
}
+
.xterm-fg-122 {
color: #87ffd7;
}
+
.xterm-fg-123 {
color: #87ffff;
}
+
.xterm-fg-124 {
color: #af0000;
}
+
.xterm-fg-125 {
color: #af005f;
}
+
.xterm-fg-126 {
color: #af0087;
}
+
.xterm-fg-127 {
color: #af00af;
}
+
.xterm-fg-128 {
color: #af00d7;
}
+
.xterm-fg-129 {
color: #af00ff;
}
+
.xterm-fg-130 {
color: #af5f00;
}
+
.xterm-fg-131 {
color: #af5f5f;
}
+
.xterm-fg-132 {
color: #af5f87;
}
+
.xterm-fg-133 {
color: #af5faf;
}
+
.xterm-fg-134 {
color: #af5fd7;
}
+
.xterm-fg-135 {
color: #af5fff;
}
+
.xterm-fg-136 {
color: #af8700;
}
+
.xterm-fg-137 {
color: #af875f;
}
+
.xterm-fg-138 {
color: #af8787;
}
+
.xterm-fg-139 {
color: #af87af;
}
+
.xterm-fg-140 {
color: #af87d7;
}
+
.xterm-fg-141 {
color: #af87ff;
}
+
.xterm-fg-142 {
color: #afaf00;
}
+
.xterm-fg-143 {
color: #afaf5f;
}
+
.xterm-fg-144 {
color: #afaf87;
}
+
.xterm-fg-145 {
color: #afafaf;
}
+
.xterm-fg-146 {
color: #afafd7;
}
+
.xterm-fg-147 {
color: #afafff;
}
+
.xterm-fg-148 {
color: #afd700;
}
+
.xterm-fg-149 {
color: #afd75f;
}
+
.xterm-fg-150 {
color: #afd787;
}
+
.xterm-fg-151 {
color: #afd7af;
}
+
.xterm-fg-152 {
color: #afd7d7;
}
+
.xterm-fg-153 {
color: #afd7ff;
}
+
.xterm-fg-154 {
color: #afff00;
}
+
.xterm-fg-155 {
color: #afff5f;
}
+
.xterm-fg-156 {
color: #afff87;
}
+
.xterm-fg-157 {
color: #afffaf;
}
+
.xterm-fg-158 {
color: #afffd7;
}
+
.xterm-fg-159 {
color: #afffff;
}
+
.xterm-fg-160 {
color: #d70000;
}
+
.xterm-fg-161 {
color: #d7005f;
}
+
.xterm-fg-162 {
color: #d70087;
}
+
.xterm-fg-163 {
color: #d700af;
}
+
.xterm-fg-164 {
color: #d700d7;
}
+
.xterm-fg-165 {
color: #d700ff;
}
+
.xterm-fg-166 {
color: #d75f00;
}
+
.xterm-fg-167 {
color: #d75f5f;
}
+
.xterm-fg-168 {
color: #d75f87;
}
+
.xterm-fg-169 {
color: #d75faf;
}
+
.xterm-fg-170 {
color: #d75fd7;
}
+
.xterm-fg-171 {
color: #d75fff;
}
+
.xterm-fg-172 {
color: #d78700;
}
+
.xterm-fg-173 {
color: #d7875f;
}
+
.xterm-fg-174 {
color: #d78787;
}
+
.xterm-fg-175 {
color: #d787af;
}
+
.xterm-fg-176 {
color: #d787d7;
}
+
.xterm-fg-177 {
color: #d787ff;
}
+
.xterm-fg-178 {
color: #d7af00;
}
+
.xterm-fg-179 {
color: #d7af5f;
}
+
.xterm-fg-180 {
color: #d7af87;
}
+
.xterm-fg-181 {
color: #d7afaf;
}
+
.xterm-fg-182 {
color: #d7afd7;
}
+
.xterm-fg-183 {
color: #d7afff;
}
+
.xterm-fg-184 {
color: #d7d700;
}
+
.xterm-fg-185 {
color: #d7d75f;
}
+
.xterm-fg-186 {
color: #d7d787;
}
+
.xterm-fg-187 {
color: #d7d7af;
}
+
.xterm-fg-188 {
color: #d7d7d7;
}
+
.xterm-fg-189 {
color: #d7d7ff;
}
+
.xterm-fg-190 {
color: #d7ff00;
}
+
.xterm-fg-191 {
color: #d7ff5f;
}
+
.xterm-fg-192 {
color: #d7ff87;
}
+
.xterm-fg-193 {
color: #d7ffaf;
}
+
.xterm-fg-194 {
color: #d7ffd7;
}
+
.xterm-fg-195 {
color: #d7ffff;
}
+
.xterm-fg-196 {
color: #f00;
}
+
.xterm-fg-197 {
color: #ff005f;
}
+
.xterm-fg-198 {
color: #ff0087;
}
+
.xterm-fg-199 {
color: #ff00af;
}
+
.xterm-fg-200 {
color: #ff00d7;
}
+
.xterm-fg-201 {
color: #f0f;
}
+
.xterm-fg-202 {
color: #ff5f00;
}
+
.xterm-fg-203 {
color: #ff5f5f;
}
+
.xterm-fg-204 {
color: #ff5f87;
}
+
.xterm-fg-205 {
color: #ff5faf;
}
+
.xterm-fg-206 {
color: #ff5fd7;
}
+
.xterm-fg-207 {
color: #ff5fff;
}
+
.xterm-fg-208 {
color: #ff8700;
}
+
.xterm-fg-209 {
color: #ff875f;
}
+
.xterm-fg-210 {
color: #ff8787;
}
+
.xterm-fg-211 {
color: #ff87af;
}
+
.xterm-fg-212 {
color: #ff87d7;
}
+
.xterm-fg-213 {
color: #ff87ff;
}
+
.xterm-fg-214 {
color: #ffaf00;
}
+
.xterm-fg-215 {
color: #ffaf5f;
}
+
.xterm-fg-216 {
color: #ffaf87;
}
+
.xterm-fg-217 {
color: #ffafaf;
}
+
.xterm-fg-218 {
color: #ffafd7;
}
+
.xterm-fg-219 {
color: #ffafff;
}
+
.xterm-fg-220 {
color: #ffd700;
}
+
.xterm-fg-221 {
color: #ffd75f;
}
+
.xterm-fg-222 {
color: #ffd787;
}
+
.xterm-fg-223 {
color: #ffd7af;
}
+
.xterm-fg-224 {
color: #ffd7d7;
}
+
.xterm-fg-225 {
color: #ffd7ff;
}
+
.xterm-fg-226 {
color: #ff0;
}
+
.xterm-fg-227 {
color: #ffff5f;
}
+
.xterm-fg-228 {
color: #ffff87;
}
+
.xterm-fg-229 {
color: #ffffaf;
}
+
.xterm-fg-230 {
color: #ffffd7;
}
+
.xterm-fg-231 {
color: #fff;
}
+
.xterm-fg-232 {
color: #080808;
}
+
.xterm-fg-233 {
color: #121212;
}
+
.xterm-fg-234 {
color: #1c1c1c;
}
+
.xterm-fg-235 {
color: #262626;
}
+
.xterm-fg-236 {
color: #303030;
}
+
.xterm-fg-237 {
color: #3a3a3a;
}
+
.xterm-fg-238 {
color: #444;
}
+
.xterm-fg-239 {
color: #4e4e4e;
}
+
.xterm-fg-240 {
color: #585858;
}
+
.xterm-fg-241 {
color: #626262;
}
+
.xterm-fg-242 {
color: #6c6c6c;
}
+
.xterm-fg-243 {
color: #767676;
}
+
.xterm-fg-244 {
color: #808080;
}
+
.xterm-fg-245 {
color: #8a8a8a;
}
+
.xterm-fg-246 {
color: #949494;
}
+
.xterm-fg-247 {
color: #9e9e9e;
}
+
.xterm-fg-248 {
color: #a8a8a8;
}
+
.xterm-fg-249 {
color: #b2b2b2;
}
+
.xterm-fg-250 {
color: #bcbcbc;
}
+
.xterm-fg-251 {
color: #c6c6c6;
}
+
.xterm-fg-252 {
color: #d0d0d0;
}
+
.xterm-fg-253 {
color: #dadada;
}
+
.xterm-fg-254 {
color: #e4e4e4;
}
+
.xterm-fg-255 {
color: #eee;
}
diff --git a/app/controllers/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/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 38e5943eb76..a62c6211372 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
- @projects = TrendingProjectsFinder.new.execute
- @projects = filter_projects(@projects)
+ @projects = filter_projects(Project.trending)
@projects = @projects.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
deleted file mode 100644
index 83eec1bf4a2..00000000000
--- a/app/controllers/namespaces_controller.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class NamespacesController < ApplicationController
- skip_before_action :authenticate_user!
-
- def show
- namespace = Namespace.find_by(path: params[:id])
-
- if namespace
- if namespace.is_a?(Group)
- group = namespace
- else
- user = namespace.owner
- end
- end
-
- if user
- redirect_to user_path(user)
- elsif group && can?(current_user, :read_group, group)
- redirect_to group_path(group)
- elsif current_user.nil?
- authenticate_user!
- else
- render_404
- end
- end
-end
diff --git a/app/controllers/projects/board_lists_controller.rb b/app/controllers/projects/board_lists_controller.rb
deleted file mode 100644
index 3cfb08d5822..00000000000
--- a/app/controllers/projects/board_lists_controller.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-class Projects::BoardListsController < Projects::ApplicationController
- respond_to :json
-
- before_action :authorize_admin_list!
-
- rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
-
- def create
- list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
-
- if list.valid?
- render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
- else
- render json: list.errors, status: :unprocessable_entity
- end
- end
-
- def update
- service = Boards::Lists::MoveService.new(project, current_user, move_params)
-
- if service.execute
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def destroy
- service = Boards::Lists::DestroyService.new(project, current_user, params)
-
- if service.execute
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def generate
- service = Boards::Lists::GenerateService.new(project, current_user)
-
- if service.execute
- render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
- else
- head :unprocessable_entity
- end
- end
-
- private
-
- def authorize_admin_list!
- return render_403 unless can?(current_user, :admin_list, project)
- end
-
- def list_params
- params.require(:list).permit(:label_id)
- end
-
- def move_params
- params.require(:list).permit(:position).merge(id: params[:id])
- end
-
- def record_not_found(exception)
- render json: { error: exception.message }, status: :not_found
- end
-end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 095af6c35eb..71eb56aed0b 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -16,9 +16,8 @@ module Projects
end
def create
- list = project.board.lists.find(params[:list_id])
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
- issue = service.execute(list)
+ issue = service.execute
if issue.valid?
render json: serialize_as_json(issue)
@@ -60,15 +59,15 @@ module Projects
end
def filter_params
- params.merge(id: params[:list_id])
+ params.merge(board_id: params[:board_id], id: params[:list_id])
end
def move_params
- params.permit(:id, :from_list_id, :to_list_id)
+ params.permit(:board_id, :id, :from_list_id, :to_list_id)
end
def issue_params
- params.require(:issue).permit(:title).merge(request: request)
+ params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
end
def serialize_as_json(resource)
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
index b995f586737..76ae41319c4 100644
--- a/app/controllers/projects/boards/lists_controller.rb
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -5,11 +5,11 @@ module Projects
before_action :authorize_read_list!, only: [:index]
def index
- render json: serialize_as_json(project.board.lists)
+ render json: serialize_as_json(board.lists)
end
def create
- list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
+ list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board)
if list.valid?
render json: serialize_as_json(list)
@@ -19,7 +19,7 @@ module Projects
end
def update
- list = project.board.lists.movable.find(params[:id])
+ list = board.lists.movable.find(params[:id])
service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
if service.execute(list)
@@ -30,8 +30,8 @@ module Projects
end
def destroy
- list = project.board.lists.destroyable.find(params[:id])
- service = ::Boards::Lists::DestroyService.new(project, current_user, params)
+ list = board.lists.destroyable.find(params[:id])
+ service = ::Boards::Lists::DestroyService.new(project, current_user)
if service.execute(list)
head :ok
@@ -43,8 +43,8 @@ module Projects
def generate
service = ::Boards::Lists::GenerateService.new(project, current_user)
- if service.execute
- render json: serialize_as_json(project.board.lists.movable)
+ if service.execute(board)
+ render json: serialize_as_json(board.lists.movable)
else
head :unprocessable_entity
end
@@ -60,6 +60,10 @@ module Projects
return render_403 unless can?(current_user, :read_list, project)
end
+ def board
+ @board ||= project.boards.find(params[:board_id])
+ end
+
def list_params
params.require(:list).permit(:label_id)
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 0035633b774..808affa4f98 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -1,12 +1,28 @@
class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
-
- respond_to :html
- before_action :authorize_read_board!, only: [:show]
+ before_action :authorize_read_board!, only: [:index, :show]
+
+ def index
+ @boards = ::Boards::ListService.new(project, current_user).execute
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: serialize_as_json(@boards)
+ end
+ end
+ end
def show
- ::Boards::CreateService.new(project, current_user).execute
+ @board = project.boards.find(params[:id])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: serialize_as_json(@board)
+ end
+ end
end
private
@@ -14,4 +30,8 @@ class Projects::BoardsController < Projects::ApplicationController
def authorize_read_board!
return access_denied! unless can?(current_user, :read_board, project)
end
+
+ def serialize_as_json(resource)
+ resource.as_json(only: [:id])
+ end
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 092ef32e6e3..923e7340e69 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -38,12 +38,12 @@ class Projects::GraphsController < Projects::ApplicationController
@languages = @languages.map do |language|
name, share = language
- color = Digest::SHA256.hexdigest(name)[0...6]
+ color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}"
{
value: (share.to_f * 100 / total).round(2),
label: name,
- color: "##{color}",
- highlight: "##{color}"
+ color: color,
+ highlight: color
}
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 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/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ffd9833e3b1..a39b47b6d95 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
+ :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]
@@ -31,7 +31,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
- before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
+ before_action :authenticate_user!, only: [:assign_related_issues]
+
+ before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index
@merge_requests = merge_requests_collection
@@ -168,6 +170,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?
@@ -182,7 +194,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
@@ -354,6 +366,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render layout: false
end
+ def assign_related_issues
+ result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute
+
+ respond_to do |format|
+ format.html do
+ case result[:count]
+ when 0
+ flash[:error] = "Failed to assign you issues related to the merge request"
+ when 1
+ flash[:notice] = "1 issue has been assigned to you"
+ else
+ flash[:notice] = "#{result[:count]} issues have been assigned to you"
+ end
+
+ redirect_to(merge_request_path(@merge_request))
+ end
+ end
+ end
+
def ci_status
pipeline = @merge_request.pipeline
if pipeline
@@ -382,6 +413,30 @@ 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)
+
+ {
+ id: environment.id,
+ name: environment.name,
+ url: namespace_project_environment_path(project.namespace, project, environment),
+ 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
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index f56b256984b..37a86ed0523 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -5,34 +5,23 @@ 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
@@ -43,6 +32,21 @@ class Projects::ProjectMembersController < Projects::ApplicationController
current_user: current_user
)
+ if params[:group_ids].present?
+ group_ids = params[:group_ids].split(',')
+ groups = Group.where(id: group_ids)
+
+ groups.each do |group|
+ next unless can?(current_user, :read_group, group)
+
+ project.project_group_links.create(
+ group: group,
+ group_access: params[:access_level],
+ expires_at: params[:expires_at]
+ )
+ end
+ end
+
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 6ea8ee62bc5..8fea20cefef 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController
def show
@tag = @repository.find_tag(params[:id])
+ return render_404 unless @tag
+
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
@commit = @repository.commit(@tag.target)
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index d198782138a..dee57e4a388 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,10 +1,10 @@
class SnippetsController < ApplicationController
include ToggleAwardEmoji
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
# Allow read snippet
- before_action :authorize_read_snippet!, only: [:show, :raw]
+ before_action :authorize_read_snippet!, only: [:show, :raw, :download]
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
@@ -12,7 +12,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
- skip_before_action :authenticate_user!, only: [:index, :show, :raw]
+ skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
layout 'snippets'
respond_to :html
@@ -75,6 +75,14 @@ class SnippetsController < ApplicationController
)
end
+ def download
+ send_data(
+ @snippet.content,
+ type: 'text/plain; charset=utf-8',
+ filename: @snippet.sanitized_file_name
+ )
+ end
+
protected
def snippet
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 838ecc837e4..6a881b271d7 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,6 +1,6 @@
class UsersController < ApplicationController
skip_before_action :authenticate_user!
- before_action :user
+ before_action :user, except: [:exists]
before_action :authorize_read_user!, only: [:show]
def show
@@ -85,6 +85,10 @@ class UsersController < ApplicationController
render 'calendar_activities', layout: false
end
+ def exists
+ render json: { exists: Namespace.where(path: params[:username].downcase).any? }
+ end
+
private
def authorize_read_user!
diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb
deleted file mode 100644
index c1e434d9926..00000000000
--- a/app/finders/trending_projects_finder.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# Finder for retrieving public trending projects in a given time range.
-class TrendingProjectsFinder
- # current_user - The currently logged in User, if any.
- # last_months - The number of months to limit the trending data to.
- def execute(months_limit = 1)
- Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
- Project.public_only.trending(months_limit.months.ago)
- end
- end
-
- private
-
- def cache_key_for(months)
- "trending_projects/#{months}"
- end
-end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
new file mode 100644
index 00000000000..b7247ffa8b2
--- /dev/null
+++ b/app/helpers/boards_helper.rb
@@ -0,0 +1,12 @@
+module BoardsHelper
+ def board_data
+ board = @board || @boards.first
+
+ {
+ endpoint: namespace_project_boards_path(@project.namespace, @project),
+ board_id: board.id,
+ disabled: !can?(current_user, :admin_list, @project),
+ issue_link_base: namespace_project_issues_path(@project.namespace, @project)
+ }
+ end
+end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index a695aceea76..85e1dc33ee8 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -15,14 +15,14 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
- css_class = data[:class] || 'btn-clipboard'
+ css_class = data[:class] || 'btn-clipboard btn-transparent'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data,
type: :button,
- title: "Copy to Clipboard"
+ title: 'Copy to Clipboard'
end
def http_clone_button(project, placement = 'right', append_link: true)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 8abe7865fed..249cb44e9d5 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -72,6 +72,19 @@ module MergeRequestsHelper
)
end
+ def mr_assign_issues_link
+ issues = MergeRequests::AssignIssuesService.new(@project,
+ current_user,
+ merge_request: @merge_request,
+ closes_issues: mr_closes_issues
+ ).assignable_issues
+ path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ if issues.present?
+ pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
+ link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
+ end
+ end
+
def source_branch_with_namespace(merge_request)
branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
@@ -110,4 +123,8 @@ module MergeRequestsHelper
def version_index(merge_request_diff)
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
+
+ def different_base?(version1, version2)
+ version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5dbf66173de..87475119b23 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,6 +1,7 @@
module Ci
class Build < CommitStatus
include TokenAuthenticatable
+ include AfterCommitQueue
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
@@ -75,25 +76,20 @@ module Ci
state_machine :status do
after_transition pending: :running do |build|
- build.execute_hooks
+ build.run_after_commit do
+ BuildHooksWorker.perform_async(id)
+ end
end
after_transition any => [:success, :failed, :canceled] do |build|
- build.update_coverage
- build.execute_hooks
+ build.run_after_commit do
+ BuildFinishedWorker.perform_async(id)
+ end
end
after_transition any => [:success] do |build|
- if build.environment.present?
- service = CreateDeploymentService.new(
- build.project, build.user,
- environment: build.environment,
- sha: build.sha,
- ref: build.ref,
- tag: build.tag,
- options: build.options.to_h[:environment],
- variables: build.variables)
- service.execute(build)
+ build.run_after_commit do
+ BuildSuccessWorker.perform_async(id)
end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 2cf9892edc5..c7b9d6cc223 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -3,6 +3,7 @@ module Ci
extend Ci::Model
include HasStatus
include Importable
+ include AfterCommitQueue
self.table_name = 'ci_commits'
@@ -48,6 +49,10 @@ 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
@@ -56,22 +61,28 @@ module Ci
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
- before_transition do |pipeline|
- pipeline.update_duration
+ after_transition [:created, :pending, :running] => :success do |pipeline|
+ pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
end
after_transition do |pipeline, transition|
- pipeline.execute_hooks unless transition.loopback?
+ next if transition.loopback?
+
+ pipeline.run_after_commit do
+ PipelineHooksWorker.perform_async(id)
+ end
end
end
@@ -292,11 +303,9 @@ module Ci
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
def merge_requests
- @merge_requests ||=
- begin
- project.merge_requests.where(source_branch: self.ref).
- select { |merge_request| merge_request.pipeline.try(:id) == self.id }
- end
+ @merge_requests ||= project.merge_requests
+ .where(source_branch: self.ref)
+ .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
end
private
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 5d6d534cd31..7b554be4f9a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -86,29 +86,19 @@ class CommitStatus < ActiveRecord::Base
end
after_transition do |commit_status, transition|
- return if transition.loopback?
+ next if transition.loopback?
commit_status.run_after_commit do
pipeline.try do |pipeline|
if complete?
- ProcessPipelineWorker.perform_async(pipeline.id)
+ PipelineProcessWorker.perform_async(pipeline.id)
else
- UpdatePipelineWorker.perform_async(pipeline.id)
+ PipelineUpdateWorker.perform_async(pipeline.id)
end
end
end
end
- after_transition [:created, :pending, :running] => :success do |commit_status|
- commit_status.run_after_commit do
- # TODO, temporary fix for race condition
- UpdatePipelineWorker.new.perform(pipeline.id)
-
- MergeRequests::MergeWhenBuildSucceedsService
- .new(pipeline.project, nil).trigger(self)
- end
- end
-
after_transition any => :failed do |commit_status|
commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
diff --git a/app/models/compare.rb b/app/models/compare.rb
index 4856510f526..3a8bbcb1acd 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -11,9 +11,10 @@ class Compare
end
end
- def initialize(compare, project)
+ def initialize(compare, project, straight: false)
@compare = compare
@project = project
+ @straight = straight
end
def commits
@@ -45,6 +46,18 @@ class Compare
end
end
+ def start_commit_sha
+ start_commit.try(:sha)
+ end
+
+ def base_commit_sha
+ base_commit.try(:sha)
+ end
+
+ def head_commit_sha
+ commit.try(:sha)
+ end
+
def raw_diffs(*args)
@compare.diffs(*args)
end
@@ -58,9 +71,9 @@ class Compare
def diff_refs
Gitlab::Diff::DiffRefs.new(
- base_sha: base_commit.try(:sha),
- start_sha: start_commit.try(:sha),
- head_sha: commit.try(:sha)
+ base_sha: @straight ? start_commit_sha : base_commit_sha,
+ start_sha: start_commit_sha,
+ head_sha: head_commit_sha
)
end
end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index be295487fd2..8ed4a56b19b 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -2,6 +2,8 @@ class CycleAnalytics
include Gitlab::Database::Median
include Gitlab::Database::DateTime
+ DEPLOYMENT_METRIC_STAGES = %i[production staging]
+
def initialize(project, from:)
@project = project
@from = from
@@ -66,7 +68,7 @@ class CycleAnalytics
# cycle analytics stage.
interval_query = Arel::Nodes::As.new(
cte_table,
- subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s))
+ subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s))
median_datetime(cte_table, interval_query, name)
end
@@ -75,7 +77,7 @@ class CycleAnalytics
# closes the given issue) with issue and merge request metrics included. The metrics
# are loaded with an inner join, so issues / merge requests without metrics are
# automatically excluded.
- def base_query
+ def base_query_for(name)
arel_table = MergeRequestsClosingIssues.arel_table
# Load issues
@@ -91,7 +93,11 @@ class CycleAnalytics
join(MergeRequest::Metrics.arel_table).
on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+ if DEPLOYMENT_METRIC_STAGES.include?(name)
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+ end
+
+ query
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 82b27b78229..3d9902d496e 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -40,7 +40,14 @@ class Deployment < ActiveRecord::Base
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,6 +84,10 @@ class Deployment < ActiveRecord::Base
take
end
+ def formatted_deployment_time
+ created_at.to_time.in_time_zone.to_s(:medium)
+ end
+
private
def ref_path
diff --git a/app/models/environment.rb b/app/models/environment.rb
index f0f3ee23223..d970bc0a005 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -48,7 +48,22 @@ 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_id = ref.split('/').last
+ deployments.find(deployment_id)
+ end
+
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
end
+
+ def formatted_external_url
+ return nil unless external_url
+
+ external_url.gsub(/\A.*?:\/\//, '')
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 314d5ba438f..0764cb8cabd 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -68,8 +68,10 @@ class Event < ActiveRecord::Base
true
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
+ elsif merge_request? || merge_request_note?
+ Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
else
- ((merge_request? || note?) && target.present?) || milestone?
+ milestone?
end
end
@@ -280,6 +282,10 @@ class Event < ActiveRecord::Base
note? && target && target.for_issue?
end
+ def merge_request_note?
+ note? && target && target.for_merge_request?
+ end
+
def project_snippet_note?
target.for_snippet?
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a743bf313ae..8c6905a442d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -688,12 +688,15 @@ 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
+ environments = source_project.environments_for(
+ source_branch, diff_head_commit)
+ environments += target_project.environments_for(
+ target_branch, diff_head_commit, with_tags: true)
+
+ environments.uniq
+ end
end
def state_human_name
@@ -868,7 +871,7 @@ class MergeRequest < ActiveRecord::Base
# files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
- rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 36b8b70870b..b8a10b7968e 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
+ # Valid types of serialized diffs allowed by Gitlab::Git::Diff
+ VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
+
belongs_to :merge_request
state_machine :state, initial: :empty do
@@ -164,12 +167,24 @@ class MergeRequestDiff < ActiveRecord::Base
self == merge_request.merge_request_diff
end
- def compare_with(sha)
- CompareService.new.execute(project, head_commit_sha, project, sha)
+ def compare_with(sha, straight: true)
+ # When compare merge request versions we want diff A..B instead of A...B
+ # so we handle cases when user does squash and rebase of the commits between versions.
+ # For this reason we set straight to true by default.
+ CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
end
private
+ # Old GitLab implementations may have generated diffs as ["--broken-diff"].
+ # Avoid an error 500 by ignoring bad elements. See:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
+ def valid_raw_diff?(raw)
+ return false unless raw.respond_to?(:each)
+
+ raw.any? { |element| VALID_CLASSES.include?(element.class) }
+ end
+
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -200,7 +215,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(raw, options)
- if raw.respond_to?(:each)
+ if valid_raw_diff?(raw)
if paths = options[:paths]
raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
diff --git a/app/models/project.rb b/app/models/project.rb
index 88e4bd14860..db3088677d8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -16,6 +16,9 @@ class Project < ActiveRecord::Base
extend Gitlab::ConfigHelper
+ class BoardLimitExceeded < StandardError; end
+
+ NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'
cache_markdown_field :description, pipeline: :description
@@ -65,8 +68,7 @@ class Project < ActiveRecord::Base
belongs_to :namespace
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
-
- has_one :board, dependent: :destroy
+ has_many :boards, before_add: :validate_board_limit, dependent: :destroy
# Project services
has_many :services
@@ -375,19 +377,9 @@ class Project < ActiveRecord::Base
%r{(?<project>#{name_pattern}/#{name_pattern})}
end
- def trending(since = 1.month.ago)
- # By counting in the JOIN we don't expose the GROUP BY to the outer query.
- # This means that calls such as "any?" and "count" just return a number of
- # the total count, instead of the counts grouped per project as a Hash.
- join_body = "INNER JOIN (
- SELECT project_id, COUNT(*) AS amount
- FROM notes
- WHERE created_at >= #{sanitize(since)}
- AND system IS FALSE
- GROUP BY project_id
- ) join_note_counts ON projects.id = join_note_counts.project_id"
-
- joins(join_body).reorder('join_note_counts.amount DESC')
+ def trending
+ joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
+ reorder('trending_projects.id ASC')
end
def cached_count
@@ -498,7 +490,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
@@ -837,11 +829,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
@@ -1349,4 +1336,8 @@ class Project < ActiveRecord::Base
shared_projects.any?
end
+
+ def validate_board_limit(board)
+ raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
+ end
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 7613cbdea93..db46def11eb 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base
belongs_to :group
validates :project_id, presence: true
- validates :group_id, presence: true
+ validates :group, presence: true
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 4da1933c189..72e473871fa 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -719,6 +719,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
@@ -1016,7 +1024,8 @@ class Repository
root_ref_commit = commit(root_ref)
if branch_commit
- is_ancestor?(branch_commit.id, root_ref_commit.id)
+ same_head = branch_commit.id == root_ref_commit.id
+ !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
diff --git a/app/models/trending_project.rb b/app/models/trending_project.rb
new file mode 100644
index 00000000000..27e3732da17
--- /dev/null
+++ b/app/models/trending_project.rb
@@ -0,0 +1,35 @@
+class TrendingProject < ActiveRecord::Base
+ belongs_to :project
+
+ # The number of months to include in the trending calculation.
+ MONTHS_TO_INCLUDE = 1
+
+ # The maximum number of projects to include in the trending set.
+ PROJECTS_LIMIT = 100
+
+ # Populates the trending projects table with the current list of trending
+ # projects.
+ def self.refresh!
+ # The calculation **must** run in a transaction. If the removal of data and
+ # insertion of new data were to run separately a user might end up with an
+ # empty list of trending projects for a short period of time.
+ transaction do
+ delete_all
+
+ timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago)
+
+ connection.execute <<-EOF.strip_heredoc
+ INSERT INTO #{table_name} (project_id)
+ SELECT project_id
+ FROM notes
+ INNER JOIN projects ON projects.id = notes.project_id
+ WHERE notes.created_at >= #{timestamp}
+ AND notes.system IS FALSE
+ AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}
+ GROUP BY project_id
+ ORDER BY count(*) DESC
+ LIMIT #{PROJECTS_LIMIT};
+ EOF
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 892ac28d5b3..f367f4616fb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -589,6 +589,11 @@ class User < ActiveRecord::Base
end
def set_projects_limit
+ # `User.select(:id)` raises
+ # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
+ # without this safeguard!
+ return unless self.has_attribute?(:projects_limit)
+
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a806cf83782..be4721d7a51 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -40,7 +40,6 @@ class ProjectPolicy < BasePolicy
can! :read_milestone
can! :read_project_snippet
can! :read_project_member
- can! :read_merge_request
can! :read_note
can! :create_project
can! :create_issue
@@ -63,6 +62,7 @@ class ProjectPolicy < BasePolicy
can! :read_pipeline
can! :read_environment
can! :read_deployment
+ can! :read_merge_request
end
# Permissions given when an user is team member of a project
@@ -117,6 +117,7 @@ class ProjectPolicy < BasePolicy
can! :read_container_image
can! :build_download_code
can! :build_read_container_image
+ can! :read_merge_request
end
def owner_access!
diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb
deleted file mode 100644
index b2069ca825a..00000000000
--- a/app/services/boards/base_service.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Boards
- class BaseService < ::BaseService
- delegate :board, to: :project
- end
-end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 072a0749285..9bdd7b6f0cf 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -1,16 +1,21 @@
module Boards
- class CreateService < Boards::BaseService
+ class CreateService < BaseService
def execute
- create_board! unless project.board.present?
- project.board
+ if project.boards.empty?
+ create_board!
+ else
+ project.boards.first
+ end
end
private
def create_board!
- project.create_board
- project.board.lists.create(list_type: :backlog)
- project.board.lists.create(list_type: :done)
+ board = project.boards.create
+ board.lists.create(list_type: :backlog)
+ board.lists.create(list_type: :done)
+
+ board
end
end
end
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
index 3701afd441f..c0d7ff5b585 100644
--- a/app/services/boards/issues/create_service.rb
+++ b/app/services/boards/issues/create_service.rb
@@ -1,14 +1,21 @@
module Boards
module Issues
- class CreateService < Boards::BaseService
- def execute(list)
- params.merge!(label_ids: [list.label_id])
- create_issue
+ class CreateService < BaseService
+ def execute
+ create_issue(params.merge(label_ids: [list.label_id]))
end
private
- def create_issue
+ def board
+ @board ||= project.boards.find(params.delete(:board_id))
+ end
+
+ def list
+ @list ||= board.lists.find(params.delete(:list_id))
+ end
+
+ def create_issue(params)
::Issues::CreateService.new(project, current_user, params).execute
end
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 435a8c6e681..fd4a462c7b2 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -1,6 +1,6 @@
module Boards
module Issues
- class ListService < Boards::BaseService
+ class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless list.movable?
@@ -10,6 +10,10 @@ module Boards
private
+ def board
+ @board ||= project.boards.find(params[:board_id])
+ end
+
def list
@list ||= board.lists.find(params[:id])
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 84dc3f70e76..96554a92a02 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -1,6 +1,6 @@
module Boards
module Issues
- class MoveService < Boards::BaseService
+ class MoveService < BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false unless valid_move?
@@ -10,6 +10,10 @@ module Boards
private
+ def board
+ @board ||= project.boards.find(params[:board_id])
+ end
+
def valid_move?
moving_from_list.present? && moving_to_list.present? &&
moving_from_list != moving_to_list
@@ -49,7 +53,7 @@ module Boards
if moving_to_list.movable?
moving_from_list.label_id
else
- board.lists.movable.pluck(:label_id)
+ project.boards.joins(:lists).merge(List.movable).pluck(:label_id)
end
Array(label_ids).compact
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
new file mode 100644
index 00000000000..84f1fc3a4e2
--- /dev/null
+++ b/app/services/boards/list_service.rb
@@ -0,0 +1,14 @@
+module Boards
+ class ListService < BaseService
+ def execute
+ create_board! if project.boards.empty?
+ project.boards
+ end
+
+ private
+
+ def create_board!
+ Boards::CreateService.new(project, current_user).execute
+ end
+ end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index b1887820bd4..abc7aeece39 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -1,23 +1,23 @@
module Boards
module Lists
- class CreateService < Boards::BaseService
- def execute
+ class CreateService < BaseService
+ def execute(board)
List.transaction do
label = project.labels.find(params[:label_id])
- position = next_position
+ position = next_position(board)
- create_list(label, position)
+ create_list(board, label, position)
end
end
private
- def next_position
+ def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ
end
- def create_list(label, position)
+ def create_list(board, label, position)
board.lists.create(label: label, list_type: :label, position: position)
end
end
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
index 25da3bfb56d..f986e05944c 100644
--- a/app/services/boards/lists/destroy_service.rb
+++ b/app/services/boards/lists/destroy_service.rb
@@ -1,9 +1,11 @@
module Boards
module Lists
- class DestroyService < Boards::BaseService
+ class DestroyService < BaseService
def execute(list)
return false unless list.destroyable?
+ @board = list.board
+
list.with_lock do
decrement_higher_lists(list)
remove_list(list)
@@ -12,6 +14,8 @@ module Boards
private
+ attr_reader :board
+
def decrement_higher_lists(list)
board.lists.movable.where('position > ?', list.position)
.update_all('position = position - 1')
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
index 830e386c98b..d8048f1c67e 100644
--- a/app/services/boards/lists/generate_service.rb
+++ b/app/services/boards/lists/generate_service.rb
@@ -1,11 +1,11 @@
module Boards
module Lists
- class GenerateService < Boards::BaseService
- def execute
+ class GenerateService < BaseService
+ def execute(board)
return false unless board.lists.movable.empty?
List.transaction do
- label_params.each { |params| create_list(params) }
+ label_params.each { |params| create_list(board, params) }
end
true
@@ -13,9 +13,9 @@ module Boards
private
- def create_list(params)
+ def create_list(board, params)
label = find_or_create_label(params)
- Lists::CreateService.new(project, current_user, label_id: label.id).execute
+ Lists::CreateService.new(project, current_user, label_id: label.id).execute(board)
end
def find_or_create_label(params)
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
new file mode 100644
index 00000000000..c579ed4c869
--- /dev/null
+++ b/app/services/boards/lists/list_service.rb
@@ -0,0 +1,9 @@
+module Boards
+ module Lists
+ class ListService < BaseService
+ def execute(board)
+ board.lists
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
index 020ff69f4a7..f2a68865f7b 100644
--- a/app/services/boards/lists/move_service.rb
+++ b/app/services/boards/lists/move_service.rb
@@ -1,7 +1,8 @@
module Boards
module Lists
- class MoveService < Boards::BaseService
+ class MoveService < BaseService
def execute(list)
+ @board = list.board
@old_position = list.position
@new_position = params[:position]
@@ -16,7 +17,7 @@ module Boards
private
- attr_reader :old_position, :new_position
+ attr_reader :board, :old_position, :new_position
def valid_move?
new_position.present? && new_position != old_position &&
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 6d6075628af..5e8fafca98c 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch)
+ def execute(source_project, source_branch, target_project, target_branch, straight: false)
source_commit = source_project.commit(source_branch)
return unless source_commit
@@ -23,9 +23,10 @@ class CompareService
raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
- source_sha
+ source_sha,
+ straight
)
- Compare.new(raw_compare, target_project)
+ Compare.new(raw_compare, target_project, straight: straight)
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index 799ad3e1bd0..ff9a8310a8c 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -2,25 +2,35 @@ 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,
+ ActiveRecord::Base.transaction do
+ @deployable = deployable
+ @environment = prepare_environment
+
+ deploy.tap do |deployment|
+ deployment.update_merge_request_metrics!
+ end
+ end
+ end
+
+ private
+
+ 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
- )
-
- deployment.update_merge_request_metrics!
-
- deployment
+ deployable: @deployable)
end
- private
-
- def find_or_create_environment
+ def prepare_environment
project.environments.find_or_create_by(name: expanded_name) do |environment|
environment.external_url = expanded_url
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/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index 566049525cb..d572a928a42 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -2,14 +2,14 @@ module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
def execute(commit_status)
- each_merge_request(commit_status) do |merge_request|
+ commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
# Closes any pending build failed todos for the parent MRs when a build is retried
def close(commit_status)
- each_merge_request(commit_status) do |merge_request|
+ commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
new file mode 100644
index 00000000000..f636e5fec4f
--- /dev/null
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -0,0 +1,35 @@
+module MergeRequests
+ class AssignIssuesService < BaseService
+ def assignable_issues
+ @assignable_issues ||= begin
+ if current_user == merge_request.author
+ closes_issues.select do |issue|
+ !issue.assignee_id? && can?(current_user, :admin_issue, issue)
+ end
+ else
+ []
+ end
+ end
+ end
+
+ def execute
+ assignable_issues.each do |issue|
+ Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue)
+ end
+
+ {
+ count: assignable_issues.count
+ }
+ end
+
+ private
+
+ def merge_request
+ params[:merge_request]
+ end
+
+ def closes_issues
+ @closes_issues ||= params[:closes_issues] || merge_request.closes_issues(current_user)
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index d0d155b7ee1..58f69a41e14 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -42,28 +42,33 @@ module MergeRequests
super(:merge_request)
end
- def merge_request_from(commit_status)
- branches = commit_status.ref
+ def merge_requests_for(branch)
+ origin_merge_requests = @project.origin_merge_requests
+ .opened.where(source_branch: branch).to_a
- # This is for ref-less builds
- branches ||= @project.repository.branch_names_contains(commit_status.sha)
+ fork_merge_requests = @project.fork_merge_requests
+ .opened.where(source_branch: branch).to_a
- return [] if branches.blank?
+ (origin_merge_requests + fork_merge_requests)
+ .uniq.select(&:source_project)
+ end
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
- merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
+ def pipeline_merge_requests(pipeline)
+ merge_requests_for(pipeline.ref).each do |merge_request|
+ next unless pipeline == merge_request.pipeline
- merge_requests.uniq.select(&:source_project)
+ yield merge_request
+ end
end
- def each_merge_request(commit_status)
- merge_request_from(commit_status).each do |merge_request|
+ def commit_status_merge_requests(commit_status)
+ merge_requests_for(commit_status.ref).each do |merge_request|
pipeline = merge_request.pipeline
next unless pipeline
next unless pipeline.sha == commit_status.sha
- yield merge_request, pipeline
+ yield merge_request
end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index e57791f6818..404f75616b5 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
merge_request = MergeRequest.new(params)
# Set MR attributes
- merge_request.can_be_created = false
+ merge_request.can_be_created = true
merge_request.compare_commits = []
merge_request.source_project = project unless merge_request.source_project
@@ -22,6 +22,12 @@ module MergeRequests
return build_failed(merge_request, message)
end
+ if merge_request.source_project == merge_request.target_project &&
+ merge_request.target_branch == merge_request.source_branch
+
+ return build_failed(merge_request, 'You must select different branches')
+ end
+
compare = CompareService.new.execute(
merge_request.source_project,
merge_request.source_branch,
@@ -29,17 +35,8 @@ module MergeRequests
merge_request.target_branch,
)
- commits = compare.commits
-
- # At this point we decide if merge request can be created
- # If we have at least one commit to merge -> creation allowed
- if commits.present?
- merge_request.compare_commits = commits
- merge_request.can_be_created = true
- merge_request.compare = compare
- else
- merge_request.can_be_created = false
- end
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
set_title_and_description(merge_request)
end
@@ -89,6 +86,8 @@ module MergeRequests
end
end
+ merge_request.title = merge_request.wip_title if commits.empty?
+
merge_request
end
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index 4ad5fb08311..dc159de0058 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -18,12 +18,13 @@ module MergeRequests
merge_request.save
end
- # Triggers the automatic merge of merge_request once the build succeeds
- def trigger(commit_status)
- each_merge_request(commit_status) do |merge_request, pipeline|
+ # Triggers the automatic merge of merge_request once the pipeline succeeds
+ def trigger(pipeline)
+ return unless pipeline.success?
+
+ pipeline_merge_requests(pipeline) do |merge_request|
next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable?
- next unless pipeline.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
index 19caa038c44..d22a1d3e0ad 100644
--- a/app/services/merge_requests/resolve_service.rb
+++ b/app/services/merge_requests/resolve_service.rb
@@ -1,5 +1,8 @@
module MergeRequests
class ResolveService < MergeRequests::BaseService
+ class MissingFiles < Gitlab::Conflict::ResolutionError
+ end
+
attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request)
@@ -10,8 +13,16 @@ module MergeRequests
fetch_their_commit!
- conflicts.files.each do |file|
- write_resolved_file_to_index(file, params[:sections])
+ params[:files].each do |file_params|
+ conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path])
+
+ write_resolved_file_to_index(conflict_file, file_params)
+ end
+
+ unless merge_index.conflicts.empty?
+ missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
+
+ raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end
commit_params = {
@@ -23,8 +34,13 @@ module MergeRequests
project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
end
- def write_resolved_file_to_index(file, resolutions)
- new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
+ def write_resolved_file_to_index(file, params)
+ new_file = if params[:sections]
+ file.resolve_lines(params[:sections]).map(&:text).join("\n")
+ elsif params[:content]
+ file.resolve_content(params[:content])
+ end
+
our_path = file.our_path
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 9dbec49d163..a37cc3fdf21 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -15,7 +15,10 @@ module MergeRequests
params.except!(:target_branch, :force_remove_source_branch)
end
- merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+ if params[:force_remove_source_branch].present?
+ merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+ end
+
handle_wip_event(merge_request)
update(merge_request)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index de8049b8e2e..72712afc07e 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -475,10 +475,12 @@ class NotificationService
end
def reject_users_without_access(recipients, target)
- return recipients unless target.is_a?(Issue)
+ return recipients unless target.is_a?(Issuable)
+
+ ability = :"read_#{target.to_ability_name}"
recipients.select do |user|
- user.can?(:read_issue, target)
+ user.can?(ability, target)
end
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 1725a30fae5..e4ae3dec8aa 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -122,7 +122,12 @@ module SlashCommands
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
- @updates[:add_label_ids] = label_ids unless label_ids.empty?
+ if label_ids.any?
+ @updates[:add_label_ids] ||= []
+ @updates[:add_label_ids] += label_ids
+
+ @updates[:add_label_ids].uniq!
+ end
end
desc 'Remove all or specific label(s)'
@@ -136,7 +141,12 @@ module SlashCommands
if labels_param.present?
label_ids = find_label_ids(labels_param)
- @updates[:remove_label_ids] = label_ids unless label_ids.empty?
+ if label_ids.any?
+ @updates[:remove_label_ids] ||= []
+ @updates[:remove_label_ids] += label_ids
+
+ @updates[:remove_label_ids].uniq!
+ end
else
@updates[:label_ids] = []
end
@@ -152,7 +162,12 @@ module SlashCommands
command :relabel do |labels_param|
label_ids = find_label_ids(labels_param)
- @updates[:label_ids] = label_ids unless label_ids.empty?
+ if label_ids.any?
+ @updates[:label_ids] ||= []
+ @updates[:label_ids] += label_ids
+
+ @updates[:label_ids].uniq!
+ end
end
desc 'Add a todo'
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 776530ac0a5..f8e6b2ef094 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -273,12 +273,12 @@ class TodoService
end
def reject_users_without_access(users, project, target)
- if target.is_a?(Note) && target.for_issue?
+ if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
target = target.noteable
end
- if target.is_a?(Issue)
- select_users(users, :read_issue, target)
+ if target.is_a?(Issuable)
+ select_users(users, :"read_#{target.to_ability_name}", target)
else
select_users(users, :read_project, project)
end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 4dc3b2ab9a0..2821ecf0a88 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -24,6 +24,7 @@ class NamespaceValidator < ActiveModel::EachValidator
projects
public
repository
+ robots.txt
s
search
services
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
index 6c51639b840..acbe17036f7 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview.html.haml
@@ -1,9 +1,12 @@
-- page_title "Preview | Appearance"
+= render 'devise/shared/tab_single', tab_title: 'Sign in preview'
.login-box
- .login-heading
- %h3 Existing user? Sign in
- %form
- = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
- = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
- = button_tag "Sign in", class: "btn-create btn"
+ %form.show-gl-field-errors
+ .form-group
+ = label_tag :login
+ = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
+ .form-group
+ = label_tag :password
+ = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.'
+ .form-group
+ = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 0d79ca7dc52..c4c68cd7891 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -221,7 +221,11 @@
%fieldset
%legend Metrics
%p
- These settings require a restart to take effect.
+ Setup InfluxDB to measure a wide variety of statistics like the time spent
+ in running SQL queries. These settings require a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index a5e82e55cc1..10fea1996aa 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -71,7 +71,7 @@
.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..a96b579c593 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,6 +1,10 @@
-= 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."
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{for: "user_remember_me"}
@@ -8,5 +12,5 @@
%span Remember me
.pull-right
= link_to "Forgot your password?", new_password_path(resource_name)
- %div
+ %div.submit-container
= 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..e82a08cdb0c 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,6 +1,10 @@
-= 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 or email', for: :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"}
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 2ef383960f4..b26efbb4535 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,6 +1,10 @@
-= 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 "#{server['label']} Login", for: :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"}
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..0e865b807c1 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"
+ .form-group
+ = 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..810dd5ab687 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -1,32 +1,15 @@
-.login-box
- - if signup_enabled?
- .login-heading
- %h3 Existing user? Sign in
- - else
- .login-heading
- %h3 Sign in
+#login-pane.login-box{ role: 'tabpanel', class: 'tab-pane active' }
.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 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'
- elsif signin_enabled?
= 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..e276e91433a
--- /dev/null
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -0,0 +1,10 @@
+%ul.new-session-tabs.nav-links.nav-tabs
+ - 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'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
new file mode 100644
index 00000000000..79b1d447a92
--- /dev/null
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -0,0 +1,5 @@
+%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'}
+ %li.active{ role: 'presentation' }
+ %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in
+ %li{ role: 'presentation'}
+ %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register
diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml
index 49c087c0646..49b2f77111f 100644
--- a/app/views/devise/unlocks/new.html.haml
+++ b/app/views/devise/unlocks/new.html.haml
@@ -1,12 +1,12 @@
+= render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions'
.login-box
- .login-heading
- %h3 Resend unlock email
.login-body
- = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f|
+ = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
- .clearfix.append-bottom-20
- = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off'
+ .form-group.append-bottom-20
+ = f.label :email
+ = f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: 'Please provide a valid email address.'
.clearfix
= f.submit 'Resend unlock instructions', class: 'btn btn-success'
diff --git a/app/views/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/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/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index e44a2bfed9d..99a58bbb676 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -116,4 +116,4 @@
-# Shortcut to issue boards
%li.hidden
- = link_to 'Issue Boards', namespace_project_board_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
+ = link_to 'Issue Boards', namespace_project_boards_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/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/_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/index.html.haml b/app/views/projects/boards/index.html.haml
new file mode 100644
index 00000000000..885f8e34b55
--- /dev/null
+++ b/app/views/projects/boards/index.html.haml
@@ -0,0 +1,16 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('boards/boards_bundle.js')
+ = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+.boards-list#board-app{ "v-cloak" => true, data: board_data }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index edbbd3f3d2a..885f8e34b55 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -10,10 +10,7 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true,
- "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}",
- "data-disabled" => "#{!can?(current_user, :admin_list, @project)}",
- "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" }
+.boards-list#board-app{ "v-cloak" => true, data: board_data }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 5217b8bf028..4480b2f22c3 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -30,8 +30,8 @@
= render 'projects/buttons/download', project: @project, ref: branch.name
- - if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ - if can?(current_user, :push_code, @project)
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: "btn btn-remove remove-row has-tooltip #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/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/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_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 547bc0c9c19..017d3ff6af2 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -5,8 +5,10 @@
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
- = render_status_with_link('build', subject.status)
+ %span.ci-status-icon
+ = render_status_with_link('build', subject.status)
.ci-status-text= subject.name
- else
- = render_status_with_link('build', subject.status)
+ %span.ci-status-icon
+ = render_status_with_link('build', subject.status)
= ci_icon_for_status(subject.status)
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index da5b9832ba5..d6916fb7f1a 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,45 +1,46 @@
-.row-content-block.build-content.middle-block.pipeline-actions
- .pull-right
- .btn.btn-grouped.btn-white.toggle-pipeline-btn
- %span.toggle-btn-text Hide
- %span pipeline graph
- = icon('caret-up')
- - if can?(current_user, :update_pipeline, pipeline.project)
- - if pipeline.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
+.pipeline-graph-container
+ .row-content-block.build-content.middle-block.pipeline-actions
+ .pull-right
+ .btn.btn-grouped.btn-white.toggle-pipeline-btn
+ %span.toggle-btn-text Hide
+ %span pipeline graph
+ %span.caret
+ - if can?(current_user, :update_pipeline, pipeline.project)
+ - if pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
- - if pipeline.builds.running_or_pending.any?
- = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+ - if pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
- .oneline.clearfix
- - if defined?(pipeline_details) && pipeline_details
- Pipeline
- = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
- with
- = pluralize pipeline.statuses.count(:id), "build"
- - if pipeline.ref
- for
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
- - if defined?(link_to_commit) && link_to_commit
- for commit
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
- - if pipeline.duration
- in
- = time_interval_in_words pipeline.duration
+ .oneline.clearfix
+ - if defined?(pipeline_details) && pipeline_details
+ Pipeline
+ = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
+ with
+ = pluralize pipeline.statuses.count(:id), "build"
+ - if pipeline.ref
+ for
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
+ - if defined?(link_to_commit) && link_to_commit
+ for commit
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
+ - if pipeline.duration
+ in
+ = time_interval_in_words pipeline.duration
-.row-content-block.build-content.middle-block.pipeline-graph
- .pipeline-visualization
- %ul.stage-column-list
- - stages = pipeline.stages_with_latest_statuses
- - stages.each do |stage, statuses|
- %li.stage-column
- .stage-name
- %a{name: stage}
- - if stage
- = stage.titleize
- .builds-container
- %ul
- = render "projects/commit/pipeline_stage", statuses: statuses
+ .row-content-block.build-content.middle-block.pipeline-graph.hidden
+ .pipeline-visualization
+ %ul.stage-column-list
+ - stages = pipeline.stages_with_latest_statuses
+ - stages.each do |stage, statuses|
+ %li.stage-column
+ .stage-name
+ %a{name: stage}
+ - if stage
+ = stage.titleize
+ .builds-container
+ %ul
+ = render "projects/commit/pipeline_stage", statuses: statuses
- if pipeline.yaml_errors.present?
@@ -55,7 +56,7 @@
\.gitlab-ci.yml not found in this commit
.table-holder.pipeline-holder
- %table.table.builds.pipeline
+ %table.table.ci-table.pipeline
%thead
%tr
%th Status
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
index 23c5c51fbc2..289aa5178b1 100644
--- a/app/views/projects/commit/_pipeline_stage.html.haml
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -10,5 +10,5 @@
- else
%li.build
.curve
- .build-content
+ .dropdown.inline.build-content
= render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
index 4e7a6f1af08..5d0d5ba0262 100644
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -1,11 +1,13 @@
- group_status = CommitStatus.where(id: subject).status
-= render_status_with_link('build', group_status)
-.dropdown.inline
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- %span.ci-status-text
- = name
- %span.badge= subject.size
- %ul.dropdown-menu.grouped-pipeline-dropdown
- .arrow
+%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %span.ci-status-icon
+ = render_status_with_link('build', group_status)
+ %span.ci-status-text
+ = name
+ %span.badge= subject.size
+.dropdown-menu.grouped-pipeline-dropdown
+ .arrow
+ %ul
- subject.each do |status|
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ %li
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 998812793a2..640651e93f5 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -4,7 +4,7 @@
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.builds
+ %table.table.ci-table
%tbody
%th Status
%th Pipeline
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/_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/edit.html.haml b/app/views/projects/edit.html.haml
index d19422c8657..c8f84b96cb7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -91,7 +91,7 @@
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3
- = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control'
+ = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
- if Gitlab.config.registry.enabled
.form-group
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..721ba156334 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -24,7 +24,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..90c59223a35 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -24,7 +24,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.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 331dc1fcc29..80fe6be49b0 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -62,5 +62,3 @@
%td.coverage
- if generic_commit_status.try(:coverage)
#{generic_commit_status.coverage}%
-
- %td
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 409f4701e4b..0a66d60accc 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,7 +1,9 @@
- if subject.target_url
= link_to subject.target_url do
- = render_status_with_link('commit status', subject.status)
+ %span.ci-status-icon
+ = render_status_with_link('commit status', subject.status)
%span.ci-status-text= subject.name
- else
- = render_status_with_link('commit status', subject.status)
+ %span.ci-status-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/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 509b01c548a..4825820c4d9 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -10,7 +10,7 @@
Issues
= nav_link(controller: :boards) do
- = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+ = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do
%span
Board
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 6901ba13ab7..52b187e7e58 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 'form'
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 49ddf901619..a1bb66cfb6c 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 'form'
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index de39964fca8..466ec1475d8 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -65,19 +65,6 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
- - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
- .light-well.append-bottom-default
- .center
- %h4
- There isn't anything to merge.
- %p.slead
- - if @merge_request.source_branch == @merge_request.target_branch
- You'll need to use different branch names to get a valid comparison.
- - else
- %span.label-branch #{@merge_request.source_branch}
- and
- %span.label-branch #{@merge_request.target_branch}
- are the same.
= f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
:javascript
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 88d8013a0d1..da6927879a4 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,29 +18,35 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
- %li.commits-tab.active
- = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
- Commits
- %span.badge= @commits.size
- - if @pipeline
- %li.builds-tab
- = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
- Builds
- %span.badge= @statuses.size
- %li.diffs-tab
- = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
- Changes
- %span.badge= @merge_request.diff_size
+ - if @commits.empty?
+ .commits-empty
+ %h4
+ There are no commits yet.
+ = custom_icon ('illustration_no_commits')
+ - else
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ %li.commits-tab.active
+ = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
+ Commits
+ %span.badge= @commits.size
+ - if @pipeline
+ %li.builds-tab
+ = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
+ Builds
+ %span.badge= @statuses.size
+ %li.diffs-tab
+ = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
+ Changes
+ %span.badge= @merge_request.diff_size
- .tab-content
- #commits.commits.tab-pane.active
- = render "projects/merge_requests/show/commits"
- #diffs.diffs.tab-pane
- - # This tab is always loaded via AJAX
- - if @pipeline
- #builds.builds.tab-pane
- = render "projects/merge_requests/show/builds"
+ .tab-content
+ #commits.commits.tab-pane.active
+ = render "projects/merge_requests/show/commits"
+ #diffs.diffs.tab-pane
+ - # This tab is always loaded via AJAX
+ - if @pipeline
+ #builds.builds.tab-pane
+ = render "projects/merge_requests/show/builds"
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 47dd51639b5..662463bc72b 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -47,7 +47,7 @@
= 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
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
%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
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index a524936f73c..d9f74d2cbfb 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,11 +1,7 @@
-- class_bindings = "{ |
- 'head': line.isHead, |
- 'origin': line.isOrigin, |
- 'match': line.hasMatch, |
- 'selected': line.isSelected, |
- 'unselected': line.isUnselected }"
-
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
+ = page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details
@@ -24,6 +20,21 @@
= render partial: "projects/merge_requests/conflicts/commit_stats"
.files-wrapper{"v-if" => "!isLoading && !hasError"}
- = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
- = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
+ .files
+ .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"}
+ .file-title
+ %i.fa.fa-fw{":class" => "file.iconClass"}
+ %strong {{file.filePath}}
+ = render partial: 'projects/merge_requests/conflicts/file_actions'
+ .diff-content.diff-wrap-lines
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
+ %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
+ = render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
+
= render partial: "projects/merge_requests/conflicts/submit_form"
+
+-# Components
+= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
index 457c467fba9..5ab3cd96163 100644
--- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -1,20 +1,16 @@
.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
- .inline-parallel-buttons
+ .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
.btn-group
- %a.btn{ |
- ":class" => "{'active': !isParallel}", |
- "@click" => "handleViewTypeChange('inline')"}
+ %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
Inline
- %a.btn{ |
- ":class" => "{'active': isParallel}", |
- "@click" => "handleViewTypeChange('parallel')"}
+ %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
Side-by-side
.js-toggle-container
.commit-stat-summary
Showing
- %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}}
+ %strong.cred {{conflictsCountText}}
between
- %strong {{conflictsData.source_branch}}
+ %strong {{conflictsData.sourceBranch}}
and
- %strong {{conflictsData.target_branch}}
+ %strong {{conflictsData.targetBranch}}
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
new file mode 100644
index 00000000000..05af57acf03
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -0,0 +1,12 @@
+.file-actions
+ .btn-group{"v-if" => "file.type === 'text'"}
+ %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
+ '@click' => "onClickResolveModeButton(file, 'interactive')",
+ type: 'button' }
+ Interactive mode
+ %button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
+ '@click' => "onClickResolveModeButton(file, 'edit')",
+ type: 'button' }
+ Edit inline
+ %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
deleted file mode 100644
index 19c7da4b5e3..00000000000
--- a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-.files{"v-show" => "!isParallel"}
- .diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"}
- .file-title
- %i.fa.fa-fw{":class" => "file.iconClass"}
- %strong {{file.filePath}}
- .file-actions
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
- View file @{{conflictsData.shortCommitSha}}
-
- .diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight
- %table
- %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
- %template{"v-if" => "!line.isHeader"}
- %td.diff-line-num.new_line{":class" => class_bindings}
- %a {{line.new_line}}
- %td.diff-line-num.old_line{":class" => class_bindings}
- %a {{line.old_line}}
- %td.line_content{":class" => class_bindings}
- {{{line.richText}}}
-
- %template{"v-if" => "line.isHeader"}
- %td.diff-line-num.header{":class" => class_bindings}
- %td.diff-line-num.header{":class" => class_bindings}
- %td.line_content.header{":class" => class_bindings}
- %strong {{{line.richText}}}
- %button.btn{"@click" => "handleSelected(line.id, line.section)"}
- {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
deleted file mode 100644
index 2e6f67c2eaf..00000000000
--- a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-.files{"v-show" => "isParallel"}
- .diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"}
- .file-title
- %i.fa.fa-fw{":class" => "file.iconClass"}
- %strong {{file.filePath}}
- .file-actions
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
- View file @{{conflictsData.shortCommitSha}}
-
- .diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight
- %table
- %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
- %template{"v-for" => "line in section"}
-
- %template{"v-if" => "line.isHeader"}
- %td.diff-line-num.header{":class" => class_bindings}
- %td.line_content.header{":class" => class_bindings}
- %strong {{line.richText}}
- %button.btn{"@click" => "handleSelected(line.id, line.section)"}
- {{line.buttonTitle}}
-
- %template{"v-if" => "!line.isHeader"}
- %td.diff-line-num.old_line{":class" => class_bindings}
- {{line.lineNumber}}
- %td.line_content.parallel{":class" => class_bindings}
- {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 78bd4133ea2..6ffaa9ad4d2 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -1,15 +1,16 @@
-.content-block.oneline-block.files-changed
- %strong.resolved-count {{resolvedCount}}
- of
- %strong.total-count {{conflictsCount}}
- conflicts have been resolved
-
- .commit-message-container.form-group
- .max-width-marker
- %textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
- {{{conflictsData.commitMessage}}}
-
- %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
- %span {{commitButtonText}}
-
- = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
+.form-horizontal.resolve-conflicts-form
+ .form-group
+ %label.col-sm-2.control-label{ "for" => "commit-message" }
+ Commit message
+ .col-sm-10
+ .commit-message-container
+ .max-width-marker
+ %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .row
+ .col-xs-6
+ %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
+ %span {{commitButtonText}}
+ .col-xs-6.text-right
+ = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
new file mode 100644
index 00000000000..3c927d362c2
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
@@ -0,0 +1,13 @@
+%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"}
+ .diff-editor-wrap{ "v-show" => "file.showEditor" }
+ .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
+ .discard-changes-alert
+ Are you sure you want to discard your changes?
+ .discard-actions
+ %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes
+ %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
+ .editor-wrap{ ":class" => "classObject" }
+ .loading
+ %i.fa.fa-spinner.fa-spin
+ .editor
+ %pre{ "style" => "height: 350px" }
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
new file mode 100644
index 00000000000..f094df7fcaa
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -0,0 +1,15 @@
+%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"}
+ %table
+ %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
+ %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %a {{line.new_line}}
+ %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %a {{line.old_line}}
+ %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ {{{line.richText}}}
+ %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %strong {{{line.richText}}}
+ %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
+ {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
new file mode 100644
index 00000000000..5690bf7419c
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
@@ -0,0 +1,10 @@
+%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
+ %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+ %strong {{line.richText}}
+ %button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
+ {{line.buttonTitle}}
+ %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ {{line.lineNumber}}
+ %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
new file mode 100644
index 00000000000..a8ecdf59393
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
@@ -0,0 +1,4 @@
+%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
+ %table
+ %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
+ %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 988ac0feae1..eab48b78cb3 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -64,6 +64,16 @@
#{@merge_request.target_branch} (base)
.monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ - if different_base?(@start_version, @merge_request_diff)
+ .content-block
+ = icon('info-circle')
+ Selected versions have different base commits.
+ Changes will include
+ = link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
+ new commits
+ from
+ %code #{@merge_request.target_branch}
+
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
= icon('info-circle')
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 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/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 6f5ee5f16c5..842b6df310d 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -35,3 +35,4 @@
Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
= succeed '.' do
!= markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
+ = mr_assign_issues_link
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index ea618263a4a..608fdf1c5f5 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -12,6 +12,7 @@
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}",
ci_message: {
@@ -33,4 +34,4 @@
merge_request_widget.clearEventListeners();
}
- merge_request_widget = new MergeRequestWidget(opts);
+ merge_request_widget = new window.gl.MergeRequestWidget(opts);
diff --git a/app/views/projects/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..c83818e9199 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
+ %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/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..9eeef5f57b4 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -43,7 +43,7 @@
.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
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/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/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg
new file mode 100644
index 00000000000..4f9d9add60d
--- /dev/null
+++ b/app/views/shared/icons/_illustration_no_commits.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c3f4e10c954..a7944a60130 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",
+ = 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", show_menu_above: true } })
.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_menu_above: true, 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?
= 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, show_menu_above: 'true' }, 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..ba9f0c27661 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -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/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/snippets/show.html.haml b/app/views/snippets/show.html.haml
index cd89155c616..27d7a6c5bb6 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -9,6 +9,7 @@
.file-actions
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
+ = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
= render 'shared/snippets/blob'
= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 9657101ace5..232ca26c1af 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -6,7 +6,7 @@
%script#js-authenticate-u2f-setup{ type: "text/template" }
%div
%p Insert your security key (if you haven't already), and press the button below.
- %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device
+ %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
new file mode 100644
index 00000000000..0680645a8db
--- /dev/null
+++ b/app/workers/build_coverage_worker.rb
@@ -0,0 +1,9 @@
+class BuildCoverageWorker
+ include Sidekiq::Worker
+ sidekiq_options queue: :default
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id)
+ .try(:update_coverage)
+ end
+end
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
new file mode 100644
index 00000000000..e7286b77ac5
--- /dev/null
+++ b/app/workers/build_finished_worker.rb
@@ -0,0 +1,10 @@
+class BuildFinishedWorker
+ include Sidekiq::Worker
+
+ 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..e22ececb3fd
--- /dev/null
+++ b/app/workers/build_hooks_worker.rb
@@ -0,0 +1,9 @@
+class BuildHooksWorker
+ include Sidekiq::Worker
+ sidekiq_options queue: :default
+
+ 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..500d357ce31
--- /dev/null
+++ b/app/workers/build_success_worker.rb
@@ -0,0 +1,27 @@
+class BuildSuccessWorker
+ include Sidekiq::Worker
+ sidekiq_options queue: :default
+
+ 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/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
new file mode 100644
index 00000000000..ab5e9f6daad
--- /dev/null
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -0,0 +1,9 @@
+class PipelineHooksWorker
+ include Sidekiq::Worker
+ sidekiq_options queue: :default
+
+ 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..7bb92df3bbd
--- /dev/null
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -0,0 +1,30 @@
+class PipelineMetricsWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ 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/process_pipeline_worker.rb b/app/workers/pipeline_process_worker.rb
index 26ea5f1c24d..f44227d7086 100644
--- a/app/workers/process_pipeline_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -1,4 +1,4 @@
-class ProcessPipelineWorker
+class PipelineProcessWorker
include Sidekiq::Worker
sidekiq_options queue: :default
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
new file mode 100644
index 00000000000..5dd443fea59
--- /dev/null
+++ b/app/workers/pipeline_success_worker.rb
@@ -0,0 +1,12 @@
+class PipelineSuccessWorker
+ include Sidekiq::Worker
+ sidekiq_options queue: :default
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+ MergeRequests::MergeWhenBuildSucceedsService
+ .new(pipeline.project, nil)
+ .trigger(pipeline)
+ end
+ end
+end
diff --git a/app/workers/update_pipeline_worker.rb b/app/workers/pipeline_update_worker.rb
index 6ef5678073e..44a7f24e401 100644
--- a/app/workers/update_pipeline_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -1,4 +1,4 @@
-class UpdatePipelineWorker
+class PipelineUpdateWorker
include Sidekiq::Worker
sidekiq_options queue: :default
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
new file mode 100644
index 00000000000..df4c4a6628b
--- /dev/null
+++ b/app/workers/trending_projects_worker.rb
@@ -0,0 +1,11 @@
+class TrendingProjectsWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :trending_projects
+
+ def perform
+ Rails.logger.info('Refreshing trending projects')
+
+ TrendingProject.refresh!
+ end
+end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
new file mode 100644
index 00000000000..03f0528cdae
--- /dev/null
+++ b/app/workers/update_merge_requests_worker.rb
@@ -0,0 +1,16 @@
+class UpdateMergeRequestsWorker
+ include Sidekiq::Worker
+
+ 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/config/application.rb b/config/application.rb
index 962ffe0708d..8a9c539cb43 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -89,6 +89,7 @@ module Gitlab
config.assets.precompile << "profile/profile_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/gitlab.yml.example b/config/gitlab.yml.example
index a79356923b2..114ceac8e1f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -112,7 +112,7 @@ production: &base
## Reply by email
# Allow users to comment on issues and merge requests by replying to notification emails.
- # For documentation on how to set this up, see http://doc.gitlab.com/ce/incoming_email/README.html
+ # For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html
incoming_email:
enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index c5ed2162c92..efe0ac9c965 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -304,6 +304,10 @@ Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
+Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'
+Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker'
+
#
# GitLab Shell
#
diff --git a/config/initializers/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/routes.rb b/config/routes.rb
index bf7c5b76128..659ea51bc75 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -87,7 +87,7 @@ Rails.application.routes.draw do
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
- get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
-
root to: "root#index"
+
+ get '*unmatched_route', to: 'application#not_found'
end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 47a8a0a53d4..06b464d79c8 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
diff --git a/config/routes/project.rb b/config/routes/project.rb
index e8807ef06a7..711a59df744 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -159,7 +159,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
get(
'/commits/*id',
to: 'commits#show',
- constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
+ constraints: { id: /.+/, format: false },
as: :commits
)
end
@@ -267,16 +267,19 @@ 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
post :resolve_conflicts
+ post :assign_related_issues
end
collection do
@@ -406,7 +409,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
@@ -416,7 +419,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
end
- resource :board, only: [:show] do
+ resources :boards, only: [:index, :show] do
scope module: :boards do
resources :issues, only: [:update]
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index 1949f215c66..3ca096f31ba 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -1,6 +1,7 @@
resources :snippets, concerns: :awardable do
member do
get 'raw'
+ get 'download'
end
end
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/db/migrate/20161007133303_precalculate_trending_projects.rb b/db/migrate/20161007133303_precalculate_trending_projects.rb
new file mode 100644
index 00000000000..b324cd94268
--- /dev/null
+++ b/db/migrate/20161007133303_precalculate_trending_projects.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PrecalculateTrendingProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :trending_projects do |t|
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
+ end
+
+ timestamp = connection.quote(1.month.ago)
+
+ # We're hardcoding the visibility level (public) here so that if it ever
+ # changes this query doesn't suddenly use the new value (which may break
+ # later migrations).
+ visibility = 20
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO trending_projects (project_id)
+ SELECT project_id
+ FROM notes
+ INNER JOIN projects ON projects.id = notes.project_id
+ WHERE notes.created_at >= #{timestamp}
+ AND notes.system IS FALSE
+ AND projects.visibility_level = #{visibility}
+ GROUP BY project_id
+ ORDER BY count(*) DESC
+ LIMIT 100;
+ EOF
+ end
+
+ def down
+ drop_table :trending_projects
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 56da70b3c02..a362fd8f228 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,101 +11,101 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160926145521) do
+ActiveRecord::Schema.define(version: 20161007133303) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
create_table "abuse_reports", force: :cascade do |t|
- t.integer "reporter_id"
- t.integer "user_id"
- t.text "message"
+ t.integer "reporter_id"
+ t.integer "user_id"
+ t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
- t.text "message_html"
+ t.text "message_html"
end
create_table "appearances", force: :cascade do |t|
- t.string "title"
- t.text "description"
- t.string "header_logo"
- t.string "logo"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.text "description_html"
+ t.string "title"
+ t.text "description"
+ t.string "header_logo"
+ t.string "logo"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "description_html"
end
create_table "application_settings", force: :cascade do |t|
- t.integer "default_projects_limit"
- t.boolean "signup_enabled"
- t.boolean "signin_enabled"
- t.boolean "gravatar_enabled"
- t.text "sign_in_text"
+ t.integer "default_projects_limit"
+ t.boolean "signup_enabled"
+ t.boolean "signin_enabled"
+ t.boolean "gravatar_enabled"
+ t.text "sign_in_text"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "home_page_url"
- t.integer "default_branch_protection", default: 2
- t.text "restricted_visibility_levels"
- t.boolean "version_check_enabled", default: true
- t.integer "max_attachment_size", default: 10, null: false
- t.integer "default_project_visibility"
- t.integer "default_snippet_visibility"
- t.text "domain_whitelist"
- t.boolean "user_oauth_applications", default: true
- t.string "after_sign_out_path"
- t.integer "session_expire_delay", default: 10080, null: false
- t.text "import_sources"
- t.text "help_page_text"
- t.string "admin_notification_email"
- t.boolean "shared_runners_enabled", default: true, null: false
- t.integer "max_artifacts_size", default: 100, null: false
- t.string "runners_registration_token"
- t.boolean "require_two_factor_authentication", default: false
- t.integer "two_factor_grace_period", default: 48
- t.boolean "metrics_enabled", default: false
- t.string "metrics_host", default: "localhost"
- t.integer "metrics_pool_size", default: 16
- t.integer "metrics_timeout", default: 10
- t.integer "metrics_method_call_threshold", default: 10
- t.boolean "recaptcha_enabled", default: false
- t.string "recaptcha_site_key"
- t.string "recaptcha_private_key"
- t.integer "metrics_port", default: 8089
- t.boolean "akismet_enabled", default: false
- t.string "akismet_api_key"
- t.integer "metrics_sample_interval", default: 15
- t.boolean "sentry_enabled", default: false
- t.string "sentry_dsn"
- t.boolean "email_author_in_body", default: false
- t.integer "default_group_visibility"
- t.boolean "repository_checks_enabled", default: false
- t.text "shared_runners_text"
- t.integer "metrics_packet_size", default: 1
- t.text "disabled_oauth_sign_in_sources"
- t.string "health_check_access_token"
- t.boolean "send_user_confirmation_email", default: false
- t.integer "container_registry_token_expire_delay", default: 5
- t.text "after_sign_up_text"
- t.boolean "user_default_external", default: false, null: false
- t.string "repository_storage", default: "default"
- t.string "enabled_git_access_protocol"
- t.boolean "domain_blacklist_enabled", default: false
- t.text "domain_blacklist"
- t.boolean "koding_enabled"
- t.string "koding_url"
- t.text "sign_in_text_html"
- t.text "help_page_text_html"
- t.text "shared_runners_text_html"
- t.text "after_sign_up_text_html"
+ t.string "home_page_url"
+ t.integer "default_branch_protection", default: 2
+ t.text "restricted_visibility_levels"
+ t.boolean "version_check_enabled", default: true
+ t.integer "max_attachment_size", default: 10, null: false
+ t.integer "default_project_visibility"
+ t.integer "default_snippet_visibility"
+ t.text "domain_whitelist"
+ t.boolean "user_oauth_applications", default: true
+ t.string "after_sign_out_path"
+ t.integer "session_expire_delay", default: 10080, null: false
+ t.text "import_sources"
+ t.text "help_page_text"
+ t.string "admin_notification_email"
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.integer "max_artifacts_size", default: 100, null: false
+ t.string "runners_registration_token"
+ t.boolean "require_two_factor_authentication", default: false
+ t.integer "two_factor_grace_period", default: 48
+ t.boolean "metrics_enabled", default: false
+ t.string "metrics_host", default: "localhost"
+ t.integer "metrics_pool_size", default: 16
+ t.integer "metrics_timeout", default: 10
+ t.integer "metrics_method_call_threshold", default: 10
+ t.boolean "recaptcha_enabled", default: false
+ t.string "recaptcha_site_key"
+ t.string "recaptcha_private_key"
+ t.integer "metrics_port", default: 8089
+ t.boolean "akismet_enabled", default: false
+ t.string "akismet_api_key"
+ t.integer "metrics_sample_interval", default: 15
+ t.boolean "sentry_enabled", default: false
+ t.string "sentry_dsn"
+ t.boolean "email_author_in_body", default: false
+ t.integer "default_group_visibility"
+ t.boolean "repository_checks_enabled", default: false
+ t.text "shared_runners_text"
+ t.integer "metrics_packet_size", default: 1
+ t.text "disabled_oauth_sign_in_sources"
+ t.string "health_check_access_token"
+ t.boolean "send_user_confirmation_email", default: false
+ t.integer "container_registry_token_expire_delay", default: 5
+ t.text "after_sign_up_text"
+ t.boolean "user_default_external", default: false, null: false
+ t.string "repository_storage", default: "default"
+ t.string "enabled_git_access_protocol"
+ t.boolean "domain_blacklist_enabled", default: false
+ t.text "domain_blacklist"
+ t.boolean "koding_enabled"
+ t.string "koding_url"
+ t.text "sign_in_text_html"
+ t.text "help_page_text_html"
+ t.text "shared_runners_text_html"
+ t.text "after_sign_up_text_html"
end
create_table "audit_events", force: :cascade do |t|
- t.integer "author_id", null: false
- t.string "type", null: false
- t.integer "entity_id", null: false
- t.string "entity_type", null: false
- t.text "details"
+ t.integer "author_id", null: false
+ t.string "type", null: false
+ t.integer "entity_id", null: false
+ t.string "entity_type", null: false
+ t.text "details"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -113,10 +113,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
create_table "award_emoji", force: :cascade do |t|
- t.string "name"
- t.integer "user_id"
- t.integer "awardable_id"
- t.string "awardable_type"
+ t.string "name"
+ t.integer "user_id"
+ t.integer "awardable_id"
+ t.string "awardable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -126,7 +126,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
create_table "boards", force: :cascade do |t|
- t.integer "project_id", null: false
+ t.integer "project_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
@@ -134,61 +134,61 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t|
- t.text "message", null: false
+ t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "color"
- t.string "font"
- t.text "message_html"
+ t.string "color"
+ t.string "font"
+ t.text "message_html"
end
create_table "ci_application_settings", force: :cascade do |t|
- t.boolean "all_broken_builds"
- t.boolean "add_pusher"
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "ci_builds", force: :cascade do |t|
- t.integer "project_id"
- t.string "status"
+ t.integer "project_id"
+ t.string "status"
t.datetime "finished_at"
- t.text "trace"
+ t.text "trace"
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "started_at"
- t.integer "runner_id"
- t.float "coverage"
- t.integer "commit_id"
- t.text "commands"
- t.integer "job_id"
- t.string "name"
- t.boolean "deploy", default: false
- t.text "options"
- t.boolean "allow_failure", default: false, null: false
- t.string "stage"
- t.integer "trigger_request_id"
- t.integer "stage_idx"
- t.boolean "tag"
- t.string "ref"
- t.integer "user_id"
- t.string "type"
- t.string "target_url"
- t.string "description"
- t.text "artifacts_file"
- t.integer "gl_project_id"
- t.text "artifacts_metadata"
- t.integer "erased_by_id"
+ t.integer "runner_id"
+ t.float "coverage"
+ t.integer "commit_id"
+ t.text "commands"
+ t.integer "job_id"
+ t.string "name"
+ t.boolean "deploy", default: false
+ t.text "options"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage"
+ t.integer "trigger_request_id"
+ t.integer "stage_idx"
+ t.boolean "tag"
+ t.string "ref"
+ t.integer "user_id"
+ t.string "type"
+ t.string "target_url"
+ t.string "description"
+ t.text "artifacts_file"
+ t.integer "gl_project_id"
+ t.text "artifacts_metadata"
+ t.integer "erased_by_id"
t.datetime "erased_at"
t.datetime "artifacts_expire_at"
- t.string "environment"
- t.integer "artifacts_size", limit: 8
- t.string "when"
- t.text "yaml_variables"
+ t.string "environment"
+ t.integer "artifacts_size", limit: 8
+ t.string "when"
+ t.text "yaml_variables"
t.datetime "queued_at"
- t.string "token"
+ t.string "token"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -203,22 +203,22 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
create_table "ci_commits", force: :cascade do |t|
- t.integer "project_id"
- t.string "ref"
- t.string "sha"
- t.string "before_sha"
- t.text "push_data"
+ t.integer "project_id"
+ t.string "ref"
+ t.string "sha"
+ t.string "before_sha"
+ t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "tag", default: false
- t.text "yaml_errors"
+ t.boolean "tag", default: false
+ t.text "yaml_errors"
t.datetime "committed_at"
- t.integer "gl_project_id"
- t.string "status"
+ t.integer "gl_project_id"
+ t.string "status"
t.datetime "started_at"
t.datetime "finished_at"
- t.integer "duration"
- t.integer "user_id"
+ t.integer "duration"
+ t.integer "user_id"
end
add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
@@ -228,140 +228,140 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
create_table "ci_events", force: :cascade do |t|
- t.integer "project_id"
- t.integer "user_id"
- t.integer "is_admin"
- t.text "description"
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "ci_jobs", force: :cascade do |t|
- t.integer "project_id", null: false
- t.text "commands"
- t.boolean "active", default: true, null: false
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "name"
- t.boolean "build_branches", default: true, null: false
- t.boolean "build_tags", default: false, null: false
- t.string "job_type", default: "parallel"
- t.string "refs"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
t.datetime "deleted_at"
end
create_table "ci_projects", force: :cascade do |t|
- t.string "name"
- t.integer "timeout", default: 3600, null: false
+ t.string "name"
+ t.integer "timeout", default: 3600, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "token"
- t.string "default_ref"
- t.string "path"
- t.boolean "always_build", default: false, null: false
- t.integer "polling_interval"
- t.boolean "public", default: false, null: false
- t.string "ssh_url_to_repo"
- t.integer "gitlab_id"
- t.boolean "allow_git_fetch", default: true, null: false
- t.string "email_recipients", default: "", null: false
- t.boolean "email_add_pusher", default: true, null: false
- t.boolean "email_only_broken_builds", default: true, null: false
- t.string "skip_refs"
- t.string "coverage_regex"
- t.boolean "shared_runners_enabled", default: false
- t.text "generated_yaml_config"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
end
create_table "ci_runner_projects", force: :cascade do |t|
- t.integer "runner_id", null: false
- t.integer "project_id"
+ t.integer "runner_id", null: false
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "gl_project_id"
end
add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
create_table "ci_runners", force: :cascade do |t|
- t.string "token"
+ t.string "token"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "description"
+ t.string "description"
t.datetime "contacted_at"
- t.boolean "active", default: true, null: false
- t.boolean "is_shared", default: false
- t.string "name"
- t.string "version"
- t.string "revision"
- t.string "platform"
- t.string "architecture"
- t.boolean "run_untagged", default: true, null: false
- t.boolean "locked", default: false, null: false
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name"
+ t.string "version"
+ t.string "revision"
+ t.string "platform"
+ t.string "architecture"
+ t.boolean "run_untagged", default: true, null: false
+ t.boolean "locked", default: false, null: false
end
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
create_table "ci_sessions", force: :cascade do |t|
- t.string "session_id", null: false
- t.text "data"
+ t.string "session_id", null: false
+ t.text "data"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "ci_taggings", force: :cascade do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context", limit: 128
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
t.datetime "created_at"
end
add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "ci_tags", force: :cascade do |t|
- t.string "name"
+ t.string "name"
t.integer "taggings_count", default: 0
end
create_table "ci_trigger_requests", force: :cascade do |t|
- t.integer "trigger_id", null: false
- t.text "variables"
+ t.integer "trigger_id", null: false
+ t.text "variables"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "commit_id"
+ t.integer "commit_id"
end
create_table "ci_triggers", force: :cascade do |t|
- t.string "token"
- t.integer "project_id"
+ t.string "token"
+ t.integer "project_id"
t.datetime "deleted_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "gl_project_id"
end
add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
create_table "ci_variables", force: :cascade do |t|
t.integer "project_id"
- t.string "key"
- t.text "value"
- t.text "encrypted_value"
- t.string "encrypted_value_salt"
- t.string "encrypted_value_iv"
+ t.string "key"
+ t.text "value"
+ t.text "encrypted_value"
+ t.string "encrypted_value_salt"
+ t.string "encrypted_value_iv"
t.integer "gl_project_id"
end
add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
create_table "deploy_keys_projects", force: :cascade do |t|
- t.integer "deploy_key_id", null: false
- t.integer "project_id", null: false
+ t.integer "deploy_key_id", null: false
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -369,15 +369,15 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
create_table "deployments", force: :cascade do |t|
- t.integer "iid", null: false
- t.integer "project_id", null: false
- t.integer "environment_id", null: false
- t.string "ref", null: false
- t.boolean "tag", null: false
- t.string "sha", null: false
- t.integer "user_id"
- t.integer "deployable_id"
- t.string "deployable_type"
+ t.integer "iid", null: false
+ t.integer "project_id", null: false
+ t.integer "environment_id", null: false
+ t.string "ref", null: false
+ t.boolean "tag", null: false
+ t.string "sha", null: false
+ t.integer "user_id"
+ t.integer "deployable_id"
+ t.string "deployable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -388,8 +388,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree
create_table "emails", force: :cascade do |t|
- t.integer "user_id", null: false
- t.string "email", null: false
+ t.integer "user_id", null: false
+ t.string "email", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -398,26 +398,26 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
create_table "environments", force: :cascade do |t|
- t.integer "project_id"
- t.string "name", null: false
+ t.integer "project_id"
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "external_url"
- t.string "environment_type"
+ t.string "external_url"
+ t.string "environment_type"
end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
create_table "events", force: :cascade do |t|
- t.string "target_type"
- t.integer "target_id"
- t.string "title"
- t.text "data"
- t.integer "project_id"
+ t.string "target_type"
+ t.integer "target_id"
+ t.string "title"
+ t.text "data"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "action"
- t.integer "author_id"
+ t.integer "action"
+ t.integer "author_id"
end
add_index "events", ["action"], name: "index_events_on_action", using: :btree
@@ -428,8 +428,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
create_table "forked_project_links", force: :cascade do |t|
- t.integer "forked_to_project_id", null: false
- t.integer "forked_from_project_id", null: false
+ t.integer "forked_to_project_id", null: false
+ t.integer "forked_from_project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -437,9 +437,9 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
create_table "identities", force: :cascade do |t|
- t.string "extern_uid"
- t.string "provider"
- t.integer "user_id"
+ t.string "extern_uid"
+ t.string "provider"
+ t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -447,37 +447,37 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "issue_metrics", force: :cascade do |t|
- t.integer "issue_id", null: false
+ t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at"
t.datetime "first_associated_with_milestone_at"
t.datetime "first_added_to_board_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree
create_table "issues", force: :cascade do |t|
- t.string "title"
- t.integer "assignee_id"
- t.integer "author_id"
- t.integer "project_id"
+ t.string "title"
+ t.integer "assignee_id"
+ t.integer "author_id"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "position", default: 0
- t.string "branch_name"
- t.text "description"
- t.integer "milestone_id"
- t.string "state"
- t.integer "iid"
- t.integer "updated_by_id"
- t.boolean "confidential", default: false
+ t.integer "position", default: 0
+ t.string "branch_name"
+ t.text "description"
+ t.integer "milestone_id"
+ t.string "state"
+ t.integer "iid"
+ t.integer "updated_by_id"
+ t.boolean "confidential", default: false
t.datetime "deleted_at"
- t.date "due_date"
- t.integer "moved_to_id"
- t.integer "lock_version"
- t.text "title_html"
- t.text "description_html"
+ t.date "due_date"
+ t.integer "moved_to_id"
+ t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -493,23 +493,23 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t|
- t.integer "user_id"
+ t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.text "key"
- t.string "title"
- t.string "type"
- t.string "fingerprint"
- t.boolean "public", default: false, null: false
+ t.text "key"
+ t.string "title"
+ t.string "type"
+ t.string "fingerprint"
+ t.boolean "public", default: false, null: false
end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
create_table "label_links", force: :cascade do |t|
- t.integer "label_id"
- t.integer "target_id"
- t.string "target_type"
+ t.integer "label_id"
+ t.integer "target_id"
+ t.string "target_type"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -518,15 +518,15 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
create_table "labels", force: :cascade do |t|
- t.string "title"
- t.string "color"
- t.integer "project_id"
+ t.string "title"
+ t.string "color"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "template", default: false
- t.string "description"
- t.integer "priority"
- t.text "description_html"
+ t.boolean "template", default: false
+ t.string "description"
+ t.integer "priority"
+ t.text "description_html"
end
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
@@ -534,18 +534,18 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
create_table "lfs_objects", force: :cascade do |t|
- t.string "oid", null: false
- t.integer "size", limit: 8, null: false
+ t.string "oid", null: false
+ t.integer "size", limit: 8, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "file"
+ t.string "file"
end
add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
create_table "lfs_objects_projects", force: :cascade do |t|
- t.integer "lfs_object_id", null: false
- t.integer "project_id", null: false
+ t.integer "lfs_object_id", null: false
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -553,12 +553,12 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
create_table "lists", force: :cascade do |t|
- t.integer "board_id", null: false
- t.integer "label_id"
- t.integer "list_type", default: 1, null: false
- t.integer "position"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "board_id", null: false
+ t.integer "label_id"
+ t.integer "list_type", default: 1, null: false
+ t.integer "position"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree
@@ -566,20 +566,20 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree
create_table "members", force: :cascade do |t|
- t.integer "access_level", null: false
- t.integer "source_id", null: false
- t.string "source_type", null: false
- t.integer "user_id"
- t.integer "notification_level", null: false
- t.string "type"
+ t.integer "access_level", null: false
+ t.integer "source_id", null: false
+ t.string "source_type", null: false
+ t.integer "user_id"
+ t.integer "notification_level", null: false
+ t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "created_by_id"
- t.string "invite_email"
- t.string "invite_token"
+ t.integer "created_by_id"
+ t.string "invite_email"
+ t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
- t.date "expires_at"
+ t.date "expires_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
@@ -589,61 +589,61 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "merge_request_diffs", force: :cascade do |t|
- t.string "state"
- t.text "st_commits"
- t.text "st_diffs"
- t.integer "merge_request_id", null: false
+ t.string "state"
+ t.text "st_commits"
+ t.text "st_diffs"
+ t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "base_commit_sha"
- t.string "real_size"
- t.string "head_commit_sha"
- t.string "start_commit_sha"
+ t.string "base_commit_sha"
+ t.string "real_size"
+ t.string "head_commit_sha"
+ t.string "start_commit_sha"
end
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
create_table "merge_request_metrics", force: :cascade do |t|
- t.integer "merge_request_id", null: false
+ t.integer "merge_request_id", null: false
t.datetime "latest_build_started_at"
t.datetime "latest_build_finished_at"
t.datetime "first_deployed_to_production_at"
t.datetime "merged_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
create_table "merge_requests", force: :cascade do |t|
- t.string "target_branch", null: false
- t.string "source_branch", null: false
- t.integer "source_project_id", null: false
- t.integer "author_id"
- t.integer "assignee_id"
- t.string "title"
+ t.string "target_branch", null: false
+ t.string "source_branch", null: false
+ t.integer "source_project_id", null: false
+ t.integer "author_id"
+ t.integer "assignee_id"
+ t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "milestone_id"
- t.string "state"
- t.string "merge_status"
- t.integer "target_project_id", null: false
- t.integer "iid"
- t.text "description"
- t.integer "position", default: 0
+ t.integer "milestone_id"
+ t.string "state"
+ t.string "merge_status"
+ t.integer "target_project_id", null: false
+ t.integer "iid"
+ t.text "description"
+ t.integer "position", default: 0
t.datetime "locked_at"
- t.integer "updated_by_id"
- t.text "merge_error"
- t.text "merge_params"
- t.boolean "merge_when_build_succeeds", default: false, null: false
- t.integer "merge_user_id"
- t.string "merge_commit_sha"
+ t.integer "updated_by_id"
+ t.text "merge_error"
+ t.text "merge_params"
+ t.boolean "merge_when_build_succeeds", default: false, null: false
+ t.integer "merge_user_id"
+ t.string "merge_commit_sha"
t.datetime "deleted_at"
- t.string "in_progress_merge_commit_sha"
- t.integer "lock_version"
- t.text "title_html"
- t.text "description_html"
+ t.string "in_progress_merge_commit_sha"
+ t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -660,26 +660,26 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "merge_requests_closing_issues", force: :cascade do |t|
- t.integer "merge_request_id", null: false
- t.integer "issue_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "merge_request_id", null: false
+ t.integer "issue_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "merge_requests_closing_issues", ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree
add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
create_table "milestones", force: :cascade do |t|
- t.string "title", null: false
- t.integer "project_id", null: false
- t.text "description"
- t.date "due_date"
+ t.string "title", null: false
+ t.integer "project_id", null: false
+ t.text "description"
+ t.date "due_date"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "state"
- t.integer "iid"
- t.text "title_html"
- t.text "description_html"
+ t.string "state"
+ t.integer "iid"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -690,20 +690,20 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
- t.integer "owner_id"
+ t.string "name", null: false
+ t.string "path", null: false
+ t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type"
- t.string "description", default: "", null: false
- t.string "avatar"
- t.boolean "share_with_group_lock", default: false
- t.integer "visibility_level", default: 20, null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.string "type"
+ t.string "description", default: "", null: false
+ t.string "avatar"
+ t.boolean "share_with_group_lock", default: false
+ t.integer "visibility_level", default: 20, null: false
+ t.boolean "request_access_enabled", default: true, null: false
t.datetime "deleted_at"
- t.boolean "lfs_enabled"
- t.text "description_html"
+ t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -716,27 +716,27 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
- t.text "note"
- t.string "noteable_type"
- t.integer "author_id"
+ t.text "note"
+ t.string "noteable_type"
+ t.integer "author_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "project_id"
- t.string "attachment"
- t.string "line_code"
- t.string "commit_id"
- t.integer "noteable_id"
- t.boolean "system", default: false, null: false
- t.text "st_diff"
- t.integer "updated_by_id"
- t.string "type"
- t.text "position"
- t.text "original_position"
+ t.integer "project_id"
+ t.string "attachment"
+ t.string "line_code"
+ t.string "commit_id"
+ t.integer "noteable_id"
+ t.boolean "system", default: false, null: false
+ t.text "st_diff"
+ t.integer "updated_by_id"
+ t.string "type"
+ t.text "position"
+ t.text "original_position"
t.datetime "resolved_at"
- t.integer "resolved_by_id"
- t.string "discussion_id"
- t.string "original_discussion_id"
- t.text "note_html"
+ t.integer "resolved_by_id"
+ t.string "discussion_id"
+ t.string "original_discussion_id"
+ t.text "note_html"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
@@ -752,13 +752,13 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
create_table "notification_settings", force: :cascade do |t|
- t.integer "user_id", null: false
- t.integer "source_id"
- t.string "source_type"
- t.integer "level", default: 0, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.text "events"
+ t.integer "user_id", null: false
+ t.integer "source_id"
+ t.string "source_type"
+ t.integer "level", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "events"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
@@ -766,27 +766,27 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
create_table "oauth_access_grants", force: :cascade do |t|
- t.integer "resource_owner_id", null: false
- t.integer "application_id", null: false
- t.string "token", null: false
- t.integer "expires_in", null: false
- t.text "redirect_uri", null: false
- t.datetime "created_at", null: false
+ t.integer "resource_owner_id", null: false
+ t.integer "application_id", null: false
+ t.string "token", null: false
+ t.integer "expires_in", null: false
+ t.text "redirect_uri", null: false
+ t.datetime "created_at", null: false
t.datetime "revoked_at"
- t.string "scopes"
+ t.string "scopes"
end
add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
create_table "oauth_access_tokens", force: :cascade do |t|
- t.integer "resource_owner_id"
- t.integer "application_id"
- t.string "token", null: false
- t.string "refresh_token"
- t.integer "expires_in"
+ t.integer "resource_owner_id"
+ t.integer "application_id"
+ t.string "token", null: false
+ t.string "refresh_token"
+ t.integer "expires_in"
t.datetime "revoked_at"
- t.datetime "created_at", null: false
- t.string "scopes"
+ t.datetime "created_at", null: false
+ t.string "scopes"
end
add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
@@ -794,40 +794,40 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
create_table "oauth_applications", force: :cascade do |t|
- t.string "name", null: false
- t.string "uid", null: false
- t.string "secret", null: false
- t.text "redirect_uri", null: false
- t.string "scopes", default: "", null: false
+ t.string "name", null: false
+ t.string "uid", null: false
+ t.string "secret", null: false
+ t.text "redirect_uri", null: false
+ t.string "scopes", default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "owner_id"
- t.string "owner_type"
+ t.integer "owner_id"
+ t.string "owner_type"
end
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
create_table "personal_access_tokens", force: :cascade do |t|
- t.integer "user_id", null: false
- t.string "token", null: false
- t.string "name", null: false
- t.boolean "revoked", default: false
+ t.integer "user_id", null: false
+ t.string "token", null: false
+ t.string "name", null: false
+ t.boolean "revoked", default: false
t.datetime "expires_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "project_features", force: :cascade do |t|
- t.integer "project_id"
- t.integer "merge_requests_access_level"
- t.integer "issues_access_level"
- t.integer "wiki_access_level"
- t.integer "snippets_access_level"
- t.integer "builds_access_level"
+ t.integer "project_id"
+ t.integer "merge_requests_access_level"
+ t.integer "issues_access_level"
+ t.integer "wiki_access_level"
+ t.integer "snippets_access_level"
+ t.integer "builds_access_level"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -835,60 +835,60 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
create_table "project_group_links", force: :cascade do |t|
- t.integer "project_id", null: false
- t.integer "group_id", null: false
+ t.integer "project_id", null: false
+ t.integer "group_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "group_access", default: 30, null: false
- t.date "expires_at"
+ t.integer "group_access", default: 30, null: false
+ t.date "expires_at"
end
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
- t.text "data"
- t.text "encrypted_credentials"
- t.string "encrypted_credentials_iv"
- t.string "encrypted_credentials_salt"
+ t.text "data"
+ t.text "encrypted_credentials"
+ t.string "encrypted_credentials_iv"
+ t.string "encrypted_credentials_salt"
end
create_table "projects", force: :cascade do |t|
- t.string "name"
- t.string "path"
- t.text "description"
+ t.string "name"
+ t.string "path"
+ t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "creator_id"
- t.integer "namespace_id"
+ t.integer "creator_id"
+ t.integer "namespace_id"
t.datetime "last_activity_at"
- t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
- t.string "avatar"
- t.string "import_status"
- t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
- t.string "import_type"
- t.string "import_source"
- t.integer "commit_count", default: 0
- t.text "import_error"
- t.integer "ci_id"
- t.boolean "shared_runners_enabled", default: true, null: false
- t.string "runners_token"
- t.string "build_coverage_regex"
- t.boolean "build_allow_git_fetch", default: true, null: false
- t.integer "build_timeout", default: 3600, null: false
- t.boolean "pending_delete", default: false
- t.boolean "public_builds", default: true, null: false
- t.boolean "last_repository_check_failed"
+ t.string "import_url"
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
+ t.string "avatar"
+ t.string "import_status"
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
+ t.string "import_type"
+ t.string "import_source"
+ t.integer "commit_count", default: 0
+ t.text "import_error"
+ t.integer "ci_id"
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.string "runners_token"
+ t.string "build_coverage_regex"
+ t.boolean "build_allow_git_fetch", default: true, null: false
+ t.integer "build_timeout", default: 3600, null: false
+ t.boolean "pending_delete", default: false
+ t.boolean "public_builds", default: true, null: false
+ t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
- t.boolean "container_registry_enabled"
- t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
- t.boolean "has_external_issue_tracker"
- t.string "repository_storage", default: "default", null: false
- t.boolean "request_access_enabled", default: true, null: false
- t.boolean "has_external_wiki"
- t.boolean "lfs_enabled"
- t.text "description_html"
+ t.boolean "container_registry_enabled"
+ t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
+ t.boolean "has_external_issue_tracker"
+ t.string "repository_storage", default: "default", null: false
+ t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "has_external_wiki"
+ t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -907,26 +907,26 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
create_table "protected_branch_merge_access_levels", force: :cascade do |t|
- t.integer "protected_branch_id", null: false
- t.integer "access_level", default: 40, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "protected_branch_id", null: false
+ t.integer "access_level", default: 40, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
create_table "protected_branch_push_access_levels", force: :cascade do |t|
- t.integer "protected_branch_id", null: false
- t.integer "access_level", default: 40, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "protected_branch_id", null: false
+ t.integer "access_level", default: 40, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
create_table "protected_branches", force: :cascade do |t|
- t.integer "project_id", null: false
- t.string "name", null: false
+ t.integer "project_id", null: false
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -934,12 +934,12 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
create_table "releases", force: :cascade do |t|
- t.string "tag"
- t.text "description"
- t.integer "project_id"
+ t.string "tag"
+ t.text "description"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.text "description_html"
+ t.text "description_html"
end
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
@@ -948,54 +948,54 @@ ActiveRecord::Schema.define(version: 20160926145521) do
create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
- t.string "noteable_type"
+ t.string "noteable_type"
t.integer "recipient_id"
- t.string "commit_id"
- t.string "reply_key", null: false
- t.string "line_code"
- t.string "note_type"
- t.text "position"
+ t.string "commit_id"
+ t.string "reply_key", null: false
+ t.string "line_code"
+ t.string "note_type"
+ t.text "position"
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
create_table "services", force: :cascade do |t|
- t.string "type"
- t.string "title"
- t.integer "project_id"
+ t.string "type"
+ t.string "title"
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "active", default: false, null: false
- t.text "properties"
- t.boolean "template", default: false
- t.boolean "push_events", default: true
- t.boolean "issues_events", default: true
- t.boolean "merge_requests_events", default: true
- t.boolean "tag_push_events", default: true
- t.boolean "note_events", default: true, null: false
- t.boolean "build_events", default: false, null: false
- t.string "category", default: "common", null: false
- t.boolean "default", default: false
- t.boolean "wiki_page_events", default: true
- t.boolean "pipeline_events", default: false, null: false
- t.boolean "confidential_issues_events", default: true, null: false
+ t.boolean "active", default: false, null: false
+ t.text "properties"
+ t.boolean "template", default: false
+ t.boolean "push_events", default: true
+ t.boolean "issues_events", default: true
+ t.boolean "merge_requests_events", default: true
+ t.boolean "tag_push_events", default: true
+ t.boolean "note_events", default: true, null: false
+ t.boolean "build_events", default: false, null: false
+ t.string "category", default: "common", null: false
+ t.boolean "default", default: false
+ t.boolean "wiki_page_events", default: true
+ t.boolean "pipeline_events", default: false, null: false
+ t.boolean "confidential_issues_events", default: true, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
add_index "services", ["template"], name: "index_services_on_template", using: :btree
create_table "snippets", force: :cascade do |t|
- t.string "title"
- t.text "content"
- t.integer "author_id", null: false
- t.integer "project_id"
+ t.string "title"
+ t.text "content"
+ t.integer "author_id", null: false
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "file_name"
- t.string "type"
- t.integer "visibility_level", default: 0, null: false
- t.text "title_html"
- t.text "content_html"
+ t.string "file_name"
+ t.string "type"
+ t.integer "visibility_level", default: 0, null: false
+ t.text "title_html"
+ t.text "content_html"
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
@@ -1006,23 +1006,23 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
create_table "spam_logs", force: :cascade do |t|
- t.integer "user_id"
- t.string "source_ip"
- t.string "user_agent"
- t.boolean "via_api"
- t.string "noteable_type"
- t.string "title"
- t.text "description"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "submitted_as_ham", default: false, null: false
+ t.integer "user_id"
+ t.string "source_ip"
+ t.string "user_agent"
+ t.boolean "via_api"
+ t.string "noteable_type"
+ t.string "title"
+ t.text "description"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "submitted_as_ham", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
- t.integer "user_id"
- t.integer "subscribable_id"
- t.string "subscribable_type"
- t.boolean "subscribed"
+ t.integer "user_id"
+ t.integer "subscribable_id"
+ t.string "subscribable_type"
+ t.boolean "subscribed"
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -1030,12 +1030,12 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
create_table "taggings", force: :cascade do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context"
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context"
t.datetime "created_at"
end
@@ -1043,24 +1043,24 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "tags", force: :cascade do |t|
- t.string "name"
+ t.string "name"
t.integer "taggings_count", default: 0
end
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
create_table "todos", force: :cascade do |t|
- t.integer "user_id", null: false
- t.integer "project_id", null: false
- t.integer "target_id"
- t.string "target_type", null: false
- t.integer "author_id"
- t.integer "action", null: false
- t.string "state", null: false
+ t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.integer "target_id"
+ t.string "target_type", null: false
+ t.integer "author_id"
+ t.integer "action", null: false
+ t.string "state", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "note_id"
- t.string "commit_id"
+ t.integer "note_id"
+ t.string "commit_id"
end
add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree
@@ -1070,89 +1070,95 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
+ create_table "trending_projects", force: :cascade do |t|
+ t.integer "project_id", null: false
+ end
+
+ add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree
+
create_table "u2f_registrations", force: :cascade do |t|
- t.text "certificate"
- t.string "key_handle"
- t.string "public_key"
- t.integer "counter"
- t.integer "user_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "name"
+ t.text "certificate"
+ t.string "key_handle"
+ t.string "public_key"
+ t.integer "counter"
+ t.integer "user_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "name"
end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
create_table "user_agent_details", force: :cascade do |t|
- t.string "user_agent", null: false
- t.string "ip_address", null: false
- t.integer "subject_id", null: false
- t.string "subject_type", null: false
- t.boolean "submitted", default: false, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.string "user_agent", null: false
+ t.string "ip_address", null: false
+ t.integer "subject_id", null: false
+ t.string "subject_type", null: false
+ t.boolean "submitted", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
+ t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0
+ t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
- t.string "last_sign_in_ip"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "name"
- t.boolean "admin", default: false, null: false
- t.integer "projects_limit", default: 10
- t.string "skype", default: "", null: false
- t.string "linkedin", default: "", null: false
- t.string "twitter", default: "", null: false
- t.string "authentication_token"
- t.integer "theme_id", default: 1, null: false
- t.string "bio"
- t.integer "failed_attempts", default: 0
+ t.string "name"
+ t.boolean "admin", default: false, null: false
+ t.integer "projects_limit", default: 10
+ t.string "skype", default: "", null: false
+ t.string "linkedin", default: "", null: false
+ t.string "twitter", default: "", null: false
+ t.string "authentication_token"
+ t.integer "theme_id", default: 1, null: false
+ t.string "bio"
+ t.integer "failed_attempts", default: 0
t.datetime "locked_at"
- t.string "username"
- t.boolean "can_create_group", default: true, null: false
- t.boolean "can_create_team", default: true, null: false
- t.string "state"
- t.integer "color_scheme_id", default: 1, null: false
+ t.string "username"
+ t.boolean "can_create_group", default: true, null: false
+ t.boolean "can_create_team", default: true, null: false
+ t.string "state"
+ t.integer "color_scheme_id", default: 1, null: false
t.datetime "password_expires_at"
- t.integer "created_by_id"
+ t.integer "created_by_id"
t.datetime "last_credential_check_at"
- t.string "avatar"
- t.string "confirmation_token"
+ t.string "avatar"
+ t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
- t.string "unconfirmed_email"
- t.boolean "hide_no_ssh_key", default: false
- t.string "website_url", default: "", null: false
- t.string "notification_email"
- t.boolean "hide_no_password", default: false
- t.boolean "password_automatically_set", default: false
- t.string "location"
- t.string "encrypted_otp_secret"
- t.string "encrypted_otp_secret_iv"
- t.string "encrypted_otp_secret_salt"
- t.boolean "otp_required_for_login", default: false, null: false
- t.text "otp_backup_codes"
- t.string "public_email", default: "", null: false
- t.integer "dashboard", default: 0
- t.integer "project_view", default: 0
- t.integer "consumed_timestep"
- t.integer "layout", default: 0
- t.boolean "hide_project_limit", default: false
- t.string "unlock_token"
+ t.string "unconfirmed_email"
+ t.boolean "hide_no_ssh_key", default: false
+ t.string "website_url", default: "", null: false
+ t.string "notification_email"
+ t.boolean "hide_no_password", default: false
+ t.boolean "password_automatically_set", default: false
+ t.string "location"
+ t.string "encrypted_otp_secret"
+ t.string "encrypted_otp_secret_iv"
+ t.string "encrypted_otp_secret_salt"
+ t.boolean "otp_required_for_login", default: false, null: false
+ t.text "otp_backup_codes"
+ t.string "public_email", default: "", null: false
+ t.integer "dashboard", default: 0
+ t.integer "project_view", default: 0
+ t.integer "consumed_timestep"
+ t.integer "layout", default: 0
+ t.boolean "hide_project_limit", default: false
+ t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
- t.boolean "ldap_email", default: false, null: false
- t.boolean "external", default: false
- t.string "organization"
+ t.boolean "ldap_email", default: false, null: false
+ t.boolean "external", default: false
+ t.string "organization"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1170,8 +1176,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
create_table "users_star_projects", force: :cascade do |t|
- t.integer "project_id", null: false
- t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.integer "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -1181,23 +1187,23 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
- t.string "url", limit: 2000
- t.integer "project_id"
+ t.string "url", limit: 2000
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
- t.integer "service_id"
- t.boolean "push_events", default: true, null: false
- t.boolean "issues_events", default: false, null: false
- t.boolean "merge_requests_events", default: false, null: false
- t.boolean "tag_push_events", default: false
- t.boolean "note_events", default: false, null: false
- t.boolean "enable_ssl_verification", default: true
- t.boolean "build_events", default: false, null: false
- t.boolean "wiki_page_events", default: false, null: false
- t.string "token"
- t.boolean "pipeline_events", default: false, null: false
- t.boolean "confidential_issues_events", default: false, null: false
+ t.string "type", default: "ProjectHook"
+ t.integer "service_id"
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ t.boolean "note_events", default: false, null: false
+ t.boolean "enable_ssl_verification", default: true
+ t.boolean "build_events", default: false, null: false
+ t.boolean "wiki_page_events", default: false, null: false
+ t.string "token"
+ t.boolean "pipeline_events", default: false, null: false
+ t.boolean "confidential_issues_events", default: false, null: false
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
@@ -1212,5 +1218,6 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
+ add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index 9017b143260..c30bf328003 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -7,7 +7,7 @@
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
-- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
@@ -20,6 +20,7 @@
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
- [University](university/README.md) Learn Git and GitLab through videos and courses.
+- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
## Administrator documentation
@@ -35,7 +36,7 @@
- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
-- [Operations](operations/README.md) Keeping GitLab up and running.
+- [Operations](administration/operations.md) Keeping GitLab up and running.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
- [Repository storages](administration/repository_storages.md) Manage the paths used to store repositories.
@@ -43,12 +44,13 @@
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
-- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
+- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
-- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint.
+- [GitLab Performance Monitoring](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
+- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 8a881ce8863..137fed35a73 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -101,9 +101,9 @@ need some additional configuration.
```ruby
gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860'
- gitlab_rails['secret_token'] = 'b719fe119132c7810908bba18315259ed12888d4f5ee5430c42a776d840a396799b0a5ef0a801348c8a357f07aa72bbd58e25a84b8f247a25c72f539c7a6c5fa'
- gitlab_ci['secret_key_base'] = '6e657410d57c71b4fc3ed0d694e7842b1895a8b401d812c17fe61caf95b48a6d703cb53c112bc01ebd197a85da81b18e29682040e99b4f26594772a4a2c98c6d'
- gitlab_ci['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964'
+ gitlab_rails['otp_key_base'] = 'b719fe119132c7810908bba18315259ed12888d4f5ee5430c42a776d840a396799b0a5ef0a801348c8a357f07aa72bbd58e25a84b8f247a25c72f539c7a6c5fa'
+ gitlab_rails['secret_key_base'] = '6e657410d57c71b4fc3ed0d694e7842b1895a8b401d812c17fe61caf95b48a6d703cb53c112bc01ebd197a85da81b18e29682040e99b4f26594772a4a2c98c6d'
+ gitlab_rails['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964'
```
1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
diff --git a/doc/administration/monitoring/performance/gitlab_configuration.md b/doc/administration/monitoring/performance/gitlab_configuration.md
new file mode 100644
index 00000000000..771584268d9
--- /dev/null
+++ b/doc/administration/monitoring/performance/gitlab_configuration.md
@@ -0,0 +1,40 @@
+# GitLab Configuration
+
+GitLab Performance Monitoring is disabled by default. To enable it and change any of its
+settings, navigate to the Admin area in **Settings > Metrics**
+(`/admin/application_settings`).
+
+The minimum required settings you need to set are the InfluxDB host and port.
+Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
+changes.
+
+---
+
+![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
+
+---
+
+Finally, a restart of all GitLab processes is required for the changes to take
+effect:
+
+```bash
+# For Omnibus installations
+sudo gitlab-ctl restart
+
+# For installations from source
+sudo service gitlab restart
+```
+
+## Pending Migrations
+
+When any migrations are pending, the metrics are disabled until the migrations
+have been performed.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
new file mode 100644
index 00000000000..7947b0fedc4
--- /dev/null
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -0,0 +1,111 @@
+# Grafana Configuration
+
+[Grafana](http://grafana.org/) is a tool that allows you to visualize time
+series metrics through graphs and dashboards. It supports several backend
+data stores, including InfluxDB. GitLab writes performance data to InfluxDB
+and Grafana will allow you to query InfluxDB to display useful graphs.
+
+For the easiest installation and configuration, install Grafana on the same
+server as InfluxDB. For larger installations, you may want to split out these
+services.
+
+## Installation
+
+Grafana supplies package repositories (Yum/Apt) for easy installation.
+See [Grafana installation documentation](http://docs.grafana.org/installation/)
+for detailed steps.
+
+> **Note**: Before starting Grafana for the first time, set the admin user
+and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
+will be `admin`.
+
+## Configuration
+
+Login as the admin user. Expand the menu by clicking the Grafana logo in the
+top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
+in the top bar.
+
+![Grafana empty data source page](img/grafana_data_source_empty.png)
+
+Fill in the configuration details for the InfluxDB data source. Save and
+Test Connection to ensure the configuration is correct.
+
+- **Name**: InfluxDB
+- **Default**: Checked
+- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
+- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
+on a separate server)
+- **Access**: proxy
+- **Database**: gitlab
+- **User**: admin (Or the username configured when setting up InfluxDB)
+- **Password**: The password configured when you set up InfluxDB
+
+![Grafana data source configurations](img/grafana_data_source_configuration.png)
+
+## Apply retention policies and create continuous queries
+
+If you intend to import the GitLab provided Grafana dashboards, you will need to
+set up the right retention policies and continuous queries. The easiest way of
+doing this is by using the [influxdb-management](https://gitlab.com/gitlab-org/influxdb-management)
+repository.
+
+To use this repository you must first clone it:
+
+```
+git clone https://gitlab.com/gitlab-org/influxdb-management.git
+cd influxdb-management
+```
+
+Next you must install the required dependencies:
+
+```
+gem install bundler
+bundle install
+```
+
+Now you must configure the repository by first copying `.env.example` to `.env`
+and then editing the `.env` file to contain the correct InfluxDB settings. Once
+configured you can simply run `bundle exec rake` and the InfluxDB database will
+be configured for you.
+
+For more information see the [influxdb-management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
+
+## Import Dashboards
+
+You can now import a set of default dashboards that will give you a good
+start on displaying useful information. GitLab has published a set of default
+[Grafana dashboards][grafana-dashboards] to get you started. Clone the
+repository or download a zip/tarball, then follow these steps to import each
+JSON file.
+
+Open the dashboard dropdown menu and click 'Import'
+
+![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png)
+
+Click 'Choose file' and browse to the location where you downloaded or cloned
+the dashboard repository. Pick one of the JSON files to import.
+
+![Grafana dashboard import](img/grafana_dashboard_import.png)
+
+Once the dashboard is imported, be sure to click save icon in the top bar. If
+you do not save the dashboard after importing it will be removed when you
+navigate away.
+
+![Grafana save icon](img/grafana_save_icon.png)
+
+Repeat this process for each dashboard you wish to import.
+
+Alternatively you can automatically import all the dashboards into your Grafana
+instance. See the README of the [Grafana dashboards][grafana-dashboards]
+repository for more information on this process.
+
+[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Installation/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
new file mode 100644
index 00000000000..7e34fad71ce
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_import.png b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
new file mode 100644
index 00000000000..f97624365c7
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
new file mode 100644
index 00000000000..7d50e4c88c2
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_empty.png b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
new file mode 100644
index 00000000000..aa39a53acae
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_save_icon.png b/doc/administration/monitoring/performance/img/grafana_save_icon.png
new file mode 100644
index 00000000000..c740e33cd1c
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/grafana_save_icon.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
new file mode 100644
index 00000000000..db396423e30
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/request_profile_result.png b/doc/administration/monitoring/performance/img/request_profile_result.png
new file mode 100644
index 00000000000..73e2fdcab67
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/request_profile_result.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/request_profiling_token.png b/doc/administration/monitoring/performance/img/request_profiling_token.png
new file mode 100644
index 00000000000..04d87567816
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/request_profiling_token.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/influxdb_configuration.md b/doc/administration/monitoring/performance/influxdb_configuration.md
new file mode 100644
index 00000000000..c30cd2950d8
--- /dev/null
+++ b/doc/administration/monitoring/performance/influxdb_configuration.md
@@ -0,0 +1,193 @@
+# InfluxDB Configuration
+
+The default settings provided by [InfluxDB] are not sufficient for a high traffic
+GitLab environment. The settings discussed in this document are based on the
+settings GitLab uses for GitLab.com, depending on your own needs you may need to
+further adjust them.
+
+If you are intending to run InfluxDB on the same server as GitLab, make sure
+you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
+
+Unless you are going with a budget setup, it's advised to run it separately.
+
+## Requirements
+
+- InfluxDB 0.9.5 or newer
+- A fairly modern version of Linux
+- At least 4GB of RAM
+- At least 10GB of storage for InfluxDB data
+
+Note that the RAM and storage requirements can differ greatly depending on the
+amount of data received/stored. To limit the amount of stored data users can
+look into [InfluxDB Retention Policies][influxdb-retention].
+
+## Installation
+
+Installing InfluxDB is out of the scope of this document. Please refer to the
+[InfluxDB documentation].
+
+## InfluxDB Server Settings
+
+Since InfluxDB has many settings that users may wish to customize themselves
+(e.g. what port to run InfluxDB on), we'll only cover the essentials.
+
+The configuration file in question is usually located at
+`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
+InfluxDB needs to be restarted.
+
+### Storage Engine
+
+InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
+storage engine is available, called [TSM Tree]. All users **must** use the new
+`tsm1` storage engine as this [will be the default engine][tsm1-commit] in
+upcoming InfluxDB releases.
+
+Make sure you have the following in your configuration file:
+
+```
+[data]
+ dir = "/var/lib/influxdb/data"
+ engine = "tsm1"
+```
+
+### Admin Panel
+
+Production environments should have the InfluxDB admin panel **disabled**. This
+feature can be disabled by adding the following to your InfluxDB configuration
+file:
+
+```
+[admin]
+ enabled = false
+```
+
+### HTTP
+
+HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana,
+thus it should be enabled. When enabling make sure to _also_ enable
+authentication:
+
+```
+[http]
+ enabled = true
+ auth-enabled = true
+```
+
+_**Note:** Before you enable authentication, you might want to [create an
+admin user](#create-a-new-admin-user)._
+
+### UDP
+
+GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
+UDP can be done using the following settings:
+
+```
+[[udp]]
+ enabled = true
+ bind-address = ":8089"
+ database = "gitlab"
+ batch-size = 1000
+ batch-pending = 5
+ batch-timeout = "1s"
+ read-buffer = 209715200
+```
+
+This does the following:
+
+1. Enable UDP and bind it to port 8089 for all addresses.
+2. Store any data received in the "gitlab" database.
+3. Define a batch of points to be 1000 points in size and allow a maximum of
+ 5 batches _or_ flush them automatically after 1 second.
+4. Define a UDP read buffer size of 200 MB.
+
+One of the most important settings here is the UDP read buffer size as if this
+value is set too low, packets will be dropped. You must also make sure the OS
+buffer size is set to the same value, the default value is almost never enough.
+
+To set the OS buffer size to 200 MB, on Linux you can run the following command:
+
+```bash
+sysctl -w net.core.rmem_max=209715200
+```
+
+To make this permanent, add the following to `/etc/sysctl.conf` and restart the
+server:
+
+```bash
+net.core.rmem_max=209715200
+```
+
+It is **very important** to make sure the buffer sizes are large enough to
+handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
+buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
+traffic, users may be able to use a smaller buffer size, but we highly recommend
+using _at least_ 100 MB.
+
+When enabling UDP, users should take care to not expose the port to the public,
+as doing so will allow anybody to write data into your InfluxDB database (as
+[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either
+whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
+allowing traffic from members of said VLAN.
+
+## Create a new admin user
+
+If you want to [enable authentication](#http), you might want to [create an
+admin user][influx-admin]:
+
+```
+influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
+```
+
+## Create the `gitlab` database
+
+Once you get InfluxDB up and running, you need to create a database for GitLab.
+Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
+before creating a database.
+
+_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
+[HTTP authentication](#http), remember to append the username (`-username <username>`)
+and password (`-password <password>`) you set earlier to the commands below._
+
+Run the following command to create a database named `gitlab`:
+
+```bash
+influx -execute 'CREATE DATABASE gitlab'
+```
+
+The name **must** be `gitlab`, do not use any other name.
+
+Next, make sure that the database was successfully created:
+
+```bash
+influx -execute 'SHOW DATABASES'
+```
+
+The output should be similar to:
+
+```
+name: databases
+---------------
+name
+_internal
+gitlab
+```
+
+That's it! Now your GitLab instance should send data to InfluxDB.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
+
+[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
+[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
+[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/
+[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/
+[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d
+[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user
diff --git a/doc/administration/monitoring/performance/influxdb_schema.md b/doc/administration/monitoring/performance/influxdb_schema.md
new file mode 100644
index 00000000000..eff0e29f58d
--- /dev/null
+++ b/doc/administration/monitoring/performance/influxdb_schema.md
@@ -0,0 +1,97 @@
+# InfluxDB Schema
+
+The following measurements are currently stored in InfluxDB:
+
+- `PROCESS_file_descriptors`
+- `PROCESS_gc_statistics`
+- `PROCESS_memory_usage`
+- `PROCESS_method_calls`
+- `PROCESS_object_counts`
+- `PROCESS_transactions`
+- `PROCESS_views`
+- `events`
+
+Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
+process type. In all series, any form of duration is stored in milliseconds.
+
+## PROCESS_file_descriptors
+
+This measurement contains the number of open file descriptors over time. The
+value field `value` contains the number of descriptors.
+
+## PROCESS_gc_statistics
+
+This measurement contains Ruby garbage collection statistics such as the amount
+of minor/major GC runs (relative to the last sampling interval), the time spent
+in garbage collection cycles, and all fields/values returned by `GC.stat`.
+
+## PROCESS_memory_usage
+
+This measurement contains the process' memory usage (in bytes) over time. The
+value field `value` contains the number of bytes.
+
+## PROCESS_method_calls
+
+This measurement contains the methods called during a transaction along with
+their duration, and a name of the transaction action that invoked the method (if
+available). The method call duration is stored in the value field `duration`,
+while the method name is stored in the tag `method`. The tag `action` contains
+the full name of the transaction action. Both the `method` and `action` fields
+are in the following format:
+
+```
+ClassName#method_name
+```
+
+For example, a method called by the `show` method in the `UsersController` class
+would have `action` set to `UsersController#show`.
+
+## PROCESS_object_counts
+
+This measurement is used to store retained Ruby objects (per class) and the
+amount of retained objects. The number of objects is stored in the `count` value
+field while the class name is stored in the `type` tag.
+
+## PROCESS_transactions
+
+This measurement is used to store basic transaction details such as the time it
+took to complete a transaction, how much time was spent in SQL queries, etc. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration` | The total duration of the transaction |
+| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
+| `method_duration` | The total time spent in method calls |
+| `sql_duration` | The total time spent in SQL queries |
+| `view_duration` | The total time spent in views |
+
+## PROCESS_views
+
+This measurement is used to store view rendering timings for a transaction. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration` | The rendering time of the view |
+| `view` | The path of the view, relative to the application's root directory |
+
+The `action` tag contains the action name of the transaction that rendered the
+view.
+
+## events
+
+This measurement is used to store generic events such as the number of Git
+pushes, Emails sent, etc. Each point in this measurement has a single value
+field called `count`. The value of this field is simply set to `1`. Each point
+also has at least one tag: `event`. This tag's value is set to the event name.
+Depending on the event type additional tags may be available as well.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
new file mode 100644
index 00000000000..79904916b7e
--- /dev/null
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -0,0 +1,65 @@
+# GitLab Performance Monitoring
+
+GitLab comes with its own application performance measuring system as of GitLab
+8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
+Community and Enterprise editions.
+
+Apart from this introduction, you are advised to read through the following
+documents in order to understand and properly configure GitLab Performance Monitoring:
+
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Install/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
+
+## Introduction to GitLab Performance Monitoring
+
+GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
+including (but not limited to):
+
+- The time it took to complete a transaction (a web request or Sidekiq job).
+- The time spent in running SQL queries and rendering HAML views.
+- The time spent executing (instrumented) Ruby methods.
+- Ruby object allocations, and retained objects in particular.
+- System statistics such as the process' memory usage and open file descriptors.
+- Ruby garbage collection statistics.
+
+Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
+data can be visualized using [Grafana][grafana] or any other application that
+supports reading data from InfluxDB. Alternatively data can be queried using the
+InfluxDB CLI.
+
+## Metric Types
+
+Two types of metrics are collected:
+
+1. Transaction specific metrics.
+1. Sampled metrics, collected at a certain interval in a separate thread.
+
+### Transaction Metrics
+
+Transaction metrics are metrics that can be associated with a single
+transaction. This includes statistics such as the transaction duration, timings
+of any executed SQL queries, time spent rendering HAML views, etc. These metrics
+are collected for every Rack request and Sidekiq job processed.
+
+### Sampled Metrics
+
+Sampled metrics are metrics that can't be associated with a single transaction.
+Examples include garbage collection statistics and retained Ruby objects. These
+metrics are collected at a regular interval. This interval is made up out of two
+parts:
+
+1. A user defined interval.
+1. A randomly generated offset added on top of the interval, the same offset
+ can't be used twice in a row.
+
+The actual interval can be anywhere between a half of the defined interval and a
+half above the interval. For example, for a user defined interval of 15 seconds
+the actual interval can be anywhere between 7.5 and 22.5. The interval is
+re-generated for every sampling run instead of being generated once and re-used
+for the duration of the process' lifetime.
+
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[grafana]: http://grafana.org/
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
new file mode 100644
index 00000000000..c358dfbead2
--- /dev/null
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -0,0 +1,16 @@
+# Request Profiling
+
+## Procedure
+1. Grab the profiling token from `Monitoring > Requests Profiles` admin page
+(highlighted in a blue in the image below).
+![Profile token](img/request_profiling_token.png)
+1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use any of these tools
+ * [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension
+ * [Modify Headers](https://addons.mozilla.org/en-US/firefox/addon/modify-headers/) Firefox extension
+ * `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project`
+1. Once request is finished (which will take a little longer than usual), you can
+view the profiling output from `Monitoring > Requests Profiles` admin page.
+![Profiling output](img/request_profile_result.png)
+
+## Cleaning up
+Profiling output will be cleared out every day via a Sidekiq worker.
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
new file mode 100644
index 00000000000..4b582d16b64
--- /dev/null
+++ b/doc/administration/operations.md
@@ -0,0 +1,6 @@
+# GitLab operations
+
+- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
+- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
+- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
+- [Moving repositories to a new location](operations/moving_repositories.md)
diff --git a/doc/administration/operations/cleaning_up_redis_sessions.md b/doc/administration/operations/cleaning_up_redis_sessions.md
new file mode 100644
index 00000000000..93521e976d5
--- /dev/null
+++ b/doc/administration/operations/cleaning_up_redis_sessions.md
@@ -0,0 +1,52 @@
+# Cleaning up stale Redis sessions
+
+Since version 6.2, GitLab stores web user sessions as key-value pairs in Redis.
+Prior to GitLab 7.3, user sessions did not automatically expire from Redis. If
+you have been running a large GitLab server (thousands of users) since before
+GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
+database after you upgrade to GitLab 7.3. You can also perform a cleanup while
+still running GitLab 7.2 or older, but in that case new stale sessions will
+start building up again after you clean up.
+
+In GitLab versions prior to 7.3.0, the session keys in Redis are 16-byte
+hexadecimal values such as '976aa289e2189b17d7ef525a6702ace9'. Starting with
+GitLab 7.3.0, the keys are
+prefixed with 'session:gitlab:', so they would look like
+'session:gitlab:976aa289e2189b17d7ef525a6702ace9'. Below we describe how to
+remove the keys in the old format.
+
+First we define a shell function with the proper Redis connection details.
+
+```
+rcli() {
+ # This example works for Omnibus installations of GitLab 7.3 or newer. For an
+ # installation from source you will have to change the socket path and the
+ # path to redis-cli.
+ sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@"
+}
+
+# test the new shell function; the response should be PONG
+rcli ping
+```
+
+Now we do a search to see if there are any session keys in the old format for
+us to clean up.
+
+```
+# returns the number of old-format session keys in Redis
+rcli keys '*' | grep '^[a-f0-9]\{32\}$' | wc -l
+```
+
+If the number is larger than zero, you can proceed to expire the keys from
+Redis. If the number is zero there is nothing to clean up.
+
+```
+# Tell Redis to expire each matched key after 600 seconds.
+rcli keys '*' | grep '^[a-f0-9]\{32\}$' | awk '{ print "expire", $0, 600 }' | rcli
+# This will print '(integer) 1' for each key that gets expired.
+```
+
+Over the next 15 minutes (10 minutes expiry time plus 5 minutes Redis
+background save interval) your Redis database will be compacted. If you are
+still using GitLab 7.2, users who are not clicking around in GitLab during the
+10 minute expiry window will be signed out of GitLab.
diff --git a/doc/administration/operations/moving_repositories.md b/doc/administration/operations/moving_repositories.md
new file mode 100644
index 00000000000..54adb99386a
--- /dev/null
+++ b/doc/administration/operations/moving_repositories.md
@@ -0,0 +1,180 @@
+# Moving repositories managed by GitLab
+
+Sometimes you need to move all repositories managed by GitLab to
+another filesystem or another server. In this document we will look
+at some of the ways you can copy all your repositories from
+`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`.
+
+We will look at three scenarios: the target directory is empty, the
+target directory contains an outdated copy of the repositories, and
+how to deal with thousands of repositories.
+
+**Each of the approaches we list can/will overwrite data in the
+target directory `/mnt/gitlab/repositories`. Do not mix up the
+source and the target.**
+
+## Target directory is empty: use a tar pipe
+
+If the target directory `/mnt/gitlab/repositories` is empty the
+simplest thing to do is to use a tar pipe. This method has low
+overhead and tar is almost always already installed on your system.
+However, it is not possible to resume an interrupted tar pipe: if
+that happens then all data must be copied again.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+ tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to see progress, replace `-xf` with `-xvf`.
+
+### Tar pipe to another server
+
+You can also use a tar pipe to copy data to another server. If your
+'git' user has SSH access to the newserver as 'git@newserver', you
+can pipe the data through SSH.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+ ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to compress the data before it goes over the network
+(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`.
+
+## The target directory contains an outdated copy of the repositories: use rsync
+
+If the target directory already contains a partial / outdated copy
+of the repositories it may be wasteful to copy all the data again
+with tar. In this scenario it is better to use rsync. This utility
+is either already installed on your system or easily installable
+via apt, yum etc.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+ /mnt/gitlab/repositories
+```
+
+The `/.` in the command above is very important, without it you can
+easily get the wrong directory structure in the target directory.
+If you want to see progress, replace `-a` with `-av`.
+
+### Single rsync to another server
+
+If the 'git' user on your source system has SSH access to the target
+server you can send the repositories over the network with rsync.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+ git@newserver:/mnt/gitlab/repositories
+```
+
+## Thousands of Git repositories: use one rsync per repository
+
+Every time you start an rsync job it has to inspect all files in
+the source directory, all files in the target directory, and then
+decide what files to copy or not. If the source or target directory
+has many contents this startup phase of rsync can become a burden
+for your GitLab server. In cases like this you can make rsync's
+life easier by dividing its work in smaller pieces, and sync one
+repository at a time.
+
+In addition to rsync we will use [GNU
+Parallel](http://www.gnu.org/software/parallel/). This utility is
+not included in GitLab so you need to install it yourself with apt
+or yum. Also note that the GitLab scripts we used below were added
+in GitLab 8.1.
+
+** This process does not clean up repositories at the target location that no
+longer exist at the source. ** If you start using your GitLab instance with
+`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos`
+after switching to the new repository storage directory.
+
+### Parallel rsync for all repositories known to GitLab
+
+This will sync repositories with 10 rsync processes at a time. We keep
+track of progress so that the transfer can be restarted if necessary.
+
+First we create a new directory, owned by 'git', to hold transfer
+logs. We assume the directory is empty before we start the transfer
+procedure, and that we are the only ones writing files in it.
+
+```
+# Omnibus
+sudo mkdir /var/opt/gitlab/transfer-logs
+sudo chown git:git /var/opt/gitlab/transfer-logs
+
+# Source
+sudo -u git -H mkdir /home/git/transfer-logs
+```
+
+We seed the process with a list of the directories we want to copy.
+
+```
+# Omnibus
+sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt'
+```
+
+Now we can start the transfer. The command below is idempotent, and
+the number of jobs done by GNU Parallel should converge to zero. If it
+does not some repositories listed in all-repos-1234.txt may have been
+deleted/renamed before they could be copied.
+
+```
+# Omnibus
+sudo -u git sh -c '
+cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\
+ /usr/bin/env JOBS=10 \
+ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
+ /var/opt/gitlab/transfer-logs/success-$(date +%s).log \
+ /var/opt/gitlab/git-data/repositories \
+ /mnt/gitlab/repositories
+'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H sh -c '
+cat /home/git/transfer-logs/* | sort | uniq -u |\
+ /usr/bin/env JOBS=10 \
+ bin/parallel-rsync-repos \
+ /home/git/transfer-logs/success-$(date +%s).log \
+ /home/git/repositories \
+ /mnt/gitlab/repositories
+`
+```
+
+### Parallel rsync only for repositories with recent activity
+
+Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
+Then you might only want to sync repositories that were changed via GitLab
+_after_ that time. You can use the 'SINCE' variable to tell 'rake
+gitlab:list_repos' to only print repositories with recent activity.
+
+```
+# Omnibus
+sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
+ sudo -u git \
+ /usr/bin/env JOBS=10 \
+ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
+ success-$(date +%s).log \
+ /var/opt/gitlab/git-data/repositories \
+ /mnt/gitlab/repositories
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
+ sudo -u git -H \
+ /usr/bin/env JOBS=10 \
+ bin/parallel-rsync-repos \
+ success-$(date +%s).log \
+ /home/git/repositories \
+ /mnt/gitlab/repositories
+```
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
new file mode 100644
index 00000000000..b5e78348989
--- /dev/null
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -0,0 +1,40 @@
+# Sidekiq MemoryKiller
+
+The GitLab Rails application code suffers from memory leaks. For web requests
+this problem is made manageable using
+[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which
+restarts Unicorn worker processes in between requests when needed. The Sidekiq
+MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
+to process background jobs.
+
+Unlike unicorn-worker-killer, which is enabled by default for all GitLab
+installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default
+_only_ for Omnibus packages. The reason for this is that the MemoryKiller
+relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab
+installations from source do not all use Runit or an equivalent.
+
+With the default settings, the MemoryKiller will cause a Sidekiq restart no
+more often than once every 15 minutes, with the restart causing about one
+minute of delay for incoming background jobs.
+
+## Configuring the MemoryKiller
+
+The MemoryKiller is controlled using environment variables.
+
+- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is
+ greater than 0, then after each Sidekiq job, the MemoryKiller will check the
+ RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq
+ process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a
+ delayed shutdown is triggered. The default value for Omnibus packages is set
+ [in the omnibus-gitlab
+ repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
+- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
+ a shutdown is triggered, the Sidekiq process will keep working normally for
+ another 15 minutes.
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
+ time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs.
+ Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
+ Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
+ restart Sidekiq.
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
+ the final signal sent to the Sidekiq process when we want it to shut down.
diff --git a/doc/administration/operations/unicorn.md b/doc/administration/operations/unicorn.md
new file mode 100644
index 00000000000..bad61151bda
--- /dev/null
+++ b/doc/administration/operations/unicorn.md
@@ -0,0 +1,86 @@
+# Understanding Unicorn and unicorn-worker-killer
+
+## Unicorn
+
+GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web
+server, to handle web requests (web browsers and Git HTTP clients). Unicorn is
+a daemon written in Ruby and C that can load and run a Ruby on Rails
+application; in our case the Rails application is GitLab Community Edition or
+GitLab Enterprise Edition.
+
+Unicorn has a multi-process architecture to make better use of available CPU
+cores (processes can run on different cores) and to have stronger fault
+tolerance (most failures stay isolated in only one process and cannot take down
+GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby
+environment with the GitLab application code, and then spawns 'workers' which
+inherit this clean initial environment. The 'master' never handles any
+requests, that is left to the workers. The operating system network stack
+queues incoming requests and distributes them among the workers.
+
+In a perfect world, the master would spawn its pool of workers once, and then
+the workers handle incoming web requests one after another until the end of
+time. In reality, worker processes can crash or time out: if the master notices
+that a worker takes too long to handle a request it will terminate the worker
+process with SIGKILL ('kill -9'). No matter how the worker process ended, the
+master process will replace it with a new 'clean' process again. Unicorn is
+designed to be able to replace 'crashed' workers without dropping user
+requests.
+
+This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The
+master process has PID 56227 below.
+
+```
+[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing
+[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10
+[2015-06-05T10:58:08.708141 #62538] INFO -- : worker=10 spawned pid=62538
+[2015-06-05T10:58:08.708824 #62538] INFO -- : worker=10 ready
+```
+
+### Tunables
+
+The main tunables for Unicorn are the number of worker processes and the
+request timeout after which the Unicorn master terminates a worker process.
+See the [omnibus-gitlab Unicorn settings
+documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md)
+if you want to adjust these settings.
+
+## unicorn-worker-killer
+
+GitLab has memory leaks. These memory leaks manifest themselves in long-running
+processes, such as Unicorn workers. (The Unicorn master process is not known to
+leak memory, probably because it does not handle user requests.)
+
+To make these memory leaks manageable, GitLab comes with the
+[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
+gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
+workers to do a memory self-check after every 16 requests. If the memory of the
+Unicorn worker exceeds a pre-set limit then the worker process exits. The
+Unicorn master then automatically replaces the worker process.
+
+This is a robust way to handle memory leaks: Unicorn is designed to handle
+workers that 'crash' so no user requests will be dropped. The
+unicorn-worker-killer gem is designed to only terminate a worker process _in
+between requests_, so no user requests are affected.
+
+This is what a Unicorn worker memory restart looks like in unicorn_stderr.log.
+You see that worker 4 (PID 125918) is inspecting itself and decides to exit.
+The threshold memory value was 254802235 bytes, about 250MB. With GitLab this
+threshold is a random value between 200 and 250 MB. The master process (PID
+117565) then reaps the worker process and spawns a new 'worker 4' with PID
+127549.
+
+```
+[2015-06-05T12:07:41.828374 #125918] WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes)
+[2015-06-05T12:07:41.828472 #125918] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1)
+[2015-06-05T12:07:42.025916 #117565] INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4
+[2015-06-05T12:07:42.034527 #127549] INFO -- : worker=4 spawned pid=127549
+[2015-06-05T12:07:42.035217 #127549] INFO -- : worker=4 ready
+```
+
+One other thing that stands out in the log snippet above, taken from
+GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
+is a normal value for our current GitLab.com setup and traffic.
+
+The high frequency of Unicorn memory restarts on some GitLab sites can be a
+source of confusion for administrators. Usually they are a [red
+herring](https://en.wikipedia.org/wiki/Red_herring).
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
new file mode 100644
index 00000000000..5a9a1582877
--- /dev/null
+++ b/doc/administration/reply_by_email.md
@@ -0,0 +1,302 @@
+# Reply by email
+
+GitLab can be set up to allow users to comment on issues and merge requests by
+replying to notification emails.
+
+## Requirement
+
+Reply by email requires an IMAP-enabled email account. GitLab allows you to use
+three strategies for this feature:
+- using email sub-addressing
+- using a dedicated email address
+- using a catch-all mailbox
+
+### Email sub-addressing
+
+**If your provider or server supports email sub-addressing, we recommend using it.**
+
+[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
+a feature where any email to `user+some_arbitrary_tag@example.com` will end up
+in the mailbox for `user@example.com`, and is supported by providers such as
+Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
+mail server which you can run on-premises.
+
+### Dedicated email address
+
+This solution is really simple to set up: you just have to create an email
+address dedicated to receive your users' replies to GitLab notifications.
+
+### Catch-all mailbox
+
+A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
+"catch all" the emails addressed to the domain that do not exist in the mail
+server.
+
+## How it works?
+
+### 1. GitLab sends a notification email
+
+When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
+header is set to the address defined in your GitLab configuration, with the
+`%{key}` placeholder (if present) replaced by a specific "reply key". In
+addition, this "reply key" is also added to the `References` header.
+
+### 2. You reply to the notification email
+
+When you reply to the notification email, your email client will:
+
+- send the email to the `Reply-To` address it got from the notification email
+- set the `In-Reply-To` header to the value of the `Message-ID` header from the
+ notification email
+- set the `References` header to the value of the `Message-ID` plus the value of
+ the notification email's `References` header.
+
+### 3. GitLab receives your reply to the notification email
+
+When GitLab receives your reply, it will look for the "reply key" in the
+following headers, in this order:
+
+1. the `To` header
+1. the `References` header
+
+If it finds a reply key, it will be able to leave your reply as a comment on
+the entity the notification was about (issue, merge request, commit...).
+
+For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
+please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
+
+## Set it up
+
+If you want to use Gmail / Google Apps with Reply by email, make sure you have
+[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
+[these instructions](./postfix.md).
+
+### Omnibus package installations
+
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
+ feature and fill in the details for your specific IMAP server and email account:
+
+ ```ruby
+ # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "incoming"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "gitlab.example.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 143
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = false
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ ```
+
+ ```ruby
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "imap.gmail.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 993
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = true
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ ```
+
+1. Reconfigure GitLab and restart mailroom for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart mailroom
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo gitlab-rake gitlab:incoming_email:check
+ ```
+
+1. Reply by email should now be working.
+
+### Installations from source
+
+1. Go to the GitLab installation directory:
+
+ ```sh
+ cd /home/git/gitlab
+ ```
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
+ and fill in the details for your specific IMAP server and email account:
+
+ ```sh
+ sudo editor config/gitlab.yml
+ ```
+
+ ```yaml
+ # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "incoming+%{key}@gitlab.example.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "incoming"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "gitlab.example.com"
+ # IMAP server port
+ port: 143
+ # Whether the IMAP server uses SSL
+ ssl: false
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ ```
+
+ ```yaml
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ ```
+
+1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
+
+ ```sh
+ sudo mkdir -p /etc/default
+ echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
+ ```
+
+1. Restart GitLab:
+
+ ```sh
+ sudo service gitlab restart
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
+ ```
+
+1. Reply by email should now be working.
+
+### Development
+
+1. Go to the GitLab installation directory.
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
+
+ ```yaml
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ ```
+
+ As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
+
+1. Uncomment the `mail_room` line in your `Procfile`:
+
+ ```yaml
+ mail_room: bundle exec mail_room -q -c config/mail_room.yml
+ ```
+
+1. Restart GitLab:
+
+ ```sh
+ bundle exec foreman start
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
+ ```
+
+1. Reply by email should now be working.
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
new file mode 100644
index 00000000000..22f10489a6c
--- /dev/null
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -0,0 +1,324 @@
+# Set up Postfix for Reply by email
+
+This document will take you through the steps of setting up a basic Postfix mail
+server with IMAP authentication on Ubuntu, to be used with [Reply by email].
+
+The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
+
+## Configure your server firewall
+
+1. Open up port 25 on your server so that people can send email into the server over SMTP.
+2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
+
+## Install packages
+
+1. Install the `postfix` package if it is not installed already:
+
+ ```sh
+ sudo apt-get install postfix
+ ```
+
+ When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
+
+1. Install the `mailutils` package.
+
+ ```sh
+ sudo apt-get install mailutils
+ ```
+
+## Create user
+
+1. Create a user for incoming email.
+
+ ```sh
+ sudo useradd -m -s /bin/bash incoming
+ ```
+
+1. Set a password for this user.
+
+ ```sh
+ sudo passwd incoming
+ ```
+
+ Be sure not to forget this, you'll need it later.
+
+## Test the out-of-the-box setup
+
+1. Connect to the local SMTP server:
+
+ ```sh
+ telnet localhost 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 127.0.0.1...
+ Connected to localhost.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, verify that `postfix` is running:
+
+ ```sh
+ sudo postfix status
+ ```
+
+ If it is not, start it:
+
+ ```sh
+ sudo postfix start
+ ```
+
+1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo localhost
+ mail from: root@localhost
+ rcpt to: incoming@localhost
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ _**Note:** The `.` is a literal period on its own line._
+
+ _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
+ then your Postfix `my_network` configuration is not correct. The error will
+ say 'Temporary lookup failure'. See
+ [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
+
+1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/var/mail/incoming": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Configure Postfix to use Maildir-style mailboxes
+
+Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
+
+1. Configure Postfix to use Maildir-style mailboxes:
+
+ ```sh
+ sudo postconf -e "home_mailbox = Maildir/"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo /etc/init.d/postfix restart
+ ```
+
+1. Test the new setup:
+
+ 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
+ 1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ MAIL=/home/incoming/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/incoming/Maildir": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+ _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
+ version of `mail` doesn't support Maildir style mailboxes. Install
+ `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
+ try the above steps again, substituting `heirloom-mailx` for the `mail`
+ command._
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Install the Courier IMAP server
+
+1. Install the `courier-imap` package:
+
+ ```sh
+ sudo apt-get install courier-imap
+ ```
+
+## Configure Postfix to receive email from the internet
+
+1. Let Postfix know about the domains that it should consider local:
+
+ ```sh
+ sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
+ ```
+
+1. Let Postfix know about the IPs that it should consider part of the LAN:
+
+ We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
+
+ ```sh
+ sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
+ ```
+
+1. Configure Postfix to receive mail on all interfaces, which includes the internet:
+
+ ```sh
+ sudo postconf -e "inet_interfaces = all"
+ ```
+
+1. Configure Postfix to use the `+` delimiter for sub-addressing:
+
+ ```sh
+ sudo postconf -e "recipient_delimiter = +"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo service postfix restart
+ ```
+
+## Test the final setup
+
+1. Test SMTP under the new setup:
+
+ 1. Connect to the SMTP server:
+
+ ```sh
+ telnet gitlab.example.com 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to gitlab.example.com.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
+
+ 1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo gitlab.example.com
+ mail from: root@gitlab.example.com
+ rcpt to: incoming@gitlab.example.com
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ (Note: The `.` is a literal period on its own line)
+
+ 1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ MAIL=/home/incoming/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/incoming/Maildir": 1 message 1 unread
+ >U 1 root@gitlab.example.com 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+ 1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+1. Test IMAP under the new setup:
+
+ 1. Connect to the IMAP server:
+
+ ```sh
+ telnet gitlab.example.com 143
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to mail.example.gitlab.com.
+ Escape character is '^]'.
+ - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information.
+ ```
+
+ 1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt:
+
+ ```
+ a login incoming PASSWORD
+ ```
+
+ Replace PASSWORD with the password you set on the `incoming` user earlier.
+
+ You should see output like this:
+
+ ```
+ a OK LOGIN Ok.
+ ```
+
+ 1. Disconnect from the IMAP server:
+
+ ```sh
+ a logout
+ ```
+
+## Done!
+
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+
+---
+
+_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
+
+[reply by email]: reply_by_email.md
diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md
index 483060395dd..b561c2f82aa 100644
--- a/doc/administration/restart_gitlab.md
+++ b/doc/administration/restart_gitlab.md
@@ -139,7 +139,7 @@ If you are using other init systems, like systemd, you can check the
[omnibus-dl]: https://about.gitlab.com/downloads/ "Download the Omnibus packages"
[install]: ../install/installation.md "Documentation to install GitLab from source"
-[mailroom]: ../incoming_email/README.md "Used for replying by email in GitLab issues and merge requests"
+[mailroom]: reply_by_email.md "Used for replying by email in GitLab issues and merge requests"
[chef]: https://www.chef.io/chef/ "Chef official website"
[src-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab "GitLab init service file"
[gl-recipes]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/init "GitLab Recipes repository"
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index d127d7b85e5..d8dce4388e1 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -144,14 +144,14 @@ separate Rails process to debug the issue:
1. Obtain the private token for your user (Profile Settings -> Account).
1. Bring up the GitLab Rails console. For omnibus users, run:
- ````
+ ```
sudo gitlab-rails console
```
1. At the Rails console, run:
```ruby
- [1] pry(main)> app.get '<URL FROM STEP 1>/private_token?<TOKEN FROM STEP 2>'
+ [1] pry(main)> app.get '<URL FROM STEP 2>/?private_token=<TOKEN FROM STEP 3>'
```
For example:
diff --git a/doc/api/README.md b/doc/api/README.md
index bbd5bcfb386..3fbe5197a21 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -17,6 +17,8 @@ following locations:
- [Commits](commits.md)
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
+- [Gitignores templates](templates/gitignores.md)
+- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)
- [Group Access Requests](access_requests.md)
- [Group Members](members.md)
@@ -25,7 +27,7 @@ following locations:
- [Labels](labels.md)
- [Merge Requests](merge_requests.md)
- [Milestones](milestones.md)
-- [Open source license templates](licenses.md)
+- [Open source license templates](templates/licenses.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md)
@@ -46,6 +48,7 @@ following locations:
- [Todos](todos.md)
- [Users](users.md)
- [Validate CI configuration](ci/lint.md)
+- [Version](version.md)
### Internal CI API
@@ -355,6 +358,19 @@ follows:
}
```
+## Unknown route
+
+When you try to access an API URL that does not exist you will receive 404 Not Found.
+
+```
+HTTP/1.1 404 Not Found
+Content-Type: application/json
+{
+ "error": "404 Not Found"
+}
+```
+
+
## Clients
There are many unofficial GitLab API Clients for most of the popular
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index c464e3f3f71..06111f4ab67 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -43,7 +43,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-15T10:09:34.206Z",
"updated_at": "2016-06-15T10:09:34.206Z",
@@ -59,7 +59,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z",
@@ -103,7 +103,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z",
@@ -146,7 +146,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z",
@@ -190,7 +190,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z",
@@ -238,7 +238,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z",
@@ -279,7 +279,7 @@ Example Response:
"id": 26,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/user4"
+ "web_url": "http://gitlab.example.com/user4"
},
"created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z",
@@ -319,7 +319,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z",
@@ -362,7 +362,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/u/root"
+ "web_url": "http://gitlab.example.com/root"
},
"created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z",
diff --git a/doc/api/builds.md b/doc/api/builds.md
index e8a9e4743d3..e40f198696d 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -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": ""
}
}
@@ -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..6e0882a94de 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",
@@ -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 27436a052da..b7791b4748a 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -436,7 +436,7 @@ Parameters:
### Get project events
Get the events for the specified project.
-Sorted from newest to latest
+Sorted from newest to oldest
```
GET /projects/:id/events
@@ -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"
}
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
new file mode 100644
index 00000000000..8235be92b12
--- /dev/null
+++ b/doc/api/templates/gitignores.md
@@ -0,0 +1,579 @@
+# Gitignores
+
+## List gitignore templates
+
+Get all gitignore templates.
+
+```
+GET /templates/gitignores
+```
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitignores
+```
+
+Example response:
+
+```json
+[
+ {
+ "name": "AppEngine"
+ },
+ {
+ "name": "Laravel"
+ },
+ {
+ "name": "Elisp"
+ },
+ {
+ "name": "SketchUp"
+ },
+ {
+ "name": "Ada"
+ },
+ {
+ "name": "Ruby"
+ },
+ {
+ "name": "Kohana"
+ },
+ {
+ "name": "Nanoc"
+ },
+ {
+ "name": "Erlang"
+ },
+ {
+ "name": "OCaml"
+ },
+ {
+ "name": "Lithium"
+ },
+ {
+ "name": "Fortran"
+ },
+ {
+ "name": "Scala"
+ },
+ {
+ "name": "Node"
+ },
+ {
+ "name": "Fancy"
+ },
+ {
+ "name": "Perl"
+ },
+ {
+ "name": "Zephir"
+ },
+ {
+ "name": "WordPress"
+ },
+ {
+ "name": "Symfony"
+ },
+ {
+ "name": "FuelPHP"
+ },
+ {
+ "name": "DM"
+ },
+ {
+ "name": "Sdcc"
+ },
+ {
+ "name": "Rust"
+ },
+ {
+ "name": "C"
+ },
+ {
+ "name": "Umbraco"
+ },
+ {
+ "name": "Actionscript"
+ },
+ {
+ "name": "Android"
+ },
+ {
+ "name": "Grails"
+ },
+ {
+ "name": "Composer"
+ },
+ {
+ "name": "ExpressionEngine"
+ },
+ {
+ "name": "Gcov"
+ },
+ {
+ "name": "Qt"
+ },
+ {
+ "name": "Phalcon"
+ },
+ {
+ "name": "ArchLinuxPackages"
+ },
+ {
+ "name": "TeX"
+ },
+ {
+ "name": "SCons"
+ },
+ {
+ "name": "Lilypond"
+ },
+ {
+ "name": "CommonLisp"
+ },
+ {
+ "name": "Rails"
+ },
+ {
+ "name": "Mercury"
+ },
+ {
+ "name": "Magento"
+ },
+ {
+ "name": "ChefCookbook"
+ },
+ {
+ "name": "GitBook"
+ },
+ {
+ "name": "C++"
+ },
+ {
+ "name": "Eagle"
+ },
+ {
+ "name": "Go"
+ },
+ {
+ "name": "OpenCart"
+ },
+ {
+ "name": "Scheme"
+ },
+ {
+ "name": "Typo3"
+ },
+ {
+ "name": "SeamGen"
+ },
+ {
+ "name": "Swift"
+ },
+ {
+ "name": "Elm"
+ },
+ {
+ "name": "Unity"
+ },
+ {
+ "name": "Agda"
+ },
+ {
+ "name": "CUDA"
+ },
+ {
+ "name": "VVVV"
+ },
+ {
+ "name": "Finale"
+ },
+ {
+ "name": "LemonStand"
+ },
+ {
+ "name": "Textpattern"
+ },
+ {
+ "name": "Julia"
+ },
+ {
+ "name": "Packer"
+ },
+ {
+ "name": "Scrivener"
+ },
+ {
+ "name": "Dart"
+ },
+ {
+ "name": "Plone"
+ },
+ {
+ "name": "Jekyll"
+ },
+ {
+ "name": "Xojo"
+ },
+ {
+ "name": "LabVIEW"
+ },
+ {
+ "name": "Autotools"
+ },
+ {
+ "name": "KiCad"
+ },
+ {
+ "name": "Prestashop"
+ },
+ {
+ "name": "ROS"
+ },
+ {
+ "name": "Smalltalk"
+ },
+ {
+ "name": "GWT"
+ },
+ {
+ "name": "OracleForms"
+ },
+ {
+ "name": "SugarCRM"
+ },
+ {
+ "name": "Nim"
+ },
+ {
+ "name": "SymphonyCMS"
+ },
+ {
+ "name": "Maven"
+ },
+ {
+ "name": "CFWheels"
+ },
+ {
+ "name": "Python"
+ },
+ {
+ "name": "ZendFramework"
+ },
+ {
+ "name": "CakePHP"
+ },
+ {
+ "name": "Concrete5"
+ },
+ {
+ "name": "PlayFramework"
+ },
+ {
+ "name": "Terraform"
+ },
+ {
+ "name": "Elixir"
+ },
+ {
+ "name": "CMake"
+ },
+ {
+ "name": "Joomla"
+ },
+ {
+ "name": "Coq"
+ },
+ {
+ "name": "Delphi"
+ },
+ {
+ "name": "Haskell"
+ },
+ {
+ "name": "Yii"
+ },
+ {
+ "name": "Java"
+ },
+ {
+ "name": "UnrealEngine"
+ },
+ {
+ "name": "AppceleratorTitanium"
+ },
+ {
+ "name": "CraftCMS"
+ },
+ {
+ "name": "ForceDotCom"
+ },
+ {
+ "name": "ExtJs"
+ },
+ {
+ "name": "MetaProgrammingSystem"
+ },
+ {
+ "name": "D"
+ },
+ {
+ "name": "Objective-C"
+ },
+ {
+ "name": "RhodesRhomobile"
+ },
+ {
+ "name": "R"
+ },
+ {
+ "name": "EPiServer"
+ },
+ {
+ "name": "Yeoman"
+ },
+ {
+ "name": "VisualStudio"
+ },
+ {
+ "name": "Processing"
+ },
+ {
+ "name": "Leiningen"
+ },
+ {
+ "name": "Stella"
+ },
+ {
+ "name": "Opa"
+ },
+ {
+ "name": "Drupal"
+ },
+ {
+ "name": "TurboGears2"
+ },
+ {
+ "name": "Idris"
+ },
+ {
+ "name": "Jboss"
+ },
+ {
+ "name": "CodeIgniter"
+ },
+ {
+ "name": "Qooxdoo"
+ },
+ {
+ "name": "Waf"
+ },
+ {
+ "name": "Sass"
+ },
+ {
+ "name": "Lua"
+ },
+ {
+ "name": "Clojure"
+ },
+ {
+ "name": "IGORPro"
+ },
+ {
+ "name": "Gradle"
+ },
+ {
+ "name": "Archives"
+ },
+ {
+ "name": "SynopsysVCS"
+ },
+ {
+ "name": "Ninja"
+ },
+ {
+ "name": "Tags"
+ },
+ {
+ "name": "OSX"
+ },
+ {
+ "name": "Dreamweaver"
+ },
+ {
+ "name": "CodeKit"
+ },
+ {
+ "name": "NotepadPP"
+ },
+ {
+ "name": "VisualStudioCode"
+ },
+ {
+ "name": "Mercurial"
+ },
+ {
+ "name": "BricxCC"
+ },
+ {
+ "name": "DartEditor"
+ },
+ {
+ "name": "Eclipse"
+ },
+ {
+ "name": "Cloud9"
+ },
+ {
+ "name": "TortoiseGit"
+ },
+ {
+ "name": "NetBeans"
+ },
+ {
+ "name": "GPG"
+ },
+ {
+ "name": "Espresso"
+ },
+ {
+ "name": "Redcar"
+ },
+ {
+ "name": "Xcode"
+ },
+ {
+ "name": "Matlab"
+ },
+ {
+ "name": "LyX"
+ },
+ {
+ "name": "SlickEdit"
+ },
+ {
+ "name": "Dropbox"
+ },
+ {
+ "name": "CVS"
+ },
+ {
+ "name": "Calabash"
+ },
+ {
+ "name": "JDeveloper"
+ },
+ {
+ "name": "Vagrant"
+ },
+ {
+ "name": "IPythonNotebook"
+ },
+ {
+ "name": "TextMate"
+ },
+ {
+ "name": "Ensime"
+ },
+ {
+ "name": "WebMethods"
+ },
+ {
+ "name": "VirtualEnv"
+ },
+ {
+ "name": "Emacs"
+ },
+ {
+ "name": "Momentics"
+ },
+ {
+ "name": "JetBrains"
+ },
+ {
+ "name": "SublimeText"
+ },
+ {
+ "name": "Kate"
+ },
+ {
+ "name": "ModelSim"
+ },
+ {
+ "name": "Redis"
+ },
+ {
+ "name": "KDevelop4"
+ },
+ {
+ "name": "Bazaar"
+ },
+ {
+ "name": "Linux"
+ },
+ {
+ "name": "Windows"
+ },
+ {
+ "name": "XilinxISE"
+ },
+ {
+ "name": "Lazarus"
+ },
+ {
+ "name": "EiffelStudio"
+ },
+ {
+ "name": "Anjuta"
+ },
+ {
+ "name": "Vim"
+ },
+ {
+ "name": "Otto"
+ },
+ {
+ "name": "MicrosoftOffice"
+ },
+ {
+ "name": "LibreOffice"
+ },
+ {
+ "name": "SBT"
+ },
+ {
+ "name": "MonoDevelop"
+ },
+ {
+ "name": "SVN"
+ },
+ {
+ "name": "FlexBuilder"
+ }
+]
+```
+
+## Single gitignore template
+
+Get a single gitignore template.
+
+```
+GET /templates/gitignores/:key
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key` | string | yes | The key of the gitignore template |
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitignores/Ruby
+```
+
+Example response:
+
+```json
+{
+ "name": "Ruby",
+ "content": "*.gem\n*.rbc\n/.config\n/coverage/\n/InstalledFiles\n/pkg/\n/spec/reports/\n/spec/examples.txt\n/test/tmp/\n/test/version_tmp/\n/tmp/\n\n# Used by dotenv library to load environment variables.\n# .env\n\n## Specific to RubyMotion:\n.dat*\n.repl_history\nbuild/\n*.bridgesupport\nbuild-iPhoneOS/\nbuild-iPhoneSimulator/\n\n## Specific to RubyMotion (use of CocoaPods):\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n#\n# vendor/Pods/\n\n## Documentation cache and generated files:\n/.yardoc/\n/_yardoc/\n/doc/\n/rdoc/\n\n## Environment normalization:\n/.bundle/\n/vendor/bundle\n/lib/bundler/man/\n\n# for a library or gem, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# Gemfile.lock\n# .ruby-version\n# .ruby-gemset\n\n# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:\n.rvmrc\n"
+}
+```
diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md
new file mode 100644
index 00000000000..e120016fbe6
--- /dev/null
+++ b/doc/api/templates/gitlab_ci_ymls.md
@@ -0,0 +1,120 @@
+# GitLab CI YMLs
+
+## List GitLab CI YML templates
+
+Get all GitLab CI YML templates.
+
+```
+GET /templates/gitlab_ci_ymls
+```
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls
+```
+
+Example response:
+
+```json
+[
+ {
+ "name": "C++"
+ },
+ {
+ "name": "Docker"
+ },
+ {
+ "name": "Elixir"
+ },
+ {
+ "name": "LaTeX"
+ },
+ {
+ "name": "Grails"
+ },
+ {
+ "name": "Rust"
+ },
+ {
+ "name": "Nodejs"
+ },
+ {
+ "name": "Ruby"
+ },
+ {
+ "name": "Scala"
+ },
+ {
+ "name": "Maven"
+ },
+ {
+ "name": "Harp"
+ },
+ {
+ "name": "Pelican"
+ },
+ {
+ "name": "Hyde"
+ },
+ {
+ "name": "Nanoc"
+ },
+ {
+ "name": "Octopress"
+ },
+ {
+ "name": "JBake"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "Hugo"
+ },
+ {
+ "name": "Metalsmith"
+ },
+ {
+ "name": "Hexo"
+ },
+ {
+ "name": "Lektor"
+ },
+ {
+ "name": "Doxygen"
+ },
+ {
+ "name": "Brunch"
+ },
+ {
+ "name": "Jekyll"
+ },
+ {
+ "name": "Middleman"
+ }
+]
+```
+
+## Single GitLab CI YML template
+
+Get a single GitLab CI YML template.
+
+```
+GET /templates/gitlab_ci_ymls/:key
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key` | string | yes | The key of the GitLab CI YML template |
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls/Ruby
+```
+
+Example response:
+
+```json
+{
+ "name": "Ruby",
+ "content": "# This file is a template, and might need editing before it works on your project.\n# Official language image. Look for the different tagged releases at:\n# https://hub.docker.com/r/library/ruby/tags/\nimage: \"ruby:2.3\"\n\n# Pick zero or more services to be used on all builds.\n# Only needed when using a docker container to run your tests in.\n# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service\nservices:\n - mysql:latest\n - redis:latest\n - postgres:latest\n\nvariables:\n POSTGRES_DB: database_name\n\n# Cache gems in between builds\ncache:\n paths:\n - vendor/ruby\n\n# This is a basic example for a gem or script which doesn't use\n# services such as redis or postgres\nbefore_script:\n - ruby -v # Print out ruby version for debugging\n # Uncomment next line if your rails app needs a JS runtime:\n # - apt-get update -q && apt-get install nodejs -yqq\n - gem install bundler --no-ri --no-rdoc # Bundler is not installed with the image\n - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby\n\n# Optional - Delete if not using `rubocop`\nrubocop:\n script:\n - rubocop\n\nrspec:\n script:\n - rspec spec\n\nrails:\n variables:\n DATABASE_URL: \"postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB\"\n script:\n - bundle exec rake db:migrate\n - bundle exec rake db:seed\n - bundle exec rake test\n"
+}
+```
diff --git a/doc/api/licenses.md b/doc/api/templates/licenses.md
index ed26d1fb7fb..ae7218cf1bd 100644
--- a/doc/api/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -5,7 +5,7 @@
Get all license templates.
```
-GET /licenses
+GET /templates/licenses
```
| Attribute | Type | Required | Description |
@@ -13,7 +13,7 @@ GET /licenses
| `popular` | boolean | no | If passed, returns only popular licenses |
```bash
-curl https://gitlab.example.com/api/v3/licenses?popular=1
+curl https://gitlab.example.com/api/v3/templates/licenses?popular=1
```
Example response:
@@ -102,7 +102,7 @@ Get a single license template. You can pass parameters to replace the license
placeholder.
```
-GET /licenses/:key
+GET /templates/licenses/:key
```
| Attribute | Type | Required | Description |
@@ -116,7 +116,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of
the authenticated user will be used to replace the copyright holder placeholder.
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/templates/licenses/mit?project=My+Cool+Project
```
Example response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 0cd644dfd2f..a5e81801024 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -44,7 +44,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"action_name": "marked",
"target_type": "MergeRequest",
@@ -67,7 +67,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"assignee": {
"name": "Administrator",
@@ -75,7 +75,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"source_project_id": 2,
"target_project_id": 2,
@@ -117,7 +117,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"action_name": "assigned",
"target_type": "MergeRequest",
@@ -140,7 +140,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"assignee": {
"name": "Administrator",
@@ -148,7 +148,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"source_project_id": 2,
"target_project_id": 2,
@@ -215,7 +215,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"action_name": "marked",
"target_type": "MergeRequest",
@@ -238,7 +238,7 @@ Example Response:
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ "web_url": "https://gitlab.example.com/craig_rutherford"
},
"assignee": {
"name": "Administrator",
@@ -246,7 +246,7 @@ Example Response:
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
+ "web_url": "https://gitlab.example.com/root"
},
"source_project_id": 2,
"target_project_id": 2,
diff --git a/doc/api/users.md b/doc/api/users.md
index 9be4f2e6ec3..2b12770d5a5 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,
@@ -627,3 +627,149 @@ Parameters:
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
+
+### Get user contribution events
+
+Get the contribution events for the specified user, sorted from newest to oldest.
+
+```
+GET /users/:id/events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/user/:id/events
+```
+
+Example response:
+
+```json
+[
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 830,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Public project search field",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "opened",
+ "target_id": null,
+ "target_type": null,
+ "author_id": 1,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "john",
+ "data": {
+ "before": "50d4420237a9de7be1304607147aec22e4a14af7",
+ "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "ref": "refs/heads/master",
+ "user_id": 1,
+ "user_name": "Dmitriy Zaporozhets",
+ "repository": {
+ "name": "gitlabhq",
+ "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
+ "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
+ "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
+ },
+ "commits": [
+ {
+ "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "message": "Add simple search to projects in public area",
+ "timestamp": "2013-05-13T18:18:08+00:00",
+ "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 1
+ },
+ "target_title": null
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 840,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Finish & merge Code search PR",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "commented on",
+ "target_id": 1312,
+ "target_type": "Note",
+ "author_id": 1,
+ "data": null,
+ "target_title": null,
+ "created_at": "2015-12-04T10:33:58.089Z",
+ "note": {
+ "id": 1312,
+ "body": "What an awesome day!",
+ "attachment": null,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2015-12-04T10:33:56.698Z",
+ "system": false,
+ "upvote": false,
+ "downvote": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
+ },
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ }
+]
+```
diff --git a/doc/api/version.md b/doc/api/version.md
new file mode 100644
index 00000000000..287d17cf97f
--- /dev/null
+++ b/doc/api/version.md
@@ -0,0 +1,23 @@
+# Version API
+
+>**Note:** This feature was introduced in GitLab 8.13
+
+Retrieve version information for this GitLab instance. Responds `200 OK` for
+authenticated users.
+
+```
+GET /version
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/version
+```
+
+Example response:
+
+```json
+{
+ "version": "8.13.0-pre",
+ "revision": "4e963fe"
+}
+```
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 40f0165deef..ffc310ec8c7 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -11,7 +11,9 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
- [Test a Scala application](test-scala-application.md)
+- [Test a Phoenix application](test-phoenix-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
+- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md
new file mode 100644
index 00000000000..150698ca04b
--- /dev/null
+++ b/doc/ci/examples/test-phoenix-application.md
@@ -0,0 +1,56 @@
+## Test a Phoenix application
+
+This example demonstrates the integration of Gitlab CI with Phoenix, Elixir and
+Postgres.
+
+### Add `.gitlab-ci.yml` file to project
+
+The following `.gitlab-ci.yml` should be added in the root of your
+repository to trigger CI:
+
+```yaml
+image: elixir:1.3
+
+services:
+ - postgres:9.6
+
+variables:
+ MIX_ENV: "test"
+
+before_script:
+ # Setup phoenix dependencies
+ - apt-get update
+ - apt-get install -y postgresql-client
+ - mix local.hex --force
+ - mix deps.get --only test
+ - mix ecto.reset
+
+test:
+ script:
+ - mix test
+```
+
+The variables will set the Mix environment to "test". The
+`before_script` will install `psql`, some Phoenix dependencies, and will also
+run your migrations.
+
+Finally, the test `script` will run your tests.
+
+### Update the Config Settings
+
+In `config/test.exs`, update the database hostname:
+
+```elixir
+config :my_app, MyApp.Repo,
+ hostname: if(System.get_env("CI"), do: "postgres", else: "localhost"),
+```
+
+### Add the Migrations Folder
+
+If you do not have any migrations yet, you will need to create an empty
+`.gitkeep` file in `priv/repo/migrations`.
+
+### Sources
+
+- https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142
+- https://davejlong.com/ci-with-phoenix-and-gitlab/
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index ca9b986a060..729c1dc8c0d 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -31,6 +31,8 @@ project.
## Seeing build status
Clicking on a pipeline will show the builds that were run for that pipeline.
+Clicking on an individual build will show you its build trace, and allow you to
+cancel the build, retry it, or erase the build trace.
## Badges
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 22d67bd9964..a4c3a731a20 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -48,6 +48,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
+| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build |
@@ -105,6 +106,39 @@ Variables can be defined at a global level, but also at a job level.
More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
+#### Debug tracing
+
+> **WARNING:** Enabling debug tracing can have severe security implications. The
+ output **will** contain the content of all your secure variables and any other
+ secrets! The output **will** be uploaded to the GitLab server and made visible
+ in build traces!
+
+By default, GitLab Runner hides most of the details of what it is doing when
+processing a job. This behaviour keeps build traces short, and prevents secrets
+from being leaked into the trace unless your script writes them to the screen.
+
+If a job isn't working as expected, this can make the problem difficult to
+investigate; in these cases, you can enable debug tracing in `.gitlab-ci.yml`.
+Available on GitLab Runner v1.7+, this feature enables the shell's execution
+trace, resulting in a verbose build trace listing all commands that were run,
+variables that were set, etc.
+
+Before enabling this, you should ensure builds are visible to
+[team members only](../../../user/permissions.md#project-features). You should
+also [erase](../pipelines.md#seeing-build-traces) all generated build traces
+before making them visible again.
+
+To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
+
+```yaml
+job1:
+ variables:
+ CI_DEBUG_TRACE: "true"
+```
+
+The [example project](https://gitlab.com/gitlab-examples/ci-debug-trace)
+demonstrates a working configuration, including build trace examples.
+
### User-defined variables (Secure Variables)
**This feature requires GitLab Runner 0.4.0 or higher**
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index cdf5ecc7a84..84ea59ab687 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -159,7 +159,8 @@ Variables can be also defined on [job level](#job-variables).
> 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.**
@@ -594,6 +595,8 @@ create the `review-apps/branch-name` environment.
This environment should be accessible under `https://branch-name.review.example.com/`.
+You can see a simple example at https://gitlab.com/gitlab-examples/review-apps-nginx/.
+
### artifacts
>**Notes:**
@@ -604,8 +607,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.
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 39b801f761d..f07d2c9af2d 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -95,10 +95,10 @@ merge request.
someone in the Merge Request
- When introducing a new document, be careful for the headings to be
grammatically and syntactically correct. It is advised to mention one or all
- of the following GitLab members for a review: `@axil`, `@rspeicher`,
- `@dblessing`, `@ashleys`. This is to ensure that no document
- with wrong heading is going live without an audit, thus preventing dead links
- and redirection issues when corrected
+ of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`,
+ `@SeanPackham`. This is to ensure that no document with wrong heading is going
+ live without an audit, thus preventing dead links and redirection issues when
+ corrected
- Leave exactly one newline after a heading
## Links
@@ -314,6 +314,29 @@ In this case:
- different highlighting languages are used for each config in the code block
- the [references](#references) guide is used for reconfigure/restart
+## Fake tokens
+
+There may be times where a token is needed to demonstrate an API call using
+cURL or a secret variable used in CI. It is strongly advised not to use real
+tokens in documentation even if the probability of a token being exploited is
+low.
+
+You can use the following fake tokens as examples.
+
+| **Token type** | **Token value** |
+| --------------------- | --------------------------------- |
+| Private user token | `9koXpg98eAheJpvBs5tK` |
+| Personal access token | `n671WNGecHugsdEDPsyo` |
+| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
+| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
+| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
+| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
+| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
+| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
+| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
+| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
+| Request profile token | `7VgpS4Ax5utVD2esNstz` |
+
## API
Here is a list of must-have items. Use them in the exact order that appears
@@ -449,4 +472,4 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
-[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
+[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index f879cd57e25..56c8516508e 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -223,3 +223,14 @@ For our currently-supported browsers, see our [requirements][requirements].
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
+
+## Common Errors
+
+### Rspec (Capybara/Poltergeist) chokes on general JavaScript errors
+
+If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but
+can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't
+have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're
+working in (`git mv <file>.js> <file.js.es6>`).
+
+
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 7ff603e2c4a..c4a964d1da3 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)
+* [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/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 3aa83975ace..d7e3aa35bdd 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,14 +2,14 @@
Step-by-step guides on the basics of working with Git and GitLab.
+- [Command line basics](command-line-commands.md)
- [Start using Git on the command line](start-using-git.md)
- [Create and add your SSH Keys](create-your-ssh-keys.md)
-- [Command Line basics](command-line-commands.md)
- [Create a project](create-project.md)
- [Create a group](create-group.md)
- [Create a branch](create-branch.md)
- [Fork a project](fork-project.md)
- [Add a file](add-file.md)
- [Add an image](add-image.md)
-- [Create a Merge Request](add-merge-request.md)
-- [Create an Issue](create-issue.md)
+- [Create an issue](create-issue.md)
+- [Create a merge request](add-merge-request.md)
diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md
index ff10a98e8f5..e9fbcbc23a9 100644
--- a/doc/gitlab-basics/add-file.md
+++ b/doc/gitlab-basics/add-file.md
@@ -1,27 +1,5 @@
# How to add a file
-You can create a file in your [shell](command-line-commands.md) or in GitLab.
-
-To create a file in GitLab, sign in to GitLab.
-
-Select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-It's a good idea to [create a branch](create-branch.md), but it's not necessary.
-
-Go to the directory where you'd like to add the file and click on the "+" sign next to the name of the project and directory:
-
-![Create a file](basicsimages/create_file.png)
-
-Name your file (you can't add spaces, so you can use hyphens or underscores). Don't forget to include the markup language you'd like to use :
-
-![File name](basicsimages/file_name.png)
-
-Add all the information that you'd like to include in your file:
-
-![Add information](basicsimages/white_space.png)
-
-Add a commit message based on what you just added and then click on "commit changes":
-
-![Commit changes](basicsimages/commit_changes.png)
+You can create a file in your [terminal](command-line-commands.md) and push
+to GitLab or you can use the
+[web interface](../user/project/repository/web_editor.md#create-a-file).
diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md
index 236b4248ea2..bf01fe51dc3 100644
--- a/doc/gitlab-basics/add-merge-request.md
+++ b/doc/gitlab-basics/add-merge-request.md
@@ -1,42 +1,33 @@
# How to create a merge request
-Merge Requests are useful to integrate separate changes that you've made to a project, on different branches.
+Merge requests are useful to integrate separate changes that you've made to a
+project, on different branches. This is a brief guide on how to create a merge
+request. For more information, check the
+[merge requests documentation](../user/project/merge_requests.md).
-To create a new Merge Request, sign in to GitLab.
+---
-Go to the project where you'd like to merge your changes:
+1. Before you start, you should have already [created a branch](create-branch.md)
+ and [pushed your changes](basic-git-commands.md) to GitLab.
-![Select a project](basicsimages/select_project.png)
+1. You can then go to the project where you'd like to merge your changes and
+ click on the **Merge requests** tab.
-Click on "Merge Requests" on the left side of your screen:
+ ![Merge requests](img/project_navbar.png)
-![Merge requests](basicsimages/merge_requests.png)
+1. Click on **New merge request** on the right side of the screen.
-Click on "+ new Merge Request" on the right side of the screen:
+ ![New Merge Request](img/merge_request_new.png)
-![New Merge Request](basicsimages/new_merge_request.png)
+1. Select a source branch and click on the **Compare branches and continue** button.
-Select a source branch or branch:
+ ![Select a branch](img/merge_request_select_branch.png)
-![Select a branch](basicsimages/select_branch.png)
+1. At a minimum, add a title and a description to your merge request. Optionally,
+ select a user to review your merge request and to accept or close it. You may
+ also select a milestone and labels.
-Click on the "compare branches" button:
+ ![New merge request page](img/merge_request_page.png)
-![Compare branches](basicsimages/compare_branches.png)
-
-Add a title and a description to your Merge Request:
-
-![Add a title and description](basicsimages/title_description_mr.png)
-
-Select a user to review your Merge Request and to accept or close it. You may also select milestones and labels (they are optional). Then click on the "submit new Merge Request" button:
-
-![Add a new merge request](basicsimages/add_new_merge_request.png)
-
-Your Merge Request will be ready to be approved and published.
-
-### Note
-
-After you created a new branch, you'll immediately find a "create a Merge Request" button at the top of your screen.
-You may automatically create a Merge Request from your recently created branch when clicking on this button:
-
-![Automatic MR button](basicsimages/button-create-mr.png)
+1. When ready, click on the **Submit merge request** button. Your merge request
+ will be ready to be approved and published.
diff --git a/doc/gitlab-basics/basicsimages/add_new_merge_request.png b/doc/gitlab-basics/basicsimages/add_new_merge_request.png
deleted file mode 100644
index e60992c4c6a..00000000000
--- a/doc/gitlab-basics/basicsimages/add_new_merge_request.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/add_sshkey.png b/doc/gitlab-basics/basicsimages/add_sshkey.png
deleted file mode 100644
index 89c86018629..00000000000
--- a/doc/gitlab-basics/basicsimages/add_sshkey.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_info.png b/doc/gitlab-basics/basicsimages/branch_info.png
deleted file mode 100644
index 2264f3c5bf2..00000000000
--- a/doc/gitlab-basics/basicsimages/branch_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_name.png b/doc/gitlab-basics/basicsimages/branch_name.png
deleted file mode 100644
index 75fe8313611..00000000000
--- a/doc/gitlab-basics/basicsimages/branch_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branches.png b/doc/gitlab-basics/basicsimages/branches.png
deleted file mode 100644
index 8621bc05776..00000000000
--- a/doc/gitlab-basics/basicsimages/branches.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png
deleted file mode 100644
index b52ab148839..00000000000
--- a/doc/gitlab-basics/basicsimages/button-create-mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/click-on-new-group.png b/doc/gitlab-basics/basicsimages/click-on-new-group.png
deleted file mode 100644
index 6450deec6fc..00000000000
--- a/doc/gitlab-basics/basicsimages/click-on-new-group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_changes.png b/doc/gitlab-basics/basicsimages/commit_changes.png
deleted file mode 100644
index a88809c5a3f..00000000000
--- a/doc/gitlab-basics/basicsimages/commit_changes.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_message.png b/doc/gitlab-basics/basicsimages/commit_message.png
deleted file mode 100644
index 4abe4517f98..00000000000
--- a/doc/gitlab-basics/basicsimages/commit_message.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commits.png b/doc/gitlab-basics/basicsimages/commits.png
deleted file mode 100644
index 2bfcaf75f01..00000000000
--- a/doc/gitlab-basics/basicsimages/commits.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/compare_branches.png b/doc/gitlab-basics/basicsimages/compare_branches.png
deleted file mode 100644
index 8a18453dd05..00000000000
--- a/doc/gitlab-basics/basicsimages/compare_branches.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_file.png b/doc/gitlab-basics/basicsimages/create_file.png
deleted file mode 100644
index 5ebe1b227dd..00000000000
--- a/doc/gitlab-basics/basicsimages/create_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_group.png b/doc/gitlab-basics/basicsimages/create_group.png
deleted file mode 100644
index 7ecc3baa990..00000000000
--- a/doc/gitlab-basics/basicsimages/create_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/edit_file.png b/doc/gitlab-basics/basicsimages/edit_file.png
deleted file mode 100644
index 9d3e817d036..00000000000
--- a/doc/gitlab-basics/basicsimages/edit_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_located.png b/doc/gitlab-basics/basicsimages/file_located.png
deleted file mode 100644
index e357cb5c6ab..00000000000
--- a/doc/gitlab-basics/basicsimages/file_located.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_name.png b/doc/gitlab-basics/basicsimages/file_name.png
deleted file mode 100644
index 01639c77d0d..00000000000
--- a/doc/gitlab-basics/basicsimages/file_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_file.png b/doc/gitlab-basics/basicsimages/find_file.png
deleted file mode 100644
index 6f26d26ae18..00000000000
--- a/doc/gitlab-basics/basicsimages/find_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_group.png b/doc/gitlab-basics/basicsimages/find_group.png
deleted file mode 100644
index 1211510aae9..00000000000
--- a/doc/gitlab-basics/basicsimages/find_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/fork.png b/doc/gitlab-basics/basicsimages/fork.png
deleted file mode 100644
index 13ff8345616..00000000000
--- a/doc/gitlab-basics/basicsimages/fork.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/group_info.png b/doc/gitlab-basics/basicsimages/group_info.png
deleted file mode 100644
index 2507d6c295b..00000000000
--- a/doc/gitlab-basics/basicsimages/group_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/groups.png b/doc/gitlab-basics/basicsimages/groups.png
deleted file mode 100644
index ef3dca60cc8..00000000000
--- a/doc/gitlab-basics/basicsimages/groups.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/https.png b/doc/gitlab-basics/basicsimages/https.png
deleted file mode 100644
index e74dbc13f9a..00000000000
--- a/doc/gitlab-basics/basicsimages/https.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/image_file.png b/doc/gitlab-basics/basicsimages/image_file.png
deleted file mode 100644
index 7f304b8e1f2..00000000000
--- a/doc/gitlab-basics/basicsimages/image_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issue_title.png b/doc/gitlab-basics/basicsimages/issue_title.png
deleted file mode 100644
index 60a6f7973be..00000000000
--- a/doc/gitlab-basics/basicsimages/issue_title.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issues.png b/doc/gitlab-basics/basicsimages/issues.png
deleted file mode 100644
index 14e9cdb64e1..00000000000
--- a/doc/gitlab-basics/basicsimages/issues.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/key.png b/doc/gitlab-basics/basicsimages/key.png
deleted file mode 100644
index 04400173ce8..00000000000
--- a/doc/gitlab-basics/basicsimages/key.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/merge_requests.png b/doc/gitlab-basics/basicsimages/merge_requests.png
deleted file mode 100644
index 570164df18b..00000000000
--- a/doc/gitlab-basics/basicsimages/merge_requests.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_issue.png b/doc/gitlab-basics/basicsimages/new_issue.png
deleted file mode 100644
index 94e7503dd8b..00000000000
--- a/doc/gitlab-basics/basicsimages/new_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_merge_request.png b/doc/gitlab-basics/basicsimages/new_merge_request.png
deleted file mode 100644
index 842f5ebed74..00000000000
--- a/doc/gitlab-basics/basicsimages/new_merge_request.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_project.png b/doc/gitlab-basics/basicsimages/new_project.png
deleted file mode 100644
index 421e8bc247b..00000000000
--- a/doc/gitlab-basics/basicsimages/new_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/newbranch.png b/doc/gitlab-basics/basicsimages/newbranch.png
deleted file mode 100644
index d5fcf33c4ea..00000000000
--- a/doc/gitlab-basics/basicsimages/newbranch.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/paste_sshkey.png b/doc/gitlab-basics/basicsimages/paste_sshkey.png
deleted file mode 100644
index 578ebee4440..00000000000
--- a/doc/gitlab-basics/basicsimages/paste_sshkey.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/profile_settings.png b/doc/gitlab-basics/basicsimages/profile_settings.png
deleted file mode 100644
index cb3f79f1879..00000000000
--- a/doc/gitlab-basics/basicsimages/profile_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/project_info.png b/doc/gitlab-basics/basicsimages/project_info.png
deleted file mode 100644
index e1adb8d48c2..00000000000
--- a/doc/gitlab-basics/basicsimages/project_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group.png b/doc/gitlab-basics/basicsimages/select-group.png
deleted file mode 100644
index 33b978dd899..00000000000
--- a/doc/gitlab-basics/basicsimages/select-group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group2.png b/doc/gitlab-basics/basicsimages/select-group2.png
deleted file mode 100644
index aee22c638db..00000000000
--- a/doc/gitlab-basics/basicsimages/select-group2.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_branch.png b/doc/gitlab-basics/basicsimages/select_branch.png
deleted file mode 100644
index f72a3ffb57f..00000000000
--- a/doc/gitlab-basics/basicsimages/select_branch.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_project.png b/doc/gitlab-basics/basicsimages/select_project.png
deleted file mode 100644
index 3bb832ea8d0..00000000000
--- a/doc/gitlab-basics/basicsimages/select_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/settings.png b/doc/gitlab-basics/basicsimages/settings.png
deleted file mode 100644
index 78637013d9b..00000000000
--- a/doc/gitlab-basics/basicsimages/settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/shh_keys.png b/doc/gitlab-basics/basicsimages/shh_keys.png
deleted file mode 100644
index c87f11a9d3d..00000000000
--- a/doc/gitlab-basics/basicsimages/shh_keys.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/submit_new_issue.png b/doc/gitlab-basics/basicsimages/submit_new_issue.png
deleted file mode 100644
index 78b854c8903..00000000000
--- a/doc/gitlab-basics/basicsimages/submit_new_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/title_description_mr.png b/doc/gitlab-basics/basicsimages/title_description_mr.png
deleted file mode 100644
index c31d61ec336..00000000000
--- a/doc/gitlab-basics/basicsimages/title_description_mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/white_space.png b/doc/gitlab-basics/basicsimages/white_space.png
deleted file mode 100644
index eaa969bdcf4..00000000000
--- a/doc/gitlab-basics/basicsimages/white_space.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index addd3b6b6eb..3b075ff5fc0 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -4,18 +4,21 @@
In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to GitLab.
-When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen.
+When you are on your Dashboard, click on the project that you'd like to clone.
+To work in the project, you can copy a link to the Git repository through a SSH
+or a HTTPS protocol. SSH is easier to use after it's been
+[setup](create-your-ssh-keys.md). While you are at the **Project** tab, select
+HTTPS or SSH from the dropdown menu and copy the link using the 'Copy to clipboard'
+button (you'll have to paste it on your shell in the next step).
-![Select a project](basicsimages/select_project.png)
-
-To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been [setup](create-your-ssh-keys.md). When you're in the project, click on the HTTPS or SSH button at the right side of your screen. Then copy the link (you'll have to paste it on your shell in the next step).
-
-![Copy the HTTPS or SSH](basicsimages/https.png)
+![Copy the HTTPS or SSH](img/project_clone_url.png)
## On the command line
### Clone your project
+
Go to your computer's shell and type the following command:
+
```
git clone PASTE HTTPS OR SSH HERE
```
@@ -23,26 +26,31 @@ git clone PASTE HTTPS OR SSH HERE
A clone of the project will be created in your computer.
### Go into a project, directory or file to work in it
+
```
cd NAME-OF-PROJECT-OR-FILE
```
### Go back one directory or file
+
```
cd ../
```
### View what’s in the directory that you are in
+
```
ls
```
### Create a directory
+
```
mkdir NAME-OF-YOUR-DIRECTORY
```
### Create a README.md or file in directory
+
```
touch README.md
nano README.md
@@ -53,27 +61,33 @@ nano README.md
```
### Remove a file
+
```
rm NAME-OF-FILE
```
### Remove a directory and all of its contents
+
```
rm -rf NAME-OF-DIRECTORY
```
### View history in the command line
+
```
history
```
### Carry out commands for which the account you are using lacks authority
+
You will be asked for an administrator’s password.
+
```
sudo
```
### Tell where you are
+
```
pwd
```
diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md
index 7556b0f663e..ad94f0dad29 100644
--- a/doc/gitlab-basics/create-branch.md
+++ b/doc/gitlab-basics/create-branch.md
@@ -2,38 +2,11 @@
A branch is an independent line of development.
-New commits are recorded in the history for the current branch, which results in taking the source from someone’s repository (the place where the history of your work is stored) at certain point in time, and apply your own changes to it in the history of the project.
-
-To add changes to your GitLab project, you should create a branch. You can do it in your [shell](basic-git-commands.md) or in GitLab.
-
-To create a new branch in GitLab, sign in and then select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-Click on "commits" on the menu on the left side of your screen:
-
-![Commits](basicsimages/commits.png)
-
-Click on the "branches" tab:
-
-![Branches](basicsimages/branches.png)
-
-Click on the "new branch" button on the right side of the screen:
-
-![New branch](basicsimages/newbranch.png)
-
-Fill out the information required:
-
-1. Add a name for your new branch (you can't add spaces, so you can use hyphens or underscores)
-
-1. On the "create from" space, add the the name of the branch you want to branch off from
-
-1. Click on the button "create branch"
-
-![Branch info](basicsimages/branch_info.png)
-
-### Note:
-
-You will be able to find and select the name of your branch in the white box next to a project's name:
-
-![Branch name](basicsimages/branch_name.png)
+New commits are recorded in the history for the current branch, which results
+in taking the source from someone’s repository (the place where the history of
+your work is stored) at certain point in time, and apply your own changes to it
+in the history of the project.
+
+To add changes to your GitLab project, you should create a branch. You can do
+it in your [terminal](basic-git-commands.md) or by
+[using the web interface](../user/project/repository/web_editor.md#create-a-new-branch).
diff --git a/doc/gitlab-basics/create-group.md b/doc/gitlab-basics/create-group.md
index f80ae62e442..64274ccd5eb 100644
--- a/doc/gitlab-basics/create-group.md
+++ b/doc/gitlab-basics/create-group.md
@@ -1,43 +1,48 @@
# How to create a group in GitLab
-## Create a group
-
Your projects in GitLab can be organized in 2 different ways:
-under your own namespace for single projects, such as ´your-name/project-1'; or under groups.
-If you organize your projects under a group, it works like a folder. You can manage your group members' permissions and access to the projects.
-
-To create a group, follow the instructions below:
+under your own namespace for single projects, such as `your-name/project-1` or
+under groups.
-Sign in to [GitLab.com](https://gitlab.com).
+If you organize your projects under a group, it works like a folder. You can
+manage your group members' permissions and access to the projects.
-When you are on your Dashboard, click on "Groups" on the left menu of your screen:
+---
-![Go to groups](basicsimages/select-group2.png)
+To create a group:
-Click on "New group" on the top right side of your screen:
+1. Expand the left sidebar by clicking the three bars at the upper left corner
+ and then navigate to **Groups**.
-![New group](basicsimages/click-on-new-group.png)
+ ![Go to groups](img/create_new_group_sidebar.png)
-Fill out the information required:
+1. Once in your groups dashboard, click on **New group**.
-1. Add a group path or group name (you can't add spaces, so you can use hyphens or underscores)
+ ![Create new group information](img/create_new_group_info.png)
-1. Add details or a group description
+1. Fill out the needed information:
-1. You can choose a group avatar if you'd like
+ 1. Set the "Group path" which will be the namespace under which your projects
+ will be hosted (path can contain only letters, digits, underscores, dashes
+ and dots; it cannot start with dashes or end in dot).
+ 1. Optionally, you can add a description so that others can briefly understand
+ what this group is about.
+ 1. Optionally, choose and avatar for your project.
+ 1. Choose the [visibility level](../public_access/public_access.md).
-1. Click on "create group"
+1. Finally, click the **Create group** button.
-![Group information](basicsimages/group_info.png)
-
-## Add a project to a group
+## Add a new project to a group
There are 2 different ways to add a new project to a group:
-* Select a group and then click on "New project" on the right side of your screen. Then you can [create a project](create-project.md)
+- Select a group and then click on the **New project** button.
+
+ ![New project](img/create_new_project_from_group.png)
-![New project](basicsimages/new_project.png)
+ You can then continue on [creating a project](create-project.md).
-* When you are [creating a project](create-project.md), click on "create a group" on the bottom right side of your screen
+- While you are [creating a project](create-project.md), select a group namespace
+ you've already created from the dropdown menu.
-![Create a group](basicsimages/create_group.png)
+ ![Select group](img/select_group_dropdown.png)
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index da9a165b8f5..13e5a738c89 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,27 +1,30 @@
# How to create an Issue in GitLab
-The Issue Tracker is a good place to add things that need to be improved or solved in a project.
+The issue tracker is a good place to add things that need to be improved or
+solved in a project.
-To create an Issue, sign in to GitLab.
+---
-Go to the project where you'd like to create the Issue:
+1. Go to the project where you'd like to create the issue and navigate to the
+ **Issues** tab on top.
-![Select a project](basicsimages/select_project.png)
+ ![Issues](img/project_navbar.png)
-Click on "Issues" on the left side of your screen:
+1. Click on the **New issue** button on the right side of your screen.
-![Issues](basicsimages/issues.png)
+ ![New issue](img/new_issue_button.png)
-Click on the "+ new issue" button on the right side of your screen:
+1. At the very minimum, add a title and a description to your issue.
+ You may assign it to a user, add a milestone or add labels (all optional).
-![New issue](basicsimages/new_issue.png)
+ ![Issue title and description](img/new_issue_page.png)
-Add a title and a description to your issue:
+1. When ready, click on **Submit issue**.
-![Issue title and description](basicsimages/issue_title.png)
+---
-You may assign the Issue to a user, add a milestone and add labels (they are all optional). Then click on "submit new issue":
-
-![Submit new issue](basicsimages/submit_new_issue.png)
-
-Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md).
+Your Issue will now be added to the issue tracker of the project you opened it
+at and will be ready to be reviewed. You can comment on it and mention the
+people involved. You can also link issues to the merge requests where the issues
+are solved. To do this, you can use an
+[issue closing pattern](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index f737dffc024..3f45a631b3a 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -1,21 +1,24 @@
# How to create a project in GitLab
-To create a new project, sign in to GitLab.
+There are two ways to create a new project in GitLab.
-Go to your Dashboard and click on "new project" on the right side of your screen.
+1. While in your dashboard, you can create a new project using the **New project**
+ green button or you can use the cross icon in the upper right corner next to
+ your avatar which is always visible.
-![Create a project](basicsimages/new_project.png)
+ ![Create a project](img/create_new_project_button.png)
-Fill out the required information:
+1. From there you can see several options.
-1. Project path or the name of your project (you can't add spaces, so you can use hyphens or underscores)
+ ![Project information](img/create_new_project_info.png)
-1. Your project's description
+1. Fill out the information:
-1. Select a [visibility level](https://gitlab.com/help/public_access/public_access)
+ 1. "Project name" is the name of your project (you can't use spaces, but you
+ can use hyphens or underscores).
+ 1. The "Project description" is optional and will be shown in your project's
+ dashboard so others can briefly understand what your project is about.
+ 1. Select a [visibility level](../public_access/public_access.md).
+ 1. You can also [import your existing projects](../workflow/importing/README.md).
-1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html)
-
-1. Click on "create project"
-
-!![Project information](basicsimages/project_info.png)
+1. Finally, click **Create project**.
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index f31c353f2cf..b6ebe374de3 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -1,33 +1,37 @@
# How to create your SSH Keys
-You need to connect your computer to your GitLab account through SSH Keys. They are unique for every computer that you link your GitLab account with.
+1. The first thing you need to do is go to your [command line](start-using-git.md)
+ and follow the [instructions](../ssh/README.md) to generate your SSH key pair.
-## Generate your SSH Key
+1. Once you do that, login to GitLab with your credentials.
+1. On the upper right corner, click on your avatar and go to your **Profile settings**.
-Create an account on GitLab. Sign up and check your email for your confirmation link.
+ ![Profile settings dropdown](img/profile_settings.png)
-After you confirm, go to GitLab and sign in to your account.
+1. Navigate to the **SSH keys** tab.
-## Add your SSH Key
+ ![SSH Keys](img/profile_settings_ssh_keys.png)
-On the left side menu, click on "profile settings" and then click on "SSH Keys":
+3. Paste your **public** key that you generated in the first step in the 'Key'
+ box.
-![SSH Keys](basicsimages/shh_keys.png)
+ ![Paste SSH public key](img/profile_settings_ssh_keys_paste_pub.png)
-Then click on the green button "Add SSH Key":
+1. Optionally, give it a descriptive title so that you can recognize it in the
+ event you add multiple keys.
-![Add SSH Key](basicsimages/add_sshkey.png)
+ ![SSH key title](img/profile_settings_ssh_keys_title.png)
-There, you should paste the SSH Key that your command line will generate for you. Below you'll find the steps to generate it:
+1. Finally, click on **Add key** to add it to GitLab. You will be able to see
+ its fingerprint, its title and creation date.
-![Paste SSH Key](basicsimages/paste_sshkey.png)
+ ![SSH key single page](img/profile_settings_ssh_keys_single_key.png)
-## To generate an SSH Key on your command line
-Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate it.
+>**Note:**
+Once you add a key, you cannot edit it, only remove it. In case the paste
+didn't work, you will have to remove the offending key and re-add it.
-Copy the SSH Key that your command line created and paste it on the "Key" box on the GitLab page. The title will be added automatically.
+---
-![Paste SSH Key](basicsimages/key.png)
-
-Now, you'll be able to use Git over SSH, instead of Git over HTTP.
+Congratulations! You are now ready to use Git over SSH, instead of Git over HTTP!
diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md
index 5f8b81ea919..6c232fe6086 100644
--- a/doc/gitlab-basics/fork-project.md
+++ b/doc/gitlab-basics/fork-project.md
@@ -1,19 +1,20 @@
# How to fork a project
-A fork is a copy of an original repository that you can put somewhere else
-or where you can experiment and apply changes that you can later decide if
+A fork is a copy of an original repository that you can put in another namespace
+where you can experiment and apply changes that you can later decide if
publishing or not, without affecting your original project.
It takes just a few steps to fork a project in GitLab.
-Sign in to GitLab.
+1. Go to a project's dashboard under the **Project** tab and click on the
+ **Fork** button.
-Select a project on the right side of your screen:
+ ![Click on Fork button](img/fork_new.png)
-![Select a project](basicsimages/select_project.png)
+1. You will be asked where to fork the repository. Click on the user or group
+ to where you'd like to add the forked project.
-Click on the "fork" button on the right side of your screen:
+ ![Choose namespace](img/fork_choose_namespace.png)
-![Fork](basicsimages/fork.png)
-
-Click on the user or group to where you'd like to add the forked project.
+1. After a few moments, depending on the repository's size, the forking will
+ complete.
diff --git a/doc/gitlab-basics/img/create_new_group_info.png b/doc/gitlab-basics/img/create_new_group_info.png
new file mode 100644
index 00000000000..c8eddfd1bbb
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_group_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_group_sidebar.png b/doc/gitlab-basics/img/create_new_group_sidebar.png
new file mode 100644
index 00000000000..28017ee02e0
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_group_sidebar.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
new file mode 100644
index 00000000000..e7c794d943f
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_from_group.png b/doc/gitlab-basics/img/create_new_project_from_group.png
new file mode 100644
index 00000000000..6d41d17f9ca
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_from_group.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
new file mode 100644
index 00000000000..16d56f0707f
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_choose_namespace.png b/doc/gitlab-basics/img/fork_choose_namespace.png
new file mode 100644
index 00000000000..82c9c3bd39e
--- /dev/null
+++ b/doc/gitlab-basics/img/fork_choose_namespace.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_new.png b/doc/gitlab-basics/img/fork_new.png
new file mode 100644
index 00000000000..41885223286
--- /dev/null
+++ b/doc/gitlab-basics/img/fork_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png
new file mode 100644
index 00000000000..0aba5743f01
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_page.png b/doc/gitlab-basics/img/merge_request_page.png
new file mode 100644
index 00000000000..68c3bbf9444
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png
new file mode 100644
index 00000000000..516436ff6cc
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_select_branch.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png
new file mode 100644
index 00000000000..46b626bed65
--- /dev/null
+++ b/doc/gitlab-basics/img/new_issue_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png
new file mode 100644
index 00000000000..843504130b7
--- /dev/null
+++ b/doc/gitlab-basics/img/new_issue_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
new file mode 100644
index 00000000000..f0abd478849
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys.png b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
new file mode 100644
index 00000000000..2c9a42fe10c
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
new file mode 100644
index 00000000000..cd7add6937f
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
new file mode 100644
index 00000000000..095beb02be8
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
new file mode 100644
index 00000000000..4b998a7f948
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_clone_url.png b/doc/gitlab-basics/img/project_clone_url.png
new file mode 100644
index 00000000000..eed430e1036
--- /dev/null
+++ b/doc/gitlab-basics/img/project_clone_url.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png
new file mode 100644
index 00000000000..97cf3cd9702
--- /dev/null
+++ b/doc/gitlab-basics/img/project_navbar.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/public_file_link.png b/doc/gitlab-basics/img/public_file_link.png
index f60df6807f4..f60df6807f4 100644
--- a/doc/gitlab-basics/basicsimages/public_file_link.png
+++ b/doc/gitlab-basics/img/public_file_link.png
Binary files differ
diff --git a/doc/gitlab-basics/img/select_group_dropdown.png b/doc/gitlab-basics/img/select_group_dropdown.png
new file mode 100644
index 00000000000..7d8b89c2df9
--- /dev/null
+++ b/doc/gitlab-basics/img/select_group_dropdown.png
Binary files differ
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 5a9a1582877..db0f03f2c98 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -1,302 +1 @@
-# Reply by email
-
-GitLab can be set up to allow users to comment on issues and merge requests by
-replying to notification emails.
-
-## Requirement
-
-Reply by email requires an IMAP-enabled email account. GitLab allows you to use
-three strategies for this feature:
-- using email sub-addressing
-- using a dedicated email address
-- using a catch-all mailbox
-
-### Email sub-addressing
-
-**If your provider or server supports email sub-addressing, we recommend using it.**
-
-[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
-a feature where any email to `user+some_arbitrary_tag@example.com` will end up
-in the mailbox for `user@example.com`, and is supported by providers such as
-Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
-mail server which you can run on-premises.
-
-### Dedicated email address
-
-This solution is really simple to set up: you just have to create an email
-address dedicated to receive your users' replies to GitLab notifications.
-
-### Catch-all mailbox
-
-A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
-"catch all" the emails addressed to the domain that do not exist in the mail
-server.
-
-## How it works?
-
-### 1. GitLab sends a notification email
-
-When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
-header is set to the address defined in your GitLab configuration, with the
-`%{key}` placeholder (if present) replaced by a specific "reply key". In
-addition, this "reply key" is also added to the `References` header.
-
-### 2. You reply to the notification email
-
-When you reply to the notification email, your email client will:
-
-- send the email to the `Reply-To` address it got from the notification email
-- set the `In-Reply-To` header to the value of the `Message-ID` header from the
- notification email
-- set the `References` header to the value of the `Message-ID` plus the value of
- the notification email's `References` header.
-
-### 3. GitLab receives your reply to the notification email
-
-When GitLab receives your reply, it will look for the "reply key" in the
-following headers, in this order:
-
-1. the `To` header
-1. the `References` header
-
-If it finds a reply key, it will be able to leave your reply as a comment on
-the entity the notification was about (issue, merge request, commit...).
-
-For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
-please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
-
-## Set it up
-
-If you want to use Gmail / Google Apps with Reply by email, make sure you have
-[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
-and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
-
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
-[these instructions](./postfix.md).
-
-### Omnibus package installations
-
-1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
- feature and fill in the details for your specific IMAP server and email account:
-
- ```ruby
- # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
- gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- gitlab_rails['incoming_email_email'] = "incoming"
- # Email account password
- gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
- # IMAP server host
- gitlab_rails['incoming_email_host'] = "gitlab.example.com"
- # IMAP server port
- gitlab_rails['incoming_email_port'] = 143
- # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_ssl'] = false
- # Whether the IMAP server uses StartTLS
- gitlab_rails['incoming_email_start_tls'] = false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- gitlab_rails['incoming_email_mailbox_name'] = "inbox"
- ```
-
- ```ruby
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
- # Email account password
- gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
- # IMAP server host
- gitlab_rails['incoming_email_host'] = "imap.gmail.com"
- # IMAP server port
- gitlab_rails['incoming_email_port'] = 993
- # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_ssl'] = true
- # Whether the IMAP server uses StartTLS
- gitlab_rails['incoming_email_start_tls'] = false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- gitlab_rails['incoming_email_mailbox_name'] = "inbox"
- ```
-
-1. Reconfigure GitLab and restart mailroom for the changes to take effect:
-
- ```sh
- sudo gitlab-ctl reconfigure
- sudo gitlab-ctl restart mailroom
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- sudo gitlab-rake gitlab:incoming_email:check
- ```
-
-1. Reply by email should now be working.
-
-### Installations from source
-
-1. Go to the GitLab installation directory:
-
- ```sh
- cd /home/git/gitlab
- ```
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
- and fill in the details for your specific IMAP server and email account:
-
- ```sh
- sudo editor config/gitlab.yml
- ```
-
- ```yaml
- # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "incoming+%{key}@gitlab.example.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "incoming"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "gitlab.example.com"
- # IMAP server port
- port: 143
- # Whether the IMAP server uses SSL
- ssl: false
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- ```
-
- ```yaml
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "gitlab-incoming@gmail.com"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "imap.gmail.com"
- # IMAP server port
- port: 993
- # Whether the IMAP server uses SSL
- ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- ```
-
-1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
-
- ```sh
- sudo mkdir -p /etc/default
- echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
- ```
-
-1. Restart GitLab:
-
- ```sh
- sudo service gitlab restart
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
- ```
-
-1. Reply by email should now be working.
-
-### Development
-
-1. Go to the GitLab installation directory.
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
-
- ```yaml
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "gitlab-incoming@gmail.com"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "imap.gmail.com"
- # IMAP server port
- port: 993
- # Whether the IMAP server uses SSL
- ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- ```
-
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
-
-1. Uncomment the `mail_room` line in your `Procfile`:
-
- ```yaml
- mail_room: bundle exec mail_room -q -c config/mail_room.yml
- ```
-
-1. Restart GitLab:
-
- ```sh
- bundle exec foreman start
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
- ```
-
-1. Reply by email should now be working.
+This document was moved to [administration/reply_by_email](../administration/reply_by_email.md).
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
index 787d21f7f8f..90833238ac5 100644
--- a/doc/incoming_email/postfix.md
+++ b/doc/incoming_email/postfix.md
@@ -1,321 +1 @@
-# Set up Postfix for Reply by email
-
-This document will take you through the steps of setting up a basic Postfix mail server with IMAP authentication on Ubuntu, to be used with Reply by email.
-
-The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
-
-## Configure your server firewall
-
-1. Open up port 25 on your server so that people can send email into the server over SMTP.
-2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
-
-## Install packages
-
-1. Install the `postfix` package if it is not installed already:
-
- ```sh
- sudo apt-get install postfix
- ```
-
- When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
-
-1. Install the `mailutils` package.
-
- ```sh
- sudo apt-get install mailutils
- ```
-
-## Create user
-
-1. Create a user for incoming email.
-
- ```sh
- sudo useradd -m -s /bin/bash incoming
- ```
-
-1. Set a password for this user.
-
- ```sh
- sudo passwd incoming
- ```
-
- Be sure not to forget this, you'll need it later.
-
-## Test the out-of-the-box setup
-
-1. Connect to the local SMTP server:
-
- ```sh
- telnet localhost 25
- ```
-
- You should see a prompt like this:
-
- ```sh
- Trying 127.0.0.1...
- Connected to localhost.
- Escape character is '^]'.
- 220 gitlab.example.com ESMTP Postfix (Ubuntu)
- ```
-
- If you get a `Connection refused` error instead, verify that `postfix` is running:
-
- ```sh
- sudo postfix status
- ```
-
- If it is not, start it:
-
- ```sh
- sudo postfix start
- ```
-
-1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
-
- ```
- ehlo localhost
- mail from: root@localhost
- rcpt to: incoming@localhost
- data
- Subject: Re: Some issue
-
- Sounds good!
- .
- quit
- ```
-
- _**Note:** The `.` is a literal period on its own line._
-
- _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
- then your Postfix `my_network` configuration is not correct. The error will
- say 'Temporary lookup failure'. See
- [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
-
-1. Check if the `incoming` user received the email:
-
- ```sh
- su - incoming
- mail
- ```
-
- You should see output like this:
-
- ```
- "/var/mail/incoming": 1 message 1 unread
- >U 1 root@localhost 59/2842 Re: Some issue
- ```
-
- Quit the mail app:
-
- ```sh
- q
- ```
-
-1. Log out of the `incoming` account and go back to being `root`:
-
- ```sh
- logout
- ```
-
-## Configure Postfix to use Maildir-style mailboxes
-
-Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
-
-1. Configure Postfix to use Maildir-style mailboxes:
-
- ```sh
- sudo postconf -e "home_mailbox = Maildir/"
- ```
-
-1. Restart Postfix:
-
- ```sh
- sudo /etc/init.d/postfix restart
- ```
-
-1. Test the new setup:
-
- 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
- 1. Check if the `incoming` user received the email:
-
- ```sh
- su - incoming
- MAIL=/home/incoming/Maildir
- mail
- ```
-
- You should see output like this:
-
- ```
- "/home/incoming/Maildir": 1 message 1 unread
- >U 1 root@localhost 59/2842 Re: Some issue
- ```
-
- Quit the mail app:
-
- ```sh
- q
- ```
-
- _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
- version of `mail` doesn't support Maildir style mailboxes. Install
- `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
- try the above steps again, substituting `heirloom-mailx` for the `mail`
- command._
-
-1. Log out of the `incoming` account and go back to being `root`:
-
- ```sh
- logout
- ```
-
-## Install the Courier IMAP server
-
-1. Install the `courier-imap` package:
-
- ```sh
- sudo apt-get install courier-imap
- ```
-
-## Configure Postfix to receive email from the internet
-
-1. Let Postfix know about the domains that it should consider local:
-
- ```sh
- sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
- ```
-
-1. Let Postfix know about the IPs that it should consider part of the LAN:
-
- We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
-
- ```sh
- sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
- ```
-
-1. Configure Postfix to receive mail on all interfaces, which includes the internet:
-
- ```sh
- sudo postconf -e "inet_interfaces = all"
- ```
-
-1. Configure Postfix to use the `+` delimiter for sub-addressing:
-
- ```sh
- sudo postconf -e "recipient_delimiter = +"
- ```
-
-1. Restart Postfix:
-
- ```sh
- sudo service postfix restart
- ```
-
-## Test the final setup
-
-1. Test SMTP under the new setup:
-
- 1. Connect to the SMTP server:
-
- ```sh
- telnet gitlab.example.com 25
- ```
-
- You should see a prompt like this:
-
- ```sh
- Trying 123.123.123.123...
- Connected to gitlab.example.com.
- Escape character is '^]'.
- 220 gitlab.example.com ESMTP Postfix (Ubuntu)
- ```
-
- If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
-
- 1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
-
- ```
- ehlo gitlab.example.com
- mail from: root@gitlab.example.com
- rcpt to: incoming@gitlab.example.com
- data
- Subject: Re: Some issue
-
- Sounds good!
- .
- quit
- ```
-
- (Note: The `.` is a literal period on its own line)
-
- 1. Check if the `incoming` user received the email:
-
- ```sh
- su - incoming
- MAIL=/home/incoming/Maildir
- mail
- ```
-
- You should see output like this:
-
- ```
- "/home/incoming/Maildir": 1 message 1 unread
- >U 1 root@gitlab.example.com 59/2842 Re: Some issue
- ```
-
- Quit the mail app:
-
- ```sh
- q
- ```
-
- 1. Log out of the `incoming` account and go back to being `root`:
-
- ```sh
- logout
- ```
-
-1. Test IMAP under the new setup:
-
- 1. Connect to the IMAP server:
-
- ```sh
- telnet gitlab.example.com 143
- ```
-
- You should see a prompt like this:
-
- ```sh
- Trying 123.123.123.123...
- Connected to mail.example.gitlab.com.
- Escape character is '^]'.
- - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information.
- ```
-
- 1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt:
-
- ```
- a login incoming PASSWORD
- ```
-
- Replace PASSWORD with the password you set on the `incoming` user earlier.
-
- You should see output like this:
-
- ```
- a OK LOGIN Ok.
- ```
-
- 1. Disconnect from the IMAP server:
-
- ```sh
- a logout
- ```
-
-## Done!
-
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
-
----------
-
-_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
+This document was moved to [administration/reply_by_email_postfix_setup](../administration/reply_by_email_postfix_setup.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 378ab6857b8..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
@@ -563,7 +563,7 @@ Using a self-signed certificate is discouraged but if you must use it follow the
### Enable Reply by email
-See the ["Reply by email" documentation](../incoming_email/README.md) for more information on how to set this up.
+See the ["Reply by email" documentation](../administration/reply_by_email.md) for more information on how to set this up.
### LDAP Authentication
diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md
index eac57bc3de4..6cf93c33ec2 100644
--- a/doc/monitoring/health_check.md
+++ b/doc/monitoring/health_check.md
@@ -1,66 +1 @@
-# Health Check
-
-> [Introduced][ce-3888] in GitLab 8.8.
-
-GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
-endpoint. The health check reports on the overall system status based on the status of
-the database connection, the state of the database migrations, and the ability to write
-and access the cache. This endpoint can be provided to uptime monitoring services like
-[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
-
-## Access Token
-
-An access token needs to be provided while accessing the health check endpoint. The current
-accepted token can be found on the `admin/health_check` page of your GitLab instance.
-
-![access token](img/health_check_token.png)
-
-The access token can be passed as a URL parameter:
-
-```
-https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
-```
-
-or as an HTTP header:
-
-```bash
-curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
-```
-
-## Using the Endpoint
-
-Once you have the access token, health information can be retrieved as plain text, JSON,
-or XML using the `health_check` endpoint:
-
-- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN`
-
-You can also ask for the status of specific services:
-
-- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN`
-
-For example, the JSON output of the following health check:
-
-```bash
-curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
-```
-
-would be like:
-
-```
-{"healthy":true,"message":"success"}
-```
-
-## Status
-
-On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
-will return a valid successful HTTP status code, and a `success` message. Ideally your
-uptime monitoring should look for the success message.
-
-[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
-[pingdom]: https://www.pingdom.com
-[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
-[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
+This document was moved to [user/admin_area/monitoring/health_check](../user/admin_area/monitoring/health_check.md).
diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md
index 771584268d9..a669bb28904 100644
--- a/doc/monitoring/performance/gitlab_configuration.md
+++ b/doc/monitoring/performance/gitlab_configuration.md
@@ -1,40 +1 @@
-# GitLab Configuration
-
-GitLab Performance Monitoring is disabled by default. To enable it and change any of its
-settings, navigate to the Admin area in **Settings > Metrics**
-(`/admin/application_settings`).
-
-The minimum required settings you need to set are the InfluxDB host and port.
-Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
-changes.
-
----
-
-![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
-
----
-
-Finally, a restart of all GitLab processes is required for the changes to take
-effect:
-
-```bash
-# For Omnibus installations
-sudo gitlab-ctl restart
-
-# For installations from source
-sudo service gitlab restart
-```
-
-## Pending Migrations
-
-When any migrations are pending, the metrics are disabled until the migrations
-have been performed.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+This document was moved to [administration/monitoring/performance/gitlab_configuration](../administration/monitoring/performance/gitlab_configuration.md).
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
index 7947b0fedc4..0d4be02ff5f 100644
--- a/doc/monitoring/performance/grafana_configuration.md
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -1,111 +1 @@
-# Grafana Configuration
-
-[Grafana](http://grafana.org/) is a tool that allows you to visualize time
-series metrics through graphs and dashboards. It supports several backend
-data stores, including InfluxDB. GitLab writes performance data to InfluxDB
-and Grafana will allow you to query InfluxDB to display useful graphs.
-
-For the easiest installation and configuration, install Grafana on the same
-server as InfluxDB. For larger installations, you may want to split out these
-services.
-
-## Installation
-
-Grafana supplies package repositories (Yum/Apt) for easy installation.
-See [Grafana installation documentation](http://docs.grafana.org/installation/)
-for detailed steps.
-
-> **Note**: Before starting Grafana for the first time, set the admin user
-and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
-will be `admin`.
-
-## Configuration
-
-Login as the admin user. Expand the menu by clicking the Grafana logo in the
-top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
-in the top bar.
-
-![Grafana empty data source page](img/grafana_data_source_empty.png)
-
-Fill in the configuration details for the InfluxDB data source. Save and
-Test Connection to ensure the configuration is correct.
-
-- **Name**: InfluxDB
-- **Default**: Checked
-- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
-- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
-on a separate server)
-- **Access**: proxy
-- **Database**: gitlab
-- **User**: admin (Or the username configured when setting up InfluxDB)
-- **Password**: The password configured when you set up InfluxDB
-
-![Grafana data source configurations](img/grafana_data_source_configuration.png)
-
-## Apply retention policies and create continuous queries
-
-If you intend to import the GitLab provided Grafana dashboards, you will need to
-set up the right retention policies and continuous queries. The easiest way of
-doing this is by using the [influxdb-management](https://gitlab.com/gitlab-org/influxdb-management)
-repository.
-
-To use this repository you must first clone it:
-
-```
-git clone https://gitlab.com/gitlab-org/influxdb-management.git
-cd influxdb-management
-```
-
-Next you must install the required dependencies:
-
-```
-gem install bundler
-bundle install
-```
-
-Now you must configure the repository by first copying `.env.example` to `.env`
-and then editing the `.env` file to contain the correct InfluxDB settings. Once
-configured you can simply run `bundle exec rake` and the InfluxDB database will
-be configured for you.
-
-For more information see the [influxdb-management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
-
-## Import Dashboards
-
-You can now import a set of default dashboards that will give you a good
-start on displaying useful information. GitLab has published a set of default
-[Grafana dashboards][grafana-dashboards] to get you started. Clone the
-repository or download a zip/tarball, then follow these steps to import each
-JSON file.
-
-Open the dashboard dropdown menu and click 'Import'
-
-![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png)
-
-Click 'Choose file' and browse to the location where you downloaded or cloned
-the dashboard repository. Pick one of the JSON files to import.
-
-![Grafana dashboard import](img/grafana_dashboard_import.png)
-
-Once the dashboard is imported, be sure to click save icon in the top bar. If
-you do not save the dashboard after importing it will be removed when you
-navigate away.
-
-![Grafana save icon](img/grafana_save_icon.png)
-
-Repeat this process for each dashboard you wish to import.
-
-Alternatively you can automatically import all the dashboards into your Grafana
-instance. See the README of the [Grafana dashboards][grafana-dashboards]
-repository for more information on this process.
-
-[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Installation/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
+This document was moved to [administration/monitoring/performance/grafana_configuration](../../administration/monitoring/performance/grafana_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md
index c30cd2950d8..02647de1eb0 100644
--- a/doc/monitoring/performance/influxdb_configuration.md
+++ b/doc/monitoring/performance/influxdb_configuration.md
@@ -1,193 +1 @@
-# InfluxDB Configuration
-
-The default settings provided by [InfluxDB] are not sufficient for a high traffic
-GitLab environment. The settings discussed in this document are based on the
-settings GitLab uses for GitLab.com, depending on your own needs you may need to
-further adjust them.
-
-If you are intending to run InfluxDB on the same server as GitLab, make sure
-you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
-
-Unless you are going with a budget setup, it's advised to run it separately.
-
-## Requirements
-
-- InfluxDB 0.9.5 or newer
-- A fairly modern version of Linux
-- At least 4GB of RAM
-- At least 10GB of storage for InfluxDB data
-
-Note that the RAM and storage requirements can differ greatly depending on the
-amount of data received/stored. To limit the amount of stored data users can
-look into [InfluxDB Retention Policies][influxdb-retention].
-
-## Installation
-
-Installing InfluxDB is out of the scope of this document. Please refer to the
-[InfluxDB documentation].
-
-## InfluxDB Server Settings
-
-Since InfluxDB has many settings that users may wish to customize themselves
-(e.g. what port to run InfluxDB on), we'll only cover the essentials.
-
-The configuration file in question is usually located at
-`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
-InfluxDB needs to be restarted.
-
-### Storage Engine
-
-InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
-storage engine is available, called [TSM Tree]. All users **must** use the new
-`tsm1` storage engine as this [will be the default engine][tsm1-commit] in
-upcoming InfluxDB releases.
-
-Make sure you have the following in your configuration file:
-
-```
-[data]
- dir = "/var/lib/influxdb/data"
- engine = "tsm1"
-```
-
-### Admin Panel
-
-Production environments should have the InfluxDB admin panel **disabled**. This
-feature can be disabled by adding the following to your InfluxDB configuration
-file:
-
-```
-[admin]
- enabled = false
-```
-
-### HTTP
-
-HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana,
-thus it should be enabled. When enabling make sure to _also_ enable
-authentication:
-
-```
-[http]
- enabled = true
- auth-enabled = true
-```
-
-_**Note:** Before you enable authentication, you might want to [create an
-admin user](#create-a-new-admin-user)._
-
-### UDP
-
-GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
-UDP can be done using the following settings:
-
-```
-[[udp]]
- enabled = true
- bind-address = ":8089"
- database = "gitlab"
- batch-size = 1000
- batch-pending = 5
- batch-timeout = "1s"
- read-buffer = 209715200
-```
-
-This does the following:
-
-1. Enable UDP and bind it to port 8089 for all addresses.
-2. Store any data received in the "gitlab" database.
-3. Define a batch of points to be 1000 points in size and allow a maximum of
- 5 batches _or_ flush them automatically after 1 second.
-4. Define a UDP read buffer size of 200 MB.
-
-One of the most important settings here is the UDP read buffer size as if this
-value is set too low, packets will be dropped. You must also make sure the OS
-buffer size is set to the same value, the default value is almost never enough.
-
-To set the OS buffer size to 200 MB, on Linux you can run the following command:
-
-```bash
-sysctl -w net.core.rmem_max=209715200
-```
-
-To make this permanent, add the following to `/etc/sysctl.conf` and restart the
-server:
-
-```bash
-net.core.rmem_max=209715200
-```
-
-It is **very important** to make sure the buffer sizes are large enough to
-handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
-buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
-traffic, users may be able to use a smaller buffer size, but we highly recommend
-using _at least_ 100 MB.
-
-When enabling UDP, users should take care to not expose the port to the public,
-as doing so will allow anybody to write data into your InfluxDB database (as
-[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either
-whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
-allowing traffic from members of said VLAN.
-
-## Create a new admin user
-
-If you want to [enable authentication](#http), you might want to [create an
-admin user][influx-admin]:
-
-```
-influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
-```
-
-## Create the `gitlab` database
-
-Once you get InfluxDB up and running, you need to create a database for GitLab.
-Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
-before creating a database.
-
-_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
-[HTTP authentication](#http), remember to append the username (`-username <username>`)
-and password (`-password <password>`) you set earlier to the commands below._
-
-Run the following command to create a database named `gitlab`:
-
-```bash
-influx -execute 'CREATE DATABASE gitlab'
-```
-
-The name **must** be `gitlab`, do not use any other name.
-
-Next, make sure that the database was successfully created:
-
-```bash
-influx -execute 'SHOW DATABASES'
-```
-
-The output should be similar to:
-
-```
-name: databases
----------------
-name
-_internal
-gitlab
-```
-
-That's it! Now your GitLab instance should send data to InfluxDB.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
-
-[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
-[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
-[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/
-[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
-[influxdb]: https://influxdata.com/time-series-platform/influxdb/
-[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/
-[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d
-[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user
+This document was moved to [administration/monitoring/performance/influxdb_configuration](../administration/monitoring/performance/influxdb_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index eff0e29f58d..a989e323e04 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -1,97 +1 @@
-# InfluxDB Schema
-
-The following measurements are currently stored in InfluxDB:
-
-- `PROCESS_file_descriptors`
-- `PROCESS_gc_statistics`
-- `PROCESS_memory_usage`
-- `PROCESS_method_calls`
-- `PROCESS_object_counts`
-- `PROCESS_transactions`
-- `PROCESS_views`
-- `events`
-
-Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
-process type. In all series, any form of duration is stored in milliseconds.
-
-## PROCESS_file_descriptors
-
-This measurement contains the number of open file descriptors over time. The
-value field `value` contains the number of descriptors.
-
-## PROCESS_gc_statistics
-
-This measurement contains Ruby garbage collection statistics such as the amount
-of minor/major GC runs (relative to the last sampling interval), the time spent
-in garbage collection cycles, and all fields/values returned by `GC.stat`.
-
-## PROCESS_memory_usage
-
-This measurement contains the process' memory usage (in bytes) over time. The
-value field `value` contains the number of bytes.
-
-## PROCESS_method_calls
-
-This measurement contains the methods called during a transaction along with
-their duration, and a name of the transaction action that invoked the method (if
-available). The method call duration is stored in the value field `duration`,
-while the method name is stored in the tag `method`. The tag `action` contains
-the full name of the transaction action. Both the `method` and `action` fields
-are in the following format:
-
-```
-ClassName#method_name
-```
-
-For example, a method called by the `show` method in the `UsersController` class
-would have `action` set to `UsersController#show`.
-
-## PROCESS_object_counts
-
-This measurement is used to store retained Ruby objects (per class) and the
-amount of retained objects. The number of objects is stored in the `count` value
-field while the class name is stored in the `type` tag.
-
-## PROCESS_transactions
-
-This measurement is used to store basic transaction details such as the time it
-took to complete a transaction, how much time was spent in SQL queries, etc. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration` | The total duration of the transaction |
-| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
-| `method_duration` | The total time spent in method calls |
-| `sql_duration` | The total time spent in SQL queries |
-| `view_duration` | The total time spent in views |
-
-## PROCESS_views
-
-This measurement is used to store view rendering timings for a transaction. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration` | The rendering time of the view |
-| `view` | The path of the view, relative to the application's root directory |
-
-The `action` tag contains the action name of the transaction that rendered the
-view.
-
-## events
-
-This measurement is used to store generic events such as the number of Git
-pushes, Emails sent, etc. Each point in this measurement has a single value
-field called `count`. The value of this field is simply set to `1`. Each point
-also has at least one tag: `event`. This tag's value is set to the event name.
-Depending on the event type additional tags may be available as well.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+This document was moved to [administration/monitoring/performance/influxdb_schema](../administration/monitoring/performance/influxdb_schema.md).
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index 79904916b7e..ab3f3ac1664 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -1,65 +1 @@
-# GitLab Performance Monitoring
-
-GitLab comes with its own application performance measuring system as of GitLab
-8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
-Community and Enterprise editions.
-
-Apart from this introduction, you are advised to read through the following
-documents in order to understand and properly configure GitLab Performance Monitoring:
-
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Install/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
-
-## Introduction to GitLab Performance Monitoring
-
-GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
-including (but not limited to):
-
-- The time it took to complete a transaction (a web request or Sidekiq job).
-- The time spent in running SQL queries and rendering HAML views.
-- The time spent executing (instrumented) Ruby methods.
-- Ruby object allocations, and retained objects in particular.
-- System statistics such as the process' memory usage and open file descriptors.
-- Ruby garbage collection statistics.
-
-Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
-data can be visualized using [Grafana][grafana] or any other application that
-supports reading data from InfluxDB. Alternatively data can be queried using the
-InfluxDB CLI.
-
-## Metric Types
-
-Two types of metrics are collected:
-
-1. Transaction specific metrics.
-1. Sampled metrics, collected at a certain interval in a separate thread.
-
-### Transaction Metrics
-
-Transaction metrics are metrics that can be associated with a single
-transaction. This includes statistics such as the transaction duration, timings
-of any executed SQL queries, time spent rendering HAML views, etc. These metrics
-are collected for every Rack request and Sidekiq job processed.
-
-### Sampled Metrics
-
-Sampled metrics are metrics that can't be associated with a single transaction.
-Examples include garbage collection statistics and retained Ruby objects. These
-metrics are collected at a regular interval. This interval is made up out of two
-parts:
-
-1. A user defined interval.
-1. A randomly generated offset added on top of the interval, the same offset
- can't be used twice in a row.
-
-The actual interval can be anywhere between a half of the defined interval and a
-half above the interval. For example, for a user defined interval of 15 seconds
-the actual interval can be anywhere between 7.5 and 22.5. The interval is
-re-generated for every sampling run instead of being generated once and re-used
-for the duration of the process' lifetime.
-
-[influxdb]: https://influxdata.com/time-series-platform/influxdb/
-[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
-[grafana]: http://grafana.org/
+This document was moved to [administration/monitoring/performance/introduction](../administration/monitoring/performance/introduction.md).
diff --git a/doc/operations/README.md b/doc/operations/README.md
index 6a35dab7b6c..58f16aff7bd 100644
--- a/doc/operations/README.md
+++ b/doc/operations/README.md
@@ -1,5 +1 @@
-# GitLab operations
-
-- [Sidekiq MemoryKiller](sidekiq_memory_killer.md)
-- [Cleaning up Redis sessions](cleaning_up_redis_sessions.md)
-- [Understanding Unicorn and unicorn-worker-killer](unicorn.md)
+This document was moved to [administration/operations](../administration/operations.md).
diff --git a/doc/operations/cleaning_up_redis_sessions.md b/doc/operations/cleaning_up_redis_sessions.md
index 93521e976d5..2a1d0a8c8eb 100644
--- a/doc/operations/cleaning_up_redis_sessions.md
+++ b/doc/operations/cleaning_up_redis_sessions.md
@@ -1,52 +1 @@
-# Cleaning up stale Redis sessions
-
-Since version 6.2, GitLab stores web user sessions as key-value pairs in Redis.
-Prior to GitLab 7.3, user sessions did not automatically expire from Redis. If
-you have been running a large GitLab server (thousands of users) since before
-GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
-database after you upgrade to GitLab 7.3. You can also perform a cleanup while
-still running GitLab 7.2 or older, but in that case new stale sessions will
-start building up again after you clean up.
-
-In GitLab versions prior to 7.3.0, the session keys in Redis are 16-byte
-hexadecimal values such as '976aa289e2189b17d7ef525a6702ace9'. Starting with
-GitLab 7.3.0, the keys are
-prefixed with 'session:gitlab:', so they would look like
-'session:gitlab:976aa289e2189b17d7ef525a6702ace9'. Below we describe how to
-remove the keys in the old format.
-
-First we define a shell function with the proper Redis connection details.
-
-```
-rcli() {
- # This example works for Omnibus installations of GitLab 7.3 or newer. For an
- # installation from source you will have to change the socket path and the
- # path to redis-cli.
- sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@"
-}
-
-# test the new shell function; the response should be PONG
-rcli ping
-```
-
-Now we do a search to see if there are any session keys in the old format for
-us to clean up.
-
-```
-# returns the number of old-format session keys in Redis
-rcli keys '*' | grep '^[a-f0-9]\{32\}$' | wc -l
-```
-
-If the number is larger than zero, you can proceed to expire the keys from
-Redis. If the number is zero there is nothing to clean up.
-
-```
-# Tell Redis to expire each matched key after 600 seconds.
-rcli keys '*' | grep '^[a-f0-9]\{32\}$' | awk '{ print "expire", $0, 600 }' | rcli
-# This will print '(integer) 1' for each key that gets expired.
-```
-
-Over the next 15 minutes (10 minutes expiry time plus 5 minutes Redis
-background save interval) your Redis database will be compacted. If you are
-still using GitLab 7.2, users who are not clicking around in GitLab during the
-10 minute expiry window will be signed out of GitLab.
+This document was moved to [administration/operations/cleaning_up_redis_sessions](../administration/operations/cleaning_up_redis_sessions.md).
diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md
index 54adb99386a..c54bca324a5 100644
--- a/doc/operations/moving_repositories.md
+++ b/doc/operations/moving_repositories.md
@@ -1,180 +1 @@
-# Moving repositories managed by GitLab
-
-Sometimes you need to move all repositories managed by GitLab to
-another filesystem or another server. In this document we will look
-at some of the ways you can copy all your repositories from
-`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`.
-
-We will look at three scenarios: the target directory is empty, the
-target directory contains an outdated copy of the repositories, and
-how to deal with thousands of repositories.
-
-**Each of the approaches we list can/will overwrite data in the
-target directory `/mnt/gitlab/repositories`. Do not mix up the
-source and the target.**
-
-## Target directory is empty: use a tar pipe
-
-If the target directory `/mnt/gitlab/repositories` is empty the
-simplest thing to do is to use a tar pipe. This method has low
-overhead and tar is almost always already installed on your system.
-However, it is not possible to resume an interrupted tar pipe: if
-that happens then all data must be copied again.
-
-```
-# As the git user
-tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
- tar -C /mnt/gitlab/repositories -xf -
-```
-
-If you want to see progress, replace `-xf` with `-xvf`.
-
-### Tar pipe to another server
-
-You can also use a tar pipe to copy data to another server. If your
-'git' user has SSH access to the newserver as 'git@newserver', you
-can pipe the data through SSH.
-
-```
-# As the git user
-tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
- ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
-```
-
-If you want to compress the data before it goes over the network
-(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`.
-
-## The target directory contains an outdated copy of the repositories: use rsync
-
-If the target directory already contains a partial / outdated copy
-of the repositories it may be wasteful to copy all the data again
-with tar. In this scenario it is better to use rsync. This utility
-is either already installed on your system or easily installable
-via apt, yum etc.
-
-```
-# As the 'git' user
-rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
- /mnt/gitlab/repositories
-```
-
-The `/.` in the command above is very important, without it you can
-easily get the wrong directory structure in the target directory.
-If you want to see progress, replace `-a` with `-av`.
-
-### Single rsync to another server
-
-If the 'git' user on your source system has SSH access to the target
-server you can send the repositories over the network with rsync.
-
-```
-# As the 'git' user
-rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
- git@newserver:/mnt/gitlab/repositories
-```
-
-## Thousands of Git repositories: use one rsync per repository
-
-Every time you start an rsync job it has to inspect all files in
-the source directory, all files in the target directory, and then
-decide what files to copy or not. If the source or target directory
-has many contents this startup phase of rsync can become a burden
-for your GitLab server. In cases like this you can make rsync's
-life easier by dividing its work in smaller pieces, and sync one
-repository at a time.
-
-In addition to rsync we will use [GNU
-Parallel](http://www.gnu.org/software/parallel/). This utility is
-not included in GitLab so you need to install it yourself with apt
-or yum. Also note that the GitLab scripts we used below were added
-in GitLab 8.1.
-
-** This process does not clean up repositories at the target location that no
-longer exist at the source. ** If you start using your GitLab instance with
-`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos`
-after switching to the new repository storage directory.
-
-### Parallel rsync for all repositories known to GitLab
-
-This will sync repositories with 10 rsync processes at a time. We keep
-track of progress so that the transfer can be restarted if necessary.
-
-First we create a new directory, owned by 'git', to hold transfer
-logs. We assume the directory is empty before we start the transfer
-procedure, and that we are the only ones writing files in it.
-
-```
-# Omnibus
-sudo mkdir /var/opt/gitlab/transfer-logs
-sudo chown git:git /var/opt/gitlab/transfer-logs
-
-# Source
-sudo -u git -H mkdir /home/git/transfer-logs
-```
-
-We seed the process with a list of the directories we want to copy.
-
-```
-# Omnibus
-sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt'
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt'
-```
-
-Now we can start the transfer. The command below is idempotent, and
-the number of jobs done by GNU Parallel should converge to zero. If it
-does not some repositories listed in all-repos-1234.txt may have been
-deleted/renamed before they could be copied.
-
-```
-# Omnibus
-sudo -u git sh -c '
-cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\
- /usr/bin/env JOBS=10 \
- /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
- /var/opt/gitlab/transfer-logs/success-$(date +%s).log \
- /var/opt/gitlab/git-data/repositories \
- /mnt/gitlab/repositories
-'
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H sh -c '
-cat /home/git/transfer-logs/* | sort | uniq -u |\
- /usr/bin/env JOBS=10 \
- bin/parallel-rsync-repos \
- /home/git/transfer-logs/success-$(date +%s).log \
- /home/git/repositories \
- /mnt/gitlab/repositories
-`
-```
-
-### Parallel rsync only for repositories with recent activity
-
-Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
-Then you might only want to sync repositories that were changed via GitLab
-_after_ that time. You can use the 'SINCE' variable to tell 'rake
-gitlab:list_repos' to only print repositories with recent activity.
-
-```
-# Omnibus
-sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
- sudo -u git \
- /usr/bin/env JOBS=10 \
- /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
- success-$(date +%s).log \
- /var/opt/gitlab/git-data/repositories \
- /mnt/gitlab/repositories
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
- sudo -u git -H \
- /usr/bin/env JOBS=10 \
- bin/parallel-rsync-repos \
- success-$(date +%s).log \
- /home/git/repositories \
- /mnt/gitlab/repositories
-```
+This document was moved to [administration/operations/moving_repositories](../administration/operations/moving_repositories.md).
diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md
index b5e78348989..cf7c3b2e2ed 100644
--- a/doc/operations/sidekiq_memory_killer.md
+++ b/doc/operations/sidekiq_memory_killer.md
@@ -1,40 +1 @@
-# Sidekiq MemoryKiller
-
-The GitLab Rails application code suffers from memory leaks. For web requests
-this problem is made manageable using
-[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which
-restarts Unicorn worker processes in between requests when needed. The Sidekiq
-MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
-to process background jobs.
-
-Unlike unicorn-worker-killer, which is enabled by default for all GitLab
-installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default
-_only_ for Omnibus packages. The reason for this is that the MemoryKiller
-relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab
-installations from source do not all use Runit or an equivalent.
-
-With the default settings, the MemoryKiller will cause a Sidekiq restart no
-more often than once every 15 minutes, with the restart causing about one
-minute of delay for incoming background jobs.
-
-## Configuring the MemoryKiller
-
-The MemoryKiller is controlled using environment variables.
-
-- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is
- greater than 0, then after each Sidekiq job, the MemoryKiller will check the
- RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq
- process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a
- delayed shutdown is triggered. The default value for Omnibus packages is set
- [in the omnibus-gitlab
- repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
- a shutdown is triggered, the Sidekiq process will keep working normally for
- another 15 minutes.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
- time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs.
- Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
- Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
- restart Sidekiq.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
- the final signal sent to the Sidekiq process when we want it to shut down.
+This document was moved to [administration/operations/sidekiq_memory_killer](../administration/operations/sidekiq_memory_killer.md).
diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md
index bad61151bda..fbc9697b755 100644
--- a/doc/operations/unicorn.md
+++ b/doc/operations/unicorn.md
@@ -1,86 +1 @@
-# Understanding Unicorn and unicorn-worker-killer
-
-## Unicorn
-
-GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web
-server, to handle web requests (web browsers and Git HTTP clients). Unicorn is
-a daemon written in Ruby and C that can load and run a Ruby on Rails
-application; in our case the Rails application is GitLab Community Edition or
-GitLab Enterprise Edition.
-
-Unicorn has a multi-process architecture to make better use of available CPU
-cores (processes can run on different cores) and to have stronger fault
-tolerance (most failures stay isolated in only one process and cannot take down
-GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby
-environment with the GitLab application code, and then spawns 'workers' which
-inherit this clean initial environment. The 'master' never handles any
-requests, that is left to the workers. The operating system network stack
-queues incoming requests and distributes them among the workers.
-
-In a perfect world, the master would spawn its pool of workers once, and then
-the workers handle incoming web requests one after another until the end of
-time. In reality, worker processes can crash or time out: if the master notices
-that a worker takes too long to handle a request it will terminate the worker
-process with SIGKILL ('kill -9'). No matter how the worker process ended, the
-master process will replace it with a new 'clean' process again. Unicorn is
-designed to be able to replace 'crashed' workers without dropping user
-requests.
-
-This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The
-master process has PID 56227 below.
-
-```
-[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing
-[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10
-[2015-06-05T10:58:08.708141 #62538] INFO -- : worker=10 spawned pid=62538
-[2015-06-05T10:58:08.708824 #62538] INFO -- : worker=10 ready
-```
-
-### Tunables
-
-The main tunables for Unicorn are the number of worker processes and the
-request timeout after which the Unicorn master terminates a worker process.
-See the [omnibus-gitlab Unicorn settings
-documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md)
-if you want to adjust these settings.
-
-## unicorn-worker-killer
-
-GitLab has memory leaks. These memory leaks manifest themselves in long-running
-processes, such as Unicorn workers. (The Unicorn master process is not known to
-leak memory, probably because it does not handle user requests.)
-
-To make these memory leaks manageable, GitLab comes with the
-[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
-gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
-workers to do a memory self-check after every 16 requests. If the memory of the
-Unicorn worker exceeds a pre-set limit then the worker process exits. The
-Unicorn master then automatically replaces the worker process.
-
-This is a robust way to handle memory leaks: Unicorn is designed to handle
-workers that 'crash' so no user requests will be dropped. The
-unicorn-worker-killer gem is designed to only terminate a worker process _in
-between requests_, so no user requests are affected.
-
-This is what a Unicorn worker memory restart looks like in unicorn_stderr.log.
-You see that worker 4 (PID 125918) is inspecting itself and decides to exit.
-The threshold memory value was 254802235 bytes, about 250MB. With GitLab this
-threshold is a random value between 200 and 250 MB. The master process (PID
-117565) then reaps the worker process and spawns a new 'worker 4' with PID
-127549.
-
-```
-[2015-06-05T12:07:41.828374 #125918] WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes)
-[2015-06-05T12:07:41.828472 #125918] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1)
-[2015-06-05T12:07:42.025916 #117565] INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4
-[2015-06-05T12:07:42.034527 #127549] INFO -- : worker=4 spawned pid=127549
-[2015-06-05T12:07:42.035217 #127549] INFO -- : worker=4 ready
-```
-
-One other thing that stands out in the log snippet above, taken from
-GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
-is a normal value for our current GitLab.com setup and traffic.
-
-The high frequency of Unicorn memory restarts on some GitLab sites can be a
-source of confusion for administrators. Usually they are a [red
-herring](https://en.wikipedia.org/wiki/Red_herring).
+This document was moved to [administration/operations/unicorn](../administration/operations/unicorn.md).
diff --git a/doc/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/user_management.md b/doc/raketasks/user_management.md
index 8a5e2d6e16b..044b104f5c2 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -70,3 +70,18 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
# installation from source
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
+
+## Clear authentication tokens for all users. Important! Data loss!
+
+Clear authentication tokens for all users in the GitLab database. This
+task is useful if your users' authentication tokens might have been exposed in
+any way. All the existing tokens will become invalid, and new tokens are
+automatically generated upon sign-in or user modification.
+
+```
+# omnibus-gitlab
+sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
+
+# installation from source
+bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
+```
diff --git a/doc/university/README.md b/doc/university/README.md
index 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/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/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
new file mode 100644
index 00000000000..eac57bc3de4
--- /dev/null
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -0,0 +1,66 @@
+# Health Check
+
+> [Introduced][ce-3888] in GitLab 8.8.
+
+GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
+endpoint. The health check reports on the overall system status based on the status of
+the database connection, the state of the database migrations, and the ability to write
+and access the cache. This endpoint can be provided to uptime monitoring services like
+[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
+
+## Access Token
+
+An access token needs to be provided while accessing the health check endpoint. The current
+accepted token can be found on the `admin/health_check` page of your GitLab instance.
+
+![access token](img/health_check_token.png)
+
+The access token can be passed as a URL parameter:
+
+```
+https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
+```
+
+or as an HTTP header:
+
+```bash
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+## Using the Endpoint
+
+Once you have the access token, health information can be retrieved as plain text, JSON,
+or XML using the `health_check` endpoint:
+
+- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN`
+
+You can also ask for the status of specific services:
+
+- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN`
+
+For example, the JSON output of the following health check:
+
+```bash
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+would be like:
+
+```
+{"healthy":true,"message":"success"}
+```
+
+## Status
+
+On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
+will return a valid successful HTTP status code, and a `success` message. Ideally your
+uptime monitoring should look for the success message.
+
+[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
+[pingdom]: https://www.pingdom.com
+[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
+[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
diff --git a/doc/monitoring/img/health_check_token.png b/doc/user/admin_area/monitoring/img/health_check_token.png
index 2d7c82a65a8..2d7c82a65a8 100644
--- a/doc/monitoring/img/health_check_token.png
+++ b/doc/user/admin_area/monitoring/img/health_check_token.png
Binary files differ
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index c0dc80325b6..d6216a8dd50 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -32,6 +32,7 @@ The following table depicts the various user permission levels in a project.
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
+| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index c16058165d7..1892ccabb70 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -3,8 +3,8 @@
> [Introduced][ce-5986] in GitLab 8.12.
>
> **Note:**
-This the first iteration of Cycle Analytics, you can follow the following issue
-to track the changes that are coming to this feature: [#20975][ce-20975].
+There are more changes coming to Cycle Analytics, you can follow the following
+issue to track the changes to this feature: [#20975][ce-20975].
Cycle Analytics measures the time it takes to go from an [idea to production] for
each project you have. This is achieved by not only indicating the total time it
@@ -48,13 +48,12 @@ You can see that there are seven stages in total:
## How the data is measured
-Cycle Analytics records cycle time so only data on the issues that have been
-deployed to production are measured. In case you just started a new project and
-you have not pushed anything to production, then you will not be able to
-properly see the Cycle Analytics of your project.
+Cycle Analytics records cycle time and data based on the project issues with the
+exception of the staging and production stages, where only data deployed to
+production are measured.
Specifically, if your CI is not set up and you have not defined a `production`
-[environment], then you will not have any data.
+[environment], then you will not have any data for those stages.
Below you can see in more detail what the various stages of Cycle Analytics mean.
@@ -76,9 +75,8 @@ Here's a little explanation of how this works behind the scenes:
`<issue, merge request>` pair, the merge request has the [issue closing pattern]
for the corresponding issue. All other issues and merge requests are **not**
considered.
-1. Then the <issue, merge request> pairs are filtered out. Any merge request
- that has **not** been deployed to production in the last XX days (specified
- by the UI - default is 90 days) prohibits these pairs from being considered.
+1. Then the <issue, merge request> pairs are filtered out by last XX days (specified
+ by the UI - default is 90 days). So it prohibits these pairs from being considered.
1. For the remaining `<issue, merge request>` pairs, we check the information that
we need for the stages, like issue creation date, merge request merge time,
etc.
@@ -86,8 +84,8 @@ Here's a little explanation of how this works behind the scenes:
To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
So, if a merge request doesn't close an issue or an issue is not labeled with a
label present in the Issue Board or assigned a milestone or a project has no
-`production` environment, the Cycle Analytics dashboard won't present any data
-at all.
+`production` environment (for staging and production stages), the Cycle Analytics
+dashboard won't present any data at all.
## Example workflow
diff --git a/doc/user/project/git_attributes.md b/doc/user/project/git_attributes.md
new file mode 100644
index 00000000000..21ef94e61f7
--- /dev/null
+++ b/doc/user/project/git_attributes.md
@@ -0,0 +1,22 @@
+# Git Attributes
+
+GitLab supports defining custom [Git attributes][gitattributes] such as what
+files to treat as binary, and what language to use for syntax highlighting
+diffs.
+
+To define these attributes, create a file called `.gitattributes` in the root
+directory of your repository and push it to the default branch of your project.
+
+## Encoding Requirements
+
+The `.gitattributes` file _must_ be encoded in UTF-8 and _must not_ contain a
+Byte Order Mark. If a different encoding is used, the file's contents will be
+ignored.
+
+## Syntax Highlighting
+
+The `.gitattributes` file can be used to define which language to use when
+syntax highlighting files and diffs. See ["Syntax
+Highlighting"](highlighting.md) for more information.
+
+[gitattributes]: https://git-scm.com/docs/gitattributes
diff --git a/doc/user/project/merge_requests/img/versions-compare.png b/doc/user/project/merge_requests/img/versions_compare.png
index 890cae7768c..890cae7768c 100644
--- a/doc/user/project/merge_requests/img/versions-compare.png
+++ b/doc/user/project/merge_requests/img/versions_compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions-dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png
index 9bab9304e14..9bab9304e14 100644
--- a/doc/user/project/merge_requests/img/versions-dropdown.png
+++ b/doc/user/project/merge_requests/img/versions_dropdown.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions_system_note.png b/doc/user/project/merge_requests/img/versions_system_note.png
new file mode 100644
index 00000000000..7c9d7715745
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions_system_note.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md
index 011f9cbc381..c138061fd40 100644
--- a/doc/user/project/merge_requests/merge_when_build_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md
@@ -1,16 +1,16 @@
# Merge When Build Succeeds
When reviewing a merge request that looks ready to merge but still has one or
-more CI builds running, you can set it to be merged automatically when all
-builds succeed. This way, you don't have to wait for the builds to finish and
-remember to merge the request manually.
+more CI builds running, you can set it to be merged automatically when the
+builds pipeline succeed. This way, you don't have to wait for the builds to
+finish and remember to merge the request manually.
![Enable](img/merge_when_build_succeeds_enable.png)
When you hit the "Merge When Build Succeeds" button, the status of the merge
request will be updated to represent the impending merge. If you cannot wait
-for the build to succeed and want to merge immediately, this option is available
-in the dropdown menu on the right of the main button.
+for the pipeline to succeed and want to merge immediately, this option is
+available in the dropdown menu on the right of the main button.
Both team developers and the author of the merge request have the option to
cancel the automatic merge if they find a reason why it shouldn't be merged
@@ -18,9 +18,9 @@ after all.
![Status](img/merge_when_build_succeeds_status.png)
-When the build succeeds, the merge request will automatically be merged. When
-the build fails, the author gets a chance to retry any failed builds, or to
-push new commits to fix the failure.
+When the pipeline succeeds, the merge request will automatically be merged.
+When the pipeline fails, the author gets a chance to retry any failed builds,
+or to push new commits to fix the failure.
When the builds are retried and succeed on the second try, the merge request
will automatically be merged after all. When the merge request is updated with
@@ -40,7 +40,7 @@ hit **Save** for the changes to take effect.
![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
-From now on, every time the build fails you will not be able to merge the merge
-request from the UI, until you make the build pass.
+From now on, every time the pipelinefails you will not be able to merge the
+merge request from the UI, until you make all relevant builds pass.
![Only allow merge if build succeeds msg](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index 2805fdf635c..77eab7ba5e3 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -7,26 +7,36 @@ of merge request diff is created. When you visit a merge request that contains
more than one pushes, you can select and compare the versions of those merge
request diffs.
-![Merge Request Versions](img/versions.png)
+![Merge request versions](img/versions.png)
+
+---
By default, the latest version of changes is shown. However, you
can select an older one from version dropdown.
-![Merge Request Versions](img/versions-dropdown.png)
+![Merge request versions dropdown](img/versions_dropdown.png)
+
+---
-You can also compare the merge request version with older one to see what is
+You can also compare the merge request version with an older one to see what has
changed since then.
-![Merge Request Versions](img/versions-compare.png)
+![Merge request versions compare](img/versions_compare.png)
+
+---
+
+Every time you push new changes to the branch, a link to compare the last
+changes appears as a system note.
-Please note that comments are disabled while viewing outdated merge versions
-or comparing to versions other than base.
+![Merge request versions system note](img/versions_system_note.png)
---
->**Note:**
-Merge request versions are based on push not on commit. So, if you pushed 5
-commits in a single push, it will be a single option in the dropdown. If you
-pushed 5 times, that will count for 5 options.
+>**Notes:**
+- Comments are disabled while viewing outdated merge versions or comparing to
+ versions other than base.
+- Merge request versions are based on push not on commit. So, if you pushed 5
+ commits in a single push, it will be a single option in the dropdown. If you
+ pushed 5 times, that will count for 5 options.
[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 5253825d507..8827b501901 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
@@ -286,7 +301,11 @@ test:
- docker run $CI_REGISTRY/group/other-project:latest
```
-[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
[build permissions]: ../permissions.md#builds-permissions
+[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
[ext]: ../permissions.md#external-users
+[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+[https]: ../admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols
[triggers]: ../../ci/triggers/README.md
+[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
+[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
diff --git a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
new file mode 100644
index 00000000000..b0a63ddf0ab
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
Binary files differ
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 993c6bfb7e9..675e89e4247 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -97,11 +97,11 @@ There are multiple ways to create a branch from GitLab's web interface.
In case your development workflow dictates to have an issue for every merge
request, you can quickly create a branch right on the issue page which will be
-tied with the issue itself. You can see a **New Branch** button after the issue
+tied with the issue itself. You can see a **New branch** button after the issue
description, unless there is already a branch with the same name or a referenced
merge request.
-![New Branch Button](img/new_branch_from_issue.png)
+![New Branch Button](img/web_editor_new_branch_from_issue.png)
Once you click it, a new branch will be created that diverges from the default
branch of your project, by default `master`. The branch name will be based on
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 7c0eb90d540..2215f37b81a 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -228,7 +228,7 @@ We'll discuss the three reasons to merge in master: leveraging code, merge confl
If you need to leverage some code that was introduced in master after you created the feature branch you can sometimes solve this by just cherry-picking a commit.
If your feature branch has a merge conflict, creating a merge commit is a normal way of solving this.
You can prevent some merge conflicts by using [gitattributes](http://git-scm.com/docs/gitattributes) for files that can be in a random order.
-For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG merge=union` so that there are fewer merge conflicts in it.
+For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG.md merge=union` so that there are fewer merge conflicts in it.
The last reason for creating merge commits is having long lived branches that you want to keep up to date with the latest state of the project.
Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI).
At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit.
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 4828bb5dce6..423b095e69e 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -4,6 +4,112 @@ Subversion (SVN) is a central version control system (VCS) while
Git is a distributed version control system. There are some major differences
between the two, for more information consult your favorite search engine.
+## Overview
+
+There are two approaches to SVN to Git migration:
+
+1. [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which:
+ - Makes the GitLab repository to mirror the SVN project.
+ - Git and SVN repositories are kept in sync; you can use either one.
+ - Smoothens the migration process and allows to manage migration risks.
+
+1. [Cut over migration](#cut-over-migration-with-svn2git) which:
+ - Translates and imports the existing data and history from SVN to Git.
+ - Is a fire and forget approach, good for smaller teams.
+
+## Smooth migration with a Git/SVN mirror using SubGit
+
+[SubGit](https://subgit.com) is a tool for a smooth, stress-free SVN to Git
+migration. It creates a writable Git mirror of a local or remote Subversion
+repository and that way you can use both Subversion and Git as long as you like.
+It requires access to your GitLab server as it talks with the Git repositories
+directly in a filesystem level.
+
+### SubGit prerequisites
+
+1. Install Oracle JRE 1.8 or newer. On Debian-based Linux distributions you can
+ follow [this article](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html).
+1. Download SubGit from https://subgit.com/download/.
+1. Unpack the downloaded SubGit zip archive to the `/opt` directory. The `subgit`
+ command will be available at `/opt/subgit-VERSION/bin/subgit`.
+
+### SubGit configuration
+
+The first step to mirror you SVN repository in GitLab is to create a new empty
+project which will be used as a mirror. For Omnibus installations the path to
+the repository will be located at
+`/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For
+installations from source, the default repository directory will be
+`/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a
+variable:
+
+```
+GIT_REPO_PATH=/var/opt/gitlab/git-data/repositories/USER/REPOS.git
+```
+
+SubGit will keep this repository in sync with a remote SVN project. For
+convenience, assign your remote SVN project URL to a variable:
+
+```
+SVN_PROJECT_URL=http://svn.company.com/repos/project
+```
+
+Next you need to run SubGit to set up a Git/SVN mirror. Make sure the following
+`subgit` command is ran on behalf of the same user that keeps ownership of
+GitLab Git repositories (by default `git`):
+
+```
+subgit configure --layout auto $SVN_PROJECT_URL $GIT_REPO_PATH
+```
+
+Adjust authors and branches mappings, if necessary. Open with your favorite
+text editor:
+
+```
+edit $GIT_REPO_PATH/subgit/authors.txt
+edit $GIT_REPO_PATH/subgit/config
+```
+
+For more information regarding the SubGit configuration options, refer to
+[SubGit's documentation](https://subgit.com/documentation.html) website.
+
+### Initial translation
+
+Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
+initial translation of existing SVN revisions into the Git repository:
+
+```
+subgit install $GIT_REPOS_PATH
+```
+
+After the initial translation is completed, the Git repository and the SVN
+project will be kept in sync by `subgit` - new Git commits will be translated to
+SVN revisions and new SVN revisions will be translated to Git commits. Mirror
+works transparently and does not require any special commands.
+
+If you would prefer to perform one-time cut over migration with `subgit`, use
+the `import` command instead of `install`:
+
+```
+subgit import $GIT_REPO_PATH
+```
+
+### SubGit licensing
+
+Running SubGit in a mirror mode requires a
+[registration](https://subgit.com/pricing.html). Registration is free for open
+source, academic and startup projects.
+
+We're currently working on deeper GitLab/SubGit integration. You may track our
+progress at [this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/990).
+
+### SubGit support
+
+For any questions related to SVN to GitLab migration with SubGit, you can
+contact the SubGit team directly at [support@subgit.com](mailto:support@subgit.com).
+
+## Cut over migration with svn2git
+
If you are currently using an SVN repository, you can migrate the repository
to Git and GitLab. We recommend a hard cut over - run the migration command once
and then have all developers start using the new GitLab repository immediately.
@@ -75,5 +181,3 @@ git push --tags origin
## Contribute to this guide
We welcome all contributions that would expand this guide with instructions on
how to migrate from SVN and other version control systems.
-
-
diff --git a/docker/README.md b/docker/README.md
index ee1f32adc26..f9e12c5733b 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -2,6 +2,6 @@
* The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/).
* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/).
-* The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/)
+* The complete usage guide can be found in [Using GitLab Docker images](https://docs.gitlab.com/omnibus/docker/)
* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
-* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#build-docker-image)
+* Check the guide for [creating Omnibus-based Docker Image](https://docs.gitlab.com/omnibus/build/README.html#build-docker-image)
diff --git a/features/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/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/merge_requests.rb b/features/steps/project/merge_requests.rb
index 4a67cf06fba..44346d99f44 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -7,6 +7,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
include SharedMarkdown
include SharedDiffNote
include SharedUser
+ include WaitForAjax
step 'I click link "New Merge Request"' do
click_link "New Merge Request"
@@ -90,6 +91,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 +116,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 +139,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 +511,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..d3b5b0bdebe 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -1,5 +1,8 @@
+require Rails.root.join('features/support/wait_for_ajax')
+
module SharedNote
include Spinach::DSL
+ include WaitForAjax
step 'I delete a comment' do
page.within('.main-notes-list') do
@@ -116,8 +119,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/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/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/lib/api/api.rb b/lib/api/api.rb
index 0bbf73a1b63..67109ceeef9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -31,11 +31,12 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::AwardEmoji
+ mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
mount ::API::Builds
- mount ::API::CommitStatuses
mount ::API::Commits
+ mount ::API::CommitStatuses
mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments
@@ -43,22 +44,20 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
- mount ::API::Boards
mount ::API::Keys
mount ::API::Labels
- mount ::API::LicenseTemplates
mount ::API::Lint
mount ::API::Members
- mount ::API::MergeRequests
mount ::API::MergeRequestDiffs
+ mount ::API::MergeRequests
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
mount ::API::Pipelines
mount ::API::ProjectHooks
- mount ::API::ProjectSnippets
mount ::API::Projects
+ mount ::API::ProjectSnippets
mount ::API::Repositories
mount ::API::Runners
mount ::API::Services
@@ -73,5 +72,10 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
+ mount ::API::Version
+
+ route :any, '*path' do
+ error!('404 Not Found', 404)
+ end
end
end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 4d5d144a02e..b14dd4f6e83 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -3,18 +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.board], with: Entities::Board
+ 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.board
- if params[:board_id].to_i == board.id
+ board = user_project.boards.first
+
+ if params[:board_id] == board.id
board
else
not_found!('Board')
@@ -26,37 +36,45 @@ 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)
end
authorize!(:admin_list, user_project)
- list = ::Boards::Lists::CreateService.new(user_project, current_user,
- { label_id: params[:label_id] }).execute
+ service = ::Boards::Lists::CreateService.new(user_project, current_user,
+ { label_id: params[:label_id] })
+
+ list = service.execute(project_board)
if list.valid?
present list, with: Entities::List
@@ -65,48 +83,47 @@ 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)
- moved = ::Boards::Lists::MoveService.new(user_project, current_user,
- { position: params[:position].to_i }).execute(list)
+ service = ::Boards::Lists::MoveService.new(user_project, current_user,
+ { position: params[:position] })
- if moved
+ if service.execute(list)
present list, with: Entities::List
else
render_api_error!({ error: "List could not be moved!" }, 400)
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
- list = board_lists.find_by(id: params[:list_id])
-
authorize!(:admin_list, user_project)
- if list
- destroyed_list = ::Boards::Lists::DestroyService.new(
- user_project, current_user).execute(list)
- present destroyed_list, with: Entities::List
+ list = board_lists.find(params[:list_id])
+
+ service = ::Boards::Lists::DestroyService.new(user_project, current_user)
+
+ if service.execute(list)
+ present list, with: Entities::List
else
- not_found!('List')
+ render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
end
diff --git a/lib/api/license_templates.rb b/lib/api/license_templates.rb
deleted file mode 100644
index d0552299ed0..00000000000
--- a/lib/api/license_templates.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-module API
- # License Templates API
- class LicenseTemplates < Grape::API
- PROJECT_TEMPLATE_REGEX =
- /[\<\{\[]
- (project|description|
- one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]/xi.freeze
- YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
- FULLNAME_TEMPLATE_REGEX =
- /[\<\{\[]
- (fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]/xi.freeze
-
- # Get the list of the available license templates
- #
- # Parameters:
- # popular - Filter licenses to only the popular ones
- #
- # Example Request:
- # GET /licenses
- # GET /licenses?popular=1
- get 'licenses' do
- options = {
- featured: params[:popular].present? ? true : nil
- }
- present Licensee::License.all(options), with: Entities::RepoLicense
- end
-
- # Get text for specific license
- #
- # Parameters:
- # key (required) - The key of a license
- # project - Copyrighted project name
- # fullname - Full name of copyright holder
- #
- # Example Request:
- # GET /licenses/mit
- #
- get 'licenses/:key', requirements: { key: /[\w\.-]+/ } do
- required_attributes! [:key]
-
- not_found!('License') unless Licensee::License.find(params[:key])
-
- # We create a fresh Licensee::License object since we'll modify its
- # content in place below.
- license = Licensee::License.new(params[:key])
-
- license.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
- license.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
-
- fullname = params[:fullname].presence || current_user.try(:name)
- license.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
-
- present license, with: Entities::RepoLicense
- end
- end
-end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index c24e8e8bd9b..da16e24d7ea 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -416,6 +416,12 @@ module API
required_attributes! [:group_id, :group_access]
attrs = attributes_for_keys [:group_id, :group_access, :expires_at]
+ group = Group.find_by_id(attrs[:group_id])
+
+ unless group && can?(current_user, :read_group, group)
+ not_found!('Group')
+ end
+
unless user_project.allowed_to_share_with_group?
return render_api_error!("The project sharing with group is disabled", 400)
end
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 22b8f90dc5c..2e76b91051f 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,20 +45,20 @@ 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
+ hook = SystemHook.find(params[:id])
+ present hook.destroy, with: Entities::Hook
rescue
# SystemHook raises an Error if no hook with id found
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index b9e718147e1..8a53d9c0095 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,39 +1,115 @@
module API
class Templates < Grape::API
GLOBAL_TEMPLATE_TYPES = {
- gitignores: Gitlab::Template::GitignoreTemplate,
- gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
+ gitignores: {
+ klass: Gitlab::Template::GitignoreTemplate,
+ gitlab_version: 8.8
+ },
+ gitlab_ci_ymls: {
+ klass: Gitlab::Template::GitlabCiYmlTemplate,
+ gitlab_version: 8.9
+ }
}.freeze
+ PROJECT_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (project|description|
+ one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+ [\>\}\]]/xi.freeze
+ YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+ FULLNAME_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (fullname|name\sof\s(author|copyright\sowner))
+ [\>\}\]]/xi.freeze
+ DEPRECATION_MESSAGE = ' This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
helpers do
+ def parsed_license_template
+ # We create a fresh Licensee::License object since we'll modify its
+ # content in place below.
+ template = Licensee::License.new(params[:name])
+
+ template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
+ template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
+
+ fullname = params[:fullname].presence || current_user.try(:name)
+ template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
+ template
+ end
+
def render_response(template_type, template)
not_found!(template_type.to_s.singularize) unless template
present template, with: Entities::Template
end
end
- GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
- # Get the list of the available template
- #
- # Example Request:
- # GET /gitignores
- # GET /gitlab_ci_ymls
- get template_type.to_s do
- present klass.all, with: Entities::TemplatesList
- end
-
- # Get the text for a specific template present in local filesystem
- #
- # Parameters:
- # name (required) - The name of a template
- #
- # Example Request:
- # GET /gitignores/Elixir
- # GET /gitlab_ci_ymls/Ruby
- get "#{template_type}/:name" do
- required_attributes! [:name]
- new_template = klass.find(params[:name])
- render_response(template_type, new_template)
+ { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status|
+ desc 'Get the list of the available license template' do
+ detailed_desc = 'This feature was introduced in GitLab 8.7.'
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::RepoLicense
+ end
+ params do
+ optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
+ end
+ get route do
+ options = {
+ featured: declared(params).popular.present? ? true : nil
+ }
+ present Licensee::License.all(options), with: Entities::RepoLicense
+ end
+ end
+
+ { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status|
+ desc 'Get the text for a specific license' do
+ detailed_desc = 'This feature was introduced in GitLab 8.7.'
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::RepoLicense
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get route, requirements: { name: /[\w\.-]+/ } do
+ not_found!('License') unless Licensee::License.find(declared(params).name)
+
+ template = parsed_license_template
+
+ present template, with: Entities::RepoLicense
+ end
+ end
+
+ GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
+ klass = properties[:klass]
+ gitlab_version = properties[:gitlab_version]
+
+ { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status|
+ desc 'Get the list of the available template' do
+ detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::TemplatesList
+ end
+ get route do
+ present klass.all, with: Entities::TemplatesList
+ end
+ end
+
+ { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status|
+ desc 'Get the text for a specific template present in local filesystem' do
+ detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success Entities::Template
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get route do
+ new_template = klass.find(declared(params).name)
+
+ render_response(template_type, new_template)
+ end
end
end
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 19df13d8aac..832b04a3bb1 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -8,18 +8,19 @@ module API
'issues' => ->(id) { find_project_issue(id) }
}
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
ISSUABLE_TYPES.each do |type, finder|
type_id_str = "#{type.singularize}_id".to_sym
- # Create a todo on an issuable
- #
- # Parameters:
- # id (required) - The ID of a project
- # issuable_id (required) - The ID of an issuable
- # Example Request:
- # POST /projects/:id/issues/:issuable_id/todo
- # POST /projects/:id/merge_requests/:issuable_id/todo
+ desc 'Create a todo on an issuable' do
+ success Entities::Todo
+ end
+ params do
+ requires type_id_str, type: Integer, desc: 'The ID of an issuable'
+ end
post ":id/#{type}/:#{type_id_str}/todo" do
issuable = instance_exec(params[type_id_str], &finder)
todo = TodoService.new.mark_todo(issuable, current_user).first
@@ -40,25 +41,21 @@ module API
end
end
- # Get a todo list
- #
- # Example Request:
- # GET /todos
- #
+ desc 'Get a todo list' do
+ success Entities::Todo
+ end
get do
todos = find_todos
present paginate(todos), with: Entities::Todo, current_user: current_user
end
- # Mark a todo as done
- #
- # Parameters:
- # id: (required) - The ID of the todo being marked as done
- #
- # Example Request:
- # DELETE /todos/:id
- #
+ desc 'Mark a todo as done' do
+ success Entities::Todo
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
+ end
delete ':id' do
todo = current_user.todos.find(params[:id])
TodoService.new.mark_todos_as_done([todo], current_user)
@@ -66,11 +63,7 @@ module API
present todo.reload, with: Entities::Todo, current_user: current_user
end
- # Mark all todos as done
- #
- # Example Request:
- # DELETE /todos
- #
+ desc 'Mark all todos as done'
delete do
todos = find_todos
TodoService.new.mark_todos_as_done(todos, current_user)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 18c4cad09ae..e868f628404 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -321,6 +321,26 @@ module API
user.activate
end
end
+
+ desc 'Get contribution events of a specified user' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ success Entities::Event
+ end
+ params do
+ requires :id, type: String, desc: 'The user ID'
+ end
+ get ':id/events' do
+ user = User.find_by(id: declared(params).id)
+ not_found!('User') unless user
+
+ events = user.recent_events.
+ merge(ProjectsFinder.new.execute(current_user)).
+ references(:project).
+ with_associations.
+ page(params[:page])
+
+ present paginate(events), with: Entities::Event
+ end
end
resource :user do
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f6495071a11..b9fb3c21dbb 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -4,27 +4,29 @@ module API
before { authenticate! }
before { authorize! :admin_build, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
resource :projects do
- # Get project variables
- #
- # Parameters:
- # id (required) - The ID of a project
- # page (optional) - The page number for pagination
- # per_page (optional) - The value of items per page to show
- # Example Request:
- # GET /projects/:id/variables
+ desc 'Get project variables' do
+ success Entities::Variable
+ end
+ params do
+ optional :page, type: Integer, desc: 'The page number for pagination'
+ optional :per_page, type: Integer, desc: 'The value of items per page to show'
+ end
get ':id/variables' do
variables = user_project.variables
present paginate(variables), with: Entities::Variable
end
- # Get specific variable of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (required) - The `key` of variable
- # Example Request:
- # GET /projects/:id/variables/:key
+ desc 'Get a specific variable from a project' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
get ':id/variables/:key' do
key = params[:key]
variable = user_project.variables.find_by(key: key.to_s)
@@ -34,18 +36,15 @@ module API
present variable, with: Entities::Variable
end
- # Create a new variable in project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (required) - The key of variable
- # value (required) - The value of variable
- # Example Request:
- # POST /projects/:id/variables
+ desc 'Create a new variable in a project' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ requires :value, type: String, desc: 'The value of the variable'
+ end
post ':id/variables' do
- required_attributes! [:key, :value]
-
- variable = user_project.variables.create(key: params[:key], value: params[:value])
+ variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h)
if variable.valid?
present variable, with: Entities::Variable
@@ -54,41 +53,37 @@ module API
end
end
- # Update existing variable of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (optional) - The `key` of variable
- # value (optional) - New value for `value` field of variable
- # Example Request:
- # PUT /projects/:id/variables/:key
+ desc 'Update an existing variable from a project' do
+ success Entities::Variable
+ end
+ params do
+ optional :key, type: String, desc: 'The key of the variable'
+ optional :value, type: String, desc: 'The value of the variable'
+ end
put ':id/variables/:key' do
- variable = user_project.variables.find_by(key: params[:key].to_s)
+ variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable
- attrs = attributes_for_keys [:value]
- if variable.update(attrs)
+ if variable.update(value: params[:value])
present variable, with: Entities::Variable
else
render_validation_error!(variable)
end
end
- # Delete existing variable of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # key (required) - The ID of a variable
- # Example Request:
- # DELETE /projects/:id/variables/:key
+ desc 'Delete an existing variable from a project' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
delete ':id/variables/:key' do
- variable = user_project.variables.find_by(key: params[:key].to_s)
+ variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable
- variable.destroy
- present variable, with: Entities::Variable
+ present variable.destroy, with: Entities::Variable
end
end
end
diff --git a/lib/api/version.rb b/lib/api/version.rb
new file mode 100644
index 00000000000..9ba576bd828
--- /dev/null
+++ b/lib/api/version.rb
@@ -0,0 +1,12 @@
+module API
+ class Version < Grape::API
+ before { authenticate! }
+
+ desc 'Get the version information of the GitLab instance.' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ end
+ get '/version' do
+ { version: Gitlab::VERSION, revision: Gitlab::REVISION }
+ end
+ end
+end
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 2492b5213ac..a8c1ca0c60a 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,6 +1,6 @@
module Banzai
module Filter
- # HTML filter that replaces :emoji: with images.
+ # HTML filter that replaces :emoji: and unicode with images.
#
# Based on HTML::Pipeline::EmojiFilter
#
@@ -13,16 +13,17 @@ module Banzai
def call
search_text_nodes(doc).each do |node|
content = node.to_html
- next unless content.include?(':')
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
- html = emoji_image_filter(content)
+ next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
+
+ html = emoji_name_image_filter(content)
+ html = emoji_unicode_image_filter(html)
next if html == content
node.replace(html)
end
-
doc
end
@@ -31,18 +32,38 @@ module Banzai
# text - String text to replace :emoji: in.
#
# Returns a String with :emoji: replaced with images.
- def emoji_image_filter(text)
+ def emoji_name_image_filter(text)
text.gsub(emoji_pattern) do |match|
name = $1
- "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{emoji_url(name)}' height='20' width='20' align='absmiddle' />"
+ emoji_image_tag(name, emoji_url(name))
end
end
+ # Replace unicode emoji with corresponding images if they exist.
+ #
+ # text - String text to replace unicode emoji in.
+ #
+ # Returns a String with unicode emoji replaced with images.
+ def emoji_unicode_image_filter(text)
+ text.gsub(emoji_unicode_pattern) do |moji|
+ emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
+ end
+ end
+
+ def emoji_image_tag(emoji_name, emoji_url)
+ "<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
+ end
+
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
end
+ # Build a regexp that matches all valid unicode emojis names.
+ def self.emoji_unicode_pattern
+ @emoji_unicode_pattern ||= /(#{Gitlab::Emoji.emojis_unicodes.map { |moji| Regexp.escape(moji) }.join('|')})/
+ end
+
private
def emoji_url(name)
@@ -60,6 +81,18 @@ module Banzai
end
end
+ def emoji_unicode_url(moji)
+ emoji_unicode_path = emoji_unicode_filename(moji)
+
+ if context[:asset_host]
+ url_to_image(emoji_unicode_path)
+ elsif context[:asset_root]
+ File.join(context[:asset_root], url_to_image(emoji_unicode_path))
+ else
+ url_to_image(emoji_unicode_path)
+ end
+ end
+
def url_to_image(image)
ActionController::Base.helpers.url_to_image(image)
end
@@ -71,6 +104,14 @@ module Banzai
def emoji_filename(name)
"#{Gitlab::Emoji.emoji_filename(name)}.png"
end
+
+ def emoji_unicode_pattern
+ self.class.emoji_unicode_pattern
+ end
+
+ def emoji_unicode_filename(name)
+ "#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
+ end
end
end
end
diff --git a/lib/banzai/filter/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/extracts_path.rb b/lib/extracts_path.rb
index a4558d157c0..9b74364849e 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -52,8 +52,7 @@ module ExtractsPath
# Append a trailing slash if we only get a ref and no file path
id += '/' unless id.ends_with?('/')
- valid_refs = @project.repository.ref_names
- valid_refs.select! { |v| id.start_with?("#{v}/") }
+ valid_refs = ref_names.select { |v| id.start_with?("#{v}/") }
if valid_refs.length == 0
# No exact ref match, so just try our best
@@ -74,6 +73,19 @@ module ExtractsPath
pair
end
+ # If we have an ID of 'foo.atom', and the controller provides Atom and HTML
+ # formats, then we have to check if the request was for the Atom version of
+ # the ID without the '.atom' suffix, or the HTML version of the ID including
+ # the suffix. We only check this if the version including the suffix doesn't
+ # match, so it is possible to create a branch which has an unroutable Atom
+ # feed.
+ def extract_ref_without_atom(id)
+ id_without_atom = id.sub(/\.atom$/, '')
+ valid_refs = ref_names.select { |v| "#{id_without_atom}/".start_with?("#{v}/") }
+
+ valid_refs.max_by(&:length)
+ end
+
# Assigns common instance variables for views working with Git tree-ish objects
#
# Assignments are:
@@ -86,6 +98,10 @@ module ExtractsPath
# If the :id parameter appears to be requesting a specific response format,
# that will be handled as well.
#
+ # If there is no path and the ref doesn't exist in the repo, try to resolve
+ # the ref without an '.atom' suffix. If _that_ ref is found, set the request's
+ # format to Atom manually.
+ #
# Automatically renders `not_found!` if a valid tree path could not be
# resolved (e.g., when a user inserts an invalid path or ref).
def assign_ref_vars
@@ -97,10 +113,18 @@ module ExtractsPath
@id = get_id
@ref, @path = extract_ref(@id)
@repo = @project.repository
- if @options[:extended_sha1].blank?
- @commit = @repo.commit(@ref)
- else
+
+ if @options[:extended_sha1].present?
@commit = @repo.commit(@options[:extended_sha1])
+ else
+ @commit = @repo.commit(@ref)
+
+ if @path.empty? && !@commit && @id.ends_with?('.atom')
+ @id = @ref = extract_ref_without_atom(@id)
+ @commit = @repo.commit(@ref)
+
+ request.format = :atom if @commit
+ end
end
raise InvalidPathError unless @commit
@@ -125,4 +149,10 @@ module ExtractsPath
id += "/" + params[:path] unless params[:path].blank?
id
end
+
+ def ref_names
+ return [] unless @project
+
+ @ref_names ||= @project.repository.ref_names
+ end
end
diff --git a/lib/gitlab/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/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/emoji.rb b/lib/gitlab/emoji.rb
index b63213ae208..bbbca8acc40 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -10,12 +10,20 @@ module Gitlab
Gemojione.index.instance_variable_get(:@emoji_by_moji)
end
+ def emojis_unicodes
+ emojis_by_moji.keys
+ end
+
def emojis_names
- emojis.keys.sort
+ emojis.keys
end
def emoji_filename(name)
emojis[name]["unicode"]
end
+
+ def emoji_unicode_filename(moji)
+ emojis_by_moji[moji]["unicode"]
+ end
end
end
diff --git a/lib/tasks/ce_to_ee_merge_check.rake b/lib/tasks/ce_to_ee_merge_check.rake
new file mode 100644
index 00000000000..424e7883060
--- /dev/null
+++ b/lib/tasks/ce_to_ee_merge_check.rake
@@ -0,0 +1,4 @@
+desc 'Checks if the branch would apply cleanly to EE'
+task ce_to_ee_merge_check: :environment do
+ Rake::Task['gitlab:dev:ce_to_ee_merge_check'].invoke
+end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 5f4a6bbfa35..2ae48a970ce 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -671,7 +671,7 @@ namespace :gitlab do
"Enable mail_room in the init.d configuration."
)
for_more_information(
- "doc/incoming_email/README.md"
+ "doc/administration/reply_by_email.md"
)
fix_and_rerun
end
@@ -690,7 +690,7 @@ namespace :gitlab do
"Enable mail_room in your Procfile."
)
for_more_information(
- "doc/incoming_email/README.md"
+ "doc/administration/reply_by_email.md"
)
fix_and_rerun
end
@@ -747,7 +747,7 @@ namespace :gitlab do
"Check that the information in config/gitlab.yml is correct"
)
for_more_information(
- "doc/incoming_email/README.md"
+ "doc/administration/reply_by_email.md"
)
fix_and_rerun
end
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
new file mode 100644
index 00000000000..47bdb2d32d2
--- /dev/null
+++ b/lib/tasks/gitlab/dev.rake
@@ -0,0 +1,107 @@
+namespace :gitlab do
+ namespace :dev do
+ desc 'Checks if the branch would apply cleanly to EE'
+ task ce_to_ee_merge_check: :environment do
+ return if defined?(Gitlab::License)
+ return unless ENV['CI']
+
+ ce_repo = ENV['CI_BUILD_REPO']
+ ce_branch = ENV['CI_BUILD_REF_NAME']
+
+ ee_repo = 'https://gitlab.com/gitlab-org/gitlab-ee.git'
+ ee_branch = "#{ce_branch}-ee"
+ ee_dir = 'gitlab-ee-merge-check'
+
+ puts "\n=> Cloning #{ee_repo} into #{ee_dir}\n"
+ `git clone #{ee_repo} #{ee_dir} --depth 1`
+ Dir.chdir(ee_dir) do
+ puts "\n => Fetching #{ce_repo}/#{ce_branch}\n"
+ `git fetch #{ce_repo} #{ce_branch} --depth 1`
+
+ # Try to merge the current tested branch to EE/master...
+ puts "\n => Merging #{ce_repo}/#{ce_branch} into #{ee_repo}/master\n"
+ `git merge FETCH_HEAD`
+
+ exit 0 if $?.success?
+
+ # Check if the <branch>-ee branch exists...
+ puts "\n => Check if #{ee_repo}/#{ee_branch} exists\n"
+ `git rev-parse --verify #{ee_branch}`
+
+ # The <branch>-ee doesn't exist
+ unless $?.success?
+ puts
+ puts <<-MSG.strip_heredoc
+ =================================================================
+ The #{ce_branch} branch cannot be merged without conflicts to the
+ current EE/master, and no #{ee_branch} branch was detected in
+ the EE repository.
+
+ Please create a #{ee_branch} branch that includes changes from
+ #{ce_branch} but also specific changes than can be applied cleanly
+ to EE/master.
+
+ You can create this branch as follows:
+
+ 1. In the EE repo:
+ $ git fetch origin
+ $ git fetch #{ce_repo} #{ce_branch}
+ $ git checkout -b #{ee_branch} FETCH_HEAD
+ $ git rebase origin/master
+ 2. At this point you will likely have conflicts, solve them, and
+ continue/finish the rebase. Note: You can squash the CE commits
+ before rebasing.
+ 3. You can squash all the original #{ce_branch} commits into a
+ single "Port of #{ce_branch} to EE".
+ 4. Push your branch to #{ee_repo}:
+ $ git push origin #{ee_branch}
+ =================================================================\n
+ MSG
+
+ exit 1
+ end
+
+ # Try to merge the <branch>-ee branch to EE/master...
+ puts "\n => Merging #{ee_repo}/#{ee_branch} into #{ee_repo}/master\n"
+ `git merge #{ee_branch} master`
+
+ # The <branch>-ee cannot be merged cleanly to EE/master...
+ unless $?.success?
+ puts
+ puts <<-MSG.strip_heredoc
+ =================================================================
+ The #{ce_branch} branch cannot be merged without conflicts to
+ EE/master, and even though the #{ee_branch} branch exists in the EE
+ repository, it cannot be merged without conflicts to EE/master.
+
+ Please update the #{ee_branch}, push it again to #{ee_repo}, and
+ retry this job.
+ =================================================================\n
+ MSG
+
+ exit 2
+ end
+
+ puts "\n => Merging #{ce_repo}/#{ce_branch} into #{ee_repo}/master\n"
+ `git merge FETCH_HEAD`
+ exit 0 if $?.success?
+
+ # The <branch>-ee can be merged cleanly to EE/master, but <branch> still
+ # cannot be merged cleanly to EE/master...
+ puts
+ puts <<-MSG.strip_heredoc
+ =================================================================
+ The #{ce_branch} branch cannot be merged without conflicts to EE, and
+ even though the #{ee_branch} branch exists in the EE repository and
+ applies cleanly to EE/master, it doesn't prevent conflicts when
+ merging #{ce_branch} into EE.
+
+ We may be in a complex situation here.
+ =================================================================\n
+ MSG
+
+ exit 3
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake
new file mode 100644
index 00000000000..3a16ace60bd
--- /dev/null
+++ b/lib/tasks/gitlab/users.rake
@@ -0,0 +1,11 @@
+namespace :gitlab do
+ namespace :users do
+ desc "GitLab | Clear the authentication token for all users"
+ task clear_all_authentication_tokens: :environment do |t, args|
+ # Do small batched updates because these updates will be slow and locking
+ User.select(:id).find_in_batches(batch_size: 100) do |batch|
+ User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
+ end
+ end
+ end
+end
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index fb4d8463981..7c4e8276902 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -10,11 +10,11 @@ then
exit 1
fi
-# Ensure that the CHANGELOG does not contain duplicate versions
-DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^v [0-9.]+' CHANGELOG | sed 's| (unreleased)||' | sort | uniq -d)
+# Ensure that the CHANGELOG.md does not contain duplicate versions
+DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d)
if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
then
- echo '✖ ERROR: Duplicate versions in CHANGELOG:' >&2
+ echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
echo "${DUPLICATE_CHANGELOG_VERSIONS}" >&2
exit 1
fi
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
deleted file mode 100644
index 2b334ed1172..00000000000
--- a/spec/controllers/namespaces_controller_spec.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-require 'spec_helper'
-
-describe NamespacesController do
- let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
-
- describe "GET show" do
- context "when the namespace belongs to a user" do
- let!(:other_user) { create(:user) }
-
- it "redirects to the user's page" do
- get :show, id: other_user.username
-
- expect(response).to redirect_to(user_path(other_user))
- end
- end
-
- context "when the namespace belongs to a group" do
- let!(:group) { create(:group) }
-
- context "when the group is public" do
- context "when not signed in" do
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
- end
-
- context "when the group is private" do
- before do
- group.update_attribute(:visibility_level, Group::PRIVATE)
- end
-
- context "when not signed in" do
- it "redirects to the sign in page" do
- get :show, id: group.path
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- context "when the user has access to the group" do
- before do
- group.add_developer(user)
- end
-
- context "when the user is blocked" do
- before do
- user.block
- end
-
- it "redirects to the sign in page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context "when the user isn't blocked" do
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
- end
-
- context "when the user doesn't have access to the group" do
- it "responds with status 404" do
- get :show, id: group.path
-
- expect(response).to have_http_status(404)
- end
- end
- end
- end
- end
-
- context "when the namespace doesn't exist" do
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- it "responds with status 404" do
- get :show, id: "doesntexist"
-
- expect(response).to have_http_status(404)
- end
- end
-
- context "when not signed in" do
- it "redirects to the sign in page" do
- get :show, id: "doesntexist"
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
- end
-end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 566658b508d..da59642f24d 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -1,15 +1,16 @@
require 'spec_helper'
describe Projects::Boards::IssuesController do
- let(:project) { create(:project_with_board) }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:planning) { create(:label, project: project, name: 'Planning') }
let(:development) { create(:label, project: project, name: 'Development') }
- let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
- let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: development, position: 1) }
before do
project.team << [user, :master]
@@ -24,7 +25,7 @@ describe Projects::Boards::IssuesController do
create(:labeled_issue, project: project, labels: [development])
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
- list_issues user: user, list_id: list2
+ list_issues user: user, board: board, list: list2
parsed_response = JSON.parse(response.body)
@@ -33,9 +34,17 @@ describe Projects::Boards::IssuesController do
end
end
+ context 'with invalid board id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, board: 999, list: list2
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
context 'with invalid list id' do
it 'returns a not found 404 response' do
- list_issues user: user, list_id: 999
+ list_issues user: user, board: board, list: 999
expect(response).to have_http_status(404)
end
@@ -47,32 +56,33 @@ describe Projects::Boards::IssuesController do
allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
end
- it 'returns a successful 403 response' do
- list_issues user: user, list_id: list2
+ it 'returns a forbidden 403 response' do
+ list_issues user: user, board: board, list: list2
expect(response).to have_http_status(403)
end
end
- def list_issues(user:, list_id:)
+ def list_issues(user:, board:, list:)
sign_in(user)
get :index, namespace_id: project.namespace.to_param,
project_id: project.to_param,
- list_id: list_id.to_param
+ board_id: board.to_param,
+ list_id: list.to_param
end
end
describe 'POST create' do
context 'with valid params' do
it 'returns a successful 200 response' do
- create_issue user: user, list: list1, title: 'New issue'
+ create_issue user: user, board: board, list: list1, title: 'New issue'
expect(response).to have_http_status(200)
end
it 'returns the created issue' do
- create_issue user: user, list: list1, title: 'New issue'
+ create_issue user: user, board: board, list: list1, title: 'New issue'
expect(response).to match_response_schema('issue')
end
@@ -81,7 +91,7 @@ describe Projects::Boards::IssuesController do
context 'with invalid params' do
context 'when title is nil' do
it 'returns an unprocessable entity 422 response' do
- create_issue user: user, list: list1, title: nil
+ create_issue user: user, board: board, list: list1, title: nil
expect(response).to have_http_status(422)
end
@@ -91,7 +101,7 @@ describe Projects::Boards::IssuesController do
it 'returns a not found 404 response' do
list = create(:list)
- create_issue user: user, list: list, title: 'New issue'
+ create_issue user: user, board: board, list: list, title: 'New issue'
expect(response).to have_http_status(404)
end
@@ -100,17 +110,18 @@ describe Projects::Boards::IssuesController do
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
- create_issue user: guest, list: list1, title: 'New issue'
+ create_issue user: guest, board: board, list: list1, title: 'New issue'
expect(response).to have_http_status(403)
end
end
- def create_issue(user:, list:, title:)
+ def create_issue(user:, board:, list:, title:)
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
list_id: list.to_param,
issue: { title: title },
format: :json
@@ -122,13 +133,13 @@ describe Projects::Boards::IssuesController do
context 'with valid params' do
it 'returns a successful 200 response' do
- move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+ move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(200)
end
it 'moves issue to the desired list' do
- move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+ move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(issue.reload.labels).to contain_exactly(development)
end
@@ -136,31 +147,44 @@ describe Projects::Boards::IssuesController do
context 'with invalid params' do
it 'returns a unprocessable entity 422 response for invalid lists' do
- move user: user, issue: issue, from_list_id: nil, to_list_id: nil
+ move user: user, board: board, issue: issue, from_list_id: nil, to_list_id: nil
expect(response).to have_http_status(422)
end
+ it 'returns a not found 404 response for invalid board id' do
+ move user: user, board: 999, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(404)
+ end
+
it 'returns a not found 404 response for invalid issue id' do
- move user: user, issue: 999, from_list_id: list1.id, to_list_id: list2.id
+ move user: user, board: board, issue: 999, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(404)
end
end
context 'with unauthorized user' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [guest, :guest]
+ end
+
it 'returns a forbidden 403 response' do
- move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+ move user: guest, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(403)
end
end
- def move(user:, issue:, from_list_id:, to_list_id:)
+ def move(user:, board:, issue:, from_list_id:, to_list_id:)
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
id: issue.to_param,
from_list_id: from_list_id,
to_list_id: to_list_id,
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index 709006a3601..34d6119429d 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Projects::Boards::ListsController do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:guest) { create(:user) }
@@ -13,7 +13,7 @@ describe Projects::Boards::ListsController do
describe 'GET index' do
it 'returns a successful 200 response' do
- read_board_list user: user
+ read_board_list user: user, board: board
expect(response).to have_http_status(200)
expect(response.content_type).to eq 'application/json'
@@ -22,7 +22,7 @@ describe Projects::Boards::ListsController do
it 'returns a list of board lists' do
create(:list, board: board)
- read_board_list user: user
+ read_board_list user: user, board: board
parsed_response = JSON.parse(response.body)
@@ -37,17 +37,18 @@ describe Projects::Boards::ListsController do
end
it 'returns a forbidden 403 response' do
- read_board_list user: user
+ read_board_list user: user, board: board
expect(response).to have_http_status(403)
end
end
- def read_board_list(user:)
+ def read_board_list(user:, board:)
sign_in(user)
get :index, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
format: :json
end
end
@@ -57,13 +58,13 @@ describe Projects::Boards::ListsController do
let(:label) { create(:label, project: project, name: 'Development') }
it 'returns a successful 200 response' do
- create_board_list user: user, label_id: label.id
+ create_board_list user: user, board: board, label_id: label.id
expect(response).to have_http_status(200)
end
it 'returns the created list' do
- create_board_list user: user, label_id: label.id
+ create_board_list user: user, board: board, label_id: label.id
expect(response).to match_response_schema('list')
end
@@ -72,7 +73,7 @@ describe Projects::Boards::ListsController do
context 'with invalid params' do
context 'when label is nil' do
it 'returns a not found 404 response' do
- create_board_list user: user, label_id: nil
+ create_board_list user: user, board: board, label_id: nil
expect(response).to have_http_status(404)
end
@@ -82,7 +83,7 @@ describe Projects::Boards::ListsController do
it 'returns a not found 404 response' do
label = create(:label, name: 'Development')
- create_board_list user: user, label_id: label.id
+ create_board_list user: user, board: board, label_id: label.id
expect(response).to have_http_status(404)
end
@@ -93,17 +94,18 @@ describe Projects::Boards::ListsController do
it 'returns a forbidden 403 response' do
label = create(:label, project: project, name: 'Development')
- create_board_list user: guest, label_id: label.id
+ create_board_list user: guest, board: board, label_id: label.id
expect(response).to have_http_status(403)
end
end
- def create_board_list(user:, label_id:)
+ def create_board_list(user:, board:, label_id:)
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
list: { label_id: label_id },
format: :json
end
@@ -115,13 +117,13 @@ describe Projects::Boards::ListsController do
context 'with valid position' do
it 'returns a successful 200 response' do
- move user: user, list: planning, position: 1
+ move user: user, board: board, list: planning, position: 1
expect(response).to have_http_status(200)
end
it 'moves the list to the desired position' do
- move user: user, list: planning, position: 1
+ move user: user, board: board, list: planning, position: 1
expect(planning.reload.position).to eq 1
end
@@ -129,7 +131,7 @@ describe Projects::Boards::ListsController do
context 'with invalid position' do
it 'returns an unprocessable entity 422 response' do
- move user: user, list: planning, position: 6
+ move user: user, board: board, list: planning, position: 6
expect(response).to have_http_status(422)
end
@@ -137,7 +139,7 @@ describe Projects::Boards::ListsController do
context 'with invalid list id' do
it 'returns a not found 404 response' do
- move user: user, list: 999, position: 1
+ move user: user, board: board, list: 999, position: 1
expect(response).to have_http_status(404)
end
@@ -145,17 +147,18 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
- move user: guest, list: planning, position: 6
+ move user: guest, board: board, list: planning, position: 6
expect(response).to have_http_status(403)
end
end
- def move(user:, list:, position:)
+ def move(user:, board:, list:, position:)
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
id: list.to_param,
list: { position: position },
format: :json
@@ -167,19 +170,19 @@ describe Projects::Boards::ListsController do
context 'with valid list id' do
it 'returns a successful 200 response' do
- remove_board_list user: user, list: planning
+ remove_board_list user: user, board: board, list: planning
expect(response).to have_http_status(200)
end
it 'removes list from board' do
- expect { remove_board_list user: user, list: planning }.to change(board.lists, :size).by(-1)
+ expect { remove_board_list user: user, board: board, list: planning }.to change(board.lists, :size).by(-1)
end
end
context 'with invalid list id' do
it 'returns a not found 404 response' do
- remove_board_list user: user, list: 999
+ remove_board_list user: user, board: board, list: 999
expect(response).to have_http_status(404)
end
@@ -187,17 +190,18 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
- remove_board_list user: guest, list: planning
+ remove_board_list user: guest, board: board, list: planning
expect(response).to have_http_status(403)
end
end
- def remove_board_list(user:, list:)
+ def remove_board_list(user:, board:, list:)
sign_in(user)
delete :destroy, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
id: list.to_param,
format: :json
end
@@ -206,13 +210,13 @@ describe Projects::Boards::ListsController do
describe 'POST generate' do
context 'when board lists is empty' do
it 'returns a successful 200 response' do
- generate_default_board_lists user: user
+ generate_default_lists user: user, board: board
expect(response).to have_http_status(200)
end
it 'returns the defaults lists' do
- generate_default_board_lists user: user
+ generate_default_lists user: user, board: board
expect(response).to match_response_schema('lists')
end
@@ -222,7 +226,7 @@ describe Projects::Boards::ListsController do
it 'returns an unprocessable entity 422 response' do
create(:list, board: board)
- generate_default_board_lists user: user
+ generate_default_lists user: user, board: board
expect(response).to have_http_status(422)
end
@@ -230,17 +234,18 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do
it 'returns a forbidden 403 response' do
- generate_default_board_lists user: guest
+ generate_default_lists user: guest, board: board
expect(response).to have_http_status(403)
end
end
- def generate_default_board_lists(user:)
+ def generate_default_lists(user:, board:)
sign_in(user)
post :generate, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ board_id: board.to_param,
format: :json
end
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 6f6e608e1f3..cc19035740e 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -9,16 +9,71 @@ describe Projects::BoardsController do
sign_in(user)
end
+ describe 'GET index' do
+ it 'creates a new project board when project does not have one' do
+ expect { list_boards }.to change(project.boards, :count).by(1)
+ end
+
+ context 'when format is HTML' do
+ it 'renders template' do
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
+
+ context 'when format is JSON' do
+ it 'returns a list of project boards' do
+ create_list(:board, 2, project: project)
+
+ list_boards format: :json
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('boards')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
+
+ it 'returns a not found 404 response' do
+ list_boards
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ def list_boards(format: :html)
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: format
+ end
+ end
+
describe 'GET show' do
- it 'creates a new board when project does not have one' do
- expect { read_board }.to change(Board, :count).by(1)
+ let!(:board) { create(:board, project: project) }
+
+ context 'when format is HTML' do
+ it 'renders template' do
+ read_board board: board
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
end
- it 'renders HTML template' do
- read_board
+ context 'when format is JSON' do
+ it 'returns project board' do
+ read_board board: board, format: :json
- expect(response).to render_template :show
- expect(response.content_type).to eq 'text/html'
+ expect(response).to match_response_schema('board')
+ end
end
context 'with unauthorized user' do
@@ -27,16 +82,27 @@ describe Projects::BoardsController do
allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
- it 'returns a successful 404 response' do
- read_board
+ it 'returns a not found 404 response' do
+ read_board board: board
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when board does not belong to project' do
+ it 'returns a not found 404 response' do
+ another_board = create(:board)
+
+ read_board board: another_board
expect(response).to have_http_status(404)
end
end
- def read_board(format: :html)
+ def read_board(board:, format: :html)
get :show, namespace_id: project.namespace.to_param,
project_id: project.to_param,
+ id: board.to_param,
format: format
end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 7e440193d7b..646b097d74e 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -102,15 +102,16 @@ describe Projects::CommitController do
describe "as patch" do
include_examples "export as", :patch
let(:format) { :patch }
+ let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') }
it "is a git email patch" do
- go(id: commit.id, format: format)
+ go(id: commit2.id, format: format)
- expect(response.body).to start_with("From #{commit.id}")
+ expect(response.body).to start_with("From #{commit2.id}")
end
it "contains a git diff" do
- go(id: commit.id, format: format)
+ go(id: commit2.id, format: format)
expect(response.body).to match(/^diff --git/)
end
@@ -135,6 +136,8 @@ describe Projects::CommitController do
describe "GET branches" do
it "contains branch and tags information" do
+ commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+
get(:branches,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -254,16 +257,17 @@ describe Projects::CommitController do
end
let(:existing_path) { '.gitmodules' }
+ let(:commit2) { project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
context 'when the commit exists' do
context 'when the user has access to the project' do
context 'when the path exists in the diff' do
it 'enables diff notes' do
- diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
expect(assigns(:comments_target)).to eq(noteable_type: 'Commit',
- commit_id: commit.id)
+ commit_id: commit2.id)
end
it 'only renders the diffs for the path given' do
@@ -272,7 +276,7 @@ describe Projects::CommitController do
meth.call(diffs)
end
- diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 2518a48e336..1ac7e03a2db 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -10,15 +10,38 @@ describe Projects::CommitsController do
end
describe "GET show" do
- context "as atom feed" do
- it "renders as atom" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: "master",
- format: "atom")
- expect(response).to be_success
- expect(response.content_type).to eq('application/atom+xml')
+ context "when the ref name ends in .atom" do
+ render_views
+
+ context "when the ref does not exist with the suffix" do
+ it "renders as atom" do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: "master.atom")
+
+ expect(response).to be_success
+ expect(response.content_type).to eq('application/atom+xml')
+ end
+ end
+
+ context "when the ref exists with the suffix" do
+ before do
+ commit = project.repository.commit('master')
+
+ allow_any_instance_of(Repository).to receive(:commit).and_call_original
+ allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
+
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: "master.atom")
+ end
+
+ it "renders as HTML" do
+ expect(response).to be_success
+ expect(response.content_type).to eq('text/html')
+ end
end
end
end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
new file mode 100644
index 00000000000..74e6603b0cb
--- /dev/null
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Projects::GraphsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'GET #languages' do
+ let(:linguist_repository) do
+ double(languages: {
+ 'Ruby' => 1000,
+ 'CoffeeScript' => 350,
+ 'PowerShell' => 15
+ })
+ end
+
+ let(:expected_values) do
+ ps_color = "##{Digest::SHA256.hexdigest('PowerShell')[0...6]}"
+ [
+ # colors from Linguist:
+ { label: "Ruby", color: "#701516", highlight: "#701516" },
+ { label: "CoffeeScript", color: "#244776", highlight: "#244776" },
+ # colors from SHA256 fallback:
+ { label: "PowerShell", color: ps_color, highlight: ps_color }
+ ]
+ end
+
+ before do
+ allow(Linguist::Repository).to receive(:new).and_return(linguist_repository)
+ end
+
+ it 'sets the correct colour according to language' do
+ get(:languages, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+
+ expected_values.each do |val|
+ expect(assigns(:languages)).to include(a_hash_including(val))
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 742edd8ba3d..d6980471ea4 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,12 +791,154 @@ 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
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ end
+ end
+ end
+
+ describe 'POST assign_related_issues' do
+ let(:issue1) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+
+ def post_assign_issues
+ merge_request.update!(description: "Closes #{issue1.to_reference} and #{issue2.to_reference}",
+ author: user,
+ source_branch: 'feature',
+ target_branch: 'master')
+
+ post :assign_related_issues,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid
+ end
+
+ it 'shows a flash message on success' do
+ post_assign_issues
+
+ expect(flash[:notice]).to eq '2 issues have been assigned to you'
+ end
+
+ it 'correctly pluralizes flash message on success' do
+ issue2.update!(assignee: user)
+
+ post_assign_issues
+
+ expect(flash[:notice]).to eq '1 issue has been assigned to you'
+ end
+
+ it 'calls MergeRequests::AssignIssuesService' do
+ expect(MergeRequests::AssignIssuesService).to receive(:new).
+ with(project, user, merge_request: merge_request).
+ and_return(double(execute: { count: 1 }))
+
+ post_assign_issues
+ end
+
+ it 'is skipped when not signed in' do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ sign_out(:user)
+
+ expect(MergeRequests::AssignIssuesService).not_to receive(:new)
+
+ post_assign_issues
+ end
+ end
+
+ describe 'GET ci_environments_status' do
+ context 'when 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/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index a6995145cc1..5e661c2c41d 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -17,4 +17,18 @@ describe Projects::TagsController do
expect(assigns(:releases)).not_to include(invalid_release)
end
end
+
+ describe 'GET show' do
+ before { get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: id }
+
+ context "valid tag" do
+ let(:id) { 'v1.0.0' }
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "invalid tag" do
+ let(:id) { 'latest' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+ end
end
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index 35c4a0b6f08..ec46146d9b5 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -1,5 +1,10 @@
FactoryGirl.define do
factory :board do
project factory: :empty_project
+
+ after(:create) do |board|
+ board.lists.create(list_type: :backlog)
+ board.lists.create(list_type: :done)
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 331172445e4..719ef17f57e 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -124,12 +124,4 @@ FactoryGirl.define do
)
end
end
-
- factory :project_with_board, parent: :empty_project do
- after(:create) do |project|
- project.create_board
- project.board.lists.create(list_type: :backlog)
- project.board.lists.create(list_type: :done)
- end
- end
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index a8833194421..f8c3ccb416b 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -53,7 +53,7 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in issue descriptions' do
- expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/
+ expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/
end
it 'has XHTML summaries in notes' do
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 470e2bdbb9b..0fb1608a0a3 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -4,7 +4,8 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
- let(:project) { create(:project_with_board, :public) }
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:user2) { create(:user) }
@@ -17,7 +18,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'no lists' do
before do
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
end
@@ -60,8 +61,8 @@ describe 'Issue Boards', feature: true, js: true do
let!(:done) { create(:label, project: project, name: 'Done') }
let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
- let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
- let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
let!(:issue1) { create(:issue, project: project, assignee: user) }
@@ -75,7 +76,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
before do
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
@@ -169,7 +170,7 @@ describe 'Issue Boards', feature: true, js: true do
create(:issue, project: project)
end
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
page.within(find('.board', match: :first)) do
@@ -468,7 +469,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'removes filtered labels' do
wait_for_vue_resource
-
+
page.within '.labels-filter' do
click_button('Label')
wait_for_ajax
@@ -603,7 +604,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'keyboard shortcuts' do
before do
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
@@ -616,7 +617,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'signed out user' do
before do
logout
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
@@ -632,7 +633,7 @@ describe 'Issue Boards', feature: true, js: true do
project.team << [user_guest, :guest]
logout
login_as(user_guest)
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
index 7ef68e9eb8d..a5fc766401f 100644
--- a/spec/features/boards/keyboard_shortcut_spec.rb
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -6,9 +6,7 @@ describe 'Issue Boards shortcut', feature: true, js: true do
let(:project) { create(:empty_project) }
before do
- project.create_board
- project.board.lists.create(list_type: :backlog)
- project.board.lists.create(list_type: :done)
+ create(:board, project: project)
login_as :admin
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index c046e6b8d79..67d6da5f39a 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -4,7 +4,8 @@ describe 'Issue Boards new issue', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
- let(:project) { create(:project_with_board, :public) }
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
context 'authorized user' do
@@ -13,7 +14,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
login_as(user)
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
@@ -69,7 +70,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
context 'unauthorized user' do
before do
- visit namespace_project_board_path(project.namespace, project)
+ visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
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/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/login_spec.rb b/spec/features/login_spec.rb
index 2523b4b7898..996f39ea06d 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -29,7 +29,7 @@ feature 'Login', feature: true do
describe 'with two-factor authentication' do
def enter_code(code)
- fill_in 'Two-Factor Authentication code', with: code
+ fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
new file mode 100644
index 00000000000..43cc6f2a2a7
--- /dev/null
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -0,0 +1,51 @@
+require 'rails_helper'
+
+feature 'Merge request issue assignment', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue1.to_reference} and #{issue2.to_reference}") }
+ let(:service) { MergeRequests::AssignIssuesService.new(merge_request, user, user, project) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ def visit_merge_request(current_user = nil)
+ login_as(current_user || user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'logged in as author' do
+ scenario 'updates related issues' do
+ visit_merge_request
+ click_link "Assign yourself to these issues"
+
+ expect(page).to have_content "2 issues have been assigned to you"
+ end
+
+ it 'returns user to the merge request' do
+ visit_merge_request
+ click_link "Assign yourself to these issues"
+
+ expect(page).to have_content merge_request.description
+ end
+
+ it "doesn't display if related issues are already assigned" do
+ [issue1, issue2].each { |issue| issue.update!(assignee: user) }
+
+ visit_merge_request
+
+ expect(page).not_to have_content "Assign yourself"
+ end
+ end
+
+ context 'not MR author' do
+ it "doesn't not show assignment link" do
+ visit_merge_request(create(:user))
+
+ expect(page).not_to have_content "Assign yourself"
+ end
+ end
+end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 759edf8ec80..d258ff52bbb 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do
end
end
- context 'when a merge request can be resolved in the UI' do
- let(:merge_request) { create_merge_request('conflict-resolvable') }
+ shared_examples "conflicts are resolved in Interactive mode" do
+ it 'conflicts are resolved in Interactive mode' do
+ within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
+ click_button 'Use ours'
+ end
+
+ within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
+ all('button', text: 'Use ours').each do |button|
+ button.click
+ end
+ end
+
+ click_button 'Commit conflict resolution'
+ wait_for_ajax
+
+ expect(page).to have_content('All merge conflicts were resolved')
+ merge_request.reload_diff
+
+ click_on 'Changes'
+ wait_for_ajax
+
+ within find('.diff-file', text: 'files/ruby/popen.rb') do
+ expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }")
+ expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }")
+ end
+
+ within find('.diff-file', text: 'files/ruby/regex.rb') do
+ expect(page).to have_selector('.line_content.new', text: "def username_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def project_name_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def path_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp")
+ expect(page).to have_selector('.line_content.new', text: "def default_regexp")
+ end
+ end
+ end
+ shared_examples "conflicts are resolved in Edit inline mode" do
+ it 'conflicts are resolved in Edit inline mode' do
+ expect(find('#conflicts')).to have_content('popen.rb')
+
+ within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
+ click_button 'Edit inline'
+ wait_for_ajax
+ execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");')
+ end
+
+ within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
+ click_button 'Edit inline'
+ wait_for_ajax
+ execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
+ end
+
+ click_button 'Commit conflict resolution'
+ wait_for_ajax
+ expect(page).to have_content('All merge conflicts were resolved')
+ merge_request.reload_diff
+
+ click_on 'Changes'
+ wait_for_ajax
+
+ expect(page).to have_content('One morning')
+ expect(page).to have_content('Gregor Samsa woke from troubled dreams')
+ end
+ end
+
+ context 'can be resolved in the UI' do
before do
project.team << [user, :developer]
login_as(user)
-
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'shows a link to the conflict resolution page' do
- expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ context 'the conflicts are resolvable' do
+ let(:merge_request) { create_merge_request('conflict-resolvable') }
+
+ before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) }
+
+ it 'shows a link to the conflict resolution page' do
+ expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ context 'in Inline view mode' do
+ before { click_link('conflicts', href: /\/conflicts\Z/) }
+
+ include_examples "conflicts are resolved in Interactive mode"
+ include_examples "conflicts are resolved in Edit inline mode"
+ end
+
+ context 'in Parallel view mode' do
+ before do
+ click_link('conflicts', href: /\/conflicts\Z/)
+ click_button 'Side-by-side'
+ end
+
+ include_examples "conflicts are resolved in Interactive mode"
+ include_examples "conflicts are resolved in Edit inline mode"
+ end
end
- context 'visiting the conflicts resolution page' do
- before { click_link('conflicts', href: /\/conflicts\Z/) }
+ context 'the conflict contain markers' do
+ let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') }
- it 'shows the conflicts' do
- begin
- expect(find('#conflicts')).to have_content('popen.rb')
- rescue Capybara::Poltergeist::JavascriptError
- retry
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ click_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ it 'conflicts can not be resolved in Interactive mode' do
+ within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
+ expect(page).not_to have_content 'Interactive mode'
+ expect(page).not_to have_content 'Edit inline'
+ end
+ end
+
+ it 'conflicts are resolved in Edit inline mode' do
+ within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
+ wait_for_ajax
+ execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
end
+
+ click_button 'Commit conflict resolution'
+ wait_for_ajax
+
+ expect(page).to have_content('All merge conflicts were resolved')
+
+ merge_request.reload_diff
+
+ click_on 'Changes'
+ wait_for_ajax
+ find('.click-to-expand').click
+ wait_for_ajax
+
+ expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end
end
end
@@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do
UNRESOLVABLE_CONFLICTS = {
'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file',
- 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
}
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 4d5d4aa121a..a506624b30d 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -45,7 +45,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 60bc07bd1a0..bc2b0ff3e2c 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -2,18 +2,26 @@ require 'spec_helper'
feature 'Merge When Build Succeeds', feature: true, js: true do
let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
- let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project,
+ author: user,
+ title: 'Bug NS-04')
+ end
- before do
- project.team << [user, :master]
- project.enable_ci
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
end
- context "Active build for Merge Request" do
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
+ before { project.team << [user, :master] }
+
+ context 'when there is active build for merge request' do
+ background do
+ create(:ci_build, pipeline: pipeline)
+ end
before do
login_as user
@@ -41,26 +49,30 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
end
end
- context 'When it is enabled' do
+ context 'when merge when build succeeds is enabled' do
let(:merge_request) do
- create(:merge_request_with_diffs, :simple, source_project: project, author: user,
- merge_user: user, title: "MepMep", merge_when_build_succeeds: true)
+ create(:merge_request_with_diffs, :simple, source_project: project,
+ author: user,
+ merge_user: user,
+ title: 'MepMep',
+ merge_when_build_succeeds: true)
end
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline)
+ end
before do
login_as user
visit_merge_request(merge_request)
end
- it 'cancels the automatic merge' do
+ it 'allows to cancel the automatic merge' do
click_link "Cancel Automatic Merge"
expect(page).to have_button "Merge When Build Succeeds"
- visit_merge_request(merge_request) # Needed to refresh the page
+ visit_merge_request(merge_request) # refresh the page
expect(page).to have_content "Canceled the automatic merge"
end
@@ -70,10 +82,21 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
click_link "Remove Source Branch When Merged"
expect(page).to have_content "The source branch will be removed"
end
+
+ context 'when build succeeds' do
+ background { build.success }
+
+ it 'merges merge request' do
+ visit_merge_request(merge_request) # refresh the page
+
+ expect(page).to have_content 'The changes were merged'
+ expect(merge_request.reload).to be_merged
+ end
+ end
end
- context 'Build is not active' do
- it "does not allow for enabling" do
+ context 'when build is not active' do
+ it "does not allow to enable merge when build succeeds" do
visit_merge_request(merge_request)
expect(page).not_to have_link "Merge When Build Succeeds"
end
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..8e23ec50d4a
--- /dev/null
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+feature 'Widget Deployments Header', feature: true, js: true do
+ include WaitForAjax
+
+ describe 'when deployed 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
+ login_as :admin
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it '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
+ end
+end
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
new file mode 100644
index 00000000000..fc88fd74af8
--- /dev/null
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+feature 'Find file keyboard shortcuts', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+
+ visit namespace_project_find_file_path(project.namespace, project, project.repository.root_ref)
+
+ wait_for_ajax
+ end
+
+ it 'opens file when pressing enter key' do
+ fill_in 'file_find', with: 'CHANGELOG'
+
+ find('#file_find').native.send_keys(:enter)
+
+ expect(page).to have_selector('.blob-content-holder')
+
+ page.within('.file-title') do
+ expect(page).to have_content('CHANGELOG')
+ end
+ end
+
+ it 'navigates files with arrow keys' do
+ fill_in 'file_find', with: 'application.'
+
+ find('#file_find').native.send_keys(:down)
+ find('#file_find').native.send_keys(:enter)
+
+ expect(page).to have_selector('.blob-content-holder')
+
+ page.within('.file-title') do
+ expect(page).to have_content('application.js')
+ end
+ end
+end
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
new file mode 100644
index 00000000000..c22441f8929
--- /dev/null
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe "Guest navigation menu" do
+ let(:project) { create :empty_project, :private }
+ let(:guest) { create :user }
+
+ before do
+ project.team << [guest, :guest]
+
+ login_as(guest)
+ end
+
+ it "shows allowed tabs only" do
+ visit namespace_project_path(project.namespace, project)
+
+ within(".nav-links") do
+ expect(page).to have_content 'Project'
+ expect(page).to have_content 'Activity'
+ expect(page).to have_content 'Issues'
+ expect(page).to have_content 'Wiki'
+
+ expect(page).not_to have_content 'Repository'
+ expect(page).not_to have_content 'Pipelines'
+ expect(page).not_to have_content 'Graphs'
+ expect(page).not_to have_content 'Merge Requests'
+ end
+ end
+end
diff --git a/spec/features/projects/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/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/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ccb5c06dab0..79417c769a8 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -203,7 +203,7 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index a752c1d7235..65544f79eba 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -14,7 +14,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq users_almost_there_path
expect(page).to have_content("Please check your email to confirm your account")
@@ -33,7 +33,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq dashboard_projects_path
expect(page).to have_content("Welcome! You have signed up successfully.")
@@ -52,7 +52,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq user_registration_path
expect(page).to have_content("error prohibited this user from being saved")
@@ -69,7 +69,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ click_button "Register"
expect(current_path).to eq user_registration_path
expect(page.body).not_to match(/#{user.password}/)
diff --git a/spec/features/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/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb
deleted file mode 100644
index cfe15b9defa..00000000000
--- a/spec/finders/trending_projects_finder_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe TrendingProjectsFinder do
- let(:user) { create(:user) }
- let(:public_project1) { create(:empty_project, :public) }
- let(:public_project2) { create(:empty_project, :public) }
- let(:private_project) { create(:empty_project, :private) }
- let(:internal_project) { create(:empty_project, :internal) }
-
- before do
- 3.times do
- create(:note_on_commit, project: public_project1)
- end
-
- 2.times do
- create(:note_on_commit, project: public_project2, created_at: 5.weeks.ago)
- end
-
- create(:note_on_commit, project: private_project)
- create(:note_on_commit, project: internal_project)
- end
-
- describe '#execute', caching: true do
- context 'without an explicit time range' do
- it 'returns public trending projects' do
- projects = described_class.new.execute
-
- expect(projects).to eq([public_project1])
- end
- end
-
- context 'with an explicit time range' do
- it 'returns public trending projects' do
- projects = described_class.new.execute(2)
-
- expect(projects).to eq([public_project1, public_project2])
- end
- end
-
- it 'caches the list of projects' do
- projects = described_class.new
-
- expect(Project).to receive(:trending).once
-
- 2.times { projects.execute }
- end
- end
-end
diff --git a/spec/fixtures/api/schemas/board.json b/spec/fixtures/api/schemas/board.json
new file mode 100644
index 00000000000..03aca4a3cc0
--- /dev/null
+++ b/spec/fixtures/api/schemas/board.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required" : [
+ "id"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "name": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/boards.json b/spec/fixtures/api/schemas/boards.json
new file mode 100644
index 00000000000..117564ef77a
--- /dev/null
+++ b/spec/fixtures/api/schemas/boards.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "board.json" }
+}
diff --git a/spec/fixtures/api/schemas/conflicts.json b/spec/fixtures/api/schemas/conflicts.json
new file mode 100644
index 00000000000..a947783d505
--- /dev/null
+++ b/spec/fixtures/api/schemas/conflicts.json
@@ -0,0 +1,137 @@
+{
+ "type": "object",
+ "required": [
+ "commit_message",
+ "commit_sha",
+ "source_branch",
+ "target_branch",
+ "files"
+ ],
+ "properties": {
+ "commit_message": {"type": "string"},
+ "commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
+ "source_branch": {"type": "string"},
+ "target_branch": {"type": "string"},
+ "files": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ { "$ref": "#/definitions/conflict-text-with-sections" },
+ { "$ref": "#/definitions/conflict-text-for-editor" }
+ ]
+ }
+ }
+ },
+ "definitions": {
+ "conflict-base": {
+ "type": "object",
+ "required": [
+ "old_path",
+ "new_path",
+ "blob_icon",
+ "blob_path"
+ ],
+ "properties": {
+ "old_path": {"type": "string"},
+ "new_path": {"type": "string"},
+ "blob_icon": {"type": "string"},
+ "blob_path": {"type": "string"}
+ }
+ },
+ "conflict-text-for-editor": {
+ "allOf": [
+ {"$ref": "#/definitions/conflict-base"},
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "content_path"
+ ],
+ "properties": {
+ "type": {"type": {"enum": ["text-editor"]}},
+ "content_path": {"type": "string"}
+ }
+ }
+ ]
+ },
+ "conflict-text-with-sections": {
+ "allOf": [
+ {"$ref": "#/definitions/conflict-base"},
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "content_path",
+ "sections"
+ ],
+ "properties": {
+ "type": {"type": {"enum": ["text"]}},
+ "content_path": {"type": "string"},
+ "sections": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ { "$ref": "#/definitions/section-context" },
+ { "$ref": "#/definitions/section-conflict" }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ },
+ "section-base": {
+ "type": "object",
+ "required": [
+ "conflict",
+ "lines"
+ ],
+ "properties": {
+ "conflict": {"type": "boolean"},
+ "lines": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "old_line",
+ "new_line",
+ "text",
+ "rich_text"
+ ],
+ "properties": {
+ "type": {"type": "string"},
+ "old_line": {"type": "string"},
+ "new_line": {"type": "string"},
+ "text": {"type": "string"},
+ "rich_text": {"type": "string"}
+ }
+ }
+ }
+ }
+ },
+ "section-context": {
+ "allOf": [
+ {"$ref": "#/definitions/section-base"},
+ {
+ "type": "object",
+ "properties": {
+ "conflict": {"enum": [false]}
+ }
+ }
+ ]
+ },
+ "section-conflict": {
+ "allOf": [
+ {"$ref": "#/definitions/section-base"},
+ {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "conflict": {"enum": [true]},
+ "id": {"type": "string"}
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index c5b5aa8c445..64aa41020c9 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -19,7 +19,7 @@ describe SearchHelper do
expect(subject.filename).to eq('CHANGELOG')
expect(subject.basename).to eq('CHANGELOG')
expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(186)
+ expect(subject.startline).to eq(188)
expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
end
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index 078e4b00023..15c305ce321 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -14,7 +14,7 @@
(() => {
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
$.cookie('issue_board_welcome_hidden', 'false');
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index 3569d1b98bd..328c6f82ab5 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -16,7 +16,7 @@ describe('Issue model', () => {
let issue;
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
issue = new ListIssue({
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index 1688b996162..ec78d82e919 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -16,7 +16,7 @@ describe('List model', () => {
let list;
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index f3797ed44d4..052455f2ca6 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -26,7 +26,7 @@ const listObjDuplicate = {
const BoardsMockData = {
'GET': {
- '/test/issue-boards/board/lists{/id}/issues': {
+ '/test/issue-boards/board/1/lists{/id}/issues': {
issues: [{
title: 'Testing',
iid: 1,
@@ -37,13 +37,13 @@ const BoardsMockData = {
}
},
'POST': {
- '/test/issue-boards/board/lists{/id}': listObj
+ '/test/issue-boards/board/1/lists{/id}': listObj
},
'PUT': {
- '/test/issue-boards/board/lists{/id}': {}
+ '/test/issue-boards/board/1/lists{/id}': {}
},
'DELETE': {
- '/test/issue-boards/board/lists{/id}': {}
+ '/test/issue-boards/board/1/lists{/id}': {}
}
};
diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml
new file mode 100644
index 00000000000..2526e5e33a5
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_field_errors.html.haml
@@ -0,0 +1,15 @@
+%form.show-gl-field-errors{action: 'submit', method: 'post'}
+ .form-group
+ %input.required-text{required: true, type: 'text'} Text
+ .form-group
+ %input.email{type: 'email', title: 'Please provide a valid email address.', required: true } Email
+ .form-group
+ %input.password{type: 'password', required: true} Password
+ .form-group
+ %input.alphanumeric{type: 'text', pattern: '[a-zA-Z0-9]', required: true} Alphanumeric
+ .form-group
+ %input.hidden{ type:'hidden' }
+ .form-group
+ %input.custom.no-gl-field-errors{ type:'text' } Custom, do not validate
+ .form-group
+ %input.submit{type: 'submit'} Submit
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
new file mode 100644
index 00000000000..36feb2b2aa5
--- /dev/null
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -0,0 +1,111 @@
+//= require jquery
+//= require gl_field_errors
+
+((global) => {
+ fixture.preload('gl_field_errors.html');
+
+ describe('GL Style Field Errors', function() {
+ beforeEach(function() {
+ fixture.load('gl_field_errors.html');
+ const $form = this.$form = $('form.show-gl-field-errors');
+ this.fieldErrors = new global.GlFieldErrors($form);
+ });
+
+ it('should properly initialize the form', function() {
+ expect(this.$form).toBeDefined();
+ expect(this.$form.length).toBe(1);
+ expect(this.fieldErrors).toBeDefined();
+ const inputs = this.fieldErrors.state.inputs;
+ expect(inputs.length).toBe(5);
+ });
+
+ it('should ignore elements with custom error handling', function() {
+ const customErrorFlag = 'no-gl-field-errors';
+ const customErrorElem = $(`.${customErrorFlag}`);
+
+ expect(customErrorElem.length).toBe(1);
+
+ const customErrors = this.fieldErrors.state.inputs.filter((input) => {
+ return input.inputElement.hasClass(customErrorFlag);
+ });
+ expect(customErrors.length).toBe(0);
+ });
+
+ it('should not show any errors before submit attempt', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
+
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(0);
+ });
+
+ it('should show errors when input valid is submitted', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
+
+ this.$form.submit();
+
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(4);
+ });
+
+ it('should properly track validity state on input after invalid submission attempt', function() {
+ this.$form.submit();
+
+ const emailInputModel = this.fieldErrors.state.inputs[1];
+ const fieldState = emailInputModel.state;
+ const emailInputElement = emailInputModel.inputElement;
+
+ // No input
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then empty input
+ emailInputElement.val('').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+ });
+
+ it('should properly infer error messages', function() {
+ this.$form.submit();
+ const trackedInputs = this.fieldErrors.state.inputs;
+ const inputHasTitle = trackedInputs[1];
+ const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
+ const inputNoTitle = trackedInputs[2];
+ const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
+
+ expect(noTitleErrorElem.text()).toBe('This field is required.');
+ expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
+ });
+
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 395032a7416..96ee5235acf 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,5 +1,6 @@
/*= require merge_request_tabs */
+//= require breakpoints
(function() {
describe('MergeRequestTabs', function() {
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 17b32914ec3..c9175e2b704 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,5 +1,5 @@
-
/*= require merge_request_widget */
+/*= require lib/utils/jquery.timeago.js */
(function() {
describe('MergeRequestWidget', function() {
@@ -8,6 +8,7 @@
window.notify = function() {};
this.opts = {
ci_status_url: "http://sampledomain.local/ci/getstatus",
+ ci_environments_status_url: "http://sampledomain.local/ci/getenvironmentsstatus",
ci_status: "",
ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
@@ -20,17 +21,48 @@
gitlab_icon: "gitlab_logo.png",
builds_path: "http://sampledomain.local/sampleBuildsPath"
};
- this["class"] = new MergeRequestWidget(this.opts);
- return this.ciStatusData = {
- "title": "Sample MR title",
- "sha": "12a34bc5",
- "status": "success",
- "coverage": 98
- };
+ this["class"] = new window.gl.MergeRequestWidget(this.opts);
});
+
+ describe('getCIEnvironmentsStatus', function() {
+ beforeEach(function() {
+ this.ciEnvironmentsStatusData = [{
+ created_at: '2016-09-12T13:38:30.636Z',
+ environment_id: 1,
+ environment_name: 'env1',
+ external_url: 'https://test-url.com',
+ external_url_formatted: 'test-url.com'
+ }];
+
+ spyOn(jQuery, 'getJSON').and.callFake((req, cb) => {
+ cb(this.ciEnvironmentsStatusData);
+ });
+ });
+
+ it('should call renderEnvironments when the environments property is set', function() {
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
+ });
+
+ it('should not call renderEnvironments when the environments property is not set', function() {
+ this.ciEnvironmentsStatusData = null;
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
return describe('getCIStatus', function() {
beforeEach(function() {
- return spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
+ this.ciStatusData = {
+ "title": "Sample MR title",
+ "sha": "12a34bc5",
+ "status": "success",
+ "coverage": 98
+ };
+
+ spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
return function(req, cb) {
return cb(_this.ciStatusData);
};
@@ -61,10 +93,10 @@
this["class"].getCIStatus(false);
return expect(spy).not.toHaveBeenCalled();
});
- return it('should not display a notification on the first check after the widget has been created', function() {
+ it('should not display a notification on the first check after the widget has been created', function() {
var spy;
spy = spyOn(window, 'notify');
- this["class"] = new MergeRequestWidget(this.opts);
+ this["class"] = new window.gl.MergeRequestWidget(this.opts);
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 7ce3884f844..784b43d4846 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -21,7 +21,7 @@
setupButton = this.container.find("#js-login-u2f-device");
setupMessage = this.container.find("p");
expect(setupMessage.text()).toContain('Insert your security key');
- expect(setupButton.text()).toBe('Login Via U2F Device');
+ expect(setupButton.text()).toBe('Sign in via U2F device');
setupButton.trigger('click');
inProgressMessage = this.container.find("p");
expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index b5b38cf0c8c..c8e62f528df 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -12,11 +12,16 @@ describe Banzai::Filter::EmojiFilter, lib: true do
ActionController::Base.asset_host = @original_asset_host
end
- it 'replaces supported emoji' do
+ it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
end
+ it 'replaces supported unicode emoji' do
+ doc = filter('<p>❤️</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+ end
+
it 'ignores unsupported emoji' do
exp = act = '<p>:foo:</p>'
doc = filter(act)
@@ -28,46 +33,96 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
end
+ it 'correctly encodes unicode to the URL' do
+ doc = filter('<p>👍</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+ end
+
it 'matches at the start of a string' do
doc = filter(':+1:')
expect(doc.css('img').size).to eq 1
end
+ it 'unicode matches at the start of a string' do
+ doc = filter("'👍'")
+ expect(doc.css('img').size).to eq 1
+ end
+
it 'matches at the end of a string' do
doc = filter('This gets a :-1:')
expect(doc.css('img').size).to eq 1
end
+ it 'unicode matches at the end of a string' do
+ doc = filter('This gets a 👍')
+ expect(doc.css('img').size).to eq 1
+ end
+
it 'matches with adjacent text' do
doc = filter('+1 (:+1:)')
expect(doc.css('img').size).to eq 1
end
+ it 'unicode matches with adjacent text' do
+ doc = filter('+1 (👍)')
+ expect(doc.css('img').size).to eq 1
+ end
+
it 'matches multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
expect(doc.css('img').size).to eq 3
end
+ it 'unicode matches multiple emoji in a row' do
+ doc = filter("'🙈🙉🙊'")
+ expect(doc.css('img').size).to eq 3
+ end
+
+ it 'mixed matches multiple emoji in a row' do
+ doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'")
+ expect(doc.css('img').size).to eq 6
+ end
+
it 'has a title attribute' do
doc = filter(':-1:')
expect(doc.css('img').first.attr('title')).to eq ':-1:'
end
+ it 'unicode has a title attribute' do
+ doc = filter("'👎'")
+ expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:'
+ end
+
it 'has an alt attribute' do
doc = filter(':-1:')
expect(doc.css('img').first.attr('alt')).to eq ':-1:'
end
+ it 'unicode has an alt attribute' do
+ doc = filter("'👎'")
+ expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:'
+ end
+
it 'has an align attribute' do
doc = filter(':8ball:')
expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
end
+ it 'unicode has an align attribute' do
+ doc = filter("'🎱'")
+ expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
+ end
+
it 'has an emoji class' do
doc = filter(':cat:')
expect(doc.css('img').first.attr('class')).to eq 'emoji'
end
+ it 'unicode has an emoji class' do
+ doc = filter("'🐱'")
+ expect(doc.css('img').first.attr('class')).to eq 'emoji'
+ end
+
it 'has height and width attributes' do
doc = filter(':dog:')
img = doc.css('img').first
@@ -76,12 +131,26 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(img.attr('height')).to eq '20'
end
+ it 'unicode has height and width attributes' do
+ doc = filter("'🐶'")
+ img = doc.css('img').first
+
+ expect(img.attr('width')).to eq '20'
+ expect(img.attr('height')).to eq '20'
+ end
+
it 'keeps whitespace intact' do
doc = filter('This deserves a :+1:, big time.')
expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
end
+ it 'unicode keeps whitespace intact' do
+ doc = filter('This deserves a 🎱, big time.')
+
+ expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+ end
+
it 'uses a custom asset_root context' do
root = Gitlab.config.gitlab.url + 'gitlab/root'
@@ -95,4 +164,18 @@ describe Banzai::Filter::EmojiFilter, lib: true do
doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
end
+
+ it 'uses a custom asset_root context' do
+ root = Gitlab.config.gitlab.url + 'gitlab/root'
+
+ doc = filter("'🎱'", asset_root: root)
+ expect(doc.css('img').first.attr('src')).to start_with(root)
+ end
+
+ it 'uses a custom asset_host context' do
+ ActionController::Base.asset_host = 'https://cdn.example.com'
+
+ doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?')
+ expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+ end
end
diff --git a/spec/lib/banzai/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/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index e10c1f5c547..0e85e302f29 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -6,6 +6,7 @@ describe ExtractsPath, lib: true do
include Gitlab::Routing.url_helpers
let(:project) { double('project') }
+ let(:request) { double('request') }
before do
@project = project
@@ -15,9 +16,10 @@ describe ExtractsPath, lib: true do
allow(project).to receive(:repository).and_return(repo)
allow(project).to receive(:path_with_namespace).
and_return('gitlab/gitlab-ci')
+ allow(request).to receive(:format=)
end
- describe '#assign_ref' do
+ describe '#assign_ref_vars' do
let(:ref) { sample_commit[:id] }
let(:params) { { path: sample_commit[:line_code_path], ref: ref } }
@@ -61,6 +63,75 @@ describe ExtractsPath, lib: true do
expect(@id).to eq(get_id)
end
end
+
+ context 'ref only exists without .atom suffix' do
+ context 'with a path' do
+ let(:params) { { ref: 'v1.0.0.atom', path: 'README.md' } }
+
+ it 'renders a 404' do
+ expect(self).to receive(:render_404)
+
+ assign_ref_vars
+ end
+ end
+
+ context 'without a path' do
+ let(:params) { { ref: 'v1.0.0.atom' } }
+ before { assign_ref_vars }
+
+ it 'sets the un-suffixed version as @ref' do
+ expect(@ref).to eq('v1.0.0')
+ end
+
+ it 'sets the request format to Atom' do
+ expect(request).to have_received(:format=).with(:atom)
+ end
+ end
+ end
+
+ context 'ref exists with .atom suffix' do
+ context 'with a path' do
+ let(:params) { { ref: 'master.atom', path: 'README.md' } }
+
+ before do
+ repository = @project.repository
+ allow(repository).to receive(:commit).and_call_original
+ allow(repository).to receive(:commit).with('master.atom').and_return(repository.commit('master'))
+
+ assign_ref_vars
+ end
+
+ it 'sets the suffixed version as @ref' do
+ expect(@ref).to eq('master.atom')
+ end
+
+ it 'does not change the request format' do
+ expect(request).not_to have_received(:format=)
+ end
+ end
+
+ context 'without a path' do
+ let(:params) { { ref: 'master.atom' } }
+
+ before do
+ repository = @project.repository
+ allow(repository).to receive(:commit).and_call_original
+ allow(repository).to receive(:commit).with('master.atom').and_return(repository.commit('master'))
+ end
+
+ it 'sets the suffixed version as @ref' do
+ assign_ref_vars
+
+ expect(@ref).to eq('master.atom')
+ end
+
+ it 'does not change the request format' do
+ expect(request).not_to receive(:format=)
+
+ assign_ref_vars
+ end
+ end
+ end
end
describe '#extract_ref' do
@@ -115,4 +186,18 @@ describe ExtractsPath, lib: true do
end
end
end
+
+ describe '#extract_ref_without_atom' do
+ it 'ignores any matching refs suffixed with atom' do
+ expect(extract_ref_without_atom('master.atom')).to eq('master')
+ end
+
+ it 'returns the longest matching ref' do
+ expect(extract_ref_without_atom('release/app/v1.0.0.atom')).to eq('release/app/v1.0.0')
+ end
+
+ it 'returns nil if there are no matching refs' do
+ expect(extract_ref_without_atom('foo.atom')).to eq(nil)
+ end
+ end
end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 60020487061..648d342ecf8 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -257,5 +257,16 @@ FILE
it 'includes the blob icon for the file' do
expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
end
+
+ context 'with the full_content option passed' do
+ it 'includes the full content of the conflict' do
+ expect(conflict_file.as_json(full_content: true)).to have_key(:content)
+ end
+
+ it 'includes the detected language of the conflict file' do
+ expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]).
+ to eq('ruby')
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index b73434e8dd7..a379f798a16 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -8,13 +8,13 @@ describe Gitlab::DataBuilder::Push, lib: true do
let(:data) { described_class.build_sample(project, user) }
it { expect(data).to be_a(Hash) }
- it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(data[:before]).to eq('1b12f15a11fc6e62177bef08f47bc7b5ce50b141') }
+ it { expect(data[:after]).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') }
it { expect(data[:ref]).to eq('refs/heads/master') }
it { expect(data[:commits].size).to eq(3) }
it { expect(data[:total_commits_count]).to eq(3) }
- it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) }
- it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) }
+ it { expect(data[:commits].first[:added]).to eq(['bar/branch-test.txt']) }
+ it { expect(data[:commits].first[:modified]).to eq([]) }
it { expect(data[:commits].first[:removed]).to eq([]) }
include_examples 'project hook data with deprecateds'
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 006569254a6..5d5836e9bee 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -118,7 +118,7 @@ project:
- creator
- group
- namespace
-- board
+- boards
- last_event
- services
- campfire_service
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 0e4130e8a3a..c8207e58e90 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -628,7 +628,7 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do
- is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
+ is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/
end
it 'contains a link to the commit' do
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index c5658bd26e1..0b72a2f979b 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Appearance, type: :model do
- subject { create(:appearance) }
+ subject { build(:appearance) }
it { is_expected.to be_valid }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 550a890797e..163c0b5c516 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -187,33 +187,24 @@ describe Ci::Pipeline, models: true do
end
end
- describe "merge request metrics" do
+ describe 'merge request metrics' do
let(:project) { FactoryGirl.create :project }
let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
- context 'when transitioning to running' do
- it 'records the build start time' do
- time = Time.now
- Timecop.freeze(time) { build.run }
-
- expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(time)
- end
-
- it 'clears the build end time' do
- build.run
+ before do
+ expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id)
+ end
- expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
+ context 'when transitioning to running' do
+ it 'schedules metrics workers' do
+ pipeline.run
end
end
context 'when transitioning to success' do
- it 'records the build end time' do
- build.run
- time = Time.now
- Timecop.freeze(time) { build.success }
-
- expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(time)
+ it 'schedules metrics workers' do
+ pipeline.succeed
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index d3e6a6648cc..51be3f36135 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -164,10 +164,10 @@ eos
let(:data) { commit.hook_attrs(with_changed_files: true) }
it { expect(data).to be_a(Hash) }
- it { expect(data[:message]).to include('Add submodule from gitlab.com') }
- it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') }
- it { expect(data[:added]).to eq(["gitlab-grack"]) }
- it { expect(data[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
+ it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') }
+ it { expect(data[:added]).to eq(["bar/branch-test.txt"]) }
+ it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) }
end
diff --git a/spec/models/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/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index b9381e33914..7691d690db0 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -8,35 +8,69 @@ describe 'CycleAnalytics#code', feature: true do
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
- generate_cycle_analytics_spec(
- phase: :code,
- data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
- start_time_conditions: [["issue mentioned in a commit",
- -> (context, data) do
- context.create_commit_referencing_issue(data[:issue])
- end]],
- end_time_conditions: [["merge request that closes issue is created",
- -> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
- end]],
- post_fn: -> (context, data) do
- context.merge_merge_requests_closing_issue(data[:issue])
- context.deploy_master
- end)
-
- context "when a regular merge request (that doesn't close the issue) is created" do
- it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
-
- create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
-
- merge_merge_requests_closing_issue(issue)
- deploy_master
+ context 'with deployment' do
+ generate_cycle_analytics_spec(
+ phase: :code,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when a regular merge request (that doesn't close the issue) is created" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.code).to be_nil
end
+ end
+ end
- expect(subject.code).to be_nil
+ context 'without deployment' do
+ generate_cycle_analytics_spec(
+ phase: :code,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end)
+
+ context "when a regular merge request (that doesn't close the issue) is created" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
+
+ merge_merge_requests_closing_issue(issue)
+ end
+
+ expect(subject.code).to be_nil
+ end
end
end
end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index e9cc71254ab..f649b44d367 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -28,7 +28,6 @@ describe 'CycleAnalytics#issue', models: true do
if data[:issue].persisted?
context.create_merge_request_closing_issue(data[:issue].reload)
context.merge_merge_requests_closing_issue(data[:issue])
- context.deploy_master
end
end)
@@ -41,7 +40,6 @@ describe 'CycleAnalytics#issue', models: true do
create_merge_request_closing_issue(issue)
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.issue).to be_nil
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 5b8c96dc992..2cdefbeef21 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -31,7 +31,6 @@ describe 'CycleAnalytics#plan', feature: true do
post_fn: -> (context, data) do
context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
context.merge_merge_requests_closing_issue(data[:issue])
- context.deploy_master
end)
context "when a regular label (instead of a list label) is added to the issue" do
@@ -44,7 +43,6 @@ describe 'CycleAnalytics#plan', feature: true do
create_merge_request_closing_issue(issue, source_branch: branch_name)
merge_merge_requests_closing_issue(issue)
- deploy_master
expect(subject.issue).to be_nil
end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index b6e26d8f261..0ed080a42b1 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -19,14 +19,12 @@ describe 'CycleAnalytics#review', feature: true do
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
end]],
- post_fn: -> (context, data) { context.deploy_master })
+ post_fn: nil)
context "when a regular merge request (that doesn't close the issue) is created and merged" do
it "returns nil" do
5.times do
MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
-
- deploy_master
end
expect(subject.review).to be_nil
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 89ace0b2742..02ddfeed9c1 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -20,7 +20,6 @@ describe 'CycleAnalytics#test', feature: true do
end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
- context.deploy_master
end)
context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
@@ -34,7 +33,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.succeed!
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.test).to be_nil
@@ -48,8 +46,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.run!
pipeline.succeed!
-
- deploy_master
end
expect(subject.test).to be_nil
@@ -67,7 +63,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.drop!
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.test).to be_nil
@@ -85,7 +80,6 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.cancel!
merge_merge_requests_closing_issue(issue)
- deploy_master
end
expect(subject.test).to be_nil
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 6a90598a629..93623e8e99b 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -1,9 +1,6 @@
require 'spec_helper'
describe DeployKey, models: true do
- let(:project) { create(:project) }
- let(:deploy_key) { create(:deploy_key, projects: [project]) }
-
describe "Associations" do
it { is_expected.to have_many(:deploy_keys_projects) }
it { is_expected.to have_many(:projects) }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index bfff639ad78..01a4a53a264 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -38,5 +38,14 @@ 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
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 6b1867a44e1..e172ee8e590 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -64,6 +64,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 }
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index af5002487cc..733b79079ed 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -27,17 +27,17 @@ describe Event, models: true do
end
describe "Push event" do
- before do
- project = create(:project)
- @user = project.owner
- @event = create_event(project, @user)
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:event) { create_event(project, user) }
+
+ it do
+ expect(event.push?).to be_truthy
+ expect(event.visible_to_user?).to be_truthy
+ expect(event.tag?).to be_falsey
+ expect(event.branch_name).to eq("master")
+ expect(event.author).to eq(user)
end
-
- it { expect(@event.push?).to be_truthy }
- it { expect(@event.visible_to_user?).to be_truthy }
- it { expect(@event.tag?).to be_falsey }
- it { expect(@event.branch_name).to eq("master") }
- it { expect(@event.author).to eq(@user) }
end
describe '#note?' do
@@ -59,8 +59,8 @@ describe Event, models: true do
describe '#visible_to_user?' do
let(:project) { create(:empty_project, :public) }
let(:non_member) { create(:user) }
- let(:member) { create(:user) }
- let(:guest) { create(:user) }
+ let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
@@ -79,23 +79,27 @@ describe Event, models: true do
context 'for non confidential issues' do
let(:target) { issue }
- it { expect(event.visible_to_user?(non_member)).to eq true }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq true }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
context 'for confidential issues' do
let(:target) { confidential_issue }
- it { expect(event.visible_to_user?(non_member)).to eq false }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq false }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
end
@@ -103,23 +107,27 @@ describe Event, models: true do
context 'on non confidential issues' do
let(:target) { note_on_issue }
- it { expect(event.visible_to_user?(non_member)).to eq true }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq true }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
context 'on confidential issues' do
let(:target) { note_on_confidential_issue }
- it { expect(event.visible_to_user?(non_member)).to eq false }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq false }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
end
end
@@ -129,12 +137,27 @@ describe Event, models: true do
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
- it { expect(event.visible_to_user?(non_member)).to eq true }
- it { expect(event.visible_to_user?(author)).to eq true }
- it { expect(event.visible_to_user?(assignee)).to eq true }
- it { expect(event.visible_to_user?(member)).to eq true }
- it { expect(event.visible_to_user?(guest)).to eq true }
- it { expect(event.visible_to_user?(admin)).to eq true }
+ it do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+
+ context 'private project' do
+ let(:project) { create(:project, :private) }
+
+ it do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(author)).to eq true
+ expect(event.visible_to_user?(assignee)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
end
end
@@ -203,6 +226,6 @@ describe Event, models: true do
action: Event::PUSHED,
data: data,
author_id: user.id
- }.merge(attrs))
+ }.merge!(attrs))
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index fd4a2beff58..7fc6ed1dd54 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -5,9 +5,6 @@ describe Key, models: true do
it { is_expected.to belong_to(:user) }
end
- describe "Mass assignment" do
- end
-
describe "Validation" do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:key) }
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index 5e6f8ca1528..c18ed8574b1 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
describe LabelLink, models: true do
- let(:label) { create(:label_link) }
- it { expect(label).to be_valid }
+ it { expect(build(:label_link)).to be_valid }
it { is_expected.to belong_to(:label) }
it { is_expected.to belong_to(:target) }
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 530a7def553..e5007424041 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -6,9 +6,9 @@ describe MergeRequestDiff, models: true do
it { expect(subject).to be_valid }
it { expect(subject).to be_persisted }
- it { expect(subject.commits.count).to eq(5) }
- it { expect(subject.diffs.count).to eq(8) }
- it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(subject.commits.count).to eq(29) }
+ it { expect(subject.diffs.count).to eq(20) }
+ it { expect(subject.head_commit_sha).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') }
it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
end
@@ -44,6 +44,16 @@ describe MergeRequestDiff, models: true do
end
end
+ context 'when the raw diffs have invalid content' do
+ before { mr_diff.update_attributes(st_diffs: ["--broken-diff"]) }
+
+ it 'returns an empty DiffCollection' do
+ expect(mr_diff.raw_diffs.to_a).to be_empty
+ expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(mr_diff.raw_diffs).to be_empty
+ end
+ end
+
context 'when the raw diffs exist' do
it 'returns the diffs' do
expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
@@ -64,27 +74,43 @@ describe MergeRequestDiff, models: true do
end
end
end
+ end
- describe '#commits_sha' do
- shared_examples 'returning all commits SHA' do
- it 'returns all commits SHA' do
- commits_sha = subject.commits_sha
+ describe '#commits_sha' do
+ shared_examples 'returning all commits SHA' do
+ it 'returns all commits SHA' do
+ commits_sha = subject.commits_sha
- expect(commits_sha).to eq(subject.commits.map(&:sha))
- end
+ expect(commits_sha).to eq(subject.commits.map(&:sha))
end
+ end
- context 'when commits were loaded' do
- before do
- subject.commits
- end
-
- it_behaves_like 'returning all commits SHA'
+ context 'when commits were loaded' do
+ before do
+ subject.commits
end
- context 'when commits were not loaded' do
- it_behaves_like 'returning all commits SHA'
- end
+ it_behaves_like 'returning all commits SHA'
+ end
+
+ context 'when commits were not loaded' do
+ it_behaves_like 'returning all commits SHA'
+ end
+ end
+
+ describe '#compare_with' do
+ subject { create(:merge_request, source_branch: 'fix').merge_request_diff }
+
+ it 'delegates compare to the service' do
+ expect(CompareService).to receive(:new).and_call_original
+
+ subject.compare_with(nil)
+ end
+
+ it 'uses git diff A..B approach by default' do
+ diffs = subject.compare_with('0b4bc9a49b562e85de7cc9e834518ea6828729b9').diffs
+
+ expect(diffs.size).to eq(3)
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 38b6da50168..91a423b670c 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -489,7 +489,7 @@ describe MergeRequest, models: true do
subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
- expect(subject.diverged_commits_count).to eq(5)
+ expect(subject.diverged_commits_count).to eq(29)
end
end
@@ -497,7 +497,7 @@ describe MergeRequest, models: true do
subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
- expect(subject.diverged_commits_count).to eq(5)
+ expect(subject.diverged_commits_count).to eq(29)
end
end
@@ -1155,12 +1155,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,6 +1166,12 @@ 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
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 2fa6715fcaf..c5ff1941378 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -11,7 +11,7 @@ describe ProjectGroupLink do
it { should validate_presence_of(:project_id) }
it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
- it { should validate_presence_of(:group_id) }
+ it { should validate_presence_of(:group) }
it { should validate_presence_of(:group_access) }
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 8aadfcb439b..67dbcc362f6 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -24,7 +24,7 @@ describe Project, models: true do
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
- it { is_expected.to have_one(:board).dependent(:destroy) }
+ it { is_expected.to have_many(:boards).dependent(:destroy) }
it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
@@ -94,6 +94,15 @@ describe Project, models: true do
end
end
end
+
+ describe '#boards' do
+ it 'raises an error when attempting to add more than one board to the project' do
+ subject.boards.build
+
+ expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
+ expect(subject.boards.size).to eq 1
+ end
+ end
end
describe 'modules' do
@@ -219,7 +228,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) }
@@ -380,26 +388,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
@@ -800,32 +788,14 @@ describe Project, models: true do
end
create(:note_on_commit, project: project2)
- end
-
- describe 'without an explicit start date' do
- subject { described_class.trending.to_a }
- it 'sorts Projects by the amount of notes in descending order' do
- expect(subject).to eq([project1, project2])
- end
+ TrendingProject.refresh!
end
- describe 'with an explicit start date' do
- let(:date) { 2.months.ago }
-
- subject { described_class.trending(date).to_a }
+ subject { described_class.trending.to_a }
- before do
- 2.times do
- # Little fix for special issue related to Fractional Seconds support for MySQL.
- # See: https://github.com/rails/rails/pull/14359/files
- create(:note_on_commit, project: project2, created_at: date + 1)
- end
- end
-
- it 'sorts Projects by the amount of notes in descending order' do
- expect(subject).to eq([project2, project1])
- end
+ it 'sorts projects by the amount of notes in descending order' do
+ expect(subject).to eq([project1, project2])
end
it 'does not take system notes into account' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 4641f297465..f977cf73673 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 }
@@ -122,11 +145,30 @@ describe Repository, models: true do
end
describe '#merged_to_root_ref?' do
- context 'merged branch' do
+ context 'merged branch without ff' do
+ subject { repository.merged_to_root_ref?('branch-merged') }
+
+ it { is_expected.to be_truthy }
+ end
+
+ # If the HEAD was ff then it will be false
+ context 'merged with ff' do
subject { repository.merged_to_root_ref?('improve/awesome') }
it { is_expected.to be_truthy }
end
+
+ context 'not merged branch' do
+ subject { repository.merged_to_root_ref?('not-merged-branch') }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'default branch' do
+ subject { repository.merged_to_root_ref?('master') }
+
+ it { is_expected.to be_falsey }
+ end
end
describe '#can_be_merged?' do
@@ -324,7 +366,7 @@ describe Repository, models: true do
subject { results.first }
it { is_expected.to be_an String }
- it { expect(subject.lines[2]).to eq("master:CHANGELOG:188: - Feature: Replace teams with group membership\n") }
+ it { expect(subject.lines[2]).to eq("master:CHANGELOG:190: - Feature: Replace teams with group membership\n") }
end
end
@@ -968,10 +1010,10 @@ describe Repository, models: true do
context 'cherry-picking a merge commit' do
it 'cherry-picks the changes' do
- expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
- repository.cherry_pick(user, pickable_merge, 'master')
- expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).not_to be_nil
+ repository.cherry_pick(user, pickable_merge, 'improve/awesome')
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
end
end
end
diff --git a/spec/models/trending_project_spec.rb b/spec/models/trending_project_spec.rb
new file mode 100644
index 00000000000..cc28c6d4004
--- /dev/null
+++ b/spec/models/trending_project_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe TrendingProject do
+ let(:user) { create(:user) }
+ let(:public_project1) { create(:empty_project, :public) }
+ let(:public_project2) { create(:empty_project, :public) }
+ let(:public_project3) { create(:empty_project, :public) }
+ let(:private_project) { create(:empty_project, :private) }
+ let(:internal_project) { create(:empty_project, :internal) }
+
+ before do
+ 3.times do
+ create(:note_on_commit, project: public_project1)
+ end
+
+ 2.times do
+ create(:note_on_commit, project: public_project2)
+ end
+
+ create(:note_on_commit, project: public_project3, created_at: 5.weeks.ago)
+ create(:note_on_commit, project: private_project)
+ create(:note_on_commit, project: internal_project)
+ end
+
+ describe '.refresh!' do
+ before do
+ described_class.refresh!
+ end
+
+ it 'populates the trending projects table' do
+ expect(described_class.count).to eq(2)
+ end
+
+ it 'removes existing rows before populating the table' do
+ described_class.refresh!
+
+ expect(described_class.count).to eq(2)
+ end
+
+ it 'stores the project IDs for every trending project' do
+ rows = described_class.order(id: :asc).all
+
+ expect(rows[0].project_id).to eq(public_project1.id)
+ expect(rows[1].project_id).to eq(public_project2.id)
+ end
+
+ it 'does not store projects that fall out of the trending time range' do
+ expect(described_class.where(project_id: public_project3).any?).to eq(false)
+ end
+
+ it 'stores only public projects' do
+ expect(described_class.where(project_id: [public_project1.id, public_project2.id]).count).to eq(2)
+ expect(described_class.where(project_id: [private_project.id, internal_project.id]).count).to eq(0)
+ end
+ end
+end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 43c8d884a47..658e3c13a73 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -12,7 +12,7 @@ describe ProjectPolicy, models: true do
[
:read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label,
:read_milestone, :read_project_snippet, :read_project_member,
- :read_merge_request, :read_note, :create_project, :create_issue, :create_note,
+ :read_note, :create_project, :create_issue, :create_note,
:upload_file
]
end
@@ -21,7 +21,8 @@ describe ProjectPolicy, models: true do
[
:download_code, :fork_project, :create_project_snippet, :update_issue,
:admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build,
- :read_container_image, :read_pipeline, :read_environment, :read_deployment
+ :read_container_image, :read_pipeline, :read_environment, :read_deployment,
+ :read_merge_request
]
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index aa610557056..66fa0c0c01f 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -53,7 +53,12 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
- expect(json_response.size).to eq(commits.size - 1)
+ if commits.size >= 20
+ expect(json_response.size).to eq(20)
+ else
+ expect(json_response.size).to eq(commits.size - 1)
+ end
+
expect(json_response.first["id"]).to eq(commits.second.id)
expect(json_response.second["id"]).to eq(commits.third.id)
end
@@ -447,11 +452,12 @@ describe API::API, api: true do
end
it 'returns the inline comment' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 7, line_type: 'new'
+ post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
+
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
- expect(json_response['line']).to eq(7)
+ expect(json_response['line']).to eq(1)
expect(json_response['line_type']).to eq('new')
end
diff --git a/spec/requests/api/license_templates_spec.rb b/spec/requests/api/license_templates_spec.rb
deleted file mode 100644
index 9a1894d63a2..00000000000
--- a/spec/requests/api/license_templates_spec.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true do
- include ApiHelpers
-
- describe 'Entity' do
- before { get api('/licenses/mit') }
-
- it { expect(json_response['key']).to eq('mit') }
- it { expect(json_response['name']).to eq('MIT License') }
- it { expect(json_response['nickname']).to be_nil }
- it { expect(json_response['popular']).to be true }
- it { expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') }
- it { expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') }
- it { expect(json_response['description']).to include('A permissive license that is short and to the point.') }
- it { expect(json_response['conditions']).to eq(%w[include-copyright]) }
- it { expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) }
- it { expect(json_response['limitations']).to eq(%w[no-liability]) }
- it { expect(json_response['content']).to include('The MIT License (MIT)') }
- end
-
- describe 'GET /licenses' do
- it 'returns a list of available license templates' do
- get api('/licenses')
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(15)
- expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
- end
-
- describe 'the popular parameter' do
- context 'with popular=1' do
- it 'returns a list of available popular license templates' do
- get api('/licenses?popular=1')
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(3)
- expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
- end
- end
- end
- end
-
- describe 'GET /licenses/:key' do
- context 'with :project and :fullname given' do
- before do
- get api("/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
- end
-
- context 'for the mit license' do
- let(:license_type) { 'mit' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('The MIT License (MIT)')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
- end
- end
-
- context 'for the agpl-3.0 license' do
- let(:license_type) { 'agpl-3.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the gpl-3.0 license' do
- let(:license_type) { 'gpl-3.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the gpl-2.0 license' do
- let(:license_type) { 'gpl-2.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
- end
- end
-
- context 'for the apache-2.0 license' do
- let(:license_type) { 'apache-2.0' }
-
- it 'returns the license text' do
- expect(json_response['content']).to include('Apache License')
- end
-
- it 'replaces placeholder values' do
- expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
- end
- end
-
- context 'for an uknown license' do
- let(:license_type) { 'muth-over9000' }
-
- it 'returns a 404' do
- expect(response).to have_http_status(404)
- end
- end
- end
-
- context 'with no :fullname given' do
- context 'with an authenticated user' do
- let(:user) { create(:user) }
-
- it 'replaces the copyright owner placeholder with the name of the current user' do
- get api('/licenses/mit', user)
-
- expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
- end
- end
- end
- end
-end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 765dc8a8f66..cfcdcad74cd 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -163,9 +163,10 @@ describe API::API, 'ProjectHooks', api: true do
expect(response).to have_http_status(404)
end
- it "returns a 405 error if hook id not given" do
+ it "returns a 404 error if hook id not given" do
delete api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(405)
+
+ expect(response).to have_http_status(404)
end
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 5f19638b460..973928d007a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -588,37 +588,39 @@ describe API::API, api: true do
before do
note = create(:note_on_issue, note: 'What an awesome day!', project: project)
EventCreateService.new.leave_note(note, note.author)
- get api("/projects/#{project.id}/events", user)
end
- it { expect(response).to have_http_status(200) }
+ it 'returns all events' do
+ get api("/projects/#{project.id}/events", user)
- context 'joined event' do
- let(:json_event) { json_response[1] }
+ expect(response).to have_http_status(200)
- it { expect(json_event['action_name']).to eq('joined') }
- it { expect(json_event['project_id'].to_i).to eq(project.id) }
- it { expect(json_event['author_username']).to eq(user3.username) }
- it { expect(json_event['author']['name']).to eq(user3.name) }
- end
+ first_event = json_response.first
- context 'comment event' do
- let(:json_event) { json_response.first }
+ expect(first_event['action_name']).to eq('commented on')
+ expect(first_event['note']['body']).to eq('What an awesome day!')
- it { expect(json_event['action_name']).to eq('commented on') }
- it { expect(json_event['note']['body']).to eq('What an awesome day!') }
+ last_event = json_response.last
+
+ expect(last_event['action_name']).to eq('joined')
+ expect(last_event['project_id'].to_i).to eq(project.id)
+ expect(last_event['author_username']).to eq(user3.username)
+ expect(last_event['author']['name']).to eq(user3.name)
end
end
it 'returns a 404 error if not found' do
get api('/projects/42/events', user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
+
get api("/projects/#{project.id}/events", other_user)
+
expect(response).to have_http_status(404)
end
end
@@ -819,6 +821,20 @@ describe API::API, api: true do
expect(response.status).to eq 400
end
+ it 'returns a 404 error when user cannot read group' do
+ private_group = create(:group, :private)
+
+ post api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response.status).to eq 404
+ end
+
+ it 'returns a 404 error when group does not exist' do
+ post api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response.status).to eq 404
+ end
+
it "returns a 409 error when wrong params passed" do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
expect(response.status).to eq 409
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 80a856a6e90..c4dc2d9006a 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -21,7 +21,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq('encoding')
+ expect(json_response.first['name']).to eq('bar')
expect(json_response.first['type']).to eq('tree')
expect(json_response.first['mode']).to eq('040000')
end
@@ -166,9 +166,9 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
contributor = json_response.first
- expect(contributor['email']).to eq('dmitriy.zaporozhets@gmail.com')
- expect(contributor['name']).to eq('Dmitriy Zaporozhets')
- expect(contributor['commits']).to eq(13)
+ expect(contributor['email']).to eq('tiagonbotelho@hotmail.com')
+ expect(contributor['name']).to eq('tiagonbotelho')
+ expect(contributor['commits']).to eq(1)
expect(contributor['additions']).to eq(0)
expect(contributor['deletions']).to eq(0)
end
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 5bd5b861792..d32ba60fc4c 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,53 +3,201 @@ require 'spec_helper'
describe API::Templates, api: true do
include ApiHelpers
- context 'global templates' do
- describe 'the Template Entity' do
- before { get api('/gitignores/Ruby') }
+ shared_examples_for 'the Template Entity' do |path|
+ before { get api(path) }
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
+
+ shared_examples_for 'the TemplateList Entity' do |path|
+ before { get api(path) }
+
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
+
+ shared_examples_for 'requesting gitignores' do |path|
+ it 'returns a list of available gitignore templates' do
+ get api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
end
+ end
- describe 'the TemplateList Entity' do
- before { get api('/gitignores') }
+ shared_examples_for 'requesting gitlab-ci-ymls' do |path|
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api(path)
- it { expect(json_response.first['name']).not_to be_nil }
- it { expect(json_response.first['content']).to be_nil }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
end
+ end
+
+ shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path|
+ it 'adds a disclaimer on the top' do
+ get api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
+ end
+
+ shared_examples_for 'the License Template Entity' do |path|
+ before { get api(path) }
+
+ it 'returns a license template' do
+ expect(json_response['key']).to eq('mit')
+ expect(json_response['name']).to eq('MIT License')
+ expect(json_response['nickname']).to be_nil
+ expect(json_response['popular']).to be true
+ expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
+ expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
+ expect(json_response['description']).to include('A permissive license that is short and to the point.')
+ expect(json_response['conditions']).to eq(%w[include-copyright])
+ expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
+ expect(json_response['limitations']).to eq(%w[no-liability])
+ expect(json_response['content']).to include('The MIT License (MIT)')
+ end
+ end
- context 'requesting gitignores' do
- describe 'GET /gitignores' do
- it 'returns a list of available gitignore templates' do
- get api('/gitignores')
+ shared_examples_for 'GET licenses' do |path|
+ it 'returns a list of available license templates' do
+ get api(path)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(15)
+ expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
+ end
+
+ describe 'the popular parameter' do
+ context 'with popular=1' do
+ it 'returns a list of available popular license templates' do
+ get api("#{path}?popular=1")
+
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
+ expect(json_response.size).to eq(3)
+ expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
end
end
end
+ end
- context 'requesting gitlab-ci-ymls' do
- describe 'GET /gitlab_ci_ymls' do
- it 'returns a list of available gitlab_ci_ymls' do
- get api('/gitlab_ci_ymls')
+ shared_examples_for 'GET licenses/:name' do |path|
+ context 'with :project and :fullname given' do
+ before do
+ get api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
+ end
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).not_to be_nil
+ context 'for the mit license' do
+ let(:license_type) { 'mit' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('The MIT License (MIT)')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the agpl-3.0 license' do
+ let(:license_type) { 'agpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the gpl-3.0 license' do
+ let(:license_type) { 'gpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the gpl-2.0 license' do
+ let(:license_type) { 'gpl-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the apache-2.0 license' do
+ let(:license_type) { 'apache-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('Apache License')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for an uknown license' do
+ let(:license_type) { 'muth-over9000' }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
end
end
end
- describe 'GET /gitlab_ci_ymls/Ruby' do
- it 'adds a disclaimer on the top' do
- get api('/gitlab_ci_ymls/Ruby')
+ context 'with no :fullname given' do
+ context 'with an authenticated user' do
+ let(:user) { create(:user) }
+
+ it 'replaces the copyright owner placeholder with the name of the current user' do
+ get api('/templates/licenses/mit', user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).not_to be_nil
- expect(json_response['content']).to start_with("# This file is a template,")
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
+ end
end
end
end
+
+ describe 'with /templates namespace' do
+ it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby'
+ it_behaves_like 'the TemplateList Entity', '/templates/gitignores'
+ it_behaves_like 'requesting gitignores', '/templates/gitignores'
+ it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls'
+ it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby'
+ it_behaves_like 'the License Template Entity', '/templates/licenses/mit'
+ it_behaves_like 'GET licenses', '/templates/licenses'
+ it_behaves_like 'GET licenses/:name', '/templates/licenses'
+ end
+
+ describe 'without /templates namespace' do
+ it_behaves_like 'the Template Entity', '/gitignores/Ruby'
+ it_behaves_like 'the TemplateList Entity', '/gitignores'
+ it_behaves_like 'requesting gitignores', '/gitignores'
+ it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls'
+ it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby'
+ it_behaves_like 'the License Template Entity', '/licenses/mit'
+ it_behaves_like 'GET licenses', '/licenses'
+ it_behaves_like 'GET licenses/:name', '/licenses'
+ end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f4ea3bebb4c..f83f4d2c9b1 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -90,8 +90,9 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "returns a 404 if invalid ID" do
+ it "returns a 404 for invalid ID" do
get api("/users/1ASDF", user)
+
expect(response).to have_http_status(404)
end
end
@@ -340,8 +341,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "raises error for invalid ID" do
- expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 if invalid ID" do
+ put api("/users/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
it 'returns 400 error if user does not validate' do
@@ -493,8 +496,9 @@ describe API::API, api: true do
end.to change{ user.emails.count }.by(1)
end
- it "raises error for invalid ID" do
+ it "returns a 400 for invalid ID" do
post api("/users/999999/emails", admin)
+
expect(response).to have_http_status(400)
end
end
@@ -525,9 +529,10 @@ describe API::API, api: true do
expect(json_response.first['email']).to eq(email.email)
end
- it "raises error for invalid ID" do
+ it "returns a 404 for invalid ID" do
put api("/users/ASDF/emails", admin)
- expect(response).to have_http_status(405)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -566,8 +571,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Email Not Found')
end
- it "raises error for invalid ID" do
- expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/ASDF/emails/bar", admin)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -600,8 +607,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it "raises error for invalid ID" do
- expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -654,6 +663,7 @@ describe API::API, api: true do
it "returns 404 Not Found within invalid ID" do
get api("/user/keys/42", user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -669,6 +679,7 @@ describe API::API, api: true do
it "returns 404 for invalid ID" do
get api("/users/keys/ASDF", admin)
+
expect(response).to have_http_status(404)
end
end
@@ -727,8 +738,10 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "raises error for invalid ID" do
- expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/keys/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -778,6 +791,7 @@ describe API::API, api: true do
it "returns 404 for invalid ID" do
get api("/users/emails/ASDF", admin)
+
expect(response).to have_http_status(404)
end
end
@@ -825,8 +839,10 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "raises error for invalid ID" do
- expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/emails/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -891,8 +907,64 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it "raises error for invalid ID" do
- expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ put api("/users/ASDF/block", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /user/:id/events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
+
+ before do
+ project.add_user(user, :developer)
+ EventCreateService.new.leave_note(note, user)
+ end
+
+ context "as a user than cannot see the event's project" do
+ it 'returns no events' do
+ other_user = create(:user)
+
+ get api("/users/#{user.id}/events", other_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_empty
+ end
+ end
+
+ context "as a user than can see the event's project" do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/users/#{user.id}/events", user) }
+ end
+
+ context 'joined event' do
+ it 'returns the "joined" event' do
+ get api("/users/#{user.id}/events", user)
+
+ comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_event['project_id'].to_i).to eq(project.id)
+ expect(comment_event['author_username']).to eq(user.username)
+ expect(comment_event['note']['id']).to eq(note.id)
+ expect(comment_event['note']['body']).to eq('What an awesome day!')
+
+ joined_event = json_response.find { |e| e['action_name'] == 'joined' }
+
+ expect(joined_event['project_id'].to_i).to eq(project.id)
+ expect(joined_event['author_username']).to eq(user.username)
+ expect(joined_event['author']['name']).to eq(user.name)
+ end
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get api('/users/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
end
end
end
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
new file mode 100644
index 00000000000..54b69a0cae7
--- /dev/null
+++ b/spec/requests/api/version_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ describe 'GET /version' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/version')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ let(:user) { create(:user) }
+
+ it 'returns the version information' do
+ get api('/version', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['version']).to eq(Gitlab::VERSION)
+ expect(json_response['revision']).to eq(Gitlab::REVISION)
+ end
+ end
+ end
+end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index c0c1e62e910..27f0fd22ae6 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -440,8 +440,8 @@ describe 'Git HTTP requests', lib: true do
before do
# Provide a dummy file in its place
allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
- allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
- Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
+ allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
+ Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt')
end
get "/#{project.path_with_namespace}/blob/master/info/refs"
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 77842057a10..2322430d212 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -337,7 +337,7 @@ describe Projects::CommitsController, 'routing' do
end
it 'to #show' do
- expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'atom')
+ expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 0dd00af878d..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
@@ -266,7 +266,15 @@ describe "Groups", "routing" do
end
it "also display group#show on the short path" do
- expect(get('/1')).to route_to('namespaces#show', id: '1')
+ allow(Group).to receive(:find_by_path).and_return(true)
+
+ expect(get('/1')).to route_to('groups#show', id: '1')
+ end
+
+ it "also display group#show with dot in the path" do
+ allow(Group).to receive(:find_by_path).and_return(true)
+
+ expect(get('/group.with.dot')).to route_to('groups#show', id: 'group.with.dot')
end
end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index a1a4dd4c57c..fde807cc410 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -2,33 +2,31 @@ require 'spec_helper'
describe Boards::CreateService, services: true do
describe '#execute' do
+ let(:project) { create(:empty_project) }
+
subject(:service) { described_class.new(project, double) }
context 'when project does not have a board' do
- let(:project) { create(:empty_project, board: nil) }
-
it 'creates a new board' do
expect { service.execute }.to change(Board, :count).by(1)
end
it 'creates default lists' do
- service.execute
+ board = service.execute
- expect(project.board.lists.size).to eq 2
- expect(project.board.lists.first).to be_backlog
- expect(project.board.lists.last).to be_done
+ expect(board.lists.size).to eq 2
+ expect(board.lists.first).to be_backlog
+ expect(board.lists.last).to be_done
end
end
context 'when project has a board' do
- let!(:project) { create(:project_with_board) }
-
- it 'does not create a new board' do
- expect { service.execute }.not_to change(Board, :count)
+ before do
+ create(:board, project: project)
end
- it 'does not create board lists' do
- expect { service.execute }.not_to change(project.board.lists, :count)
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(project.boards, :count)
end
end
end
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index 33e10e79f6d..360ee398f77 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -2,13 +2,13 @@ require 'spec_helper'
describe Boards::Issues::CreateService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project, name: 'in-progress') }
let!(:list) { create(:list, board: board, label: label, position: 0) }
- subject(:service) { described_class.new(project, user, title: 'New issue') }
+ subject(:service) { described_class.new(project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
before do
project.team << [user, :developer]
@@ -17,15 +17,15 @@ describe Boards::Issues::CreateService, services: true do
it 'delegates the create proceedings to Issues::CreateService' do
expect_any_instance_of(Issues::CreateService).to receive(:execute).once
- service.execute(list)
+ service.execute
end
it 'creates a new issue' do
- expect { service.execute(list) }.to change(project.issues, :count).by(1)
+ expect { service.execute }.to change(project.issues, :count).by(1)
end
it 'adds the label of the list to the issue' do
- issue = service.execute(list)
+ issue = service.execute
expect(issue.labels).to eq [label]
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 5b9f454fd2d..7c206cf3ce7 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe Boards::Issues::ListService, services: true do
describe '#execute' do
let(:user) { create(:user) }
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
@@ -13,10 +13,10 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
- let!(:backlog) { project.board.backlog_list }
+ let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { project.board.done_list }
+ let!(:done) { create(:done_list, board: board) }
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
@@ -37,7 +37,7 @@ describe Boards::Issues::ListService, services: true do
end
it 'delegates search to IssuesFinder' do
- params = { id: list1.id }
+ params = { board_id: board.id, id: list1.id }
expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
@@ -46,7 +46,7 @@ describe Boards::Issues::ListService, services: true do
context 'sets default order to priority' do
it 'returns opened issues when listing issues from Backlog' do
- params = { id: backlog.id }
+ params = { board_id: board.id, id: backlog.id }
issues = described_class.new(project, user, params).execute
@@ -54,7 +54,7 @@ describe Boards::Issues::ListService, services: true do
end
it 'returns closed issues when listing issues from Done' do
- params = { id: done.id }
+ params = { board_id: board.id, id: done.id }
issues = described_class.new(project, user, params).execute
@@ -62,12 +62,29 @@ describe Boards::Issues::ListService, services: true do
end
it 'returns opened issues that have label list applied when listing issues from a label list' do
- params = { id: list1.id }
+ params = { board_id: board.id, id: list1.id }
issues = described_class.new(project, user, params).execute
expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
end
end
+
+ context 'with list that does not belong to the board' do
+ it 'raises an error' do
+ list = create(:list)
+ service = described_class.new(project, user, board_id: board.id, id: list.id)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'raises an error' do
+ service = described_class.new(project, user, board_id: board.id, id: nil)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
end
end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 180f1b08631..c43b2aec490 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe Boards::Issues::MoveService, services: true do
describe '#execute' do
let(:user) { create(:user) }
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board1) { create(:board, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
- let!(:backlog) { project.board.backlog_list }
- let!(:list1) { create(:list, board: board, label: development, position: 0) }
- let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { project.board.done_list }
+ let!(:backlog) { create(:backlog_list, board: board1) }
+ let!(:list1) { create(:list, board: board1, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board1) }
before do
project.team << [user, :developer]
@@ -22,7 +22,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from backlog' do
it 'adds the label of the list it goes to' do
issue = create(:labeled_issue, project: project, labels: [bug])
- params = { from_list_id: backlog.id, to_list_id: list1.id }
+ params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id }
described_class.new(project, user, params).execute(issue)
@@ -33,7 +33,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving to backlog' do
it 'removes all list-labels' do
issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
- params = { from_list_id: list1.id, to_list_id: backlog.id }
+ params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id }
described_class.new(project, user, params).execute(issue)
@@ -44,7 +44,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from backlog to done' do
it 'closes the issue' do
issue = create(:labeled_issue, project: project, labels: [bug])
- params = { from_list_id: backlog.id, to_list_id: done.id }
+ params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id }
described_class.new(project, user, params).execute(issue)
issue.reload
@@ -56,7 +56,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
- let(:params) { { from_list_id: list1.id, to_list_id: list2.id } }
+ let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
it 'delegates the label changes to Issues::UpdateService' do
expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
@@ -72,8 +72,12 @@ describe Boards::Issues::MoveService, services: true do
end
context 'when moving to done' do
- let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing]) }
- let(:params) { { from_list_id: list2.id, to_list_id: done.id } }
+ let(:board2) { create(:board, project: project) }
+ let(:regression) { create(:label, project: project, name: 'Regression') }
+ let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
+
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
+ let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: done.id } }
it 'delegates the close proceedings to Issues::CloseService' do
expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
@@ -81,7 +85,7 @@ describe Boards::Issues::MoveService, services: true do
described_class.new(project, user, params).execute(issue)
end
- it 'removes all list-labels and close the issue' do
+ it 'removes all list-labels from project boards and close the issue' do
described_class.new(project, user, params).execute(issue)
issue.reload
@@ -92,7 +96,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from done' do
let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
- let(:params) { { from_list_id: done.id, to_list_id: list2.id } }
+ let(:params) { { board_id: board1.id, from_list_id: done.id, to_list_id: list2.id } }
it 'delegates the re-open proceedings to Issues::ReopenService' do
expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
@@ -112,7 +116,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving from done to backlog' do
it 'reopens the issue' do
issue = create(:labeled_issue, :closed, project: project, labels: [bug])
- params = { from_list_id: done.id, to_list_id: backlog.id }
+ params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id }
described_class.new(project, user, params).execute(issue)
issue.reload
@@ -124,7 +128,7 @@ describe Boards::Issues::MoveService, services: true do
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
- let(:params) { { from_list_id: list1.id, to_list_id: list1.id } }
+ let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
it 'returns false' do
expect(described_class.new(project, user, params).execute(issue)).to eq false
diff --git a/spec/services/boards/list_service_spec.rb b/spec/services/boards/list_service_spec.rb
new file mode 100644
index 00000000000..dff33e4bcbb
--- /dev/null
+++ b/spec/services/boards/list_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Boards::ListService, services: true do
+ describe '#execute' do
+ let(:project) { create(:empty_project) }
+
+ subject(:service) { described_class.new(project, double) }
+
+ context 'when project does not have a board' do
+ it 'creates a new project board' do
+ expect { service.execute }.to change(project.boards, :count).by(1)
+ end
+
+ it 'delegates the project board creation to Boards::CreateService' do
+ expect_any_instance_of(Boards::CreateService).to receive(:execute).once
+
+ service.execute
+ end
+ end
+
+ context 'when project has a board' do
+ before do
+ create(:board, project: project)
+ end
+
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(project.boards, :count)
+ end
+ end
+
+ it 'returns project boards' do
+ board = create(:board, project: project)
+
+ expect(service.execute).to match_array [board]
+ end
+ end
+end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index bff9c1fd1fe..e7806add916 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Boards::Lists::CreateService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project, name: 'in-progress') }
@@ -11,7 +11,7 @@ describe Boards::Lists::CreateService, services: true do
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
- list = service.execute
+ list = service.execute(board)
expect(list.position).to eq 0
end
@@ -19,7 +19,7 @@ describe Boards::Lists::CreateService, services: true do
context 'when board lists has backlog, and done lists' do
it 'creates a new list at beginning of the list' do
- list = service.execute
+ list = service.execute(board)
expect(list.position).to eq 0
end
@@ -30,7 +30,7 @@ describe Boards::Lists::CreateService, services: true do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
- list = service.execute
+ list = service.execute(board)
expect(list.position).to eq 2
end
@@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
- list2 = service.execute
+ list2 = service.execute(board)
expect(list1.reload.position).to eq 0
expect(list2.reload.position).to eq 1
@@ -52,7 +52,7 @@ describe Boards::Lists::CreateService, services: true do
label = create(:label, name: 'in-development')
service = described_class.new(project, user, label_id: label.id)
- expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 474c4512471..628caf03476 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Boards::Lists::DestroyService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
context 'when list type is label' do
@@ -15,11 +15,11 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
- backlog = project.board.backlog_list
+ backlog = board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
- done = project.board.done_list
+ done = board.done_list
described_class.new(project, user).execute(development)
@@ -31,14 +31,14 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'does not remove list from board when list type is backlog' do
- list = project.board.backlog_list
+ list = board.backlog_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
end
it 'does not remove list from board when list type is done' do
- list = project.board.done_list
+ list = board.done_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
index 4171e4d816c..8b2f5e81338 100644
--- a/spec/services/boards/lists/generate_service_spec.rb
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -2,15 +2,15 @@ require 'spec_helper'
describe Boards::Lists::GenerateService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
subject(:service) { described_class.new(project, user) }
context 'when board lists is empty' do
it 'creates the default lists' do
- expect { service.execute }.to change(board.lists, :count).by(2)
+ expect { service.execute(board) }.to change(board.lists, :count).by(2)
end
end
@@ -18,13 +18,13 @@ describe Boards::Lists::GenerateService, services: true do
it 'does not creates the default lists' do
create(:list, board: board)
- expect { service.execute }.not_to change(board.lists, :count)
+ expect { service.execute(board) }.not_to change(board.lists, :count)
end
end
context 'when project labels does not contains any list label' do
it 'creates labels' do
- expect { service.execute }.to change(project.labels, :count).by(2)
+ expect { service.execute(board) }.to change(project.labels, :count).by(2)
end
end
@@ -32,7 +32,7 @@ describe Boards::Lists::GenerateService, services: true do
it 'creates the missing labels' do
create(:label, project: project, name: 'Doing')
- expect { service.execute }.to change(project.labels, :count).by(1)
+ expect { service.execute(board) }.to change(project.labels, :count).by(1)
end
end
end
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
new file mode 100644
index 00000000000..334cee3f06d
--- /dev/null
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Boards::Lists::ListService, services: true do
+ describe '#execute' do
+ it "returns board's lists" do
+ project = create(:empty_project)
+ board = create(:board, project: project)
+ label = create(:label, project: project)
+ list = create(:list, board: board, label: label)
+
+ service = described_class.new(project, double)
+
+ expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list]
+ end
+ end
+end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 102ed67449d..63fa0bb8c5f 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -2,16 +2,16 @@ require 'spec_helper'
describe Boards::Lists::MoveService, services: true do
describe '#execute' do
- let(:project) { create(:project_with_board) }
- let(:board) { project.board }
+ let(:project) { create(:empty_project) }
+ let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
- let!(:backlog) { project.board.backlog_list }
+ let!(:backlog) { create(:backlog_list, board: board) }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
- let!(:done) { project.board.done_list }
+ let!(:done) { create(:done_list, board: board) }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
new file mode 100644
index 00000000000..3760f19aaa2
--- /dev/null
+++ b/spec/services/compare_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe CompareService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new }
+
+ describe '#execute' do
+ context 'compare with base, like feature...fix' do
+ subject { service.execute(project, 'feature', project, 'fix', straight: false) }
+
+ it { expect(subject.diffs.size).to eq(1) }
+ end
+
+ context 'straight compare, like feature..fix' do
+ subject { service.execute(project, 'feature', project, 'fix', straight: true) }
+
+ it { expect(subject.diffs.size).to eq(3) }
+ end
+ end
+end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 343b4385bf2..5fe56e7725f 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -84,11 +84,22 @@ describe CreateDeploymentService, services: true do
expect(subject).to be_persisted
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 +144,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) }
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 8e3e12114f2..dd2a9e9903a 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
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
new file mode 100644
index 00000000000..7aeb95a15ea
--- /dev/null
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe MergeRequests::AssignIssuesService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue.to_reference}") }
+ let(:service) { described_class.new(project, user, merge_request: merge_request) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'finds unassigned issues fixed in merge request' do
+ expect(service.assignable_issues.map(&:id)).to include(issue.id)
+ end
+
+ it 'ignores issues already assigned to any user' do
+ issue.update!(assignee: create(:user))
+
+ expect(service.assignable_issues).to be_empty
+ end
+
+ it 'ignores issues the user cannot update assignee on' do
+ project.team.truncate
+
+ expect(service.assignable_issues).to be_empty
+ end
+
+ it 'ignores all issues unless current_user is merge_request.author' do
+ merge_request.update!(author: create(:user))
+
+ expect(service.assignable_issues).to be_empty
+ end
+
+ it 'accepts precomputed data for closes_issues' do
+ issue2 = create(:issue, project: project)
+ service2 = described_class.new(project,
+ user,
+ merge_request: merge_request,
+ closes_issues: [issue, issue2])
+
+ expect(service2.assignable_issues.count).to eq 2
+ end
+
+ it 'assigns these to the merge request owner' do
+ expect { service.execute }.to change { issue.reload.assignee }.to(user)
+ end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 0d586e2216b..3a3f07ddcb9 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -52,12 +52,28 @@ describe MergeRequests::BuildService, services: true do
end
end
- context 'no commits in the diff' do
- let(:commits) { [] }
+ context 'same source and target branch' do
+ let(:source_branch) { 'master' }
it 'forbids the merge request from being created' do
expect(merge_request.can_be_created).to eq(false)
end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly('You must select different branches')
+ end
+ end
+
+ context 'no commits in the diff' do
+ let(:commits) { [] }
+
+ it 'allows the merge request to be created' do
+ expect(merge_request.can_be_created).to eq(true)
+ end
+
+ it 'adds a WIP prefix to the merge request title' do
+ expect(merge_request.title).to eq('WIP: Feature branch')
+ end
end
context 'one commit in the diff' do
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index 9a29e400654..b80cfd8f450 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -58,57 +58,67 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end
describe "#trigger" do
- context 'build with ref' do
- let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+ let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
+ let(:merge_request_head) do
+ project.commit(mr_merge_if_green_enabled.source_branch).id
+ end
- it "merges all merge requests with merge when build succeeds enabled" do
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
- allow(pipeline).to receive(:success?).and_return(true)
+ context 'when triggered by pipeline with valid ref and sha' do
+ let(:triggering_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: merge_request_head, status: 'success')
+ end
+ it "merges all merge requests with merge when build succeeds enabled" do
expect(MergeWorker).to receive(:perform_async)
- service.trigger(build)
+ service.trigger(triggering_pipeline)
end
end
- context 'triggered by an old build' do
- let(:old_build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
- let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
-
- it "merges all merge requests with merge when build succeeds enabled" do
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
- allow(pipeline).to receive(:success?).and_return(true)
- allow(old_build).to receive(:sha).and_return('1234abcdef')
+ context 'when triggered by an old pipeline' do
+ let(:old_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: '1234abcdef', status: 'success')
+ end
+ it 'it does not merge merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(old_build)
+ service.trigger(old_pipeline)
end
end
- context 'commit status without ref' do
- let(:commit_status) { create(:generic_commit_status, status: 'success') }
-
- before { mr_merge_if_green_enabled }
-
- it "doesn't merge a requests for status on other branch" do
- allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([])
+ context 'when triggered by pipeline from a different branch' do
+ let(:unrelated_pipeline) do
+ create(:ci_pipeline, project: project, ref: 'feature',
+ sha: merge_request_head, status: 'success')
+ end
+ it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(commit_status)
+ service.trigger(unrelated_pipeline)
end
+ end
+ end
- it 'discovers branches and merges all merge requests when status is success' do
- allow(project.repository).to receive(:branch_names_contains).
- with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch])
- allow(pipeline).to receive(:success?).and_return(true)
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
- allow(pipeline).to receive(:success?).and_return(true)
+ describe "#cancel" do
+ before do
+ service.cancel(mr_merge_if_green_enabled)
+ end
- expect(MergeWorker).to receive(:perform_async)
- service.trigger(commit_status)
- end
+ it "resets all the merge_when_build_succeeds params" do
+ expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
+ expect(mr_merge_if_green_enabled.merge_params).to eq({})
+ expect(mr_merge_if_green_enabled.merge_user).to be nil
+ end
+
+ it 'Posts a system note' do
+ note = mr_merge_if_green_enabled.notes.last
+ expect(note.note).to include 'Canceled the automatic merge'
end
+ end
- context 'properly handles multiple stages' do
+ describe 'pipeline integration' do
+ context 'when there are multiple stages in the pipeline' do
let(:ref) { mr_merge_if_green_enabled.source_branch }
let(:sha) { project.commit(ref).id }
@@ -148,21 +158,4 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end
end
end
-
- describe "#cancel" do
- before do
- service.cancel(mr_merge_if_green_enabled)
- end
-
- it "resets all the merge_when_build_succeeds params" do
- expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
- expect(mr_merge_if_green_enabled.merge_params).to eq({})
- expect(mr_merge_if_green_enabled.merge_user).to be nil
- end
-
- it 'Posts a system note' do
- note = mr_merge_if_green_enabled.notes.last
- expect(note.note).to include 'Canceled the automatic merge'
- end
- end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 59d3912018a..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 }
@@ -118,7 +119,7 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes).to be_empty }
it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') }
+ it { expect(@fork_merge_request.notes.last.note).to include('Added 28 commits') }
it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
@@ -169,7 +170,7 @@ describe MergeRequests::RefreshService, services: true do
notes = @fork_merge_request.notes.reorder(:created_at).map(&:note)
expect(notes[0]).to include('Restored source branch `master`')
- expect(notes[1]).to include('Added 4 commits')
+ expect(notes[1]).to include('Added 28 commits')
expect(@fork_merge_request).to be_open
end
end
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index d71932458fa..388abb6a0df 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do
end
describe '#execute' do
- context 'with valid params' do
+ context 'with section params' do
let(:params) do
{
- sections: {
- '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
- '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
- },
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ sections: {
+ '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
+ }
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ sections: {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ }
+ }
+ ],
commit_message: 'This is a commit message!'
}
end
@@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
- '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b'])
+ '824be604a34828eb682305f0d963056cfac87b2d'])
end
end
@@ -74,8 +85,96 @@ describe MergeRequests::ResolveService do
end
end
- context 'when a resolution is missing' do
- let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } }
+ context 'with content and sections params' do
+ let(:popen_content) { "class Popen\nend" }
+
+ let(:params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: popen_content
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ sections: {
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ }
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ before do
+ MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+ end
+
+ it 'creates a commit with the message' do
+ expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
+ end
+
+ it 'creates a commit with the correct parents' do
+ expect(merge_request.source_branch_head.parents.map(&:id)).
+ to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
+ '824be604a34828eb682305f0d963056cfac87b2d'])
+ end
+
+ it 'sets the content to the content given' do
+ blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha,
+ 'files/ruby/popen.rb')
+
+ expect(blob.data).to eq(popen_content)
+ end
+ end
+
+ context 'when a resolution section is missing' do
+ let(:invalid_params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: ''
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ sections: { '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' }
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+ it 'raises a MissingResolution error' do
+ expect { service.execute(merge_request) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+ end
+ end
+
+ context 'when the content of a file is unchanged' do
+ let(:invalid_params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: ''
+ }, {
+ old_path: 'files/ruby/regex.rb',
+ new_path: 'files/ruby/regex.rb',
+ content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
it 'raises a MissingResolution error' do
@@ -83,5 +182,27 @@ describe MergeRequests::ResolveService do
to raise_error(Gitlab::Conflict::File::MissingResolution)
end
end
+
+ context 'when a file is missing' do
+ let(:invalid_params) do
+ {
+ files: [
+ {
+ old_path: 'files/ruby/popen.rb',
+ new_path: 'files/ruby/popen.rb',
+ content: ''
+ }
+ ],
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+ it 'raises a MissingFiles error' do
+ expect { service.execute(merge_request) }.
+ to raise_error(MergeRequests::ResolveService::MissingFiles)
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 33db34c0f62..2433a7dad06 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -17,6 +17,7 @@ describe MergeRequests::UpdateService, services: true do
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [user3, :developer]
end
describe 'execute' do
@@ -104,6 +105,18 @@ describe MergeRequests::UpdateService, services: true do
expect(note).not_to be_nil
expect(note.note).to eq 'Target branch changed from `master` to `target`'
end
+
+ context 'when not including source branch removal options' do
+ before do
+ opts.delete(:force_remove_source_branch)
+ end
+
+ it 'maintains the original options' do
+ update_merge_request(opts)
+
+ expect(@merge_request.merge_params["force_remove_source_branch"]).to eq("1")
+ end
+ end
end
context 'todos' do
@@ -188,6 +201,11 @@ describe MergeRequests::UpdateService, services: true do
let!(:non_subscriber) { create(:user) }
let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ before do
+ project.team << [non_subscriber, :developer]
+ project.team << [subscriber, :developer]
+ end
+
it 'sends notifications for subscribers of newly added labels' do
opts = { label_ids: [label.id] }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index d820646ebdf..699b9925b4e 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -331,7 +331,7 @@ describe NotificationService, services: true do
describe '#new_note' do
it "records sent notifications" do
# Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note
- expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original
+ expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original
notification.new_note(note)
@@ -1169,6 +1169,61 @@ describe NotificationService, services: true do
end
end
+ context 'guest user in private project' do
+ let(:private_project) { create(:empty_project, :private) }
+ let(:guest) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) }
+ let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") }
+ let(:note) { create(:note, noteable: merge_request, project: private_project) }
+
+ before do
+ private_project.team << [assignee, :developer]
+ private_project.team << [developer, :developer]
+ private_project.team << [guest, :guest]
+
+ ActionMailer::Base.deliveries.clear
+ end
+
+ it 'filters out guests when new note is created' do
+ expect(SentNotification).to receive(:record).with(merge_request, any_args).exactly(1).times
+
+ notification.new_note(note)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when new merge request is created' do
+ notification.new_merge_request(merge_request1, @u_disabled)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when merge request is closed' do
+ notification.close_mr(merge_request, developer)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when merge request is reopened' do
+ notification.reopen_mr(merge_request, developer)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+
+ it 'filters out guests when merge request is merged' do
+ notification.merge_mr(merge_request, developer)
+
+ should_not_email(guest)
+ should_email(assignee)
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index ae4d286d250..b57e338b782 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -86,6 +86,25 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'multiple label command' do
+ it 'fetches label ids and populates add_label_ids if content contains multiple /label' do
+ bug # populate the label
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [inprogress.id, bug.id])
+ end
+ end
+
+ shared_examples 'multiple label with same argument' do
+ it 'prevents duplicate label ids and populates add_label_ids if content contains multiple /label' do
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [inprogress.id])
+ end
+ end
+
shared_examples 'unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
issuable.update(label_ids: [inprogress.id]) # populate the label
@@ -95,6 +114,15 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'multiple unlabel command' do
+ it 'fetches label ids and populates remove_label_ids if content contains mutiple /unlabel' do
+ issuable.update(label_ids: [inprogress.id, bug.id]) # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
+ end
+ end
+
shared_examples 'unlabel command with no argument' do
it 'populates label_ids: [] if content contains /unlabel with no arguments' do
issuable.update(label_ids: [inprogress.id]) # populate the label
@@ -285,6 +313,16 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ it_behaves_like 'multiple label command' do
+ let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{bug.title}) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'multiple label with same argument' do
+ let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{inprogress.title}) }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'unlabel command' do
let(:content) { %(/unlabel ~"#{inprogress.title}") }
let(:issuable) { issue }
@@ -295,6 +333,11 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ it_behaves_like 'multiple unlabel command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}" \n/unlabel ~#{bug.title}) }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'unlabel command with no argument' do
let(:content) { %(/unlabel) }
let(:issuable) { issue }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 304d4e62396..b4ba28dfe8e 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -54,7 +54,7 @@ describe SystemNoteService, services: true do
it 'adds a message line for each commit' do
new_commits.each_with_index do |commit, i|
# Skip the header
- expect(note_lines[i + 1]).to eq "* #{commit.short_id} - #{commit.title}"
+ expect(HTMLEntities.new.decode(note_lines[i + 1])).to eq "* #{commit.short_id} - #{commit.title}"
end
end
end
@@ -81,7 +81,7 @@ describe SystemNoteService, services: true do
end
it 'includes a commit count' do
- expect(summary_line).to end_with " - 2 commits from branch `feature`"
+ expect(summary_line).to end_with " - 26 commits from branch `feature`"
end
end
@@ -91,7 +91,7 @@ describe SystemNoteService, services: true do
end
it 'includes a commit count' do
- expect(summary_line).to end_with " - 2 commits from branch `feature`"
+ expect(summary_line).to end_with " - 26 commits from branch `feature`"
end
end
@@ -537,7 +537,7 @@ describe SystemNoteService, services: true do
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.jira_service }
- let(:commit) { project.commit }
+ let(:commit) { project.repository.commits('master').find { |commit| commit.id == '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } }
context 'in JIRA issue tracker' do
before do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index b41f6f14fbd..ed55791d24e 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -345,7 +345,7 @@ describe TodoService, services: true do
service.new_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
- should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -357,7 +357,7 @@ describe TodoService, services: true do
service.update_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
- should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -381,6 +381,7 @@ describe TodoService, services: true do
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
it 'does not raise an error when description not change' do
@@ -430,6 +431,11 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
end
+
+ it 'does not create a todo for guests' do
+ service.reassigned_merge_request(mr_assigned, author)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ end
end
describe '#merge_merge_request' do
@@ -441,6 +447,11 @@ describe TodoService, services: true do
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
end
+
+ it 'does not create todo for guests' do
+ service.merge_merge_request(mr_assigned, john_doe)
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ end
end
describe '#new_award_emoji' do
@@ -495,6 +506,13 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: legacy_diff_note_on_merge_request)
end
+
+ it 'does not create todo for guests' do
+ note_on_merge_request = create :note_on_merge_request, project: project, noteable: mr_assigned, note: mentions
+ service.new_note(note_on_merge_request, author)
+
+ should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+ end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index f313bd4f249..b19f5824236 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -50,11 +50,6 @@ RSpec.configure do |config|
example.run
Rails.cache = caching_store
end
-
- config.after(:each) do
- FileUtils.rm_rf("tmp/tests/repositories")
- FileUtils.mkdir_p("tmp/tests/repositories")
- end
end
FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 0097dbf8fad..c79975d8667 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,6 +5,8 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
+ 'not-merged-branch' => 'b83d6e3',
+ 'branch-merged' => '498214d',
'empty-branch' => '7efb185',
'ends-with.json' => '98b0d8b',
'flatten-dir' => 'e56497b',
@@ -14,7 +16,8 @@ module TestEnv
'improve/awesome' => '5937ac0',
'markdown' => '0ed8c6c',
'lfs' => 'be93687',
- 'master' => '5937ac0',
+ 'master' => 'b83d6e3',
+ 'merge-test' => '5937ac0',
"'test'" => 'e56497b',
'orphaned-branch' => '45127a9',
'binary-encoding' => '7b1cf43',
@@ -24,10 +27,10 @@ module TestEnv
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
'crlf-diff' => '5938907',
- 'conflict-start' => '75284c7',
+ 'conflict-start' => '824be60',
'conflict-resolvable' => '1450cd6',
'conflict-binary-file' => '259a6fb',
- 'conflict-contains-conflict-markers' => '5e0964c',
+ 'conflict-contains-conflict-markers' => '78a3086',
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
@@ -95,7 +98,9 @@ module TestEnv
def setup_gitlab_shell
unless File.directory?(Gitlab.config.gitlab_shell.path)
- `rake gitlab:shell:install`
+ unless system('rake', 'gitlab:shell:install')
+ raise 'Can`t clone gitlab-shell'
+ end
end
end
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
new file mode 100644
index 00000000000..e6ebef82b78
--- /dev/null
+++ b/spec/tasks/gitlab/users_rake_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+require 'rake'
+
+describe 'gitlab:users namespace rake task' do
+ let(:enable_registry) { true }
+
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/users'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ def run_rake_task(task_name)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task task_name
+ end
+
+ describe 'clear_all_authentication_tokens' do
+ before do
+ # avoid writing task output to spec progress
+ allow($stdout).to receive :write
+ end
+
+ context 'gitlab version' do
+ it 'clears the authentication token for all users' do
+ create_list(:user, 2)
+
+ expect(User.pluck(:authentication_token)).to all(be_present)
+
+ run_rake_task('gitlab:users:clear_all_authentication_tokens')
+
+ expect(User.pluck(:authentication_token)).to all(be_nil)
+ end
+ end
+ end
+end
diff --git a/spec/views/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/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 7ca2c29da1c..036d037f3f9 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -57,7 +57,7 @@ describe EmailsOnPushWorker do
end
it "sends a mail with the correct subject" do
- expect(email.subject).to include('Change some files')
+ expect(email.subject).to include('adds bar folder and branch-test text file')
end
it "mentions force pushing in the body" do
@@ -73,7 +73,7 @@ describe EmailsOnPushWorker do
before { perform }
it "sends a mail with the correct subject" do
- expect(email.subject).to include('Change some files')
+ expect(email.subject).to include('adds bar folder and branch-test text file')
end
it "does not mention force pushing in the body" do
diff --git a/spec/workers/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..232478c9735
--- /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_within(1.second).of(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_within(1.second).of(pipeline.finished_at)
+ end
+ end
+ end
+end
diff --git a/spec/workers/process_pipeline_worker_spec.rb b/spec/workers/pipeline_proccess_worker_spec.rb
index 7b5f98d5763..86e9d7f6684 100644
--- a/spec/workers/process_pipeline_worker_spec.rb
+++ b/spec/workers/pipeline_proccess_worker_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe ProcessPipelineWorker do
+describe PipelineProcessWorker do
describe '#perform' do
context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) }
diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb
new file mode 100644
index 00000000000..5e31cc2c8e7
--- /dev/null
+++ b/spec/workers/pipeline_success_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe PipelineSuccessWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline, status: 'success') }
+
+ it 'performs "merge when pipeline succeeds"' do
+ expect_any_instance_of(
+ MergeRequests::MergeWhenBuildSucceedsService
+ ).to receive(:trigger)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/update_pipeline_worker_spec.rb b/spec/workers/pipeline_update_worker_spec.rb
index fadc42b22f0..0b456cfd0da 100644
--- a/spec/workers/update_pipeline_worker_spec.rb
+++ b/spec/workers/pipeline_update_worker_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe UpdatePipelineWorker do
+describe PipelineUpdateWorker do
describe '#perform' do
context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) }
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/trending_projects_worker_spec.rb b/spec/workers/trending_projects_worker_spec.rb
new file mode 100644
index 00000000000..c3c6fdcf2d5
--- /dev/null
+++ b/spec/workers/trending_projects_worker_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe TrendingProjectsWorker do
+ describe '#perform' do
+ it 'refreshes the trending projects' do
+ expect(TrendingProject).to receive(:refresh!)
+
+ described_class.new.perform
+ end
+ end
+end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
new file mode 100644
index 00000000000..c78a69eda67
--- /dev/null
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe UpdateMergeRequestsWorker do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ let(:oldrev) { "123456" }
+ let(:newrev) { "789012" }
+ let(:ref) { "refs/heads/test" }
+
+ def perform
+ subject.perform(project.id, user.id, oldrev, newrev, ref)
+ end
+
+ it 'executes MergeRequests::RefreshService with expected values' do
+ expect(MergeRequests::RefreshService).to receive(:new).with(project, user).and_call_original
+ expect_any_instance_of(MergeRequests::RefreshService).to receive(:execute).with(oldrev, newrev, ref)
+
+ perform
+ end
+
+ it 'executes SystemHooksService with expected values' do
+ push_data = double('push_data')
+ expect(Gitlab::DataBuilder::Push).to receive(:build).with(project, user, oldrev, newrev, ref, []).and_return(push_data)
+
+ system_hook_service = double('system_hook_service')
+ expect(SystemHooksService).to receive(:new).and_return(system_hook_service)
+ expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks)
+
+ perform
+ end
+ end
+end