summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md7
-rw-r--r--.gitlab/merge_request_templates/Security Release.md7
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js33
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js42
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue52
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue11
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/issuable_form.js1
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue14
-rw-r--r--app/assets/javascripts/lib/utils/file_upload.js3
-rw-r--r--app/assets/javascripts/member_expiration_date.js1
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue6
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/explore/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/metrics/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js18
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js10
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue40
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue91
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue278
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue1
-rw-r--r--app/assets/stylesheets/framework/common.scss9
-rw-r--r--app/assets/stylesheets/framework/files.scss20
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/diff.scss52
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss43
-rw-r--r--app/assets/stylesheets/pages/projects.scss17
-rw-r--r--app/controllers/clusters/clusters_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb6
-rw-r--r--app/controllers/profiles/preferences_controller.rb12
-rw-r--r--app/controllers/projects/environments_controller.rb5
-rw-r--r--app/controllers/projects/error_tracking_controller.rb40
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb3
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auto_devops_helper.rb37
-rw-r--r--app/helpers/preferences_helper.rb15
-rw-r--r--app/models/application_record.rb6
-rw-r--r--app/models/application_setting.rb8
-rw-r--r--app/models/clusters/cluster.rb39
-rw-r--r--app/models/clusters/platforms/kubernetes.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb23
-rw-r--r--app/models/gpg_signature.rb7
-rw-r--r--app/models/pool_repository.rb4
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_auto_devops.rb5
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/repository.rb5
-rw-r--r--app/models/user.rb3
-rw-r--r--app/serializers/error_tracking/project_serializer.rb2
-rw-r--r--app/services/error_tracking/list_projects_service.rb44
-rw-r--r--app/services/labels/update_service.rb1
-rw-r--r--app/services/merge_requests/update_service.rb7
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/services/task_list_toggle_service.rb3
-rw-r--r--app/views/admin/application_settings/_localization.html.haml11
-rw-r--r--app/views/admin/application_settings/preferences.html.haml11
-rw-r--r--app/views/clusters/clusters/_form.html.haml17
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml21
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml19
-rw-r--r--app/views/shared/projects/_project.html.haml41
-rw-r--r--app/workers/repository_fork_worker.rb10
-rwxr-xr-xbin/secpick16
-rw-r--r--changelogs/unreleased/2105-add-setting-for-first-day-of-the-week.yml5
-rw-r--r--changelogs/unreleased/44332-add-openid-profile-scopes.yml5
-rw-r--r--changelogs/unreleased/52347-lines-changed-statistics-is-not-easily-visible-in-mr-changes-view.yml5
-rw-r--r--changelogs/unreleased/52363-ui-changes-to-cluster-and-ado-pages.yml5
-rw-r--r--changelogs/unreleased/56014-api-merge-request-squash-commit-messages.yml5
-rw-r--r--changelogs/unreleased/56014-better-squash-commit-messages.yml6
-rw-r--r--changelogs/unreleased/56543-project-lists-further-iteration-improvements.yml5
-rw-r--r--changelogs/unreleased/56788-unicorn-metric-labels.yml5
-rw-r--r--changelogs/unreleased/56938-diff-file-headers-on-compare-not-quite-right.yml5
-rw-r--r--changelogs/unreleased/adriel-remove-feature-flag.yml5
-rw-r--r--changelogs/unreleased/api-group-labels.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-race-condition-creating-signature.yml5
-rw-r--r--changelogs/unreleased/fix-repo-settings-file-upload-error.yml5
-rw-r--r--changelogs/unreleased/gitaly-update-1.18.0.yml5
-rw-r--r--changelogs/unreleased/jlenny-NewAndroidTemplate.yml5
-rw-r--r--changelogs/unreleased/local-markdown-version-bkp3.yml5
-rw-r--r--changelogs/unreleased/support-chunking-in-client.yml5
-rw-r--r--changelogs/unreleased/workhorse-8-3-0.yml5
-rw-r--r--config/initializers/doorkeeper_openid_connect.rb23
-rw-r--r--config/locales/doorkeeper.en.yml6
-rw-r--r--config/routes/project.rb6
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb2
-rw-r--r--db/migrate/20181027114222_add_first_day_of_week_to_user_preferences.rb9
-rw-r--r--db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb16
-rw-r--r--db/migrate/20190130091630_add_local_cached_markdown_version.rb11
-rw-r--r--db/post_migrate/20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb49
-rw-r--r--db/schema.rb5
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/invalidate_markdown_cache.md16
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md2
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/group_labels.md201
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/settings.md10
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/contributing/issue_workflow.md2
-rw-r--r--doc/development/contributing/merge_request_workflow.md3
-rw-r--r--doc/development/contributing/style_guides.md1
-rw-r--r--doc/development/go_guide/index.md216
-rw-r--r--doc/install/installation.md17
-rw-r--r--doc/topics/autodevops/index.md39
-rw-r--r--doc/user/group/clusters/index.md15
-rw-r--r--doc/user/profile/preferences.md8
-rw-r--r--doc/user/project/clusters/index.md18
-rw-r--r--doc/user/project/merge_requests/img/squash_mr_message.pngbin0 -> 150302 bytes
-rw-r--r--doc/user/project/merge_requests/squash_and_merge.md15
-rw-r--r--doc/user/project/pages/getting_started_part_three.md19
-rw-r--r--doc/workflow/repository_mirroring.md8
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb13
-rw-r--r--lib/api/group_labels.rb63
-rw-r--r--lib/api/helpers.rb11
-rw-r--r--lib/api/helpers/label_helpers.rb82
-rw-r--r--lib/api/labels.rb81
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/api/subscriptions.rb87
-rw-r--r--lib/backup/repository.rb2
-rw-r--r--lib/gitlab/auth.rb5
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb6
-rw-r--r--lib/gitlab/bitbucket_import/wiki_formatter.rb25
-rw-r--r--lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml121
-rw-r--r--lib/gitlab/ci/templates/Android.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml41
-rw-r--r--lib/gitlab/git/object_pool.rb9
-rw-r--r--lib/gitlab/git/repository.rb7
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb33
-rw-r--r--lib/gitlab/gitaly_client/util.rb8
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb9
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/gpg/commit.rb7
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb2
-rw-r--r--lib/gitlab/legacy_github_import/wiki_formatter.rb4
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb8
-rw-r--r--lib/gitlab/shell.rb67
-rw-r--r--locale/gitlab.pot130
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb5
-rw-r--r--qa/qa/resource/kubernetes_cluster.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb6
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb21
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb3
-rw-r--r--spec/controllers/projects/error_tracking_controller_spec.rb114
-rw-r--r--spec/features/clusters/cluster_detail_page_spec.rb69
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb17
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb4
-rw-r--r--spec/features/merge_request/user_customizes_merge_commit_message_spec.rb10
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb31
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb25
-rw-r--r--spec/features/snippets/show_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/group_labels.json18
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb35
-rw-r--r--spec/helpers/preferences_helper_spec.rb24
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js33
-rw-r--r--spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js126
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js4
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js4
-rw-r--r--spec/javascripts/diffs/components/diff_stats_spec.js33
-rw-r--r--spec/javascripts/diffs/components/tree_list_spec.js6
-rw-r--r--spec/javascripts/helpers/vue_test_utils_helper.js19
-rw-r--r--spec/javascripts/helpers/vue_test_utils_helper_spec.js48
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js10
-rw-r--r--spec/javascripts/lib/utils/file_upload_spec.js44
-rw-r--r--spec/javascripts/merge_request_spec.js53
-rw-r--r--spec/javascripts/monitoring/charts/area_spec.js220
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js16
-rw-r--r--spec/javascripts/monitoring/mock_data.js1
-rw-r--r--spec/javascripts/notes/components/noteable_note_spec.js93
-rw-r--r--spec/javascripts/notes/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js85
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js61
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js110
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js153
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js2
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb2
-rw-r--r--spec/lib/gitlab/auth_spec.rb2
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb8
-rw-r--r--spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb29
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb2
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb4
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb4
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb2
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb2
-rw-r--r--spec/lib/gitlab/git/remote_repository_spec.rb16
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb97
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb2
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/util_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb14
-rw-r--r--spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb4
-rw-r--r--spec/lib/gitlab/shell_spec.rb17
-rw-r--r--spec/migrations/clean_up_for_members_spec.rb4
-rw-r--r--spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb106
-rw-r--r--spec/models/application_record_spec.rb15
-rw-r--r--spec/models/application_setting_spec.rb7
-rw-r--r--spec/models/clusters/cluster_spec.rb105
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb13
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb55
-rw-r--r--spec/models/gpg_signature_spec.rb35
-rw-r--r--spec/models/project_spec.rb8
-rw-r--r--spec/models/project_wiki_spec.rb4
-rw-r--r--spec/models/repository_spec.rb2
-rw-r--r--spec/models/resource_label_event_spec.rb6
-rw-r--r--spec/presenters/blob_presenter_spec.rb2
-rw-r--r--spec/requests/api/group_labels_spec.rb258
-rw-r--r--spec/requests/api/merge_requests_spec.rb23
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/openid_connect_spec.rb44
-rw-r--r--spec/services/error_tracking/list_projects_service_spec.rb149
-rw-r--r--spec/services/issues/update_service_spec.rb72
-rw-r--r--spec/services/merge_requests/update_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb6
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb2
-rw-r--r--spec/services/task_list_toggle_service_spec.rb11
-rw-r--r--spec/support/shared_examples/issuable_shared_examples.rb73
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb53
-rw-r--r--spec/workers/post_receive_spec.rb15
-rw-r--r--spec/workers/repository_fork_worker_spec.rb7
246 files changed, 4616 insertions, 1174 deletions
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 4bc4215d21b..aaa16145399 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -20,10 +20,9 @@ Set the title to: `Description of the original issue`
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases, plus the current RC if between the 7th and 22nd of the month.
- [ ] At this point, it might be easy to squash the commits from the MR into one
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation]
- - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- - [ ] Create each MR targetting the security branch `security-X-Y`
- - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
-- [ ] Add the ~"Merge into Security" label to all of the MRs.
+ - [ ] Create each MR targetting the stable branch `X-Y-stable`, using the "Security Release" merge request template.
+ - Every merge request will have its own set of TODOs, so make sure to
+ complete those.
- [ ] Make sure all MRs have a link in the [links section](#links)
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
diff --git a/.gitlab/merge_request_templates/Security Release.md b/.gitlab/merge_request_templates/Security Release.md
index 9a0979f27a7..246f2dae009 100644
--- a/.gitlab/merge_request_templates/Security Release.md
+++ b/.gitlab/merge_request_templates/Security Release.md
@@ -4,6 +4,9 @@ This MR should be created on `dev.gitlab.org`.
See [the general developer security release guidelines](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md).
+This merge request _must not_ close the corresponding security issue _unless_ it
+targets master.
+
-->
## Related issues
@@ -12,7 +15,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
## Developer checklist
- [ ] Link to the developer security workflow issue on `dev.gitlab.org`
-- [ ] MR targets `master` or `security-X-Y` for backports
+- [ ] MR targets `master`, or `X-Y-stable` for backports
- [ ] Milestone is set for the version this MR applies to
- [ ] Title of this MR is the same as for all backports
- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security`
@@ -25,4 +28,4 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
- [ ] Correct milestone is applied and the title is matching across all backports
- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines
-/label ~security ~"Merge into Security"
+/label ~security
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 092afa15df4..744068368fb 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.17.0
+1.18.0 \ No newline at end of file
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index fbb9ea12de3..2bf50aaf17a 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.2.0
+8.3.0
diff --git a/Gemfile b/Gemfile
index a2527b17b70..a3b01c275ce 100644
--- a/Gemfile
+++ b/Gemfile
@@ -422,7 +422,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.5.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.10.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index f661da41507..0b2bd2c96bd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -278,7 +278,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.5.0)
+ gitaly-proto (1.10.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -1020,7 +1020,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.5.0)
+ gitaly-proto (~> 1.10.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-markup (~> 1.6.5)
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 947d019c725..52d9f2f0322 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,8 +1,5 @@
import $ from 'jquery';
-import { DOMParser } from 'prosemirror-model';
import { getSelectedFragment } from '~/lib/utils/common_utils';
-import schema from './schema';
-import markdownSerializer from './serializer';
export class CopyAsGFM {
constructor() {
@@ -39,9 +36,13 @@ export class CopyAsGFM {
div.appendChild(el.cloneNode(true));
const html = div.innerHTML;
- clipboardData.setData('text/plain', el.textContent);
- clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
- clipboardData.setData('text/html', html);
+ CopyAsGFM.nodeToGFM(el)
+ .then(res => {
+ clipboardData.setData('text/plain', el.textContent);
+ clipboardData.setData('text/x-gfm', res);
+ clipboardData.setData('text/html', html);
+ })
+ .catch(() => {});
}
static pasteGFM(e) {
@@ -137,11 +138,21 @@ export class CopyAsGFM {
}
static nodeToGFM(node) {
- const wrapEl = document.createElement('div');
- wrapEl.appendChild(node.cloneNode(true));
- const doc = DOMParser.fromSchema(schema).parse(wrapEl);
-
- return markdownSerializer.serialize(doc);
+ return Promise.all([
+ import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
+ import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
+ import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
+ ])
+ .then(([prosemirrorModel, schema, markdownSerializer]) => {
+ const { DOMParser } = prosemirrorModel;
+ const wrapEl = document.createElement('div');
+ wrapEl.appendChild(node.cloneNode(true));
+ const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
+
+ const res = markdownSerializer.default.serialize(doc);
+ return res;
+ })
+ .catch(() => {});
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 0eb067d4963..680f2031409 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -64,26 +64,30 @@ export default class ShortcutsIssuable extends Shortcuts {
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const blockquoteEl = document.createElement('blockquote');
blockquoteEl.appendChild(el);
- const text = CopyAsGFM.nodeToGFM(blockquoteEl);
-
- if (text.trim() === '') {
- return false;
- }
-
- // If replyField already has some content, add a newline before our quote
- const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
- $replyField
- .val((a, current) => `${current}${separator}${text}\n\n`)
- .trigger('input')
- .trigger('change');
-
- // Trigger autosize
- const event = document.createEvent('Event');
- event.initEvent('autosize:update', true, false);
- $replyField.get(0).dispatchEvent(event);
+ CopyAsGFM.nodeToGFM(blockquoteEl)
+ .then(text => {
+ if (text.trim() === '') {
+ return false;
+ }
+
+ // If replyField already has some content, add a newline before our quote
+ const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+ $replyField
+ .val((a, current) => `${current}${separator}${text}\n\n`)
+ .trigger('input')
+ .trigger('change');
+
+ // Trigger autosize
+ const event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ $replyField.get(0).dispatchEvent(event);
+
+ // Focus the input field
+ $replyField.focus();
- // Focus the input field
- $replyField.focus();
+ return false;
+ })
+ .catch(() => {});
return false;
}
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 3ef54752436..0bf2dde8b96 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -6,6 +6,7 @@ import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
import SettingsDropdown from './settings_dropdown.vue';
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -14,6 +15,7 @@ export default {
GlLink,
GlButton,
SettingsDropdown,
+ DiffStats,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,8 +37,15 @@ export default {
},
},
computed: {
- ...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']),
- ...mapGetters('diffs', ['hasCollapsedFile']),
+ ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']),
+ ...mapState('diffs', [
+ 'commit',
+ 'showTreeList',
+ 'startVersion',
+ 'latestVersionPath',
+ 'addedLines',
+ 'removedLines',
+ ]),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
@@ -104,6 +113,11 @@ export default {
<gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link>
</div>
<div class="inline-parallel-buttons d-none d-md-flex ml-auto">
+ <diff-stats
+ :diff-files-length="diffFilesLength"
+ :added-lines="addedLines"
+ :removed-lines="removedLines"
+ />
<gl-button
v-if="commit || startVersion"
:href="latestVersionPath"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index b58f704bebb..60586d4a607 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -9,6 +9,7 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import EditButton from './edit_button.vue';
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -16,6 +17,7 @@ export default {
EditButton,
Icon,
FileIcon,
+ DiffStats,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -202,6 +204,7 @@ export default {
v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-none d-sm-block"
>
+ <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<template v-if="diffFile.blob && diffFile.blob.readable_text">
<button
:disabled="!diffHasDiscussions(diffFile)"
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
new file mode 100644
index 00000000000..2e5855380af
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -0,0 +1,52 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { n__ } from '~/locale';
+
+export default {
+ components: { Icon },
+ props: {
+ addedLines: {
+ type: Number,
+ required: true,
+ },
+ removedLines: {
+ type: Number,
+ required: true,
+ },
+ diffFilesLength: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ filesText() {
+ return n__('File', 'Files', this.diffFilesLength);
+ },
+ isCompareVersionsHeader() {
+ return Boolean(this.diffFilesLength);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="diff-stats"
+ :class="{
+ 'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
+ 'd-inline-flex': !isCompareVersionsHeader,
+ }"
+ >
+ <div v-if="diffFilesLength !== null" class="diff-stats-group">
+ <icon name="doc-code" class="diff-stats-icon text-secondary" />
+ <strong>{{ diffFilesLength }} {{ filesText }}</strong>
+ </div>
+ <div class="diff-stats-group cgreen">
+ <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong>
+ </div>
+ <div class="diff-stats-group cred">
+ <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index a0f09932593..96ae197d8b8 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -14,8 +14,8 @@ export default {
FileRow,
},
computed: {
- ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']),
- ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
+ ...mapState('diffs', ['tree', 'renderTreeList']),
+ ...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
return this.renderTreeList ? this.tree : this.allBlobs;
},
@@ -64,13 +64,6 @@ export default {
{{ s__('MergeRequest|No files found') }}
</p>
</div>
- <div v-once class="pt-3 pb-3 text-center">
- {{ n__('%d changed file', '%d changed files', diffFilesLength) }}
- <div>
- <span class="cgreen"> {{ n__('%d addition', '%d additions', addedLines) }} </span>
- <span class="cred"> {{ n__('%d deleted', '%d deletions', removedLines) }} </span>
- </div>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 6ee33d9fc6d..47f78a5db54 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -11,6 +11,8 @@ const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({
isLoading: true,
+ addedLines: null,
+ removedLines: null,
endpoint: '',
basePath: '',
commit: null,
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index dbfcf8cc921..cb1b1173190 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -64,6 +64,7 @@ class DueDateSelect {
this.saveDueDate(true);
}
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($dueDateInput.val()));
@@ -183,6 +184,7 @@ export default class DueDateSelectors {
onSelect(dateText) {
$datePicker.val(calendar.toString(dateText));
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate(datePickerVal));
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 4d2533d01f1..9336b71cfd7 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -44,6 +44,7 @@ export default class IssuableForm {
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index e664269b199..58f14bac8c8 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
-import { __ } from '~/locale';
+import { s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
@@ -91,9 +92,14 @@ export default {
},
taskListUpdateError() {
- window.Flash(
- __(
- 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.',
+ createFlash(
+ sprintf(
+ s__(
+ 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
+ ),
+ {
+ issueType: this.issuableType,
+ },
),
);
diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js
index b41ffb44971..82ee83e4348 100644
--- a/app/assets/javascripts/lib/utils/file_upload.js
+++ b/app/assets/javascripts/lib/utils/file_upload.js
@@ -1,6 +1,9 @@
export default (buttonSelector, fileSelector) => {
const btn = document.querySelector(buttonSelector);
const fileInput = document.querySelector(fileSelector);
+
+ if (!btn || !fileInput) return;
+
const form = btn.closest('form');
btn.addEventListener('click', () => {
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 0beedcacf33..0dabb28ea66 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -33,6 +33,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d
toggleClearInput.call($input);
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($input.val()));
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index ac3b47cd218..3b42a154af8 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
+import createFlash from '~/flash';
import TaskList from './task_list';
import MergeRequestTabs from './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
@@ -40,6 +41,13 @@ function MergeRequest(opts) {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
},
+ onError: () => {
+ createFlash(
+ __(
+ 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
+ ),
+ );
+ },
});
}
}
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index ec0e33a1927..14c02db7bcc 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -189,8 +189,8 @@ export default {
<template>
<div class="prometheus-graph col-12 col-lg-6">
<div class="prometheus-graph-header">
- <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div class="prometheus-graph-widgets"><slot></slot></div>
+ <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
+ <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
ref="areaChart"
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 9c5fd93f7d1..895a57785bc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -160,7 +160,8 @@ export default {
{{ s__('Metrics|Environment') }}
<div class="dropdown prepend-left-10">
<button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
- <span> {{ currentEnvironmentName }} </span> <icon name="chevron-down" />
+ <span>{{ currentEnvironmentName }}</span>
+ <icon name="chevron-down" />
</button>
<div
v-if="store.environmentsData.length > 0"
@@ -172,9 +173,8 @@ export default {
:href="environment.metrics_path"
:class="{ 'is-active': environment.name == currentEnvironmentName }"
class="dropdown-item"
+ >{{ environment.name }}</a
>
- {{ environment.name }}
- </a>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js
index 8f98be79640..01001d4f3ff 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index.js
+++ b/app/assets/javascripts/pages/dashboard/projects/index.js
@@ -1,7 +1,5 @@
import ProjectsList from '~/projects_list';
-import Star from '../../../star';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
- new Star('.project-row'); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js
index 8f98be79640..01001d4f3ff 100644
--- a/app/assets/javascripts/pages/explore/projects/index.js
+++ b/app/assets/javascripts/pages/explore/projects/index.js
@@ -1,7 +1,5 @@
import ProjectsList from '~/projects_list';
-import Star from '../../../star';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
- new Star('.project-row'); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js
index 0b644780ad4..0d69a689316 100644
--- a/app/assets/javascripts/pages/projects/environments/metrics/index.js
+++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js
@@ -1,3 +1,3 @@
-import monitoringBundle from '~/monitoring/monitoring_bundle';
+import monitoringBundle from 'ee_else_ce/monitoring/monitoring_bundle';
document.addEventListener('DOMContentLoaded', monitoringBundle);
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 3ccad513c05..26d7fa7371d 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -43,10 +43,26 @@ document.addEventListener('DOMContentLoaded', () => {
],
});
+ const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
+
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
+
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+ };
+
const hourData = chartData(projectChartData.hour);
responsiveChart($('#hour-chart'), hourData);
- const dayData = chartData(projectChartData.weekDays);
+ const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week);
+ const dayData = chartData(weekDays);
responsiveChart($('#weekday-chart'), dayData);
const monthData = chartData(projectChartData.month);
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 8a84ac37dab..afa099d0e0b 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -159,7 +159,7 @@ export default class ActivityCalendar {
.append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
- if (a === 0 && stamp.day === 0) {
+ if (a === 0 && stamp.day === this.firstDayOfWeek) {
const month = stamp.date.getMonth();
const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
@@ -205,6 +205,14 @@ export default class ActivityCalendar {
y: 29 + this.dayYPos(5),
},
];
+
+ if (this.firstDayOfWeek === 1) {
+ days.push({
+ text: 'S',
+ y: 29 + this.dayYPos(7),
+ });
+ }
+
this.svg
.append('g')
.selectAll('text')
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 1c3fd58ca74..39cd891c111 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -234,7 +234,7 @@ export default class UserTabs {
data,
calendarActivitiesPath,
utcOffset,
- 0,
+ gon.first_day_of_week,
monthsAgo,
);
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
new file mode 100644
index 00000000000..a38f25cce35
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -0,0 +1,40 @@
+<script>
+export default {
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <div class="commit-message-editor">
+ <div class="d-flex flex-wrap align-items-center justify-content-between">
+ <label class="col-form-label" :for="inputId">
+ <strong>{{ label }}</strong>
+ </label>
+ <slot name="header"></slot>
+ </div>
+ <textarea
+ :id="inputId"
+ :value="value"
+ class="form-control js-gfm-input append-bottom-default commit-message-edit"
+ required="required"
+ rows="7"
+ @input="$emit('input', $event.target.value)"
+ ></textarea>
+ <slot name="checkbox"></slot>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
new file mode 100644
index 00000000000..b3c1c0e329d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ commits: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-dropdown
+ right
+ no-caret
+ text="Use an existing commit message"
+ variant="link"
+ class="mr-commit-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="commit in commits"
+ :key="commit.short_id"
+ class="text-nowrap text-truncate"
+ @click="$emit('input', commit.message)"
+ >
+ <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
new file mode 100644
index 00000000000..a1d3a09cca4
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import _ from 'underscore';
+import { __, n__, sprintf, s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlButton,
+ },
+ props: {
+ isSquashEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ commitsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ computed: {
+ collapseIcon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ commitsCountMessage() {
+ return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount);
+ },
+ modifyLinkMessage() {
+ return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit');
+ },
+ ariaLabel() {
+ return this.expanded ? __('Collapse') : __('Expand');
+ },
+ message() {
+ return sprintf(
+ s__(
+ 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
+ ),
+ {
+ commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`,
+ mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`,
+ targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ toggle() {
+ this.expanded = !this.expanded;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2"
+ @click="toggle()"
+ >
+ <gl-button
+ :aria-label="ariaLabel"
+ variant="blank"
+ class="commit-edit-toggle mr-2"
+ @click.stop="toggle()"
+ >
+ <icon :name="collapseIcon" :size="16" />
+ </gl-button>
+ <span v-if="expanded">{{ __('Collapse') }}</span>
+ <span v-else>
+ <span v-html="message"></span>
+ <gl-button variant="link" class="modify-message-button">
+ {{ modifyLinkMessage }}
+ </gl-button>
+ </span>
+ </div>
+ <div v-show="expanded"><slot></slot></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index b8f29649eb5..ce4207864ea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -2,17 +2,24 @@
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
+import { __ } from '~/locale';
import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import SquashBeforeMerge from './squash_before_merge.vue';
+import CommitsHeader from './commits_header.vue';
+import CommitEdit from './commit_edit.vue';
+import CommitMessageDropdown from './commit_message_dropdown.vue';
export default {
name: 'ReadyToMerge',
components: {
statusIcon,
SquashBeforeMerge,
+ CommitsHeader,
+ CommitEdit,
+ CommitMessageDropdown,
},
props: {
mr: { type: Object, required: true },
@@ -22,27 +29,20 @@ export default {
return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
- useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
- showCommitMessageEditor: false,
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
squashBeforeMerge: this.mr.squash,
successSvg,
warningSvg,
+ squashCommitMessage: this.mr.squashCommitMessage,
};
},
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
},
- commitMessageLinkTitle() {
- const withDesc = 'Include description in commit message';
- const withoutDesc = "Don't include description in commit message";
-
- return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
- },
status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
@@ -84,9 +84,9 @@ export default {
},
mergeButtonText() {
if (this.isMergingImmediately) {
- return 'Merge in progress';
+ return __('Merge in progress');
} else if (this.shouldShowMergeWhenPipelineSucceedsText) {
- return 'Merge when pipeline succeeds';
+ return __('Merge when pipeline succeeds');
}
return 'Merge';
@@ -98,7 +98,7 @@ export default {
const { commitMessage } = this;
return Boolean(
!commitMessage.length ||
- !this.shouldShowMergeControls() ||
+ !this.shouldShowMergeControls ||
this.isMakingRequest ||
this.mr.preventMerge,
);
@@ -110,18 +110,14 @@ export default {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
},
- },
- methods: {
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
},
- updateCommitMessage() {
- const cmwd = this.mr.commitMessageWithDescription;
- this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription;
- this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage;
- },
- toggleCommitMessageEditor() {
- this.showCommitMessageEditor = !this.showCommitMessageEditor;
+ },
+ methods: {
+ updateMergeCommitMessage(includeDescription) {
+ const { commitMessageWithDescription, commitMessage } = this.mr;
+ this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
},
handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) {
// TODO: Remove no-param-reassign
@@ -139,6 +135,7 @@ export default {
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
+ squash_commit_message: this.squashCommitMessage,
};
this.isMakingRequest = true;
@@ -158,7 +155,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line
});
},
initiateMergePolling() {
@@ -194,7 +191,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line
});
},
initiateRemoveSourceBranchPolling() {
@@ -223,7 +220,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while deleting the source branch. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line
});
},
},
@@ -231,127 +228,136 @@ export default {
</script>
<template>
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
- <div class="media-body">
- <div class="mr-widget-body-controls media space-children">
- <span class="btn-group">
- <button
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button"
- class="qa-merge-button"
- @click="handleMergeButtonClick()"
- >
- <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- {{ mergeButtonText }}
- </button>
- <button
- v-if="shouldShowMergeOptionsDropdown"
- :disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
- data-toggle="dropdown"
- aria-label="Select merge moment"
- >
- <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
- </button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- >
- <li>
- <a
- class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
- href="#"
- @click.prevent="handleMergeButtonClick(true)"
- >
- <span class="media">
- <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
- <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
- </span>
- </a>
- </li>
- <li>
- <a
- class="accept-merge-request qa-merge-immediately-option"
- href="#"
- @click.prevent="handleMergeButtonClick(false, true)"
- >
- <span class="media">
- <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
- <span class="media-body merge-opt-title">Merge immediately</span>
- </span>
- </a>
- </li>
- </ul>
- </span>
- <div class="media-body-wrap space-children">
- <template v-if="shouldShowMergeControls()">
- <label v-if="mr.canRemoveSourceBranch">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- :disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox"
- type="checkbox"
- />
- Delete source branch
- </label>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- v-model="squashBeforeMerge"
- :help-path="mr.squashBeforeMergeHelpPath"
- :is-disabled="isMergeButtonDisabled"
- />
-
- <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message">
- Fast-forward merge without a merge commit
- </span>
+ <div>
+ <div class="mr-widget-body media">
+ <status-icon :status="iconClass" />
+ <div class="media-body">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group">
<button
- v-else
:disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-sm"
+ :class="mergeButtonClass"
type="button"
- @click="toggleCommitMessageEditor"
+ class="qa-merge-button"
+ @click="handleMergeButtonClick()"
>
- Modify commit message
+ <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ {{ mergeButtonText }}
</button>
- </template>
- <template v-else>
- <span class="bold js-resolve-mr-widget-items-message">
- You can only merge once the items above are resolved
- </span>
- </template>
- </div>
- </div>
- <div v-if="showCommitMessageEditor" class="prepend-top-default commit-message-editor">
- <div class="form-group clearfix">
- <label class="col-form-label" for="commit-message"> Commit message </label>
- <div class="col-sm-10">
- <div class="commit-message-container">
- <div class="max-width-marker"></div>
- <textarea
- id="commit-message"
- v-model="commitMessage"
- class="form-control js-commit-message"
- required="required"
- rows="14"
- name="Commit message"
- ></textarea>
- </div>
- <p class="hint">
- Try to keep the first line under 52 characters and the others under 72
- </p>
- <div class="hint">
- <a href="#" @click.prevent="updateCommitMessage"> {{ commitMessageLinkTitle }} </a>
- </div>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment"
+ >
+ <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu"
+ >
+ <li>
+ <a
+ class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
+ href="#"
+ @click.prevent="handleMergeButtonClick(true)"
+ >
+ <span class="media">
+ <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
+ <span class="media-body merge-opt-title">{{
+ __('Merge when pipeline succeeds')
+ }}</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ class="accept-merge-request qa-merge-immediately-option"
+ href="#"
+ @click.prevent="handleMergeButtonClick(false, true)"
+ >
+ <span class="media">
+ <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
+ <span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </span>
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls">
+ <label v-if="mr.canRemoveSourceBranch">
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ class="js-remove-source-branch-checkbox"
+ type="checkbox"
+ />
+ {{ __('Delete source branch') }}
+ </label>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ v-model="squashBeforeMerge"
+ :help-path="mr.squashBeforeMergeHelpPath"
+ :is-disabled="isMergeButtonDisabled"
+ />
+ </template>
+ <template v-else>
+ <span class="bold js-resolve-mr-widget-items-message">
+ {{ __('You can only merge once the items above are resolved') }}
+ </span>
+ </template>
</div>
</div>
</div>
</div>
+ <template v-if="shouldShowMergeControls">
+ <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
+ {{ __('Fast-forward merge without a merge commit') }}
+ </div>
+ <template v-else>
+ <commits-header
+ :is-squash-enabled="squashBeforeMerge"
+ :commits-count="mr.commitsCount"
+ :target-branch="mr.targetBranch"
+ >
+ <ul class="border-top content-list commits-list flex-list">
+ <commit-edit
+ v-if="squashBeforeMerge"
+ v-model="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ squash
+ >
+ <commit-message-dropdown
+ slot="header"
+ v-model="squashCommitMessage"
+ :commits="mr.commits"
+ />
+ </commit-edit>
+ <commit-edit
+ v-model="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ >
+ <label slot="checkbox">
+ <input
+ id="include-description"
+ type="checkbox"
+ @change="updateMergeCommitMessage($event.target.checked)"
+ />
+ {{ __('Include merge request description') }}
+ </label>
+ </commit-edit>
+ </ul>
+ </commits-header>
+ </template>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 57c4dfbe3b7..abbbe19c5ef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -315,7 +315,7 @@ export default {
:endpoint="mr.testResultsPath"
/>
- <div class="mr-widget-section">
+ <div class="mr-widget-section p-0">
<component :is="componentName" :mr="mr" :service="service" />
<section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 36cac230d9d..58363f632a9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -42,6 +42,8 @@ export default class MergeRequestStore {
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
+ this.commits = data.commits_without_merge_commits || [];
+ this.squashCommitMessage = data.default_squash_commit_message;
this.initRebase(data);
if (data.issues_links) {
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 8bdb5bf22c2..13eb46437dd 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -40,6 +40,7 @@ export default {
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
+ firstDay: gon.first_day_of_week,
});
this.$el.append(this.calendar.el);
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index cb449b642e7..c5c3b66438c 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -391,6 +391,11 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.ws-initial { white-space: initial; }
.overflow-auto { overflow: auto; }
+.d-flex-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
/** COMMON SIZING CLASSES **/
.w-0 { width: 0; }
@@ -402,6 +407,10 @@ img.emoji {
.min-height-0 { min-height: 0; }
+.w-3 { width: #{3 * $grid-size}; }
+
+.h-3 { width: #{3 * $grid-size}; }
+
/** COMMON SPACING CLASSES **/
.gl-pl-0 { padding-left: 0; }
.gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 037a5adfb7e..6108eaa1ad0 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -4,6 +4,7 @@
*/
.file-holder {
border: 1px solid $border-color;
+ border-top: 0;
border-radius: $border-radius-default;
&.file-holder-no-border {
@@ -51,6 +52,7 @@
position: absolute;
top: 5px;
right: 15px;
+ margin-left: auto;
.btn {
padding: 0 10px;
@@ -324,10 +326,12 @@ span.idiff {
&,
.file-holder & {
display: flex;
+ flex-wrap: wrap;
align-items: center;
justify-content: space-between;
background-color: $gray-light;
border-bottom: 1px solid $border-color;
+ border-top: 1px solid $border-color;
padding: 5px $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
@@ -365,16 +369,12 @@ span.idiff {
margin: 0 10px 0 0;
}
- .file-actions {
- white-space: nowrap;
-
- .btn {
- padding: 0 10px;
- font-size: 13px;
- line-height: 28px;
- display: inline-block;
- float: none;
- }
+ .file-actions .btn {
+ padding: 0 10px;
+ font-size: 13px;
+ line-height: 28px;
+ display: inline-block;
+ float: none;
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 9eae9a831fa..96dab609a13 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -243,6 +243,7 @@ $gl-padding-8: 8px;
$gl-padding: 16px;
$gl-padding-24: 24px;
$gl-padding-32: 32px;
+$gl-padding-50: 50px;
$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
@@ -490,6 +491,7 @@ $builds-trace-bg: #111;
*/
$commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0);
+$commit-stat-summary-height: 36px;
/*
* Common
@@ -664,8 +666,14 @@ $priority-label-empty-state-width: 114px;
Issues Analytics
*/
$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
+
/*
Merge Requests
*/
$mr-tabs-height: 51px;
$mr-version-controls-height: 56px;
+
+/*
+Compare Branches
+*/
+$compare-branches-sticky-header-height: 68px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 02aac58a475..e3b98b26a11 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -7,22 +7,13 @@
cursor: pointer;
@media (min-width: map-get($grid-breakpoints, md)) {
+ $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height;
+
position: -webkit-sticky;
position: sticky;
- top: $mr-version-controls-height + $header-height + $mr-tabs-height;
- margin-left: -1px;
- border-left: 1px solid $border-color;
+ top: $mr-file-header-top;
z-index: 102;
- &.is-commit {
- top: $header-height + 36px;
-
- .with-performance-bar & {
- top: $header-height + 36px + $performance-bar-height;
-
- }
- }
-
&::before {
content: '';
position: absolute;
@@ -35,7 +26,23 @@
}
.with-performance-bar & {
- top: $header-height + $performance-bar-height + $mr-version-controls-height + $mr-tabs-height;
+ top: $mr-file-header-top + $performance-bar-height;
+ }
+
+ &.is-commit {
+ top: $header-height + $commit-stat-summary-height;
+
+ .with-performance-bar & {
+ top: $header-height + $commit-stat-summary-height + $performance-bar-height;
+ }
+ }
+
+ &.is-compare {
+ top: $header-height + $compare-branches-sticky-header-height;
+
+ .with-performance-bar & {
+ top: $performance-bar-height + $header-height + $compare-branches-sticky-header-height;
+ }
}
}
@@ -501,6 +508,25 @@
}
}
+.diff-stats {
+ align-items: center;
+ padding: 0 .25rem;
+
+ .diff-stats-group {
+ padding: 0 .25rem;
+ }
+
+ svg.diff-stats-icon {
+ vertical-align: text-bottom;
+ }
+
+ &.is-compare-versions-header {
+ .diff-stats-group {
+ padding: 0 .5rem;
+ }
+ }
+}
+
.file-content .diff-file {
margin: 0;
border: 0;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 38a7e199c6a..135730d71e9 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -38,9 +38,7 @@
}
.mr-widget-section {
- .media {
- align-items: center;
- }
+ border-radius: $border-radius-default $border-radius-default 0 0;
.code-text {
flex: 1;
@@ -56,6 +54,11 @@
.mr-widget-extension {
border-top: 1px solid $border-color;
background-color: $gray-light;
+
+ &.clickable:hover {
+ background-color: $gl-gray-200;
+ cursor: pointer;
+ }
}
.mr-widget-workflow {
@@ -78,6 +81,7 @@
border-top: 0;
}
+.mr-widget-body,
.mr-widget-section,
.mr-widget-content,
.mr-widget-footer {
@@ -87,11 +91,38 @@
.mr-state-widget {
color: $gl-text-color;
+ .commit-message-edit {
+ border-radius: $border-radius-default;
+ }
+
.mr-widget-section,
.mr-widget-footer {
border-top: solid 1px $border-color;
}
+ .mr-fast-forward-message {
+ padding-left: $gl-padding-50;
+ padding-bottom: $gl-padding;
+ }
+
+ .commits-list {
+ > li {
+ padding: $gl-padding;
+
+ @include media-breakpoint-up(md) {
+ padding-left: $gl-padding-50;
+ }
+ }
+ }
+
+ .mr-commit-dropdown {
+ .dropdown-menu {
+ @include media-breakpoint-up(md) {
+ width: 150%;
+ }
+ }
+ }
+
.mr-widget-footer {
padding: 0;
}
@@ -405,7 +436,7 @@
}
.mr-widget-help {
- padding: 10px 16px 10px 48px;
+ padding: 10px 16px 10px $gl-padding-50;
font-style: italic;
}
@@ -423,10 +454,6 @@
}
}
-.mr-widget-body-controls {
- flex-wrap: wrap;
-}
-
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 2342c284a5e..3eb02cd4358 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -946,6 +946,11 @@ pre.light-well {
.flex-wrapper {
min-width: 0;
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
+ flex: 1 1 100%;
+
+ .project-title {
+ line-height: 20px;
+ }
}
p,
@@ -984,14 +989,16 @@ pre.light-well {
}
.controls {
- margin-top: $gl-padding-8;
+ @include media-breakpoint-down(xs) {
+ margin-top: $gl-padding-8;
+ }
- @include media-breakpoint-down(md) {
+ @include media-breakpoint-up(sm) {
margin-top: 0;
}
- @include media-breakpoint-down(xs) {
- margin-top: $gl-padding-8;
+ @include media-breakpoint-up(lg) {
+ flex: 1 1 40%;
}
.icon-wrapper {
@@ -1041,7 +1048,7 @@ pre.light-well {
min-height: 40px;
min-width: 40px;
- .identicon.s64 {
+ .identicon.s48 {
font-size: 16px;
}
}
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index b9717b97640..3bd91b71d92 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -127,6 +127,7 @@ class Clusters::ClustersController < Clusters::BaseController
params.require(:cluster).permit(
:enabled,
:environment_scope,
+ :base_domain,
platform_kubernetes_attributes: [
:namespace
]
@@ -136,6 +137,7 @@ class Clusters::ClustersController < Clusters::BaseController
:enabled,
:name,
:environment_scope,
+ :base_domain,
platform_kubernetes_attributes: [
:api_url,
:token,
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 97120273d6b..cc2bb99f55b 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -116,8 +116,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
+ def build_auth_user(auth_user_class)
+ auth_user_class.new(oauth)
+ end
+
def sign_in_user_flow(auth_user_class)
- auth_user = auth_user_class.new(oauth)
+ auth_user = build_auth_user(auth_user_class)
user = auth_user.find_and_update!
if auth_user.valid_sign_in?
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 37ac11dc6a1..94002095739 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -33,12 +33,10 @@ class Profiles::PreferencesController < Profiles::ApplicationController
end
def preferences_params
- params.require(:user).permit(
- :color_scheme_id,
- :layout,
- :dashboard,
- :project_view,
- :theme_id
- )
+ params.require(:user).permit(preferences_param_names)
+ end
+
+ def preferences_param_names
+ [:color_scheme_id, :layout, :dashboard, :project_view, :theme_id, :first_day_of_week]
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 79685e8b675..e9cd475a199 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -11,11 +11,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
- before_action do
- push_frontend_feature_flag(:area_chart, project)
- end
-
- # Returns all environments or all folders based on the :nested param
def index
@environments = project.environments
.with_state(params[:scope] || :available)
diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb
index 9e403e1d25b..88d0755f41f 100644
--- a/app/controllers/projects/error_tracking_controller.rb
+++ b/app/controllers/projects/error_tracking_controller.rb
@@ -15,6 +15,14 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end
end
+ def list_projects
+ respond_to do |format|
+ format.json do
+ render_project_list_json
+ end
+ end
+ end
+
private
def render_index_json
@@ -32,6 +40,32 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
}
end
+ def render_project_list_json
+ service = ErrorTracking::ListProjectsService.new(
+ project,
+ current_user,
+ list_projects_params
+ )
+ result = service.execute
+
+ if result[:status] == :success
+ render json: {
+ projects: serialize_projects(result[:projects])
+ }
+ else
+ return render(
+ status: result[:http_status] || :bad_request,
+ json: {
+ message: result[:message]
+ }
+ )
+ end
+ end
+
+ def list_projects_params
+ params.require(:error_tracking_setting).permit([:api_host, :token])
+ end
+
def set_polling_interval
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
end
@@ -41,4 +75,10 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
.new(project: project, user: current_user)
.represent(errors)
end
+
+ def serialize_projects(projects)
+ ErrorTracking::ProjectSerializer
+ .new(project: project, user: current_user)
+ .represent(projects)
+ end
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 54ff7ded8e5..6045ee4e171 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -34,7 +34,8 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:task_num,
:title,
:discussion_locked,
- label_ids: []
+ label_ids: [],
+ update_task: [:index, :checked, :line_number, :line_source]
]
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 3e08c0ccd8f..23af2e0521c 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -305,7 +305,7 @@ class IssuableFinder
def use_subquery_for_search?
strong_memoize(:use_subquery_for_search) do
attempt_group_search_optimizations? &&
- Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: false)
+ Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: true)
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index c8e4e2e3df9..e635f608237 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -150,6 +150,7 @@ module ApplicationSettingsHelper
:email_author_in_body,
:enabled_git_access_protocol,
:enforce_terms,
+ :first_day_of_week,
:gitaly_timeout_default,
:gitaly_timeout_medium,
:gitaly_timeout_fast,
@@ -231,7 +232,8 @@ module ApplicationSettingsHelper
:web_ide_clientside_preview_enabled,
:diff_max_patch_bytes,
:commit_email_hostname,
- :protected_ci_variables
+ :protected_ci_variables,
+ :local_markdown_version
]
end
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 516c8a353ea..67e7e475920 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -9,41 +9,4 @@ module AutoDevopsHelper
!project.repository.gitlab_ci_yml &&
!project.ci_service
end
-
- def auto_devops_warning_message(project)
- if missing_auto_devops_service?(project)
- params = {
- kubernetes: link_to('Kubernetes cluster', project_clusters_path(project))
- }
-
- if missing_auto_devops_domain?(project)
- _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params
- else
- _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params
- end
- elsif missing_auto_devops_domain?(project)
- _('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def cluster_ingress_ip(project)
- project
- .cluster_ingresses
- .where("external_ip is not null")
- .limit(1)
- .pluck(:external_ip)
- .first
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def missing_auto_devops_domain?(project)
- !(project.auto_devops || project.build_auto_devops)&.has_domain?
- end
-
- def missing_auto_devops_service?(project)
- !project.deployment_platform&.active?
- end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index f4f46b0fe96..bc1742e8167 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -43,6 +43,17 @@ module PreferencesHelper
]
end
+ def first_day_of_week_choices
+ [
+ [_('Sunday'), 0],
+ [_('Monday'), 1]
+ ]
+ end
+
+ def first_day_of_week_choices_with_default
+ first_day_of_week_choices.unshift([_('System default (%{default})') % { default: default_first_day_of_week }, nil])
+ end
+
def user_application_theme
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
@@ -66,4 +77,8 @@ module PreferencesHelper
def excluded_dashboard_choices
['operations']
end
+
+ def default_first_day_of_week
+ first_day_of_week_choices.rassoc(Gitlab::CurrentSettings.first_day_of_week).first
+ end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index c4e310e638d..a3d662d8250 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -7,6 +7,12 @@ class ApplicationRecord < ActiveRecord::Base
where(id: ids)
end
+ def self.safe_find_or_create_by!(*args)
+ safe_find_or_create_by(*args).tap do |record|
+ record.validate! unless record.persisted?
+ end
+ end
+
def self.safe_find_or_create_by(*args)
transaction(requires_new: true) do
find_or_create_by(*args)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 88746375c67..daadf9427ba 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -193,6 +193,10 @@ class ApplicationSetting < ActiveRecord::Base
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
+ validates :local_markdown_version,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -246,6 +250,7 @@ class ApplicationSetting < ActiveRecord::Base
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
ed25519_key_restriction: 0,
+ first_day_of_week: 0,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
@@ -303,7 +308,8 @@ class ApplicationSetting < ActiveRecord::Base
usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname,
- protected_ci_variables: false
+ protected_ci_variables: false,
+ local_markdown_version: 0
}
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index a2c48973fa5..f2f5b89e3bb 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -18,6 +18,7 @@ module Clusters
Applications::Knative.application_name => Applications::Knative
}.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
+ KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze
belongs_to :user
@@ -49,7 +50,7 @@ module Clusters
validates :name, cluster_name: true
validates :cluster_type, presence: true
- validates :domain, allow_nil: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true }
+ validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true }
validate :restrict_modification, on: :update
validate :no_groups, unless: :group_type?
@@ -65,6 +66,9 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
delegate :available?, to: :application_knative, prefix: true, allow_nil: true
+ delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
+
+ alias_attribute :base_domain, :domain
enum cluster_type: {
instance_type: 1,
@@ -193,8 +197,41 @@ module Clusters
project_type?
end
+ def kube_ingress_domain
+ @kube_ingress_domain ||= domain.presence || instance_domain || legacy_auto_devops_domain
+ end
+
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless kube_ingress_domain
+
+ variables.append(key: KUBE_INGRESS_BASE_DOMAIN, value: kube_ingress_domain)
+ end
+ end
+
private
+ def instance_domain
+ @instance_domain ||= Gitlab::CurrentSettings.auto_devops_domain
+ end
+
+ # To keep backward compatibility with AUTO_DEVOPS_DOMAIN
+ # environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN
+ # is set if AUTO_DEVOPS_DOMAIN is set on any of the following options:
+ # ProjectAutoDevops#Domain, project variables or group variables,
+ # as the AUTO_DEVOPS_DOMAIN is needed for CI_ENVIRONMENT_URL
+ #
+ # This method should be removed on 12.0
+ def legacy_auto_devops_domain
+ if project_type?
+ project&.auto_devops&.domain.presence ||
+ project.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence ||
+ project.group&.variables&.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence
+ elsif group_type?
+ group.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence
+ end
+ end
+
def restrict_modification
if provider&.on_creation?
errors.add(:base, "cannot modify during creation")
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 8f3424db295..c8969351ed9 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -98,6 +98,8 @@ module Clusters
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
.append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
end
+
+ variables.concat(cluster.predefined_variables)
end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 5fa6f79bdaa..1a8570b80c3 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -115,7 +115,28 @@ module CacheMarkdownField
end
def latest_cached_markdown_version
- CacheMarkdownField::CACHE_COMMONMARK_VERSION
+ @latest_cached_markdown_version ||= (CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) | local_version
+ end
+
+ def local_version
+ # because local_markdown_version is stored in application_settings which
+ # uses cached_markdown_version too, we check explicitly to avoid
+ # endless loop
+ return local_markdown_version if has_attribute?(:local_markdown_version)
+
+ settings = Gitlab::CurrentSettings.current_application_settings
+
+ # Following migrations are not properly isolated and
+ # use real models (by calling .ghost method), in these migrations
+ # local_markdown_version attribute doesn't exist yet, so we
+ # use a default value:
+ # db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
+ # db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb
+ if settings.respond_to?(:local_markdown_version)
+ settings.local_markdown_version
+ else
+ 0
+ end
end
included do
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 0816778deae..7f9ff7bbda6 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class GpgSignature < ActiveRecord::Base
+class GpgSignature < ApplicationRecord
include ShaAttribute
sha_attribute :commit_sha
@@ -33,6 +33,11 @@ class GpgSignature < ActiveRecord::Base
)
end
+ def self.safe_create!(attributes)
+ create_with(attributes)
+ .safe_find_or_create_by!(commit_sha: attributes[:commit_sha])
+ end
+
def gpg_key=(model)
case model
when GpgKey
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 34220c1b450..4635fc72dc7 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -96,7 +96,9 @@ class PoolRepository < ActiveRecord::Base
@object_pool ||= Gitlab::Git::ObjectPool.new(
shard.name,
disk_path + '.git',
- source_project.repository.raw)
+ source_project.repository.raw,
+ source_project.full_path
+ )
end
def inspect
diff --git a/app/models/project.rb b/app/models/project.rb
index d4e2ed883bc..8f746f6e094 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1288,7 +1288,7 @@ class Project < ActiveRecord::Base
# Forked import is handled asynchronously
return if forked? && !force
- if gitlab_shell.create_repository(repository_storage, disk_path)
+ if gitlab_shell.create_project_repository(self)
repository.after_create
true
else
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 2253ad7b543..b6c5c7c4c87 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -24,6 +24,11 @@ class ProjectAutoDevops < ActiveRecord::Base
domain.present? || instance_domain.present?
end
+ # From 11.8, AUTO_DEVOPS_DOMAIN has been replaced by KUBE_INGRESS_BASE_DOMAIN.
+ # See Clusters::Cluster#predefined_variables and https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580
+ # for more info.
+ # Support for AUTO_DEVOPS_DOMAIN support will be dropped on 12.0 on
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/52363
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
if has_domain?
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 559e4f99294..c43bd45a62f 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -60,7 +60,7 @@ class ProjectWiki
def wiki
@wiki ||= begin
gl_repository = Gitlab::GlRepository.gl_repository(project, true)
- raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository)
+ raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository, full_path)
create_repo!(raw_repository) unless raw_repository.exists?
@@ -175,7 +175,7 @@ class ProjectWiki
private
def create_repo!(raw_repository)
- gitlab_shell.create_repository(project.repository_storage, disk_path)
+ gitlab_shell.create_wiki_repository(project)
raise CouldNotCreateWikiError unless raw_repository.exists?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index bfd2608bed4..7c50b4488e5 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1104,6 +1104,9 @@ class Repository
end
def initialize_raw_repository
- Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
+ Gitlab::Git::Repository.new(project.repository_storage,
+ disk_path + '.git',
+ Gitlab::GlRepository.gl_repository(project, is_wiki),
+ project.full_path)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 9c091ac366c..24101eda0b1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -228,6 +228,9 @@ class User < ApplicationRecord
delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :notes_filter_for, to: :user_preference
delegate :set_notes_filter, to: :user_preference
+ delegate :first_day_of_week, :first_day_of_week=, to: :user_preference
+
+ accepts_nested_attributes_for :user_preference, update_only: true
state_machine :state, initial: :active do
event :block do
diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb
index b2406f4d631..68724088fff 100644
--- a/app/serializers/error_tracking/project_serializer.rb
+++ b/app/serializers/error_tracking/project_serializer.rb
@@ -2,6 +2,6 @@
module ErrorTracking
class ProjectSerializer < BaseSerializer
- entity ProjectEntity
+ entity ErrorTracking::ProjectEntity
end
end
diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb
new file mode 100644
index 00000000000..c6e8be0f2be
--- /dev/null
+++ b/app/services/error_tracking/list_projects_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ListProjectsService < ::BaseService
+ def execute
+ return error('access denied') unless can_read?
+
+ setting = project_error_tracking_setting
+
+ unless setting.valid?
+ return error(setting.errors.full_messages.join(', '), :bad_request)
+ end
+
+ begin
+ result = setting.list_sentry_projects
+ rescue Sentry::Client::Error => e
+ return error(e.message, :bad_request)
+ rescue Sentry::Client::SentryError => e
+ return error(e.message, :unprocessable_entity)
+ end
+
+ success(projects: result[:projects])
+ end
+
+ private
+
+ def project_error_tracking_setting
+ (project.error_tracking_setting || project.build_error_tracking_setting).tap do |setting|
+ setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
+ api_host: params[:api_host],
+ organization_slug: nil,
+ project_slug: nil
+ )
+
+ setting.token = params[:token]
+ setting.enabled = true
+ end
+ end
+
+ def can_read?
+ can?(current_user, :read_sentry_issue, project)
+ end
+ end
+end
diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb
index e563447c64c..be33947d0eb 100644
--- a/app/services/labels/update_service.rb
+++ b/app/services/labels/update_service.rb
@@ -8,6 +8,7 @@ module Labels
# returns the updated label
def execute(label)
+ params[:name] = params.delete(:new_name) if params.key?(:new_name)
params[:color] = convert_color_name_to_hex if params[:color].present?
label.update(params)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 86a04587f79..8112c2a4299 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -21,7 +21,7 @@ module MergeRequests
end
handle_wip_event(merge_request)
- update(merge_request)
+ update_task_event(merge_request) || update(merge_request)
end
# rubocop:disable Metrics/AbcSize
@@ -83,6 +83,11 @@ module MergeRequests
end
# rubocop:enable Metrics/AbcSize
+ def handle_task_changes(merge_request)
+ todo_service.mark_pending_todos_as_done(merge_request, current_user)
+ todo_service.update_merge_request(merge_request, current_user)
+ end
+
def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge)
return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 5861b803996..7214e9efaf6 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -73,7 +73,7 @@ module Projects
project.ensure_repository
project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
else
- gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url)
+ gitlab_shell.import_project_repository(project)
end
rescue Gitlab::Shell::Error => e
# Expire cache to prevent scenarios such as:
diff --git a/app/services/task_list_toggle_service.rb b/app/services/task_list_toggle_service.rb
index b5c4cd3235d..cfe187d9b12 100644
--- a/app/services/task_list_toggle_service.rb
+++ b/app/services/task_list_toggle_service.rb
@@ -32,7 +32,8 @@ class TaskListToggleService
source_line_index = line_number - 1
markdown_task = source_lines[source_line_index]
- return unless markdown_task == line_source
+ # The source in the DB could be using either \n or \r\n line endings
+ return unless markdown_task == line_source || markdown_task == line_source + "\r"
return unless source_checkbox = Taskable::ITEM_PATTERN.match(markdown_task)
currently_checked = TaskList::Item.new(source_checkbox[1]).complete?
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
new file mode 100644
index 00000000000..95d016a94a5
--- /dev/null
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -0,0 +1,11 @@
+= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-localization-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :first_day_of_week, _('Default first day of the week'), class: 'label-bold'
+ = f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control'
+ .form-text.text-muted
+ = _('Default first day of the week in calendars and date pickers.')
+
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index 00000b86ab7..c468d69d7b8 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -56,3 +56,14 @@
= _('Configure Gitaly timeouts.')
.settings-content
= render 'gitaly'
+
+%section.settings.as-localization.no-animate#js-localization-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Localization')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Various localization settings.')
+ .settings-content
+ = render 'localization'
diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml
index 4c47e11927e..7acd9ce0562 100644
--- a/app/views/clusters/clusters/_form.html.haml
+++ b/app/views/clusters/clusters/_form.html.haml
@@ -20,12 +20,27 @@
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
- else
= text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true
- - environment_scope_url = 'https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope-premium'
+ - environment_scope_url = help_page_path('user/project/clusters/index', anchor: 'base-domain')
- environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url }
.form-text.text-muted
%code *
= s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe }
+ .form-group
+ %h5= s_('ClusterIntegration|Base domain')
+ = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus'
+ .form-text.text-muted
+ - auto_devops_url = help_page_path('topics/autodevops/index')
+ - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
+ = s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
+ - if @cluster.application_ingress_external_ip.present?
+ = s_('ClusterIntegration|Alternatively')
+ %code #{@cluster.application_ingress_external_ip}.nip.io
+ = s_('ClusterIntegration| can be used instead of a custom domain.')
+ - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-cluster-ip')
+ - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url }
+ = s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe }
+
- if can?(current_user, :update_cluster, @cluster)
.form-group
= field.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index bf475c07711..3fbaaafe89e 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -36,7 +36,7 @@
%span
= _('Activity')
- = render_if_exists 'groups/sidebar/security_dashboard'
+ = render_if_exists 'groups/sidebar/security_dashboard' # EE-specific
- if group_sidebar_link?(:contribution_analytics)
= nav_link(path: 'analytics#show') do
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 7c378633667..1a9aca1f6bf 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -51,11 +51,30 @@
= f.label :dashboard, class: 'label-bold' do
Default dashboard
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
+
+ = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
+
.form-group
= f.label :project_view, class: 'label-bold' do
Project overview content
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.form-text.text-muted
Choose what content you want to see on a project’s overview page.
+
+ .col-sm-12
+ %hr
+
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = _('Localization')
+ %p
+ = _('Customize language and region related settings.')
+ = succeed '.' do
+ = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank'
+ .col-lg-8
+ .form-group
+ = f.label :first_day_of_week, class: 'label-bold' do
+ = _('First day of the week')
+ = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control'
.form-group
- = f.submit 'Save changes', class: 'btn btn-success'
+ = f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 06f0cd9675e..fe9a8ac4182 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -10,7 +10,7 @@
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
= render "ci_menu"
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-commit"
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index b6bebbabed0..5774b48a054 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-compare"
- else
.card.bg-light
.center
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index cc2d0d3b2d8..2dba3fcd664 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -2,7 +2,7 @@
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
-- is_commit = local_assigns.fetch(:is_commit, false)
+- diff_page_context = local_assigns.fetch(:diff_page_context, nil)
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.files-changed-inner
@@ -25,4 +25,4 @@
= render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, is_commit: is_commit }
+ = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 5565ae1d98b..855b719dc45 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,11 +1,11 @@
- environment = local_assigns.fetch(:environment, nil)
-- is_commit = local_assigns.fetch(:is_commit, false)
+- diff_page_context = local_assigns.fetch(:diff_page_context, nil)
- file_hash = hexdigest(diff_file.file_path)
- image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
- image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) }
- .js-file-title.file-title-flex-parent{ class: is_commit ? "is-commit" : "" }
+ .js-file-title.file-title-flex-parent{ class: diff_page_context }
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}"
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 5ec5a06396e..8c4d1c32ebe 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -4,10 +4,6 @@
= form_errors(@project)
%fieldset.builds-feature.js-auto-devops-settings
.form-group
- - message = auto_devops_warning_message(@project)
- - if message
- %p.auto-devops-warning-message.settings-message.text-center
- = message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.card.auto-devops-card
.card-body
@@ -21,19 +17,12 @@
= s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
.card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' }
- = form.label :domain do
- %strong= _('Domain')
- = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
- .form-text.text-muted
- = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
- - if cluster_ingress_ip = cluster_ingress_ip(@project)
- = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
-
+ %p.settings-message.text-center
+ - kubernetes_cluster_link = help_page_path('user/project/clusters/index')
+ - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link }
+ = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe }
%label.prepend-top-10
%strong= s_('CICD|Deployment strategy')
- %p.settings-message.text-center
- = s_('CICD|Deployment strategy needs a domain name to work correctly.')
.form-check
= form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
= form.label :deploy_strategy_continuous, class: 'form-check-label' do
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index e1564d57426..df17ae95e2a 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,21 +12,20 @@
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
-- css_details_class = compact_mode ? "d-flex flex-column flex-sm-row flex-md-row align-items-sm-center" : "align-items-center flex-md-fill flex-lg-column d-sm-flex d-lg-block"
-- css_controls_class = compact_mode ? "" : "align-items-md-end align-items-lg-center flex-lg-row"
+- css_controls_class = compact_mode ? "" : "flex-lg-row justify-content-lg-between"
%li.project-row.d-flex{ class: css_class }
= cache(cache_key) do
- if avatar
- .avatar-container.s64.flex-grow-0.flex-shrink-0
+ .avatar-container.s48.flex-grow-0.flex-shrink-0
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
- = image_tag avatar_icon_for_user(project.creator, 64), class: "avatar s65", alt:''
+ = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s65", alt:''
- else
- = project_icon(project, alt: '', class: 'avatar project-avatar s64', width: 64, height: 64)
- .project-details.flex-sm-fill{ class: css_details_class }
- .flex-wrapper.flex-fill
- .d-flex.align-items-center.flex-wrap
+ = project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48)
+ .project-details.d-sm-flex.flex-sm-fill.align-items-center
+ .flex-wrapper
+ .d-flex.align-items-center.flex-wrap.project-title
%h2.d-flex.prepend-top-8
= link_to project_path(project), class: 'text-plain' do
%span.project-full-name.append-right-8><
@@ -52,13 +51,13 @@
%span.user-access-role.d-block= Gitlab::Access.human_access(access)
- if show_last_commit_as_description
- .description.d-none.d-sm-block.prepend-top-8.append-right-default
+ .description.d-none.d-sm-block.append-right-default
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
- elsif project.description.present?
- .description.d-none.d-sm-block.prepend-top-8.append-right-default
+ .description.d-none.d-sm-block.append-right-default
= markdown_field(project, :description)
- .controls.d-flex.flex-row.flex-sm-column.flex-md-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class }
+ .controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class }
.icon-container.d-flex.align-items-center
- if project.archived
%span.d-flex.icon-wrapper.badge.badge-warning archived
@@ -74,13 +73,13 @@
= number_with_delimiter(project.forks_count)
- if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project),
- class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip",
+ class: "d-none d-xl-flex align-items-center icon-wrapper merge-requests has-tooltip",
title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_merge_requests_count)
- if show_issue_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project),
- class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip",
+ class: "d-none d-xl-flex align-items-center icon-wrapper issues has-tooltip",
title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_issues_count)
@@ -89,19 +88,3 @@
= render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top')
.updated-note
%span Updated #{updated_tooltip}
-
- .d-none.d-lg-flex.align-item-stretch
- - unless compact_mode
- - if current_user
- %button.star-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(project, :json) } }
- - if current_user.starred?(project)
- = sprite_icon('star', { css_class: 'icon' })
- %span.starred= s_('ProjectOverview|Unstar')
- - else
- = sprite_icon('star-o', { css_class: 'icon' })
- %span= s_('ProjectOverview|Star')
-
- - else
- = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
- = sprite_icon('star-o', { css_class: 'icon' })
- %span= s_('ProjectOverview|Star')
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 7eae07d3f6b..a9b88a133be 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -15,19 +15,19 @@ class RepositoryForkWorker
return target_project.import_state.mark_as_failed(_('Source project cannot be found.'))
end
- fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
+ fork_repository(target_project, source_project)
end
private
- def fork_repository(target_project, source_repository_storage_name, source_disk_path)
+ def fork_repository(target_project, source_project)
return unless start_fork(target_project)
Gitlab::Metrics.add_event(:fork_repository)
- result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path,
- target_project.repository_storage, target_project.disk_path)
- raise "Unable to fork project #{target_project.id} for repository #{source_disk_path} -> #{target_project.disk_path}" unless result
+ result = gitlab_shell.fork_repository(source_project, target_project)
+
+ raise "Unable to fork project #{target_project.id} for repository #{source_project.disk_path} -> #{target_project.disk_path}" unless result
target_project.after_import
end
diff --git a/bin/secpick b/bin/secpick
index be120a304c9..8f956d300a7 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -10,6 +10,7 @@ using Rainbow
module Secpick
BRANCH_PREFIX = 'security'.freeze
+ STABLE_SUFFIX = 'stable'.freeze
DEFAULT_REMOTE = 'dev'.freeze
NEW_MR_URL = 'https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/new'.freeze
@@ -36,16 +37,16 @@ module Secpick
branch.freeze
end
- def security_branch
- "#{BRANCH_PREFIX}-#{@options[:version]}".tap do |name|
+ def stable_branch
+ "#{@options[:version]}-#{STABLE_SUFFIX}".tap do |name|
name << "-ee" if ee?
end.freeze
end
def git_commands
- ["git fetch #{@options[:remote]} #{security_branch}",
- "git checkout #{security_branch}",
- "git pull #{@options[:remote]} #{security_branch}",
+ ["git fetch #{@options[:remote]} #{stable_branch}",
+ "git checkout #{stable_branch}",
+ "git pull #{@options[:remote]} #{stable_branch}",
"git checkout -B #{source_branch}",
"git cherry-pick #{@options[:sha]}",
"git push #{@options[:remote]} #{source_branch}",
@@ -56,9 +57,8 @@ module Secpick
{
merge_request: {
source_branch: source_branch,
- target_branch: security_branch,
- title: "[#{@options[:version].tr('-', '.')}] ",
- description: '/label ~security ~"Merge into Security"'
+ target_branch: stable_branch,
+ description: '/label ~security'
}
}
end
diff --git a/changelogs/unreleased/2105-add-setting-for-first-day-of-the-week.yml b/changelogs/unreleased/2105-add-setting-for-first-day-of-the-week.yml
new file mode 100644
index 00000000000..f4a52b1aacd
--- /dev/null
+++ b/changelogs/unreleased/2105-add-setting-for-first-day-of-the-week.yml
@@ -0,0 +1,5 @@
+---
+title: Add setting for first day of the week
+merge_request: 22755
+author: Fabian Schneider @fabsrc
+type: added
diff --git a/changelogs/unreleased/44332-add-openid-profile-scopes.yml b/changelogs/unreleased/44332-add-openid-profile-scopes.yml
new file mode 100644
index 00000000000..b554fab5139
--- /dev/null
+++ b/changelogs/unreleased/44332-add-openid-profile-scopes.yml
@@ -0,0 +1,5 @@
+---
+title: GitLab now supports the profile and email scopes from OpenID Connect
+merge_request: 24335
+author: Goten Xiao
+type: added
diff --git a/changelogs/unreleased/52347-lines-changed-statistics-is-not-easily-visible-in-mr-changes-view.yml b/changelogs/unreleased/52347-lines-changed-statistics-is-not-easily-visible-in-mr-changes-view.yml
new file mode 100644
index 00000000000..cf1c4378f18
--- /dev/null
+++ b/changelogs/unreleased/52347-lines-changed-statistics-is-not-easily-visible-in-mr-changes-view.yml
@@ -0,0 +1,5 @@
+---
+title: Show MR statistics in diff comparisons
+merge_request: !24569
+author:
+type: changed
diff --git a/changelogs/unreleased/52363-ui-changes-to-cluster-and-ado-pages.yml b/changelogs/unreleased/52363-ui-changes-to-cluster-and-ado-pages.yml
new file mode 100644
index 00000000000..eb4851971fb
--- /dev/null
+++ b/changelogs/unreleased/52363-ui-changes-to-cluster-and-ado-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Moves domain setting from Auto DevOps to Cluster's page
+merge_request: 24580
+author:
+type: added
diff --git a/changelogs/unreleased/56014-api-merge-request-squash-commit-messages.yml b/changelogs/unreleased/56014-api-merge-request-squash-commit-messages.yml
new file mode 100644
index 00000000000..e324baa94a3
--- /dev/null
+++ b/changelogs/unreleased/56014-api-merge-request-squash-commit-messages.yml
@@ -0,0 +1,5 @@
+---
+title: API allows setting the squash commit message when squashing a merge request
+merge_request: 24784
+author:
+type: added
diff --git a/changelogs/unreleased/56014-better-squash-commit-messages.yml b/changelogs/unreleased/56014-better-squash-commit-messages.yml
deleted file mode 100644
index b08d584ac0a..00000000000
--- a/changelogs/unreleased/56014-better-squash-commit-messages.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Default squash commit message is now selected from the longest commit when
- squashing merge requests
-merge_request: 24518
-author:
-type: changed
diff --git a/changelogs/unreleased/56543-project-lists-further-iteration-improvements.yml b/changelogs/unreleased/56543-project-lists-further-iteration-improvements.yml
new file mode 100644
index 00000000000..388ff1d062a
--- /dev/null
+++ b/changelogs/unreleased/56543-project-lists-further-iteration-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Project list UI improvements
+merge_request: 24855
+author:
+type: other
diff --git a/changelogs/unreleased/56788-unicorn-metric-labels.yml b/changelogs/unreleased/56788-unicorn-metric-labels.yml
new file mode 100644
index 00000000000..824c981780c
--- /dev/null
+++ b/changelogs/unreleased/56788-unicorn-metric-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up unicorn sampler metric labels
+merge_request: 24626
+author: bjk-gitlab
+type: fixed
diff --git a/changelogs/unreleased/56938-diff-file-headers-on-compare-not-quite-right.yml b/changelogs/unreleased/56938-diff-file-headers-on-compare-not-quite-right.yml
new file mode 100644
index 00000000000..f619a009a63
--- /dev/null
+++ b/changelogs/unreleased/56938-diff-file-headers-on-compare-not-quite-right.yml
@@ -0,0 +1,5 @@
+---
+title: Correct spacing for comparison page
+merge_request: !24783
+author:
+type: fixed
diff --git a/changelogs/unreleased/adriel-remove-feature-flag.yml b/changelogs/unreleased/adriel-remove-feature-flag.yml
new file mode 100644
index 00000000000..d442e120d60
--- /dev/null
+++ b/changelogs/unreleased/adriel-remove-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Update metrics dashboard graph design
+merge_request: 24653
+author:
+type: changed
diff --git a/changelogs/unreleased/api-group-labels.yml b/changelogs/unreleased/api-group-labels.yml
new file mode 100644
index 00000000000..0df6f15a9b6
--- /dev/null
+++ b/changelogs/unreleased/api-group-labels.yml
@@ -0,0 +1,5 @@
+---
+title: 'API: Add support for group labels'
+merge_request: 21368
+author: Robert Schilling
+type: added
diff --git a/changelogs/unreleased/bvl-fix-race-condition-creating-signature.yml b/changelogs/unreleased/bvl-fix-race-condition-creating-signature.yml
new file mode 100644
index 00000000000..307b4f526bb
--- /dev/null
+++ b/changelogs/unreleased/bvl-fix-race-condition-creating-signature.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid race conditions when creating GpgSignature
+merge_request: 24939
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-repo-settings-file-upload-error.yml b/changelogs/unreleased/fix-repo-settings-file-upload-error.yml
new file mode 100644
index 00000000000..b219fdfaa1e
--- /dev/null
+++ b/changelogs/unreleased/fix-repo-settings-file-upload-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug causing repository mirror settings UI to break
+merge_request: 23712
+author:
+type: fixed
diff --git a/changelogs/unreleased/gitaly-update-1.18.0.yml b/changelogs/unreleased/gitaly-update-1.18.0.yml
new file mode 100644
index 00000000000..392527f5e5d
--- /dev/null
+++ b/changelogs/unreleased/gitaly-update-1.18.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade gitaly to 1.18.0
+merge_request: 24981
+author:
+type: other
diff --git a/changelogs/unreleased/jlenny-NewAndroidTemplate.yml b/changelogs/unreleased/jlenny-NewAndroidTemplate.yml
new file mode 100644
index 00000000000..ae8c58da859
--- /dev/null
+++ b/changelogs/unreleased/jlenny-NewAndroidTemplate.yml
@@ -0,0 +1,5 @@
+---
+title: Add template for Android with Fastlane
+merge_request: 24722
+author:
+type: changed
diff --git a/changelogs/unreleased/local-markdown-version-bkp3.yml b/changelogs/unreleased/local-markdown-version-bkp3.yml
new file mode 100644
index 00000000000..ce5bff6ae6b
--- /dev/null
+++ b/changelogs/unreleased/local-markdown-version-bkp3.yml
@@ -0,0 +1,5 @@
+---
+title: Allow admins to invalidate markdown texts by setting local markdown version.
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/support-chunking-in-client.yml b/changelogs/unreleased/support-chunking-in-client.yml
new file mode 100644
index 00000000000..e50648ea4b2
--- /dev/null
+++ b/changelogs/unreleased/support-chunking-in-client.yml
@@ -0,0 +1,5 @@
+---
+title: Fix code search when text is larger than max gRPC message size
+merge_request: 24111
+author:
+type: changed
diff --git a/changelogs/unreleased/workhorse-8-3-0.yml b/changelogs/unreleased/workhorse-8-3-0.yml
new file mode 100644
index 00000000000..6ae01d64ae5
--- /dev/null
+++ b/changelogs/unreleased/workhorse-8-3-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update Workhorse to v8.3.0
+merge_request: 24959
+author:
+type: other
diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb
index e97c0fcbd6b..fd5a62c39c6 100644
--- a/config/initializers/doorkeeper_openid_connect.rb
+++ b/config/initializers/doorkeeper_openid_connect.rb
@@ -31,8 +31,27 @@ Doorkeeper::OpenidConnect.configure do
o.claim(:name) { |user| user.name }
o.claim(:nickname) { |user| user.username }
- o.claim(:email) { |user| user.public_email }
- o.claim(:email_verified) { |user| true if user.public_email? }
+
+ # Check whether the application has access to the email scope, and grant
+ # access to the user's primary email address if so, otherwise their
+ # public email address (if present)
+ # This allows existing solutions built for GitLab's old behavior to keep
+ # working without modification.
+ o.claim(:email) do |user, scopes|
+ scopes.exists?(:email) ? user.email : user.public_email
+ end
+ o.claim(:email_verified) do |user, scopes|
+ if scopes.exists?(:email)
+ user.primary_email_verified?
+ elsif user.public_email?
+ user.verified_email?(user.public_email)
+ else
+ # If there is no public email set, tell doorkicker-openid-connect to
+ # exclude the email_verified claim by returning nil.
+ nil
+ end
+ end
+
o.claim(:website) { |user| user.full_website_url if user.website_url? }
o.claim(:profile) { |user| Gitlab::Routing.url_helpers.user_url user }
o.claim(:picture) { |user| user.avatar_url(only_path: false) }
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 9f451046462..a2dff92908e 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -64,6 +64,8 @@ en:
read_registry: Grants permission to read container registry images
openid: Authenticate using OpenID Connect
sudo: Perform API actions as any user in the system
+ profile: Allows read-only access to the user's personal information using OpenID Connect
+ email: Allows read-only access to the user's primary email address using OpenID Connect
scope_desc:
api:
Grants complete read/write access to the API, including all groups and projects.
@@ -77,6 +79,10 @@ en:
Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships.
sudo:
Grants permission to perform API actions as any user in the system, when authenticated as an admin user.
+ profile:
+ Grants read-only access to the user's profile data using OpenID Connect.
+ email:
+ Grants read-only access to the user's primary email address using OpenID Connect.
flash:
applications:
create:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index d730479cf2b..b4ebc7df4fe 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -444,7 +444,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :error_tracking, only: [:index], controller: :error_tracking
+ resources :error_tracking, only: [:index], controller: :error_tracking do
+ collection do
+ post :list_projects
+ end
+ end
# Since both wiki and repository routing contains wildcard characters
# its preferable to keep it below all other project routes
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index a69b02cddc4..bff1f01c654 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -11,7 +11,7 @@ class MigrateRepoSize < ActiveRecord::Migration[4.2]
path = File.join(namespace_path, project['project_path'] + '.git')
begin
- repo = Gitlab::Git::Repository.new('default', path, '')
+ repo = Gitlab::Git::Repository.new('default', path, '', '')
if repo.empty?
print '-'
else
diff --git a/db/migrate/20181027114222_add_first_day_of_week_to_user_preferences.rb b/db/migrate/20181027114222_add_first_day_of_week_to_user_preferences.rb
new file mode 100644
index 00000000000..a0e76c2186e
--- /dev/null
+++ b/db/migrate/20181027114222_add_first_day_of_week_to_user_preferences.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddFirstDayOfWeekToUserPreferences < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :user_preferences, :first_day_of_week, :integer
+ end
+end
diff --git a/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb b/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb
new file mode 100644
index 00000000000..53cfaa289f6
--- /dev/null
+++ b/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddFirstDayOfWeekToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:application_settings, :first_day_of_week, :integer, default: 0)
+ end
+
+ def down
+ remove_column(:application_settings, :first_day_of_week)
+ end
+end
diff --git a/db/migrate/20190130091630_add_local_cached_markdown_version.rb b/db/migrate/20190130091630_add_local_cached_markdown_version.rb
new file mode 100644
index 00000000000..00570e6458c
--- /dev/null
+++ b/db/migrate/20190130091630_add_local_cached_markdown_version.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddLocalCachedMarkdownVersion < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :local_markdown_version, :integer, default: 0, null: false
+ end
+end
diff --git a/db/post_migrate/20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb b/db/post_migrate/20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb
new file mode 100644
index 00000000000..392e64eeade
--- /dev/null
+++ b/db/post_migrate/20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class MigrateAutoDevOpsDomainToClusterDomain < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ execute(update_clusters_domain_query)
+ end
+
+ def down
+ # no-op
+ end
+
+ private
+
+ def update_clusters_domain_query
+ if Gitlab::Database.mysql?
+ mysql_query
+ else
+ postgresql_query
+ end
+ end
+
+ def mysql_query
+ <<~HEREDOC
+ UPDATE clusters, project_auto_devops, cluster_projects
+ SET
+ clusters.domain = project_auto_devops.domain
+ WHERE
+ cluster_projects.cluster_id = clusters.id
+ AND project_auto_devops.project_id = cluster_projects.project_id
+ AND project_auto_devops.domain != ''
+ HEREDOC
+ end
+
+ def postgresql_query
+ <<~HEREDOC
+ UPDATE clusters
+ SET domain = project_auto_devops.domain
+ FROM cluster_projects, project_auto_devops
+ WHERE
+ cluster_projects.cluster_id = clusters.id
+ AND project_auto_devops.project_id = cluster_projects.project_id
+ AND project_auto_devops.domain != ''
+ HEREDOC
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 20c8dab4c3e..023eee5f33e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190131122559) do
+ActiveRecord::Schema.define(version: 20190204115450) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -168,6 +168,8 @@ ActiveRecord::Schema.define(version: 20190131122559) do
t.string "commit_email_hostname"
t.boolean "protected_ci_variables", default: false, null: false
t.string "runners_registration_token_encrypted"
+ t.integer "local_markdown_version", default: 0, null: false
+ t.integer "first_day_of_week", default: 0, null: false
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -2153,6 +2155,7 @@ ActiveRecord::Schema.define(version: 20190131122559) do
t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.integer "first_day_of_week"
t.string "issues_sort"
t.string "merge_requests_sort"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 184754cd467..12fec2753bf 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -65,6 +65,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance.
- [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq MemoryKiller, Unicorn).
- [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components.
+- [Invalidate markdown cache](invalidate_markdown_cache.md): Invalidate any cached markdown.
#### Updating GitLab
diff --git a/doc/administration/invalidate_markdown_cache.md b/doc/administration/invalidate_markdown_cache.md
new file mode 100644
index 00000000000..ad64cb077c1
--- /dev/null
+++ b/doc/administration/invalidate_markdown_cache.md
@@ -0,0 +1,16 @@
+# Invalidate Markdown Cache
+
+For performance reasons, GitLab caches the HTML version of markdown text
+(e.g. issue and merge request descriptions, comments). It's possible
+that these cached versions become outdated, for example
+when the `external_url` configuration option is changed - causing links
+in the cached text to refer to the old URL.
+
+To avoid this problem, the administrator can invalidate the existing cache by
+increasing the `local_markdown_version` setting in application settings. This can
+be done by [changing the application settings through
+the API](../api/settings.md#change-application-settings):
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings?local_markdown_version=<increased_number>
+```
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index c9a2778b3a4..6ea0ac0d495 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -48,6 +48,8 @@ The following metrics are available:
| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
+| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
+| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
### Ruby metrics
diff --git a/doc/api/README.md b/doc/api/README.md
index 692f63a400c..a060e0481bf 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -29,6 +29,7 @@ The following API resources are available:
- [Group access requests](access_requests.md)
- [Group badges](group_badges.md)
- [Group issue boards](group_boards.md)
+ - [Group labels](group_labels.md)
- [Group-level variables](group_level_variables.md)
- [Group members](members.md)
- [Group milestones](group_milestones.md)
diff --git a/doc/api/group_labels.md b/doc/api/group_labels.md
new file mode 100644
index 00000000000..c36d34b4af1
--- /dev/null
+++ b/doc/api/group_labels.md
@@ -0,0 +1,201 @@
+# Group Label API
+
+>**Note:** This feature was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21368) in GitLab 11.8.
+
+This API supports managing of [group labels](../user/project/labels.md#project-labels-and-group-labels). It allows to list, create, update, and delete group labels. Furthermore, users can subscribe and unsubscribe to and from group labels.
+
+## List group labels
+
+Get all labels for a given group.
+
+```
+GET /groups/:id/labels
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 7,
+ "name": "bug",
+ "color": "#FF0000",
+ "description": null,
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": false
+ },
+ {
+ "id": 4,
+ "name": "feature",
+ "color": "#228B22",
+ "description": null,
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": false
+ }
+]
+```
+
+## Create a new group label
+
+Create a new group label for a given group.
+
+```
+POST /groups/:id/labels
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the label |
+| `color` | string | yes | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
+| `description` | string | no | The description of the label |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "Feature Proposal", "color": "#FFA500", "description": "Describes new ideas" }' https://gitlab.example.com/api/v4/groups/5/labels
+```
+
+Example response:
+
+```json
+{
+ "id": 9,
+ "name": "Feature Proposal",
+ "color": "#FFA500",
+ "description": "Describes new ideas",
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": false
+}
+```
+
+## Update a group label
+
+Updates an existing group label. At least one parameter is required, to update the group label.
+
+```
+PUT /groups/:id/labels
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the label |
+| `new_name` | string | no | The new name of the label |
+| `color` | string | no | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
+| `description` | string | no | The description of the label |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "Feature Proposal", "new_name": "Feature Idea" }' https://gitlab.example.com/api/v4/groups/5/labels
+```
+
+Example response:
+
+```json
+{
+ "id": 9,
+ "name": "Feature Idea",
+ "color": "#FFA500",
+ "description": "Describes new ideas",
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": false
+}
+```
+
+## Delete a group label
+
+Deletes a group label with a given name.
+
+```
+DELETE /groups/:id/labels
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the label |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels?name=bug
+```
+
+## Subscribe to a group label
+
+Subscribes the authenticated user to a group label to receive notifications. If
+the user is already subscribed to the label, the status code `304` is returned.
+
+```
+POST /groups/:id/labels/:label_id/subscribe
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ----------------- | -------- | ------------------------------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `label_id` | integer or string | yes | The ID or title of a group's label |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels/9/subscribe
+```
+
+Example response:
+
+```json
+{
+ "id": 9,
+ "name": "Feature Idea",
+ "color": "#FFA500",
+ "description": "Describes new ideas",
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": true
+}
+```
+
+## Unsubscribe from a group label
+
+Unsubscribes the authenticated user from a group label to not receive
+notifications from it. If the user is not subscribed to the label, the status
+code `304` is returned.
+
+```
+POST /groups/:id/labels/:label_id/unsubscribe
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ----------------- | -------- | ------------------------------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `label_id` | integer or string | yes | The ID or title of a group's label |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels/9/unsubscribe
+```
+
+Example response:
+
+```json
+{
+ "id": 9,
+ "name": "Feature Idea",
+ "color": "#FFA500",
+ "description": "Describes new ideas",
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": false
+}
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 802ff1d1df9..d58cd45538d 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -994,6 +994,8 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
- `merge_commit_message` (optional) - Custom merge commit message
+- `squash_commit_message` (optional) - Custom squash commit message
+- `squash` (optional) - if `true` the commits will be squashed into a single commit on merge
- `should_remove_source_branch` (optional) - if `true` removes the source branch
- `merge_when_pipeline_succeeds` (optional) - if `true` the MR is merged when the pipeline succeeds
- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail
diff --git a/doc/api/settings.md b/doc/api/settings.md
index c329e3cdf24..2e0a2a09133 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -57,11 +57,13 @@ Example response:
"dsa_key_restriction": 0,
"ecdsa_key_restriction": 0,
"ed25519_key_restriction": 0,
+ "first_day_of_week": 0,
"enforce_terms": true,
"terms": "Hello world!",
"performance_bar_allowed_group_id": 42,
"instance_statistics_visibility_private": false,
- "user_show_add_ssh_key_message": true
+ "user_show_add_ssh_key_message": true,
+ "local_markdown_version": 0
}
```
@@ -113,11 +115,13 @@ Example response:
"dsa_key_restriction": 0,
"ecdsa_key_restriction": 0,
"ed25519_key_restriction": 0,
+ "first_day_of_week": 0,
"enforce_terms": true,
"terms": "Hello world!",
"performance_bar_allowed_group_id": 42,
"instance_statistics_visibility_private": false,
- "user_show_add_ssh_key_message": true
+ "user_show_add_ssh_key_message": true,
+ "local_markdown_version": 0
}
```
@@ -157,6 +161,7 @@ are listed in the descriptions of the relevant settings.
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. |
+| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday and `1` for Monday. |
| `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. |
| `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. |
| `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. |
@@ -235,3 +240,4 @@ are listed in the descriptions of the relevant settings.
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider. |
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
+| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. |
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 97e133a2e2f..32c73c4f398 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -98,7 +98,7 @@ future GitLab releases.**
| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
| **CI_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL |
-| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
+| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
diff --git a/doc/development/README.md b/doc/development/README.md
index 05715274a81..d5829e31343 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -123,3 +123,7 @@ description: 'Learn how to contribute to GitLab.'
## Compliance
- [Licensing](licensing.md) for ensuring license compliance
+
+## Go guides
+
+- [Go Guidelines](go_guide/index.md)
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 24feb1378a1..c5344139ab4 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -20,7 +20,7 @@ All labels, their meaning and priority are defined on the
If you come across an issue that has none of these, and you're allowed to set
labels, you can _always_ add the team and type, and often also the subject.
-[milestones-page]: https://gitlab.com/gitlab-org/gitlab-ce/milestones
+[milestones-page]: https://gitlab.com/groups/gitlab-org/-/milestones
## Type labels
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 9bef0635e3f..19b6181c9a2 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -86,6 +86,9 @@ request is as follows:
guidelines](../merge_request_performance_guidelines.md).
1. For tests that use Capybara or PhantomJS, see this [article on how
to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
+1. If your merge request introduces changes that require additional steps when
+ installing GitLab from source, add them to `doc/install/installation.md` in
+ the same merge request.
Please keep the change in a single MR **as small as possible**. If you want to
contribute a large feature think very hard what the minimum viable change is.
diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md
index 6f1ba5d62a5..0eedef5e14f 100644
--- a/doc/development/contributing/style_guides.md
+++ b/doc/development/contributing/style_guides.md
@@ -21,6 +21,7 @@
of _prohibited this user from being saved due to the following errors:_ the
text should be _sorry, we could not create your account because:_
1. Code should be written in [US English][us-english]
+1. [Go](../go_guide/index.md)
This is also the style used by linting tools such as
[RuboCop](https://github.com/bbatsov/rubocop) and [Hound CI](https://houndci.com).
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
new file mode 100644
index 00000000000..cdc806a2d31
--- /dev/null
+++ b/doc/development/go_guide/index.md
@@ -0,0 +1,216 @@
+# Go standards and style guidelines
+
+This document describes various guidelines and best practices for GitLab
+projects using the [Go language](https://golang.org).
+
+## Overview
+
+GitLab is built on top of [Ruby on Rails](https://rubyonrails.org/), but we're
+also using Go for projects where it makes sense. Go is a very powerful
+language, with many advantages, and is best suited for projects with a lot of
+IO (disk/network access), HTTP requests, parallel processing, etc. Since we
+have both Ruby on Rails and Go at GitLab, we should evaluate carefully which of
+the two is best for the job.
+
+This page aims to define and organize our Go guidelines, based on our various
+experiences. Several projects were started with different standards and they
+can still have specifics. They will be described in their respective
+`README.md` or `PROCESS.md` files.
+
+## Code Review
+
+We follow the common principles of
+[Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
+
+Reviewers and maintainers should pay attention to:
+
+- `defer` functions: ensure the presence when needed, and after `err` check.
+- Inject dependencies as parameters.
+- Void structs when marshalling to JSON (generates `null` instead of `[]`).
+
+### Security
+
+Security is our top priority at GitLab. During code reviews, we must take care
+of possible security breaches in our code:
+
+- XSS when using text/template
+- CSRF Protection using Gorilla
+- Use a Go version without known vulnerabilities
+- Don't leak secret tokens
+- SQL injections
+
+Remember to run
+[SAST](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html)
+**[ULTIMATE]** on your project (or at least the [gosec
+analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)),
+and to follow our [Security
+requirements](../code_review.md#security-requirements).
+
+Web servers can take advantages of middlewares like [Secure](https://github.com/unrolled/secure).
+
+### Finding a reviewer
+
+Many of our projects are too small to have full-time maintainers. That's why we
+have a shared pool of Go reviewers at GitLab. To find a reviewer, use the
+[Engineering Projects](https://about.gitlab.com/handbook/engineering/projects/)
+page in the handbook. "GitLab Community Edition (CE)" and "GitLab Community
+Edition (EE)" both have a "Go" section with its list of reviewers.
+
+To add yourself to this list, add the following to your profile in the
+[team.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/team.yml)
+file and ask your manager to review and merge.
+
+```yaml
+projects:
+ gitlab-ee: reviewer go
+ gitlab-ce: reviewer go
+```
+
+## Code style and format
+
+- Avoid global variables, even in packages. By doing so you will introduce side
+ effects if the package is included multiple times.
+- Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a
+ tool that automatically formats Go source code).
+
+### Automatic linting
+
+All Go projects should include these GitLab CI/CD jobs:
+
+```yaml
+go lint:
+ image: golang:1.11
+ script:
+ - go get -u golang.org/x/lint/golint
+ - golint -set_exit_status
+```
+
+Once [recursive includes](https://gitlab.com/gitlab-org/gitlab-ce/issues/56836)
+become available, you will be able to share job templates like this
+[analyzer](https://gitlab.com/gitlab-org/security-products/ci-templates/raw/master/includes-dev/analyzer.yml).
+
+## Dependencies
+
+Dependencies should be kept to the minimum. The introduction of a new
+dependency should be argued in the merge request, as per our [Approval
+Guidelines](../code_review.html#approval-guidelines). Both [License
+Management](https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html)
+**[ULTIMATE]** and [Dependency
+Scanning](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html)
+**[ULTIMATE]** should be activated on all projects to ensure new dependencies
+security status and license compatibility.
+
+### Modules
+
+Since Go 1.11, a standard dependency system is available behind the name [Go
+Modules](https://github.com/golang/go/wiki/Modules). It provides a way to
+define and lock dependencies for reproducible builds. It should be used
+whenever possible.
+
+There was a [bug on modules
+checksums](https://github.com/golang/go/issues/29278) in Go < v1.11.4, so make
+sure to use at least this version to avoid `checksum mismatch` errors.
+
+### ORM
+
+We don't use object-relational mapping libraries (ORMs) at GitLab (except
+[ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) in
+Ruby on Rails). Projects can be structured with services to avoid them.
+[PQ](https://github.com/lib/pq) should be enough to interact with PostgreSQL
+databases.
+
+### Migrations
+
+In the rare event of managing a hosted database, it's necessary to use a
+migration system like ActiveRecord is providing. A simple library like
+[Journey](https://github.com/db-journey/journey), designed to be used in
+`postgres` containers, can be deployed as long-running pods. New versions will
+deploy a new pod, migrating the data automatically.
+
+## Testing
+
+We should not use any specific library or framework for testing, as the
+[standard library](https://golang.org/pkg/) provides already everything to get
+started. For example, some external dependencies might be worth considering in
+case we decide to use a specific library or framework:
+
+- [Testify](https://github.com/stretchr/testify)
+- [httpexpect](https://github.com/gavv/httpexpect)
+
+Use [subtests](https://blog.golang.org/subtests) whenever possible to improve
+code readability and test output.
+
+### Benchmarks
+
+Programs handling a lot of IO or complex operations should always include
+[benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks), to ensure
+performance consistency over time.
+
+## CLIs
+
+Every Go program is launched from the command line.
+[cli](https://github.com/urfave/cli) is a convenient package to create command
+line apps. It should be used whether the project is a daemon or a simple cli
+tool. Flags can be mapped to [environment
+variables](https://github.com/urfave/cli#values-from-the-environment) directly,
+which documents and centralizes at the same time all the possible command line
+interactions with the program. Don't use `os.GetEnv`, it hides variables deep
+in the code.
+
+## Daemons
+
+### Logging
+
+The usage of a logging library is strongly recommended for daemons. Even though
+there is a `log` package in the standard library, we generally use
+[logrus](https://github.com/sirupsen/logrus). Its plugin ("hooks") system
+makes it a powerful logging library, with the ability to add notifiers and
+formatters at the logger level directly.
+
+### Tracing and Correlation
+
+[LabKit](https://gitlab.com/gitlab-org/labkit) is a place to keep common
+libraries for Go services. Currently it's vendored into two projects:
+Workhorse and Gitaly, and it exports two main (but related) pieces of
+functionality:
+
+- [`gitlab.com/gitlab-org/labkit/correlation`](https://gitlab.com/gitlab-org/labkit/tree/master/correlation):
+ for propagating and extracting correlation ids between services.
+- [`gitlab.com/gitlab-org/labkit/tracing`](https://gitlab.com/gitlab-org/labkit/tree/master/tracing):
+ for instrumenting Go libraries for distributed tracing.
+
+This gives us a thin abstraction over underlying implementations that is
+consistent across Workhorse, Gitaly, and, in future, other Go servers. For
+example, in the case of `gitlab.com/gitlab-org/labkit/tracing` we can switch
+from using Opentracing directly to using Zipkin or Gokit's own tracing wrapper
+without changes to the application code, while still keeping the same
+consistent configuration mechanism (i.e. the `GITLAB_TRACING` environment
+variable).
+
+### Context
+
+Since daemons are long-running applications, they should have mechanisms to
+manage cancellations, and avoid unnecessary resources consumption (which could
+lead to DDOS vulnerabilities). [Go
+Context](https://github.com/golang/go/wiki/CodeReviewComments#contexts) should
+be used in functions that can block and passed as the first parameter.
+
+## Dockerfiles
+
+Every project should have a `Dockerfile` at the root of their repository, to
+build and run the project. Since Go program are static binaries, they should
+not require any external dependency, and shells in the final image are useless.
+We encourage [Multistage
+builds](https://docs.docker.com/develop/develop-images/multistage-build/):
+
+- They let the user build the project with the right Go version and
+ dependencies.
+- They generate a small, self-contained image, derived from `Scratch`.
+
+Generated docker images should have the program at their `Entrypoint` to create
+portable commands. That way, anyone can run the image, and without parameters
+it will display its help message (if `cli` has been used).
+
+---
+
+[Return to Development documentation](../README.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 1f65e3415d1..a8064ae046e 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -139,8 +139,8 @@ Then select 'Internet Site' and press enter to confirm the hostname.
The Ruby interpreter is required to run GitLab.
-**Note:** The current supported Ruby (MRI) version is 2.3.x. GitLab 9.0 dropped
-support for Ruby 2.1.x.
+**Note:** The current supported Ruby (MRI) version is 2.5.x. GitLab 11.6
+ dropped support for Ruby 2.4.x.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
@@ -345,11 +345,15 @@ cd /home/git
```sh
# Clone GitLab repository
-sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab
+sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b X-Y-stable gitlab
```
+Make sure to replace `X-Y-stable` with the stable branch that matches the
+version you want to install. For example, if you want to install 11.8 you would
+use the branch name `11-8-stable`.
+
CAUTION: **Caution:**
-You can change `11-7-stable` to `master` if you want the *bleeding edge* version, but never install `master` on a production server!
+You can change `X-Y-stable` to `master` if you want the *bleeding edge* version, but never install `master` on a production server!
### Configure It
@@ -691,6 +695,11 @@ sudo nginx -t
You should receive `syntax is okay` and `test is successful` messages. If you receive errors check your `gitlab` or `gitlab-ssl` Nginx config file for typos, etc. as indicated in the error message given.
+NOTE: **Note:**
+Verify that the installed version is greater than 1.12.1 by running `nginx -v`. If it's lower, you may receive the error below:
+`nginx: [emerg] unknown "start$temp=[filtered]$rest" variable
+nginx: configuration file /etc/nginx/nginx.conf test failed`
+
### Restart
```sh
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 325de50cab0..463bdd59282 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -126,14 +126,22 @@ Auto Deploy, and Auto Monitoring will be silently skipped.
## Auto DevOps base domain
+NOTE: **Note**
+`AUTO_DEVOPS_DOMAIN` environment variable is deprecated and
+[is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-ce/issues/56959) in GitLab 12.0.
+
The Auto DevOps base domain is required if you want to make use of [Auto
Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined
-in three places:
+in any of the following places:
-- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops)
+- either under the cluster's settings, whether for [projects](../../user/project/clusters/index.md#base-domain) or [groups](../../user/group/clusters/index.md#base-domain)
- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section
-- or at the project as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters))
-- or at the group level as a variable: `AUTO_DEVOPS_DOMAIN`
+- or at the project level as a variable: `KUBE_INGRESS_BASE_DOMAIN`
+- or at the group level as a variable: `KUBE_INGRESS_BASE_DOMAIN`.
+
+NOTE: **Note**
+The Auto DevOps base domain variable (`KUBE_INGRESS_BASE_DOMAIN`) follows the same order of precedence
+as other environment [variables](../../ci/variables/README.md#priority-of-variables).
A wildcard DNS A record matching the base domain(s) is required, for example,
given a base domain of `example.com`, you'd need a DNS entry like:
@@ -170,13 +178,13 @@ In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ce/blob/maste
Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so
except for the environment scope, they would also need to have a different
domain they would be deployed to. This is why you need to define a separate
-`AUTO_DEVOPS_DOMAIN` variable for all the above
+`KUBE_INGRESS_BASE_DOMAIN` variable for all the above
[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables).
The following table is an example of how the three different clusters would
be configured.
-| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes |
+| Cluster name | Cluster environment scope | `KUBE_INGRESS_BASE_DOMAIN` variable value | Variable environment scope | Notes |
| ------------ | -------------- | ----------------------------- | ------------- | ------ |
| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. |
| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). |
@@ -190,14 +198,11 @@ To add a different cluster for each environment:
![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png)
1. After the clusters are created, navigate to each one and install Helm Tiller
- and Ingress.
+ and Ingress. Wait for the Ingress IP address to be assigned.
1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the
specified Auto DevOps domains.
-1. Navigate to your project's **Settings > CI/CD > Environment variables** and add
- the `AUTO_DEVOPS_DOMAIN` variables with their respective environment
- scope.
-
- ![Auto DevOps domain variables](img/autodevops_domain_variables.png)
+1. Navigate to each cluster's page, through **Operations > Kubernetes**,
+ and add the domain based on its Ingress IP address.
Now that all is configured, you can test your setup by creating a merge request
and verifying that your app is deployed as a review app in the Kubernetes
@@ -205,10 +210,9 @@ cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
NOTE: **Note:**
-Auto DevOps is not supported for a group with multiple clusters, as it
-is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group
-level. This will be resolved in the future with the [following issue](
-https://gitlab.com/gitlab-org/gitlab-ce/issues/52363).
+From GitLab 11.8, `KUBE_INGRESS_BASE_DOMAIN` replaces `AUTO_DEVOPS_DOMAIN`.
+`AUTO_DEVOPS_DOMAIN` [is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-ce/issues/56959)
+in GitLab 12.0.
## Enabling/Disabling Auto DevOps
@@ -681,7 +685,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| **Variable** | **Description** |
| ------------ | --------------- |
-| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). |
+| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain). By default, set automatically by the [Auto DevOps setting](#enabling-auto-devops). This variable is deprecated and [is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-ce/issues/56959) in GitLab 12.0. Use `KUBE_INGRESS_BASE_DOMAIN` instead. |
| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/auto-deploy-app). |
| `AUTO_DEVOPS_CHART_REPOSITORY` | The Helm Chart repository used to search for charts; defaults to `https://charts.gitlab.io`. |
| `REPLICAS` | The number of replicas to deploy; defaults to 1. |
@@ -711,6 +715,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. |
| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. |
| `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. |
+| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. |
TIP: **Tip:**
Set up the replica variables using a
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index 9f9b2da23e1..9fc50741407 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -59,11 +59,16 @@ Add another cluster similar to the first one and make sure to
[set an environment scope](#environment-scopes) that will
differentiate the new cluster from the rest.
-NOTE: **Note:**
-Auto DevOps is not supported for a group with multiple clusters, as it
-is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group
-level. This will be resolved in the future with the [following issue](
-https://gitlab.com/gitlab-org/gitlab-ce/issues/52363).
+## Base domain
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8.
+
+Domains at the cluster level permit support for multiple domains
+per [multiple Kubernetes clusters](#multiple-kubernetes-clusters-premium). When specifying a domain,
+this will be automatically set as an environment variable (`KUBE_INGRESS_BASE_DOMAIN`) during
+the [Auto DevOps](../../../topics/autodevops/index.md) stages.
+
+The domain should have a wildcard DNS configured to the Ingress IP address.
## Environment scopes **[PREMIUM]**
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index eb2d731343e..363d3db8db1 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -87,3 +87,11 @@ You can choose between 3 options:
- Files and Readme (default)
- Readme
- Activity
+
+## Localization
+
+### First day of the week
+
+The first day of the week can be customised for calendar views and date pickers.
+
+You can choose **Sunday** or **Monday** as the first day of the week. If you select **System Default**, the system-wide default setting will be used.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index bb815695cb1..85a4af24dc5 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -172,6 +172,17 @@ functionalities needed to successfully build and deploy a containerized
application. Bear in mind that the same credentials are used for all the
applications running on the cluster.
+## Base domain
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8.
+
+Domains at the cluster level permit support for multiple domains
+per [multiple Kubernetes clusters](#multiple-kubernetes-clusters-premium). When specifying a domain,
+this will be automatically set as an environment variable (`KUBE_INGRESS_BASE_DOMAIN`) during
+the [Auto DevOps](../../../topics/autodevops/index.md) stages.
+
+The domain should have a wildcard DNS configured to the Ingress IP address.
+
## Access controls
When creating a cluster in GitLab, you will be asked if you would like to create an
@@ -254,6 +265,12 @@ install it manually.
## Installing applications
+NOTE: **Note:**
+Before starting the installation of applications, make sure that time is synchronized
+between your GitLab server and your Kubernetes cluster. Otherwise, installation could fail
+and you may get errors like `Error: remote error: tls: bad certificate`
+in the `stdout` of pods created by GitLab in your Kubernetes cluster.
+
GitLab provides a one-click install for various applications which can
be added directly to your configured cluster. Those applications are
needed for [Review Apps](../../../ci/review_apps/index.md) and
@@ -449,6 +466,7 @@ GitLab CI/CD build environment.
| `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. |
| `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. |
+| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](#base-domain) for more information. | 
NOTE: **NOTE:**
Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main
diff --git a/doc/user/project/merge_requests/img/squash_mr_message.png b/doc/user/project/merge_requests/img/squash_mr_message.png
new file mode 100644
index 00000000000..8734cab29aa
--- /dev/null
+++ b/doc/user/project/merge_requests/img/squash_mr_message.png
Binary files differ
diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md
index 34cba867e2c..4ff8ec3a7e6 100644
--- a/doc/user/project/merge_requests/squash_and_merge.md
+++ b/doc/user/project/merge_requests/squash_and_merge.md
@@ -23,11 +23,14 @@ The squashed commit's commit message will be either:
- Taken from the first multi-line commit message in the merge.
- The merge request's title if no multi-line commit message is found.
-Note that the squashed commit is still followed by a merge commit,
-as the merge method for this example repository uses a merge commit.
-Squashing also works with the fast-forward merge strategy, see
-[squashing and fast-forward merge](#squash-and-fast-forward-merge) for more
-details.
+It can be customized before merging a merge request.
+
+![A squash commit message editor](img/squash_mr_message.png)
+
+NOTE: **Note:**
+The squashed commit in this example is followed by a merge commit, as the merge method for this example repository uses a merge commit.
+
+Squashing also works with the fast-forward merge strategy, see [squashing and fast-forward merge](#squash-and-fast-forward-merge) for more details.
## Use cases
@@ -60,7 +63,7 @@ This can then be overridden at the time of accepting the merge request:
The squashed commit has the following metadata:
-- Message: the message of the squash commit.
+- Message: the message of the squash commit, or a customized message.
- Author: the author of the merge request.
- Committer: the user who initiated the squash.
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 68dd3330d7a..b2da1c85c62 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -79,11 +79,14 @@ running on your instance).
![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated_2018.png)
-NOTE: **Note:**
-Note that if you use your root domain for your GitLab Pages website **only**, and if
-your domain registrar supports this feature, you can add a DNS apex `CNAME`
-record instead of an `A` record. The main advantage of doing so is that when GitLab Pages
-IP on GitLab.com changes for whatever reason, you don't need to update your `A` record.
+CAUTION: **Caution:**
+Note that if you use your root domain for your GitLab Pages website
+**only**, and if your domain registrar supports this feature, you can
+add a DNS apex `CNAME` record instead of an `A` record. The main
+advantage of doing so is that when GitLab Pages IP on GitLab.com
+changes for whatever reason, you don't need to update your `A` record.
+There may be a few exceptions, but **this method is not recommended**
+as it most likely won't work if you set an `MX` record for your root domain.
#### DNS CNAME record
@@ -114,14 +117,16 @@ co-exist, so you need to place the TXT record in a special subdomain of its own.
#### TL;DR
-If the domain has multiple uses (e.g., you host email on it as well):
+For root domains (`domain.com`), set a DNS `A` record and verify your
+domain's ownership with a TXT record:
| From | DNS Record | To |
| ---- | ---------- | -- |
| domain.com | A | 35.185.44.232 |
| domain.com | TXT | gitlab-pages-verification-code=00112233445566778899aabbccddeeff |
-If the domain is dedicated to GitLab Pages use and no other services run on it:
+For subdomains (`subdomain.domain.com`), set a DNS `CNAME` record and
+verify your domain's ownership with a TXT record:
| From | DNS Record | To |
| ---- | ---------- | -- |
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 1213474b7d8..8a2f4e1b40e 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -88,6 +88,14 @@ The mirrored repository will be listed. For example, `https://*****:*****@github
The repository will push soon. To force a push, click the appropriate button.
+## Setting up a push mirror to another GitLab instance with 2FA activated
+
+1. On the destination GitLab instance, create a [personal access token](../user/profile/personal_access_tokens.md) with `API` scope.
+1. On the source GitLab instance:
+ 1. Fill in the **Git repository URL** field using this format: `https://oauth2@<destination host>/<your_gitlab_group_or_name>/<your_gitlab_project>.git`.
+ 1. Fill in **Password** field with the GitLab personal access token created on the destination GitLab instance.
+ 1. Click the **Mirror repository** button.
+
## Pulling from a remote repository **[STARTER]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 9cbfc0e35ff..4dd1b459554 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -109,6 +109,7 @@ module API
mount ::API::Features
mount ::API::Files
mount ::API::GroupBoards
+ mount ::API::GroupLabels
mount ::API::GroupMilestones
mount ::API::Groups
mount ::API::GroupVariables
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index a1f0efa3c68..beb8ce349b4 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1019,12 +1019,17 @@ module API
label.open_merge_requests_count(options[:current_user])
end
- expose :priority do |label, options|
- label.priority(options[:project])
+ expose :subscribed do |label, options|
+ label.subscribed?(options[:current_user], options[:parent])
end
+ end
- expose :subscribed do |label, options|
- label.subscribed?(options[:current_user], options[:project])
+ class GroupLabel < Label
+ end
+
+ class ProjectLabel < Label
+ expose :priority do |label, options|
+ label.priority(options[:parent])
end
end
diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb
new file mode 100644
index 00000000000..0dbc5f45a68
--- /dev/null
+++ b/lib/api/group_labels.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module API
+ class GroupLabels < Grape::API
+ include PaginationParams
+ helpers ::API::Helpers::LabelHelpers
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all labels of the group' do
+ detail 'This feature was added in GitLab 11.8'
+ success Entities::GroupLabel
+ end
+ params do
+ use :pagination
+ end
+ get ':id/labels' do
+ get_labels(user_group, Entities::GroupLabel)
+ end
+
+ desc 'Create a new label' do
+ detail 'This feature was added in GitLab 11.8'
+ success Entities::GroupLabel
+ end
+ params do
+ use :label_create_params
+ end
+ post ':id/labels' do
+ create_label(user_group, Entities::GroupLabel)
+ end
+
+ desc 'Update an existing label. At least one optional parameter is required.' do
+ detail 'This feature was added in GitLab 11.8'
+ success Entities::GroupLabel
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be updated'
+ optional :new_name, type: String, desc: 'The new name of the label'
+ optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
+ optional :description, type: String, desc: 'The new description of label'
+ at_least_one_of :new_name, :color, :description
+ end
+ put ':id/labels' do
+ update_label(user_group, Entities::GroupLabel)
+ end
+
+ desc 'Delete an existing label' do
+ detail 'This feature was added in GitLab 11.8'
+ success Entities::GroupLabel
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be deleted'
+ end
+ delete ':id/labels' do
+ delete_label(user_group)
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index e3d0b981065..2eb7b04711a 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -84,8 +84,8 @@ module API
page || not_found!('Wiki Page')
end
- def available_labels_for(label_parent)
- search_params = { include_ancestor_groups: true }
+ def available_labels_for(label_parent, include_ancestor_groups: true)
+ search_params = { include_ancestor_groups: include_ancestor_groups }
if label_parent.is_a?(Project)
search_params[:project_id] = label_parent.id
@@ -170,13 +170,6 @@ module API
end
end
- def find_project_label(id)
- labels = available_labels_for(user_project)
- label = labels.find_by_id(id) || labels.find_by_title(id)
-
- label || not_found!('Label')
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def find_project_issue(iid)
IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
diff --git a/lib/api/helpers/label_helpers.rb b/lib/api/helpers/label_helpers.rb
new file mode 100644
index 00000000000..c11e7d614ab
--- /dev/null
+++ b/lib/api/helpers/label_helpers.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module LabelHelpers
+ extend Grape::API::Helpers
+
+ params :label_create_params do
+ requires :name, type: String, desc: 'The name of the label to be created'
+ requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
+ optional :description, type: String, desc: 'The description of label to be created'
+ end
+
+ def find_label(parent, id, include_ancestor_groups: true)
+ labels = available_labels_for(parent, include_ancestor_groups: include_ancestor_groups)
+ label = labels.find_by_id(id) || labels.find_by_title(id)
+
+ label || not_found!('Label')
+ end
+
+ def get_labels(parent, entity)
+ present paginate(available_labels_for(parent)), with: entity, current_user: current_user, parent: parent
+ end
+
+ def create_label(parent, entity)
+ authorize! :admin_label, parent
+
+ label = available_labels_for(parent).find_by_title(params[:name])
+ conflict!('Label already exists') if label
+
+ priority = params.delete(:priority)
+ label_params = declared_params(include_missing: false)
+
+ label =
+ if parent.is_a?(Project)
+ ::Labels::CreateService.new(label_params).execute(project: parent)
+ else
+ ::Labels::CreateService.new(label_params).execute(group: parent)
+ end
+
+ if label.persisted?
+ if parent.is_a?(Project)
+ label.prioritize!(parent, priority) if priority
+ end
+
+ present label, with: entity, current_user: current_user, parent: parent
+ else
+ render_validation_error!(label)
+ end
+ end
+
+ def update_label(parent, entity)
+ authorize! :admin_label, parent
+
+ label = find_label(parent, params[:name], include_ancestor_groups: false)
+ update_priority = params.key?(:priority)
+ priority = params.delete(:priority)
+
+ label = ::Labels::UpdateService.new(declared_params(include_missing: false)).execute(label)
+ render_validation_error!(label) unless label.valid?
+
+ if parent.is_a?(Project) && update_priority
+ if priority.nil?
+ label.unprioritize!(parent)
+ else
+ label.prioritize!(parent, priority)
+ end
+ end
+
+ present label, with: entity, current_user: current_user, parent: parent
+ end
+
+ def delete_label(parent)
+ authorize! :admin_label, parent
+
+ label = find_label(parent, params[:name], include_ancestor_groups: false)
+
+ destroy_conditionally!(label)
+ end
+ end
+ end
+end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index d5eb2b94669..d729d3ee625 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -3,6 +3,7 @@
module API
class Labels < Grape::API
include PaginationParams
+ helpers ::API::Helpers::LabelHelpers
before { authenticate! }
@@ -11,62 +12,28 @@ module API
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all labels of the project' do
- success Entities::Label
+ success Entities::ProjectLabel
end
params do
use :pagination
end
get ':id/labels' do
- present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project
+ get_labels(user_project, Entities::ProjectLabel)
end
desc 'Create a new label' do
- success Entities::Label
+ success Entities::ProjectLabel
end
params do
- requires :name, type: String, desc: 'The name of the label to be created'
- requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
- optional :description, type: String, desc: 'The description of label to be created'
+ use :label_create_params
optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
end
- # rubocop: disable CodeReuse/ActiveRecord
post ':id/labels' do
- authorize! :admin_label, user_project
-
- label = available_labels_for(user_project).find_by(title: params[:name])
- conflict!('Label already exists') if label
-
- priority = params.delete(:priority)
- label = ::Labels::CreateService.new(declared_params(include_missing: false)).execute(project: user_project)
-
- if label.valid?
- label.prioritize!(user_project, priority) if priority
- present label, with: Entities::Label, current_user: current_user, project: user_project
- else
- render_validation_error!(label)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- desc 'Delete an existing label' do
- success Entities::Label
- end
- params do
- requires :name, type: String, desc: 'The name of the label to be deleted'
- end
- # rubocop: disable CodeReuse/ActiveRecord
- delete ':id/labels' do
- authorize! :admin_label, user_project
-
- label = user_project.labels.find_by(title: params[:name])
- not_found!('Label') unless label
-
- destroy_conditionally!(label)
+ create_label(user_project, Entities::ProjectLabel)
end
- # rubocop: enable CodeReuse/ActiveRecord
desc 'Update an existing label. At least one optional parameter is required.' do
- success Entities::Label
+ success Entities::ProjectLabel
end
params do
requires :name, type: String, desc: 'The name of the label to be updated'
@@ -76,33 +43,19 @@ module API
optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
at_least_one_of :new_name, :color, :description, :priority
end
- # rubocop: disable CodeReuse/ActiveRecord
put ':id/labels' do
- authorize! :admin_label, user_project
-
- label = user_project.labels.find_by(title: params[:name])
- not_found!('Label not found') unless label
-
- update_priority = params.key?(:priority)
- priority = params.delete(:priority)
- label_params = declared_params(include_missing: false)
- # Rename new name to the actual label attribute name
- label_params[:name] = label_params.delete(:new_name) if label_params.key?(:new_name)
-
- label = ::Labels::UpdateService.new(label_params).execute(label)
- render_validation_error!(label) unless label.valid?
-
- if update_priority
- if priority.nil?
- label.unprioritize!(user_project)
- else
- label.prioritize!(user_project, priority)
- end
- end
+ update_label(user_project, Entities::ProjectLabel)
+ end
- present label, with: Entities::Label, current_user: current_user, project: user_project
+ desc 'Delete an existing label' do
+ success Entities::ProjectLabel
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be deleted'
+ end
+ delete ':id/labels' do
+ delete_label(user_project)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 4179aaa93a0..df46b4446ff 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -343,6 +343,7 @@ module API
end
params do
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :squash_commit_message, type: String, desc: 'Custom squash commit message'
optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible'
optional :merge_when_pipeline_succeeds, type: Boolean,
@@ -370,6 +371,7 @@ module API
merge_params = {
commit_message: params[:merge_commit_message],
+ squash_commit_message: params[:squash_commit_message],
should_remove_source_branch: params[:should_remove_source_branch]
}
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 95371961398..b16faffe335 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -121,6 +121,7 @@ module API
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
+ optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated"
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 74ad3c35a61..dfb54446ddf 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -2,51 +2,88 @@
module API
class Subscriptions < Grape::API
+ helpers ::API::Helpers::LabelHelpers
+
before { authenticate! }
- subscribable_types = {
- 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
- 'issues' => proc { |id| find_project_issue(id) },
- 'labels' => proc { |id| find_project_label(id) }
- }
+ subscribables = [
+ {
+ type: 'merge_requests',
+ entity: Entities::MergeRequest,
+ source: Project,
+ finder: ->(id) { find_merge_request_with_access(id, :update_merge_request) }
+ },
+ {
+ type: 'issues',
+ entity: Entities::Issue,
+ source: Project,
+ finder: ->(id) { find_project_issue(id) }
+ },
+ {
+ type: 'labels',
+ entity: Entities::ProjectLabel,
+ source: Project,
+ finder: ->(id) { find_label(user_project, id) }
+ },
+ {
+ type: 'labels',
+ entity: Entities::GroupLabel,
+ source: Group,
+ finder: ->(id) { find_label(user_group, id) }
+ }
+ ]
- params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :subscribable_id, type: String, desc: 'The ID of a resource'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- subscribable_types.each do |type, finder|
- type_singularized = type.singularize
- entity_class = Entities.const_get(type_singularized.camelcase)
+ subscribables.each do |subscribable|
+ source_type = subscribable[:source].name.underscore
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ requires :subscribable_id, type: String, desc: 'The ID of a resource'
+ end
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Subscribe to a resource' do
- success entity_class
+ success subscribable[:entity]
end
- post ":id/#{type}/:subscribable_id/subscribe" do
- resource = instance_exec(params[:subscribable_id], &finder)
+ post ":id/#{subscribable[:type]}/:subscribable_id/subscribe" do
+ parent = parent_resource(source_type)
+ resource = instance_exec(params[:subscribable_id], &subscribable[:finder])
- if resource.subscribed?(current_user, user_project)
+ if resource.subscribed?(current_user, parent)
not_modified!
else
- resource.subscribe(current_user, user_project)
- present resource, with: entity_class, current_user: current_user, project: user_project
+ resource.subscribe(current_user, parent)
+ present resource, with: subscribable[:entity], current_user: current_user, project: parent, parent: parent
end
end
desc 'Unsubscribe from a resource' do
- success entity_class
+ success subscribable[:entity]
end
- post ":id/#{type}/:subscribable_id/unsubscribe" do
- resource = instance_exec(params[:subscribable_id], &finder)
+ post ":id/#{subscribable[:type]}/:subscribable_id/unsubscribe" do
+ parent = parent_resource(source_type)
+ resource = instance_exec(params[:subscribable_id], &subscribable[:finder])
- if !resource.subscribed?(current_user, user_project)
+ if !resource.subscribed?(current_user, parent)
not_modified!
else
- resource.unsubscribe(current_user, user_project)
- present resource, with: entity_class, current_user: current_user, project: user_project
+ resource.unsubscribe(current_user, parent)
+ present resource, with: subscribable[:entity], current_user: current_user, project: parent, parent: parent
end
end
end
end
+
+ private
+
+ helpers do
+ def parent_resource(source_type)
+ case source_type
+ when 'project'
+ user_project
+ else
+ nil
+ end
+ end
+ end
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 184c7418e75..22ed1d8e7b4 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -93,7 +93,7 @@ module Backup
progress.puts "Error: #{e}".color(:red)
end
else
- restore_repo_success = gitlab_shell.create_repository(project.repository_storage, project.disk_path)
+ restore_repo_success = gitlab_shell.create_project_repository(project)
end
if restore_repo_success
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 7aa02009aa0..b2ef04d23d7 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -12,6 +12,9 @@ module Gitlab
# Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze
+ # OpenID Connect profile scopes
+ PROFILE_SCOPES = [:profile, :email].freeze
+
# Default scopes for OAuth applications that don't define their own
DEFAULT_SCOPES = [:api].freeze
@@ -284,7 +287,7 @@ module Gitlab
# Other available scopes
def optional_scopes
- available_scopes + OPENID_SCOPES - DEFAULT_SCOPES
+ available_scopes + OPENID_SCOPES + PROFILE_SCOPES - DEFAULT_SCOPES
end
def registry_scopes
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index eaead41a720..75a3f17f549 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -65,9 +65,9 @@ module Gitlab
def import_wiki
return if project.wiki.repository_exists?
- disk_path = project.wiki.disk_path
- import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
- gitlab_shell.import_repository(project.repository_storage, disk_path, import_url)
+ wiki = WikiFormatter.new(project)
+
+ gitlab_shell.import_wiki_repository(project, wiki)
rescue StandardError => e
errors << { type: :wiki, errors: e.message }
end
diff --git a/lib/gitlab/bitbucket_import/wiki_formatter.rb b/lib/gitlab/bitbucket_import/wiki_formatter.rb
new file mode 100644
index 00000000000..b8ff43b777b
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/wiki_formatter.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ class WikiFormatter
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def disk_path
+ project.wiki.disk_path
+ end
+
+ def full_path
+ project.wiki.full_path
+ end
+
+ def import_url
+ project.import_url.sub(/\.git\z/, ".git/wiki")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml
new file mode 100644
index 00000000000..9c534b2b8e7
--- /dev/null
+++ b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml
@@ -0,0 +1,121 @@
+# Read more about how to use this script on this blog post https://about.gitlab.com/2019/01/28/android-publishing-with-gitlab-and-fastlane/
+# You will also need to configure your build.gradle, Dockerfile, and fastlane configuration to make this work.
+# If you are looking for a simpler template that does not publish, see the Android template.
+
+stages:
+ - environment
+ - build
+ - test
+ - internal
+ - alpha
+ - beta
+ - production
+
+
+.updateContainerJob:
+ image: docker:stable
+ stage: environment
+ services:
+ - docker:dind
+ script:
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG || true
+ - docker build --cache-from $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG .
+ - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+
+updateContainer:
+ extends: .updateContainerJob
+ only:
+ changes:
+ - Dockerfile
+
+ensureContainer:
+ extends: .updateContainerJob
+ allow_failure: true
+ before_script:
+ - "mkdir -p ~/.docker && echo '{\"experimental\": \"enabled\"}' > ~/.docker/config.json"
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ # Skip update container `script` if the container already exists
+ # via https://gitlab.com/gitlab-org/gitlab-ce/issues/26866#note_97609397 -> https://stackoverflow.com/a/52077071/796832
+ - docker manifest inspect $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG > /dev/null && exit || true
+
+
+.build_job:
+ image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ stage: build
+ before_script:
+ # We store this binary file in a variable as hex with this command: `xxd -p android-app.jks`
+ # Then we convert the hex back to a binary file
+ - echo "$signing_jks_file_hex" | xxd -r -p - > android-signing-keystore.jks
+ - "export VERSION_CODE=$CI_PIPELINE_IID && echo $VERSION_CODE"
+ - "export VERSION_SHA=`echo ${CI_COMMIT_SHA:0:8}` && echo $VERSION_SHA"
+ after_script:
+ - rm -f android-signing-keystore.jks || true
+ artifacts:
+ paths:
+ - app/build/outputs
+
+buildDebug:
+ extends: .build_job
+ script:
+ - bundle exec fastlane buildDebug
+
+buildRelease:
+ extends: .build_job
+ script:
+ - bundle exec fastlane buildRelease
+ environment:
+ name: production
+
+testDebug:
+ image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ stage: test
+ dependencies:
+ - buildDebug
+ script:
+ - bundle exec fastlane test
+
+publishInternal:
+ image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ stage: internal
+ dependencies:
+ - buildRelease
+ when: manual
+ before_script:
+ - echo $google_play_service_account_api_key_json > ~/google_play_api_key.json
+ after_script:
+ - rm ~/google_play_api_key.json
+ script:
+ - bundle exec fastlane internal
+
+.promote_job:
+ image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ when: manual
+ dependencies: []
+ before_script:
+ - echo $google_play_service_account_api_key_json > ~/google_play_api_key.json
+ after_script:
+ - rm ~/google_play_api_key.json
+
+promoteAlpha:
+ extends: .promote_job
+ stage: alpha
+ script:
+ - bundle exec fastlane promote_internal_to_alpha
+
+promoteBeta:
+ extends: .promote_job
+ stage: beta
+ script:
+ - bundle exec fastlane promote_alpha_to_beta
+
+promoteProduction:
+ extends: .promote_job
+ stage: production
+ # We only allow production promotion on `master` because
+ # it has its own production scoped secret variables
+ only:
+ - master
+ script:
+ - bundle exec fastlane promote_beta_to_production
+ \ No newline at end of file
diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
index 6e138639b71..c169e3f7686 100644
--- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
@@ -1,4 +1,6 @@
# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny
+# If you are interested in using Android with FastLane for publishing take a look at the Android-Fastlane template.
+
image: openjdk:8-jdk
variables:
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 75a5bf142d2..e369d26f22f 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -21,8 +21,8 @@
#
# In order to deploy, you must have a Kubernetes cluster configured either
# via a project integration, or via group/project variables.
-# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
-# level, or manually added below.
+# KUBE_INGRESS_BASE_DOMAIN must also be set on the cluster settings,
+# as a variable at the group or project level, or manually added below.
#
# Continuous deployment to production is enabled by default.
# If you want to deploy to staging first, set STAGING_ENABLED environment variable.
@@ -41,8 +41,8 @@
image: alpine:latest
variables:
- # AUTO_DEVOPS_DOMAIN is the application deployment domain and should be set as a variable at the group or project level.
- # AUTO_DEVOPS_DOMAIN: domain.example.com
+ # KUBE_INGRESS_BASE_DOMAIN is the application deployment domain and should be set as a variable at the group or project level.
+ # KUBE_INGRESS_BASE_DOMAIN: domain.example.com
POSTGRES_USER: user
POSTGRES_PASSWORD: testing-password
@@ -251,7 +251,7 @@ review:
- persist_environment_url
environment:
name: review/$CI_COMMIT_REF_NAME
- url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$AUTO_DEVOPS_DOMAIN
+ url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
on_stop: stop_review
artifacts:
paths: [environment_url.txt]
@@ -306,7 +306,7 @@ staging:
- deploy
environment:
name: staging
- url: http://$CI_PROJECT_PATH_SLUG-staging.$AUTO_DEVOPS_DOMAIN
+ url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
only:
refs:
- master
@@ -330,7 +330,7 @@ canary:
- deploy canary
environment:
name: production
- url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
when: manual
only:
refs:
@@ -354,7 +354,7 @@ canary:
- persist_environment_url
environment:
name: production
- url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
artifacts:
paths: [environment_url.txt]
@@ -403,7 +403,7 @@ production_manual:
- persist_environment_url
environment:
name: production
- url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
artifacts:
paths: [environment_url.txt]
@@ -689,7 +689,7 @@ rollout 100%:
--set application.database_url="$DATABASE_URL" \
--set application.secretName="$APPLICATION_SECRET_NAME" \
--set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
- --set service.commonName="le.$AUTO_DEVOPS_DOMAIN" \
+ --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \
--set service.url="$CI_ENVIRONMENT_URL" \
--set service.additionalHosts="$additional_hosts" \
--set replicaCount="$replicas" \
@@ -725,7 +725,7 @@ rollout 100%:
--set application.database_url="$DATABASE_URL" \
--set application.secretName="$APPLICATION_SECRET_NAME" \
--set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
- --set service.commonName="le.$AUTO_DEVOPS_DOMAIN" \
+ --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \
--set service.url="$CI_ENVIRONMENT_URL" \
--set service.additionalHosts="$additional_hosts" \
--set replicaCount="$replicas" \
@@ -823,11 +823,24 @@ rollout 100%:
kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
}
+
+ # Function to ensure backwards compatibility with AUTO_DEVOPS_DOMAIN
+ function ensure_kube_ingress_base_domain() {
+ if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ]; then
+ export KUBE_INGRESS_BASE_DOMAIN=$AUTO_DEVOPS_DOMAIN
+ fi
+ }
+
function check_kube_domain() {
- if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then
- echo "In order to deploy or use Review Apps, AUTO_DEVOPS_DOMAIN variable must be set"
- echo "You can do it in Auto DevOps project settings or defining a variable at group or project level"
+ ensure_kube_ingress_base_domain
+
+ if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ]; then
+ echo "In order to deploy or use Review Apps,"
+ echo "AUTO_DEVOPS_DOMAIN or KUBE_INGRESS_BASE_DOMAIN variables must be set"
+ echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings"
+ echo "or by defining a variable at group or project level."
echo "You can also manually add it in .gitlab-ci.yml"
+ echo "AUTO_DEVOPS_DOMAIN support will be dropped on 12.0"
false
else
true
diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb
index 1c6242b444a..e93ca3e11f8 100644
--- a/lib/gitlab/git/object_pool.rb
+++ b/lib/gitlab/git/object_pool.rb
@@ -10,12 +10,13 @@ module Gitlab
delegate :exists?, :size, to: :repository
delegate :unlink_repository, :delete, to: :object_pool_service
- attr_reader :storage, :relative_path, :source_repository
+ attr_reader :storage, :relative_path, :source_repository, :gl_project_path
- def initialize(storage, relative_path, source_repository)
+ def initialize(storage, relative_path, source_repository, gl_project_path)
@storage = storage
@relative_path = relative_path
@source_repository = source_repository
+ @gl_project_path = gl_project_path
end
def create
@@ -31,12 +32,12 @@ module Gitlab
end
def to_gitaly_repository
- Gitlab::GitalyClient::Util.repository(storage, relative_path, GL_REPOSITORY)
+ Gitlab::GitalyClient::Util.repository(storage, relative_path, GL_REPOSITORY, gl_project_path)
end
# Allows for reusing other RPCs by 'tricking' Gitaly to think its a repository
def repository
- @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY)
+ @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY, gl_project_path)
end
private
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 786c90f9272..54bbd531398 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -67,7 +67,7 @@ module Gitlab
# Relative path of repo
attr_reader :relative_path
- attr_reader :storage, :gl_repository, :relative_path
+ attr_reader :storage, :gl_repository, :relative_path, :gl_project_path
# This remote name has to be stable for all types of repositories that
# can join an object pool. If it's structure ever changes, a migration
@@ -78,10 +78,11 @@ module Gitlab
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
- def initialize(storage, relative_path, gl_repository)
+ def initialize(storage, relative_path, gl_repository, gl_project_path)
@storage = storage
@relative_path = relative_path
@gl_repository = gl_repository
+ @gl_project_path = gl_project_path
@name = @relative_path.split("/").last
end
@@ -872,7 +873,7 @@ module Gitlab
end
def gitaly_repository
- Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
+ Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository, @gl_project_path)
end
def gitaly_ref_client
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 8a1abfbf874..a7e20d9429e 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -324,13 +324,40 @@ module Gitlab
GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
end
- def search_files_by_content(ref, query)
- request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
- GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches)
+ def search_files_by_content(ref, query, chunked_response: true)
+ request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query, chunked_response: chunked_response)
+ response = GitalyClient.call(@storage, :repository_service, :search_files_by_content, request)
+
+ search_results_from_response(response)
end
private
+ def search_results_from_response(gitaly_response)
+ matches = []
+ current_match = +""
+
+ gitaly_response.each do |message|
+ next if message.nil?
+
+ # Old client will ignore :chunked_response flag
+ # and return messages with `matches` key.
+ # This code path will be removed post 12.0 release
+ if message.matches.any?
+ matches += message.matches
+ else
+ current_match << message.match_data
+
+ if message.end_of_match
+ matches << current_match
+ current_match = +""
+ end
+ end
+ end
+
+ matches
+ end
+
def gitaly_fetch_stream_to_file(save_path, rpc_name, request_class, timeout)
request = request_class.new(repository: @gitaly_repo)
response = GitalyClient.call(
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index dce5d6a8ad0..899921f76e4 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -4,7 +4,7 @@ module Gitlab
module GitalyClient
module Util
class << self
- def repository(repository_storage, relative_path, gl_repository)
+ def repository(repository_storage, relative_path, gl_repository, gl_project_path)
git_env = Gitlab::Git::HookEnv.all(gl_repository)
git_object_directory = git_env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence
git_alternate_object_directories = Array.wrap(git_env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'])
@@ -14,14 +14,16 @@ module Gitlab
relative_path: relative_path,
gl_repository: gl_repository.to_s,
git_object_directory: git_object_directory.to_s,
- git_alternate_object_directories: git_alternate_object_directories
+ git_alternate_object_directories: git_alternate_object_directories,
+ gl_project_path: gl_project_path
)
end
def git_repository(gitaly_repository)
Gitlab::Git::Repository.new(gitaly_repository.storage_name,
gitaly_repository.relative_path,
- gitaly_repository.gl_repository)
+ gitaly_repository.gl_repository,
+ gitaly_repository.gl_project_path)
end
end
end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index bc3ea9e9226..e2dfb00dcc5 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -6,11 +6,12 @@ module Gitlab
class RepositoryImporter
include Gitlab::ShellAdapter
- attr_reader :project, :client
+ attr_reader :project, :client, :wiki_formatter
def initialize(project, client)
@project = project
@client = client
+ @wiki_formatter = ::Gitlab::LegacyGithubImport::WikiFormatter.new(project)
end
# Returns true if we should import the wiki for the project.
@@ -57,9 +58,7 @@ module Gitlab
end
def import_wiki_repository
- wiki_path = "#{project.disk_path}.wiki"
-
- gitlab_shell.import_repository(project.repository_storage, wiki_path, wiki_url)
+ gitlab_shell.import_wiki_repository(project, wiki_formatter)
true
rescue Gitlab::Shell::Error => e
@@ -72,7 +71,7 @@ module Gitlab
end
def wiki_url
- project.import_url.sub(/\.git\z/, '.wiki.git')
+ wiki_formatter.import_url
end
def update_clone_time
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 9b1794eec91..3235d3ccc4e 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -24,6 +24,7 @@ module Gitlab
gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites')
gon.test_env = Rails.env.test?
gon.suggested_label_colors = LabelsHelper.suggested_colors
+ gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 4fbb87385c3..5ff415b6126 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -88,9 +88,10 @@ module Gitlab
def create_cached_signature!
using_keychain do |gpg_key|
- signature = GpgSignature.new(attributes(gpg_key))
- signature.save! unless Gitlab::Database.read_only?
- signature
+ attributes = attributes(gpg_key)
+ break GpgSignature.new(attributes) if Gitlab::Database.read_only?
+
+ GpgSignature.safe_create!(attributes)
end
end
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index c526d31a591..f3323c98af2 100644
--- a/lib/gitlab/legacy_github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -267,7 +267,7 @@ module Gitlab
def import_wiki
unless project.wiki.repository_exists?
wiki = WikiFormatter.new(project)
- gitlab_shell.import_repository(project.repository_storage, wiki.disk_path, wiki.import_url)
+ gitlab_shell.import_wiki_repository(project, wiki)
end
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
diff --git a/lib/gitlab/legacy_github_import/wiki_formatter.rb b/lib/gitlab/legacy_github_import/wiki_formatter.rb
index ea52be5ee0f..cf1e21ad1e1 100644
--- a/lib/gitlab/legacy_github_import/wiki_formatter.rb
+++ b/lib/gitlab/legacy_github_import/wiki_formatter.rb
@@ -13,6 +13,10 @@ module Gitlab
project.wiki.disk_path
end
+ def full_path
+ project.wiki.full_path
+ end
+
def import_url
project.import_url.sub(/\.git\z/, ".wiki.git")
end
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index 4c4ec026823..4c5b849cc51 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -23,13 +23,13 @@ module Gitlab
def sample
Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active)
- unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued)
+ unicorn_active_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.active)
+ unicorn_queued_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.queued)
end
Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active)
- unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued)
+ unicorn_active_connections.set({ socket_type: 'unix', socket_address: addr }, stats.active)
+ unicorn_queued_connections.set({ socket_type: 'unix', socket_address: addr }, stats.queued)
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index bdf21cf3134..1153e69d3de 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -64,27 +64,48 @@ module Gitlab
end
end
+ # Convenience methods for initializing a new repository with a Project model.
+ def create_project_repository(project)
+ create_repository(project.repository_storage, project.disk_path, project.full_path)
+ end
+
+ def create_wiki_repository(project)
+ create_repository(project.repository_storage, project.wiki.disk_path, project.wiki.full_path)
+ end
+
# Init new repository
#
# storage - the shard key
- # name - project disk path
+ # disk_path - project disk path
+ # gl_project_path - project name
#
# Ex.
- # create_repository("default", "gitlab/gitlab-ci")
+ # create_repository("default", "path/to/gitlab-ci", "gitlab/gitlab-ci")
#
- def create_repository(storage, name)
- relative_path = name.dup
+ def create_repository(storage, disk_path, gl_project_path)
+ relative_path = disk_path.dup
relative_path << '.git' unless relative_path.end_with?('.git')
- repository = Gitlab::Git::Repository.new(storage, relative_path, '')
+ # During creation of a repository, gl_repository may not be known
+ # because that depends on a yet-to-be assigned project ID in the
+ # database (e.g. project-1234), so for now it is blank.
+ repository = Gitlab::Git::Repository.new(storage, relative_path, '', gl_project_path)
wrapped_gitaly_errors { repository.gitaly_repository_client.create_repository }
true
rescue => err # Once the Rugged codes gets removes this can be improved
- Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}")
+ Rails.logger.error("Failed to add repository #{storage}/#{disk_path}: #{err}")
false
end
+ def import_wiki_repository(project, wiki_formatter)
+ import_repository(project.repository_storage, wiki_formatter.disk_path, wiki_formatter.import_url, project.wiki.full_path)
+ end
+
+ def import_project_repository(project)
+ import_repository(project.repository_storage, project.disk_path, project.import_url, project.full_path)
+ end
+
# Import repository
#
# storage - project's storage name
@@ -94,13 +115,13 @@ module Gitlab
# Ex.
# import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git")
#
- def import_repository(storage, name, url)
+ def import_repository(storage, name, url, gl_project_path)
if url.start_with?('.', '/')
raise Error.new("don't use disk paths with import_repository: #{url.inspect}")
end
relative_path = "#{name}.git"
- cmd = GitalyGitlabProjects.new(storage, relative_path)
+ cmd = GitalyGitlabProjects.new(storage, relative_path, gl_project_path)
success = cmd.import_project(url, git_timeout)
raise Error, cmd.output unless success
@@ -125,18 +146,13 @@ module Gitlab
end
# Fork repository to new path
- # forked_from_storage - forked-from project's storage name
- # forked_from_disk_path - project disk relative path
- # forked_to_storage - forked-to project's storage name
- # forked_to_disk_path - forked project disk relative path
- #
- # Ex.
- # fork_repository("nfs-file06", "gitlab/gitlab-ci", "nfs-file07", "new-namespace/gitlab-ci")
- def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path)
- forked_from_relative_path = "#{forked_from_disk_path}.git"
- fork_args = [forked_to_storage, "#{forked_to_disk_path}.git"]
+ # source_project - forked-from Project
+ # target_project - forked-to Project
+ def fork_repository(source_project, target_project)
+ forked_from_relative_path = "#{source_project.disk_path}.git"
+ fork_args = [target_project.repository_storage, "#{target_project.disk_path}.git", target_project.full_path]
- GitalyGitlabProjects.new(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args)
+ GitalyGitlabProjects.new(source_project.repository_storage, forked_from_relative_path, source_project.full_path).fork_repository(*fork_args)
end
# Removes a repository from file system, using rm_diretory which is an alias
@@ -397,16 +413,17 @@ module Gitlab
end
class GitalyGitlabProjects
- attr_reader :shard_name, :repository_relative_path, :output
+ attr_reader :shard_name, :repository_relative_path, :output, :gl_project_path
- def initialize(shard_name, repository_relative_path)
+ def initialize(shard_name, repository_relative_path, gl_project_path)
@shard_name = shard_name
@repository_relative_path = repository_relative_path
@output = ''
+ @gl_project_path = gl_project_path
end
def import_project(source, _timeout)
- raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
+ raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil, gl_project_path)
Gitlab::GitalyClient::RepositoryService.new(raw_repository).import_repository(source)
true
@@ -415,9 +432,9 @@ module Gitlab
false
end
- def fork_repository(new_shard_name, new_repository_relative_path)
- target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil)
- raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
+ def fork_repository(new_shard_name, new_repository_relative_path, new_project_name)
+ target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil, new_project_name)
+ raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil, gl_project_path)
Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository)
rescue GRPC::BadStatus => e
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9ec590f90d8..937db5b7305 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -22,16 +22,6 @@ msgstr ""
msgid " or "
msgstr ""
-msgid "%d addition"
-msgid_plural "%d additions"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "%d changed file"
-msgid_plural "%d changed files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] ""
@@ -42,10 +32,8 @@ msgid_plural "%d commits behind"
msgstr[0] ""
msgstr[1] ""
-msgid "%d deleted"
-msgid_plural "%d deletions"
-msgstr[0] ""
-msgstr[1] ""
+msgid "%d commits"
+msgstr ""
msgid "%d exporter"
msgid_plural "%d exporters"
@@ -138,9 +126,6 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
-msgid "%{nip_domain} can be used as an alternative to a custom domain."
-msgstr ""
-
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -900,15 +885,6 @@ msgstr ""
msgid "Auto DevOps, runners and job artifacts"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
-msgstr ""
-
-msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly."
-msgstr ""
-
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
-
msgid "Auto-cancel redundant, pending pipelines"
msgstr ""
@@ -1281,9 +1257,6 @@ msgstr ""
msgid "CICD|Deployment strategy"
msgstr ""
-msgid "CICD|Deployment strategy needs a domain name to work correctly."
-msgstr ""
-
msgid "CICD|Jobs"
msgstr ""
@@ -1293,7 +1266,7 @@ msgstr ""
msgid "CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found."
msgstr ""
-msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
+msgid "CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly."
msgstr ""
msgid "CICD|instance enabled"
@@ -1566,6 +1539,12 @@ msgstr ""
msgid "Closed (moved)"
msgstr ""
+msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
+msgstr ""
+
+msgid "ClusterIntegration| can be used instead of a custom domain."
+msgstr ""
+
msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}"
msgstr ""
@@ -1596,6 +1575,9 @@ msgstr ""
msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}"
msgstr ""
+msgid "ClusterIntegration|Alternatively"
+msgstr ""
+
msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
msgstr ""
@@ -1617,6 +1599,9 @@ msgstr ""
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
msgstr ""
+msgid "ClusterIntegration|Base domain"
+msgstr ""
+
msgid "ClusterIntegration|CA Certificate"
msgstr ""
@@ -1941,6 +1926,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
+msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
+msgstr ""
+
msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
msgstr ""
@@ -2444,6 +2432,9 @@ msgstr ""
msgid "Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import."
msgstr ""
+msgid "Customize language and region related settings."
+msgstr ""
+
msgid "Customize your pipeline configuration, view your pipeline status and coverage report."
msgstr ""
@@ -2507,6 +2498,12 @@ msgstr ""
msgid "Default Branch"
msgstr ""
+msgid "Default first day of the week"
+msgstr ""
+
+msgid "Default first day of the week in calendars and date pickers."
+msgstr ""
+
msgid "Default: Directly import the Google Code email address or username"
msgstr ""
@@ -2546,6 +2543,9 @@ msgstr ""
msgid "Delete list"
msgstr ""
+msgid "Delete source branch"
+msgstr ""
+
msgid "Delete this attachment"
msgstr ""
@@ -3271,6 +3271,9 @@ msgstr ""
msgid "Failure"
msgstr ""
+msgid "Fast-forward merge without a merge commit"
+msgstr ""
+
msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
@@ -3283,6 +3286,11 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
+msgid "File"
+msgid_plural "Files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "File added"
msgstr ""
@@ -3355,6 +3363,9 @@ msgstr ""
msgid "Finished"
msgstr ""
+msgid "First day of the week"
+msgstr ""
+
msgid "FirstPushedBy|First"
msgstr ""
@@ -3915,6 +3926,9 @@ msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
+msgid "Include merge request description"
+msgstr ""
+
msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>."
msgstr ""
@@ -4297,6 +4311,9 @@ msgstr ""
msgid "Loading…"
msgstr ""
+msgid "Localization"
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -4447,9 +4464,18 @@ msgstr ""
msgid "Merge Requests"
msgstr ""
+msgid "Merge commit message"
+msgstr ""
+
msgid "Merge events"
msgstr ""
+msgid "Merge immediately"
+msgstr ""
+
+msgid "Merge in progress"
+msgstr ""
+
msgid "Merge request"
msgstr ""
@@ -4459,6 +4485,9 @@ msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
+msgid "Merge when pipeline succeeds"
+msgstr ""
+
msgid "MergeRequests|Add a reply"
msgstr ""
@@ -4615,6 +4644,15 @@ msgstr ""
msgid "Modal|Close"
msgstr ""
+msgid "Modify commit messages"
+msgstr ""
+
+msgid "Modify merge commit"
+msgstr ""
+
+msgid "Monday"
+msgstr ""
+
msgid "Monitor your errors by integrating with Sentry"
msgstr ""
@@ -6605,7 +6643,10 @@ msgstr ""
msgid "Snippets"
msgstr ""
-msgid "Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again."
+msgid "Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again."
+msgstr ""
+
+msgid "Someone edited this merge request at the same time you did. Please refresh the page to see changes."
msgstr ""
msgid "Something went wrong on our end"
@@ -6632,6 +6673,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
+msgid "Something went wrong while deleting the source branch. Please try again."
+msgstr ""
+
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
@@ -6644,6 +6688,9 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while merging this merge request. Please try again."
+msgstr ""
+
msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr ""
@@ -6788,6 +6835,9 @@ msgstr ""
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Squash commit message"
+msgstr ""
+
msgid "Squash commits"
msgstr ""
@@ -6926,6 +6976,9 @@ msgstr ""
msgid "Suggested change"
msgstr ""
+msgid "Sunday"
+msgstr ""
+
msgid "Support for custom certificates is disabled. Ask your system's administrator to enable it."
msgstr ""
@@ -6938,6 +6991,9 @@ msgstr ""
msgid "System Info"
msgstr ""
+msgid "System default (%{default})"
+msgstr ""
+
msgid "System metrics (Custom)"
msgstr ""
@@ -7977,6 +8033,9 @@ msgstr ""
msgid "Various email settings."
msgstr ""
+msgid "Various localization settings."
+msgstr ""
+
msgid "Various settings that affect GitLab performance."
msgstr ""
@@ -8304,6 +8363,9 @@ msgstr ""
msgid "You can only edit files when you are on a branch"
msgstr ""
+msgid "You can only merge once the items above are resolved"
+msgstr ""
+
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
@@ -8598,6 +8660,12 @@ msgstr[1] ""
msgid "missing"
msgstr ""
+msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
+msgstr ""
+
+msgid "mrWidgetCommitsAdded|1 merge commit"
+msgstr ""
+
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr ""
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 12c2409a5a7..2de39b8ebf5 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -13,9 +13,7 @@ module QA # rubocop:disable Naming/FileName
view 'app/views/projects/settings/ci_cd/_autodevops_form.html.haml' do
element :enable_auto_devops_field, 'check_box :enabled' # rubocop:disable QA/ElementWithPattern
- element :domain_field, 'text_field :domain' # rubocop:disable QA/ElementWithPattern
element :enable_auto_devops_button, "%strong= s_('CICD|Default to Auto DevOps pipeline')" # rubocop:disable QA/ElementWithPattern
- element :domain_input, "%strong= _('Domain')" # rubocop:disable QA/ElementWithPattern
element :save_changes_button, "submit _('Save changes')" # rubocop:disable QA/ElementWithPattern
end
@@ -31,10 +29,9 @@ module QA # rubocop:disable Naming/FileName
end
end
- def enable_auto_devops_with_domain(domain)
+ def enable_auto_devops
expand_section(:autodevops_settings) do
check 'Default to Auto DevOps pipeline'
- fill_in 'Domain', with: domain
click_on 'Save changes'
end
end
diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb
index d67e5f6da20..986b31da528 100644
--- a/qa/qa/resource/kubernetes_cluster.rb
+++ b/qa/qa/resource/kubernetes_cluster.rb
@@ -6,12 +6,16 @@ module QA
module Resource
class KubernetesCluster < Base
attr_writer :project, :cluster,
- :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
+ :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain
attribute :ingress_ip do
Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
end
+ attribute :domain do
+ "#{ingress_ip}.nip.io"
+ end
+
def fabricate!
@project.visit!
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index b0ff83db86b..5c8ec465143 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -52,13 +52,13 @@ module QA
end
kubernetes_cluster.populate(:ingress_ip)
-
@project.visit!
Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Settings::CICD.perform do |p|
- p.enable_auto_devops_with_domain(
- "#{kubernetes_cluster.ingress_ip}.nip.io")
+ p.enable_auto_devops
end
+
+ kubernetes_cluster.populate(:domain)
end
after(:all) do
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 0f28499194e..360030102e0 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -429,12 +429,14 @@ describe Groups::ClustersController do
end
let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
+ let(:domain) { 'test-domain.com' }
let(:params) do
{
cluster: {
enabled: false,
- name: 'my-new-cluster-name'
+ name: 'my-new-cluster-name',
+ base_domain: domain
}
}
end
@@ -447,6 +449,20 @@ describe Groups::ClustersController do
expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.domain).to eq('test-domain.com')
+ end
+
+ context 'when domain is invalid' do
+ let(:domain) { 'not-a-valid-domain' }
+
+ it 'should not update cluster attributes' do
+ go
+
+ cluster.reload
+ expect(response).to render_template(:show)
+ expect(cluster.name).not_to eq('my-new-cluster-name')
+ expect(cluster.domain).not_to eq('test-domain.com')
+ end
end
context 'when format is json' do
@@ -456,7 +472,8 @@ describe Groups::ClustersController do
{
cluster: {
enabled: false,
- name: 'my-new-cluster-name'
+ name: 'my-new-cluster-name',
+ domain: domain
}
}
end
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index 012f016b091..760c0fab130 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -42,7 +42,8 @@ describe Profiles::PreferencesController do
prefs = {
color_scheme_id: '1',
dashboard: 'stars',
- theme_id: '2'
+ theme_id: '2',
+ first_day_of_week: '1'
}.with_indifferent_access
expect(user).to receive(:assign_attributes).with(ActionController::Parameters.new(prefs).permit!)
diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb
index 6464398cea1..844c61f1ace 100644
--- a/spec/controllers/projects/error_tracking_controller_spec.rb
+++ b/spec/controllers/projects/error_tracking_controller_spec.rb
@@ -107,8 +107,11 @@ describe Projects::ErrorTrackingController do
let(:http_status) { :no_content }
before do
- expect(list_issues_service).to receive(:execute)
- .and_return(status: :error, message: error_message, http_status: http_status)
+ expect(list_issues_service).to receive(:execute).and_return(
+ status: :error,
+ message: error_message,
+ http_status: http_status
+ )
end
it 'returns http_status with message' do
@@ -122,6 +125,113 @@ describe Projects::ErrorTrackingController do
end
end
+ describe 'POST #list_projects' do
+ context 'with insufficient permissions' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns 404' do
+ post :list_projects, params: list_projects_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to sign-in page' do
+ post :list_projects, params: list_projects_params
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with authorized user' do
+ let(:list_projects_service) { spy(:list_projects_service) }
+ let(:sentry_project) { build(:error_tracking_project) }
+
+ let(:permitted_params) do
+ ActionController::Parameters.new(
+ list_projects_params[:error_tracking_setting]
+ ).permit!
+ end
+
+ before do
+ allow(ErrorTracking::ListProjectsService)
+ .to receive(:new).with(project, user, permitted_params)
+ .and_return(list_projects_service)
+ end
+
+ context 'service result is successful' do
+ before do
+ expect(list_projects_service).to receive(:execute)
+ .and_return(status: :success, projects: [sentry_project])
+ end
+
+ it 'returns a list of projects' do
+ post :list_projects, params: list_projects_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('error_tracking/list_projects')
+ expect(json_response['projects']).to eq([sentry_project].as_json)
+ end
+ end
+
+ context 'service result is erroneous' do
+ let(:error_message) { 'error message' }
+
+ context 'without http_status' do
+ before do
+ expect(list_projects_service).to receive(:execute)
+ .and_return(status: :error, message: error_message)
+ end
+
+ it 'returns 400 with message' do
+ get :list_projects, params: list_projects_params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+
+ context 'with explicit http_status' do
+ let(:http_status) { :no_content }
+
+ before do
+ expect(list_projects_service).to receive(:execute).and_return(
+ status: :error,
+ message: error_message,
+ http_status: http_status
+ )
+ end
+
+ it 'returns http_status with message' do
+ get :list_projects, params: list_projects_params
+
+ expect(response).to have_gitlab_http_status(http_status)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+ end
+ end
+
+ private
+
+ def list_projects_params(opts = {})
+ project_params(
+ format: :json,
+ error_tracking_setting: {
+ api_host: 'gitlab.com',
+ token: 'token'
+ }
+ )
+ end
+ end
+
private
def project_params(opts = {})
diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb
new file mode 100644
index 00000000000..0a9c4bcaf12
--- /dev/null
+++ b/spec/features/clusters/cluster_detail_page_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Clusterable > Show page' do
+ let(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ shared_examples 'editing domain' do
+ before do
+ clusterable.add_maintainer(current_user)
+ end
+
+ it 'allow the user to set domain' do
+ visit cluster_path
+
+ within '#cluster-integration' do
+ fill_in('cluster_base_domain', with: 'test.com')
+ click_on 'Save changes'
+ end
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
+ end
+
+ context 'when there is a cluster with ingress and external ip' do
+ before do
+ cluster.create_application_ingress!(external_ip: '192.168.1.100')
+
+ visit cluster_path
+ end
+
+ it 'shows help text with the domain as an alternative to custom domain' do
+ within '#cluster-integration' do
+ expect(page).to have_content('Alternatively 192.168.1.100.nip.io can be used instead of a custom domain')
+ end
+ end
+ end
+
+ context 'when there is no ingress' do
+ it 'alternative to custom domain is not shown' do
+ visit cluster_path
+
+ within '#cluster-integration' do
+ expect(page).not_to have_content('can be used instead of a custom domain.')
+ end
+ end
+ end
+ end
+
+ context 'when clusterable is a project' do
+ it_behaves_like 'editing domain' do
+ let(:clusterable) { create(:project) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
+ let(:cluster_path) { project_cluster_path(clusterable, cluster) }
+ end
+ end
+
+ context 'when clusterable is a group' do
+ it_behaves_like 'editing domain' do
+ let(:clusterable) { create(:group) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
+ let(:cluster_path) { group_cluster_path(clusterable, cluster) }
+ end
+ end
+end
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 16754035076..60ddb02da2c 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -843,6 +843,7 @@ describe 'Copy as GFM', :js do
def verify(selector, gfm, target: nil)
html = html_for_selector(selector)
output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
+ wait_for_requests
expect(output_gfm.strip).to eq(gfm.strip)
end
end
@@ -861,6 +862,9 @@ describe 'Copy as GFM', :js do
def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<~JS
(function(html) {
+ // Setting it off so the import already starts
+ window.CopyAsGFM.nodeToGFM(document.createElement('div'));
+
var transformer = window.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div');
@@ -875,9 +879,18 @@ describe 'Copy as GFM', :js do
node = transformer(node, target);
if (!node) return null;
- return window.CopyAsGFM.nodeToGFM(node);
+
+ window.gfmCopytestRes = null;
+ window.CopyAsGFM.nodeToGFM(node)
+ .then((res) => {
+ window.gfmCopytestRes = res;
+ });
})("#{escape_javascript(html)}")
JS
- page.evaluate_script(js)
+ page.execute_script(js)
+
+ loop until page.evaluate_script('window.gfmCopytestRes !== null')
+
+ page.evaluate_script('window.gfmCopytestRes')
end
end
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index 00ac7c72a11..5fa23dbb998 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -80,8 +80,8 @@ describe 'User accepts a merge request', :js do
end
it 'accepts a merge request' do
- click_button('Modify commit message')
- fill_in('Commit message', with: 'wow such merge')
+ find('.js-mr-widget-commits-count').click
+ fill_in('merge-message-edit', with: 'wow such merge')
click_button('Merge')
diff --git a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
index 8d2d4279d3c..c6b11fce388 100644
--- a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
+++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
@@ -13,7 +13,7 @@ describe 'Merge request < User customizes merge commit message', :js do
description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}"
)
end
- let(:textbox) { page.find(:css, '.js-commit-message', visible: false) }
+ let(:textbox) { page.find(:css, '#merge-message-edit', visible: false) }
let(:default_message) do
[
"Merge branch 'feature' into 'master'",
@@ -38,16 +38,16 @@ describe 'Merge request < User customizes merge commit message', :js do
end
it 'toggles commit message between message with description and without description' do
- expect(page).not_to have_selector('.js-commit-message')
- click_button "Modify commit message"
+ expect(page).not_to have_selector('#merge-message-edit')
+ first('.js-mr-widget-commits-count').click
expect(textbox).to be_visible
expect(textbox.value).to eq(default_message)
- click_link "Include description in commit message"
+ check('Include merge request description')
expect(textbox.value).to eq(message_with_description)
- click_link "Don't include description in commit message"
+ uncheck('Include merge request description')
expect(textbox.value).to eq(default_message)
end
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 50c723776a3..16c058ab6bd 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -37,6 +37,8 @@ describe 'Merge request > User resolves conflicts', :js do
click_on 'Changes'
wait_for_requests
+ find('.js-toggle-tree-list').click
+
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 }")
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 63d8decc2d2..aa91ade46ca 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -42,7 +42,7 @@ describe 'Merge request > User sees versions', :js do
expect(page).to have_content 'latest version'
end
- expect(page).to have_content '8 changed files'
+ expect(page).to have_content '8 Files'
end
it_behaves_like 'allows commenting',
@@ -76,7 +76,7 @@ describe 'Merge request > User sees versions', :js do
end
it 'shows comments that were last relevant at that version' do
- expect(page).to have_content '5 changed files'
+ expect(page).to have_content '5 Files'
position = Gitlab::Diff::Position.new(
old_path: ".gitmodules",
@@ -120,8 +120,15 @@ describe 'Merge request > User sees versions', :js do
diff_id: merge_request_diff3.id,
start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
)
- expect(page).to have_content '4 changed files'
- expect(page).to have_content '15 additions 6 deletions'
+ expect(page).to have_content '4 Files'
+
+ additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition')
+ .ancestor('.diff-stats-group').text
+ deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion')
+ .ancestor('.diff-stats-group').text
+
+ expect(additions_content).to eq '15'
+ expect(deletions_content).to eq '6'
position = Gitlab::Diff::Position.new(
old_path: ".gitmodules",
@@ -141,8 +148,14 @@ describe 'Merge request > User sees versions', :js do
end
it 'show diff between new and old version' do
- expect(page).to have_content '4 changed files'
- expect(page).to have_content '15 additions 6 deletions'
+ additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition')
+ .ancestor('.diff-stats-group').text
+ deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion')
+ .ancestor('.diff-stats-group').text
+
+ expect(page).to have_content '4 Files'
+ expect(additions_content).to eq '15'
+ expect(deletions_content).to eq '6'
end
it 'returns to latest version when "Show latest version" button is clicked' do
@@ -150,7 +163,7 @@ describe 'Merge request > User sees versions', :js do
page.within '.mr-version-dropdown' do
expect(page).to have_content 'latest version'
end
- expect(page).to have_content '8 changed files'
+ expect(page).to have_content '8 Files'
end
it_behaves_like 'allows commenting',
@@ -176,7 +189,7 @@ describe 'Merge request > User sees versions', :js do
find('.btn-default').click
click_link 'version 1'
end
- expect(page).to have_content '0 changed files'
+ expect(page).to have_content '0 Files'
end
end
@@ -202,7 +215,7 @@ describe 'Merge request > User sees versions', :js do
expect(page).to have_content 'version 1'
end
- expect(page).to have_content '0 changed files'
+ expect(page).to have_content '0 Files'
end
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 6f8ec0015ad..4c85abe9971 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -98,14 +98,12 @@ describe "Projects > Settings > Pipelines settings" do
expect(page).not_to have_content('instance enabled')
expect(find_field('project_auto_devops_attributes_enabled')).not_to be_checked
check 'Default to Auto DevOps pipeline'
- fill_in('project_auto_devops_attributes_domain', with: 'test.com')
click_on 'Save changes'
end
expect(page.status_code).to eq(200)
expect(project.auto_devops).to be_present
expect(project.auto_devops).to be_enabled
- expect(project.auto_devops.domain).to eq('test.com')
page.within '#autodevops-settings' do
expect(find_field('project_auto_devops_attributes_enabled')).to be_checked
@@ -113,29 +111,6 @@ describe "Projects > Settings > Pipelines settings" do
end
end
end
-
- context 'when there is a cluster with ingress and external_ip' do
- before do
- cluster = create(:cluster, projects: [project])
- cluster.create_application_ingress!(external_ip: '192.168.1.100')
- end
-
- it 'shows the help text with the nip.io domain as an alternative to custom domain' do
- visit project_settings_ci_cd_path(project)
- expect(page).to have_content('192.168.1.100.nip.io can be used as an alternative to a custom domain')
- end
- end
-
- context 'when there is no ingress' do
- before do
- create(:cluster, projects: [project])
- end
-
- it 'alternative to custom domain is not shown' do
- visit project_settings_ci_cd_path(project)
- expect(page).not_to have_content('can be used as an alternative to a custom domain')
- end
- end
end
describe 'runners registration token' do
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index eb974c7c7fd..74fdfcf492e 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -79,14 +79,6 @@ describe 'Snippet', :js do
expect(page).not_to have_xpath("//ol//li//ul")
end
end
-
- context 'with cached CommonMark html' do
- let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
-
- it 'renders correctly' do
- expect(page).not_to have_xpath("//ol//li//ul")
- end
- end
end
context 'switching to the simple viewer' do
diff --git a/spec/fixtures/api/schemas/public_api/v4/group_labels.json b/spec/fixtures/api/schemas/public_api/v4/group_labels.json
new file mode 100644
index 00000000000..f6c327abfdd
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/group_labels.json
@@ -0,0 +1,18 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties" : {
+ "id" : { "type": "integer" },
+ "name" : { "type": "string "},
+ "color" : { "type": "string "},
+ "description" : { "type": "string "},
+ "open_issues_count" : { "type": "integer "},
+ "closed_issues_count" : { "type": "integer "},
+ "open_merge_requests_count" : { "type": "integer "},
+ "subscribed" : { "type": "boolean" },
+ "priority" : { "type": "null" }
+ },
+ "additionalProperties": false
+ }
+}
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index 75c30dbfe48..223e562238d 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -90,39 +90,4 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
end
-
- describe '.auto_devops_warning_message' do
- subject { helper.auto_devops_warning_message(project) }
-
- context 'when the service is missing' do
- before do
- allow(helper).to receive(:missing_auto_devops_service?).and_return(true)
- end
-
- context 'when the domain is missing' do
- before do
- allow(helper).to receive(:missing_auto_devops_domain?).and_return(true)
- end
-
- it { is_expected.to match(/Auto Review Apps and Auto Deploy need a domain name and a .* to work correctly./) }
- end
-
- context 'when the domain is not missing' do
- before do
- allow(helper).to receive(:missing_auto_devops_domain?).and_return(false)
- end
-
- it { is_expected.to match(/Auto Review Apps and Auto Deploy need a .* to work correctly./) }
- end
- end
-
- context 'when the domain is missing' do
- before do
- allow(helper).to receive(:missing_auto_devops_service?).and_return(false)
- allow(helper).to receive(:missing_auto_devops_domain?).and_return(true)
- end
-
- it { is_expected.to eq('Auto Review Apps and Auto Deploy need a domain name to work correctly.') }
- end
- end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index c112c8ed633..4c395248644 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -35,6 +35,30 @@ describe PreferencesHelper do
end
end
+ describe '#first_day_of_week_choices' do
+ it 'returns Sunday and Monday as choices' do
+ expect(helper.first_day_of_week_choices).to eq [
+ ['Sunday', 0],
+ ['Monday', 1]
+ ]
+ end
+ end
+
+ describe '#first_day_of_week_choices_with_default' do
+ it 'returns choices including system default' do
+ expect(helper.first_day_of_week_choices_with_default).to eq [
+ ['System default (Sunday)', nil], ['Sunday', 0], ['Monday', 1]
+ ]
+ end
+
+ it 'returns choices including system default set to Monday' do
+ stub_application_setting(first_day_of_week: 1)
+ expect(helper.first_day_of_week_choices_with_default).to eq [
+ ['System default (Monday)', nil], ['Sunday', 0], ['Monday', 1]
+ ]
+ end
+ end
+
describe '#user_application_theme' do
context 'with a user' do
it "returns user's theme's css_class" do
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index 6179a02ce16..ca849f75860 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -1,4 +1,4 @@
-import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
+import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
@@ -79,27 +79,46 @@ describe('CopyAsGFM', () => {
return clipboardData;
};
+ beforeAll(done => {
+ initCopyAsGFM();
+
+ // Fake call to nodeToGfm so the import of lazy bundle happened
+ CopyAsGFM.nodeToGFM(document.createElement('div'))
+ .then(() => {
+ done();
+ })
+ .catch(done.fail);
+ });
+
beforeEach(() => spyOn(clipboardData, 'setData'));
describe('list handling', () => {
- it('uses correct gfm for unordered lists', () => {
+ it('uses correct gfm for unordered lists', done => {
const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL');
+
spyOn(window, 'getSelection').and.returnValue(selection);
simulateCopy();
- const expectedGFM = '* List Item1\n\n* List Item2';
+ setTimeout(() => {
+ const expectedGFM = '* List Item1\n\n* List Item2';
- expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ done();
+ });
});
- it('uses correct gfm for ordered lists', () => {
+ it('uses correct gfm for ordered lists', done => {
const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL');
+
spyOn(window, 'getSelection').and.returnValue(selection);
simulateCopy();
- const expectedGFM = '1. List Item1\n\n1. List Item2';
+ setTimeout(() => {
+ const expectedGFM = '1. List Item1\n\n1. List Item2';
- expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
index fe827bb1e18..4843a0386b5 100644
--- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -3,17 +3,26 @@
*/
import $ from 'jquery';
-import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm';
+import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-initCopyAsGFM();
-
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
describe('ShortcutsIssuable', function() {
const fixtureName = 'snippets/show.html.raw';
preloadFixtures(fixtureName);
+ beforeAll(done => {
+ initCopyAsGFM();
+
+ // Fake call to nodeToGfm so the import of lazy bundle happened
+ CopyAsGFM.nodeToGFM(document.createElement('div'))
+ .then(() => {
+ done();
+ })
+ .catch(done.fail);
+ });
+
beforeEach(() => {
loadFixtures(fixtureName);
$('body').append(
@@ -63,17 +72,22 @@ describe('ShortcutsIssuable', function() {
stubSelection('<p>Selected text.</p>');
});
- it('leaves existing input intact', () => {
+ it('leaves existing input intact', done => {
$(FORM_SELECTOR).val('This text was already here.');
expect($(FORM_SELECTOR).val()).toBe('This text was already here.');
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
+ setTimeout(() => {
+ expect($(FORM_SELECTOR).val()).toBe(
+ 'This text was already here.\n\n> Selected text.\n\n',
+ );
+ done();
+ });
});
- it('triggers `input`', () => {
+ it('triggers `input`', done => {
let triggered = false;
$(FORM_SELECTOR).on('input', () => {
triggered = true;
@@ -81,36 +95,48 @@ describe('ShortcutsIssuable', function() {
ShortcutsIssuable.replyWithSelectedText(true);
- expect(triggered).toBe(true);
+ setTimeout(() => {
+ expect(triggered).toBe(true);
+ done();
+ });
});
- it('triggers `focus`', () => {
+ it('triggers `focus`', done => {
const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- expect(spy).toHaveBeenCalled();
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled();
+ done();
+ });
});
});
describe('with a one-line selection', () => {
- it('quotes the selection', () => {
+ it('quotes the selection', done => {
stubSelection('<p>This text has been selected.</p>');
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
+ setTimeout(() => {
+ expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
+ done();
+ });
});
});
describe('with a multi-line selection', () => {
- it('quotes the selected lines as a group', () => {
+ it('quotes the selected lines as a group', done => {
stubSelection(
'<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>',
);
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe(
- '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
- );
+ setTimeout(() => {
+ expect($(FORM_SELECTOR).val()).toBe(
+ '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
+ );
+ done();
+ });
});
});
@@ -119,17 +145,23 @@ describe('ShortcutsIssuable', function() {
stubSelection('<p>Selected text.</p>', true);
});
- it('does not add anything to the input', () => {
+ it('does not add anything to the input', done => {
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe('');
+ setTimeout(() => {
+ expect($(FORM_SELECTOR).val()).toBe('');
+ done();
+ });
});
- it('triggers `focus`', () => {
+ it('triggers `focus`', done => {
const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- expect(spy).toHaveBeenCalled();
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled();
+ done();
+ });
});
});
@@ -138,20 +170,26 @@ describe('ShortcutsIssuable', function() {
stubSelection('<div class="md">Selected text.</div><p>Invalid selected text.</p>', true);
});
- it('only adds the valid part to the input', () => {
+ it('only adds the valid part to the input', done => {
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n');
+ setTimeout(() => {
+ expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n');
+ done();
+ });
});
- it('triggers `focus`', () => {
+ it('triggers `focus`', done => {
const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- expect(spy).toHaveBeenCalled();
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled();
+ done();
+ });
});
- it('triggers `input`', () => {
+ it('triggers `input`', done => {
let triggered = false;
$(FORM_SELECTOR).on('input', () => {
triggered = true;
@@ -159,7 +197,10 @@ describe('ShortcutsIssuable', function() {
ShortcutsIssuable.replyWithSelectedText(true);
- expect(triggered).toBe(true);
+ setTimeout(() => {
+ expect(triggered).toBe(true);
+ done();
+ });
});
});
@@ -183,20 +224,26 @@ describe('ShortcutsIssuable', function() {
});
});
- it('adds the quoted selection to the input', () => {
+ it('adds the quoted selection to the input', done => {
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
+ setTimeout(() => {
+ expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
+ done();
+ });
});
- it('triggers `focus`', () => {
+ it('triggers `focus`', done => {
const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- expect(spy).toHaveBeenCalled();
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled();
+ done();
+ });
});
- it('triggers `input`', () => {
+ it('triggers `input`', done => {
let triggered = false;
$(FORM_SELECTOR).on('input', () => {
triggered = true;
@@ -204,7 +251,10 @@ describe('ShortcutsIssuable', function() {
ShortcutsIssuable.replyWithSelectedText(true);
- expect(triggered).toBe(true);
+ setTimeout(() => {
+ expect(triggered).toBe(true);
+ done();
+ });
});
});
@@ -228,17 +278,23 @@ describe('ShortcutsIssuable', function() {
});
});
- it('does not add anything to the input', () => {
+ it('does not add anything to the input', done => {
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe('');
+ setTimeout(() => {
+ expect($(FORM_SELECTOR).val()).toBe('');
+ done();
+ });
});
- it('triggers `focus`', () => {
+ it('triggers `focus`', done => {
const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
ShortcutsIssuable.replyWithSelectedText(true);
- expect(spy).toHaveBeenCalled();
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled();
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
index 2f0385454d7..e886f962d2f 100644
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -10,6 +10,10 @@ describe('CompareVersions', () => {
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
beforeEach(() => {
+ store.state.diffs.addedLines = 10;
+ store.state.diffs.removedLines = 20;
+ store.state.diffs.diffFiles.push('test');
+
vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, {
mergeRequestDiffs: diffsMockData,
mergeRequestDiff: diffsMockData[0],
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index b77907ff26f..787a81fd88f 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -24,6 +24,10 @@ describe('diff_file_header', () => {
beforeEach(() => {
const diffFile = diffDiscussionMock.diff_file;
+
+ diffFile.added_lines = 2;
+ diffFile.removed_lines = 1;
+
props = {
diffFile: { ...diffFile },
canCurrentUserFork: false,
diff --git a/spec/javascripts/diffs/components/diff_stats_spec.js b/spec/javascripts/diffs/components/diff_stats_spec.js
new file mode 100644
index 00000000000..984b3026209
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_stats_spec.js
@@ -0,0 +1,33 @@
+import { shallowMount } from '@vue/test-utils';
+import DiffStats from '~/diffs/components/diff_stats.vue';
+
+describe('diff_stats', () => {
+ it('does not render a group if diffFileLengths is not passed in', () => {
+ const wrapper = shallowMount(DiffStats, {
+ propsData: {
+ addedLines: 1,
+ removedLines: 2,
+ },
+ });
+ const groups = wrapper.findAll('.diff-stats-group');
+
+ expect(groups.length).toBe(2);
+ });
+
+ it('shows amount of files changed, lines added and lines removed when passed all props', () => {
+ const wrapper = shallowMount(DiffStats, {
+ propsData: {
+ addedLines: 100,
+ removedLines: 200,
+ diffFilesLength: 300,
+ },
+ });
+ const additions = wrapper.find('icon-stub[name="file-addition"]').element.parentNode;
+ const deletions = wrapper.find('icon-stub[name="file-deletion"]').element.parentNode;
+ const filesChanged = wrapper.find('icon-stub[name="doc-code"]').element.parentNode;
+
+ expect(additions.textContent).toContain('100');
+ expect(deletions.textContent).toContain('200');
+ expect(filesChanged.textContent).toContain('300');
+ });
+});
diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js
index c5ef48a81e9..9e556698f34 100644
--- a/spec/javascripts/diffs/components/tree_list_spec.js
+++ b/spec/javascripts/diffs/components/tree_list_spec.js
@@ -35,12 +35,6 @@ describe('Diffs tree list component', () => {
vm.$destroy();
});
- it('renders diff stats', () => {
- expect(vm.$el.textContent).toContain('1 changed file');
- expect(vm.$el.textContent).toContain('10 additions');
- expect(vm.$el.textContent).toContain('20 deletions');
- });
-
it('renders empty text', () => {
expect(vm.$el.textContent).toContain('No files found');
});
diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js
new file mode 100644
index 00000000000..19e27388eeb
--- /dev/null
+++ b/spec/javascripts/helpers/vue_test_utils_helper.js
@@ -0,0 +1,19 @@
+/* eslint-disable import/prefer-default-export */
+
+const vNodeContainsText = (vnode, text) =>
+ (vnode.text && vnode.text.includes(text)) ||
+ (vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length);
+
+/**
+ * Determines whether a `shallowMount` Wrapper contains text
+ * within one of it's slots. This will also work on Wrappers
+ * acquired with `find()`, but only if it's parent Wrapper
+ * was shallowMounted.
+ * NOTE: Prefer checking the rendered output of a component
+ * wherever possible using something like `text()` instead.
+ * @param {Wrapper} shallowWrapper - Vue test utils wrapper (shallowMounted)
+ * @param {String} slotName
+ * @param {String} text
+ */
+export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =>
+ !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length;
diff --git a/spec/javascripts/helpers/vue_test_utils_helper_spec.js b/spec/javascripts/helpers/vue_test_utils_helper_spec.js
new file mode 100644
index 00000000000..41714066da5
--- /dev/null
+++ b/spec/javascripts/helpers/vue_test_utils_helper_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount } from '@vue/test-utils';
+import { shallowWrapperContainsSlotText } from './vue_test_utils_helper';
+
+describe('Vue test utils helpers', () => {
+ describe('shallowWrapperContainsSlotText', () => {
+ const mockText = 'text';
+ const mockSlot = `<div>${mockText}</div>`;
+ let mockComponent;
+
+ beforeEach(() => {
+ mockComponent = shallowMount(
+ {
+ render(h) {
+ h(`<div>mockedComponent</div>`);
+ },
+ },
+ {
+ slots: {
+ default: mockText,
+ namedSlot: mockSlot,
+ },
+ },
+ );
+ });
+
+ it('finds text within shallowWrapper default slot', () => {
+ expect(shallowWrapperContainsSlotText(mockComponent, 'default', mockText)).toBe(true);
+ });
+
+ it('finds text within shallowWrapper named slot', () => {
+ expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', mockText)).toBe(true);
+ });
+
+ it('returns false when text is not present', () => {
+ const searchText = 'absent';
+
+ expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false);
+ expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false);
+ });
+
+ it('searches with case-sensitivity', () => {
+ const searchText = mockText.toUpperCase();
+
+ expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false);
+ expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 72716b97f5f..2eeed6770be 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -21,7 +21,8 @@ describe('Description component', () => {
if (!document.querySelector('.issuable-meta')) {
const metaData = document.createElement('div');
metaData.classList.add('issuable-meta');
- metaData.innerHTML = '<span id="task_status"></span><span id="task_status_short"></span>';
+ metaData.innerHTML =
+ '<div class="flash-container"></div><span id="task_status"></span><span id="task_status_short"></span>';
document.body.appendChild(metaData);
}
@@ -33,6 +34,10 @@ describe('Description component', () => {
vm.$destroy();
});
+ afterAll(() => {
+ $('.issuable-meta .flash-container').remove();
+ });
+
it('animates description changes', done => {
vm.descriptionHtml = 'changed';
@@ -192,12 +197,11 @@ describe('Description component', () => {
it('should create flash notification and emit an event to parent', () => {
const msg =
'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.';
- spyOn(window, 'Flash');
spyOn(vm, '$emit');
vm.taskListUpdateError();
- expect(window.Flash).toHaveBeenCalledWith(msg);
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
expect(vm.$emit).toHaveBeenCalledWith('taskListUpdateFailed');
});
});
diff --git a/spec/javascripts/lib/utils/file_upload_spec.js b/spec/javascripts/lib/utils/file_upload_spec.js
index 92c9cc70aaf..8f7092f63de 100644
--- a/spec/javascripts/lib/utils/file_upload_spec.js
+++ b/spec/javascripts/lib/utils/file_upload_spec.js
@@ -9,28 +9,56 @@ describe('File upload', () => {
<span class="js-filename"></span>
</form>
`);
+ });
+
+ describe('when there is a matching button and input', () => {
+ beforeEach(() => {
+ fileUpload('.js-button', '.js-input');
+ });
+
+ it('clicks file input after clicking button', () => {
+ const btn = document.querySelector('.js-button');
+ const input = document.querySelector('.js-input');
+
+ spyOn(input, 'click');
+
+ btn.click();
+
+ expect(input.click).toHaveBeenCalled();
+ });
+
+ it('updates file name text', () => {
+ const input = document.querySelector('.js-input');
- fileUpload('.js-button', '.js-input');
+ input.value = 'path/to/file/index.js';
+
+ input.dispatchEvent(new CustomEvent('change'));
+
+ expect(document.querySelector('.js-filename').textContent).toEqual('index.js');
+ });
});
- it('clicks file input after clicking button', () => {
- const btn = document.querySelector('.js-button');
+ it('fails gracefully when there is no matching button', () => {
const input = document.querySelector('.js-input');
+ const btn = document.querySelector('.js-button');
+ fileUpload('.js-not-button', '.js-input');
spyOn(input, 'click');
btn.click();
- expect(input.click).toHaveBeenCalled();
+ expect(input.click).not.toHaveBeenCalled();
});
- it('updates file name text', () => {
+ it('fails gracefully when there is no matching input', () => {
const input = document.querySelector('.js-input');
+ const btn = document.querySelector('.js-button');
+ fileUpload('.js-button', '.js-not-input');
- input.value = 'path/to/file/index.js';
+ spyOn(input, 'click');
- input.dispatchEvent(new CustomEvent('change'));
+ btn.click();
- expect(document.querySelector('.js-filename').textContent).toEqual('index.js');
+ expect(input.click).not.toHaveBeenCalled();
});
});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 32623d1781a..ab809930804 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -40,30 +40,51 @@ describe('MergeRequest', function() {
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', done => {
+ describe('tasklist', () => {
const lineNumber = 8;
const lineSource = '- [ ] item 8';
const index = 3;
const checked = true;
- $('.js-task-list-field').trigger({
- type: 'tasklist:changed',
- detail: { lineNumber, lineSource, index, checked },
+ it('submits an ajax request on tasklist:changed', done => {
+ $('.js-task-list-field').trigger({
+ type: 'tasklist:changed',
+ detail: { lineNumber, lineSource, index, checked },
+ });
+
+ setTimeout(() => {
+ expect(axios.patch).toHaveBeenCalledWith(
+ `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ {
+ merge_request: {
+ description: '- [ ] Task List Item',
+ lock_version: undefined,
+ update_task: { line_number: lineNumber, line_source: lineSource, index, checked },
+ },
+ },
+ );
+
+ done();
+ });
});
- setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(
- `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
- {
- merge_request: {
- description: '- [ ] Task List Item',
- lock_version: undefined,
- update_task: { line_number: lineNumber, line_source: lineSource, index, checked },
- },
- },
- );
+ it('shows an error notification when tasklist update failed', done => {
+ mock
+ .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`)
+ .reply(409, {});
+
+ $('.js-task-list-field').trigger({
+ type: 'tasklist:changed',
+ detail: { lineNumber, lineSource, index, checked },
+ });
+
+ setTimeout(() => {
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
+ );
- done();
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js
new file mode 100644
index 00000000000..0b36fc9f5f7
--- /dev/null
+++ b/spec/javascripts/monitoring/charts/area_spec.js
@@ -0,0 +1,220 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
+import Area from '~/monitoring/components/charts/area.vue';
+import MonitoringStore from '~/monitoring/stores/monitoring_store';
+import MonitoringMock, { deploymentData } from '../mock_data';
+
+describe('Area component', () => {
+ const mockWidgets = 'mockWidgets';
+ let mockGraphData;
+ let areaChart;
+ let spriteSpy;
+
+ beforeEach(() => {
+ const store = new MonitoringStore();
+ store.storeMetrics(MonitoringMock.data);
+ store.storeDeploymentData(deploymentData);
+
+ [mockGraphData] = store.groups[0].metrics;
+
+ areaChart = shallowMount(Area, {
+ propsData: {
+ graphData: mockGraphData,
+ containerWidth: 0,
+ deploymentData: store.deploymentData,
+ },
+ slots: {
+ default: mockWidgets,
+ },
+ });
+
+ spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake(
+ () => new Promise(resolve => resolve()),
+ );
+ });
+
+ afterEach(() => {
+ areaChart.destroy();
+ });
+
+ it('renders chart title', () => {
+ expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title);
+ });
+
+ it('contains graph widgets from slot', () => {
+ expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets);
+ });
+
+ describe('wrapped components', () => {
+ describe('GitLab UI area chart', () => {
+ let glAreaChart;
+
+ beforeEach(() => {
+ glAreaChart = areaChart.find(GlAreaChart);
+ });
+
+ it('is a Vue instance', () => {
+ expect(glAreaChart.isVueInstance()).toBe(true);
+ });
+
+ it('receives data properties needed for proper chart render', () => {
+ const props = glAreaChart.props();
+
+ expect(props.data).toBe(areaChart.vm.chartData);
+ expect(props.option).toBe(areaChart.vm.chartOptions);
+ expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText);
+ expect(props.thresholds).toBe(areaChart.props('alertData'));
+ });
+
+ it('recieves a tooltip title', () => {
+ const mockTitle = 'mockTitle';
+ areaChart.vm.tooltip.title = mockTitle;
+
+ expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipTitle', mockTitle)).toBe(true);
+ });
+
+ it('recieves tooltip content', () => {
+ const mockContent = 'mockContent';
+ areaChart.vm.tooltip.content = mockContent;
+
+ expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockContent)).toBe(
+ true,
+ );
+ });
+
+ describe('when tooltip is showing deployment data', () => {
+ beforeEach(() => {
+ areaChart.vm.tooltip.isDeployment = true;
+ });
+
+ it('uses deployment title', () => {
+ expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipTitle', 'Deployed')).toBe(
+ true,
+ );
+ });
+
+ it('renders commit sha in tooltip content', () => {
+ const mockSha = 'mockSha';
+ areaChart.vm.tooltip.sha = mockSha;
+
+ expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockSha)).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ const mockDate = deploymentData[0].created_at;
+ const generateSeriesData = type => ({
+ seriesData: [
+ {
+ componentSubType: type,
+ value: [mockDate, 5.55555],
+ },
+ ],
+ value: mockDate,
+ });
+
+ describe('series is of line type', () => {
+ beforeEach(() => {
+ areaChart.vm.formatTooltipText(generateSeriesData('line'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(areaChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip content', () => {
+ expect(areaChart.vm.tooltip.content).toBe('CPU (Cores) 5.556');
+ });
+ });
+
+ describe('series is of scatter type', () => {
+ beforeEach(() => {
+ areaChart.vm.formatTooltipText(generateSeriesData('scatter'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(areaChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip sha', () => {
+ expect(areaChart.vm.tooltip.sha).toBe('f5bcd1d9');
+ });
+ });
+ });
+
+ describe('getScatterSymbol', () => {
+ beforeEach(() => {
+ areaChart.vm.getScatterSymbol();
+ });
+
+ it('gets rocket svg path content for use as deployment data symbol', () => {
+ expect(spriteSpy).toHaveBeenCalledWith('rocket');
+ });
+ });
+
+ describe('onResize', () => {
+ const mockWidth = 233;
+ const mockHeight = 144;
+
+ beforeEach(() => {
+ spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
+ width: mockWidth,
+ height: mockHeight,
+ }));
+ areaChart.vm.onResize();
+ });
+
+ it('sets area chart width', () => {
+ expect(areaChart.vm.width).toBe(mockWidth);
+ });
+
+ it('sets area chart height', () => {
+ expect(areaChart.vm.height).toBe(mockHeight);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('chartData', () => {
+ it('utilizes all data points', () => {
+ expect(Object.keys(areaChart.vm.chartData)).toEqual(['Cores']);
+ expect(areaChart.vm.chartData.Cores.length).toBe(297);
+ });
+
+ it('creates valid data', () => {
+ const data = areaChart.vm.chartData.Cores;
+
+ expect(
+ data.filter(([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number')
+ .length,
+ ).toBe(data.length);
+ });
+ });
+
+ describe('scatterSeries', () => {
+ it('utilizes deployment data', () => {
+ expect(areaChart.vm.scatterSeries.data).toEqual([
+ ['2017-05-31T21:23:37.881Z', 0],
+ ['2017-05-30T20:08:04.629Z', 0],
+ ['2017-05-30T17:42:38.409Z', 0],
+ ]);
+ });
+ });
+
+ describe('xAxisLabel', () => {
+ it('constructs a label for the chart x-axis', () => {
+ expect(areaChart.vm.xAxisLabel).toBe('Core Usage');
+ });
+ });
+
+ describe('yAxisLabel', () => {
+ it('constructs a label for the chart y-axis', () => {
+ expect(areaChart.vm.yAxisLabel).toBe('CPU (Cores)');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 97b9671c809..b1778029a77 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -25,15 +25,22 @@ export default propsData;
describe('Dashboard', () => {
let DashboardComponent;
+ let mock;
beforeEach(() => {
setFixtures(`
<div class="prometheus-graphs"></div>
<div class="layout-page"></div>
`);
+
+ mock = new MockAdapter(axios);
DashboardComponent = Vue.extend(Dashboard);
});
+ afterEach(() => {
+ mock.restore();
+ });
+
describe('no metrics are available yet', () => {
it('shows a getting started empty state when no metrics are present', () => {
const component = new DashboardComponent({
@@ -47,16 +54,10 @@ describe('Dashboard', () => {
});
describe('requests information to the server', () => {
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
});
- afterEach(() => {
- mock.restore();
- });
-
it('shows up a loading state', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
@@ -152,15 +153,12 @@ describe('Dashboard', () => {
});
describe('when the window resizes', () => {
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
jasmine.clock().install();
});
afterEach(() => {
- mock.restore();
jasmine.clock().uninstall();
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index b4e2cd75d47..ffc7148fde2 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -326,6 +326,7 @@ export const metricsGroupsAPIResponse = {
{
id: 6,
title: 'CPU usage',
+ y_label: 'CPU',
weight: 1,
queries: [
{
diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js
index 4a143ef089a..8ade6fc2ced 100644
--- a/spec/javascripts/notes/components/noteable_note_spec.js
+++ b/spec/javascripts/notes/components/noteable_note_spec.js
@@ -1,105 +1,64 @@
+import $ from 'jquery';
import _ from 'underscore';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import createStore from '~/notes/stores';
import issueNote from '~/notes/components/noteable_note.vue';
-import NoteHeader from '~/notes/components/note_header.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import NoteActions from '~/notes/components/note_actions.vue';
-import NoteBody from '~/notes/components/note_body.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note', () => {
let store;
- let wrapper;
+ let vm;
beforeEach(() => {
+ const Component = Vue.extend(issueNote);
+
store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
- const localVue = createLocalVue();
- wrapper = shallowMount(issueNote, {
+ vm = new Component({
store,
propsData: {
note,
},
- sync: false,
- localVue,
- });
+ }).$mount();
});
afterEach(() => {
- wrapper.destroy();
+ vm.$destroy();
});
it('should render user information', () => {
- const { author } = note;
- const avatar = wrapper.find(UserAvatarLink);
- const avatarProps = avatar.props();
-
- expect(avatarProps.linkHref).toBe(author.path);
- expect(avatarProps.imgSrc).toBe(author.avatar_url);
- expect(avatarProps.imgAlt).toBe(author.name);
- expect(avatarProps.imgSize).toBe(40);
+ expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(
+ note.author.avatar_url,
+ );
});
it('should render note header content', () => {
- const noteHeader = wrapper.find(NoteHeader);
- const noteHeaderProps = noteHeader.props();
+ const el = vm.$el.querySelector('.note-header .note-header-author-name');
- expect(noteHeaderProps.author).toEqual(note.author);
- expect(noteHeaderProps.createdAt).toEqual(note.created_at);
- expect(noteHeaderProps.noteId).toEqual(note.id);
+ expect(el.textContent.trim()).toEqual(note.author.name);
});
it('should render note actions', () => {
- const { author } = note;
- const noteActions = wrapper.find(NoteActions);
- const noteActionsProps = noteActions.props();
-
- expect(noteActionsProps.authorId).toBe(author.id);
- expect(noteActionsProps.noteId).toBe(note.id);
- expect(noteActionsProps.noteUrl).toBe(note.noteable_note_url);
- expect(noteActionsProps.accessLevel).toBe(note.human_access);
- expect(noteActionsProps.canEdit).toBe(note.current_user.can_edit);
- expect(noteActionsProps.canAwardEmoji).toBe(note.current_user.can_award_emoji);
- expect(noteActionsProps.canDelete).toBe(note.current_user.can_edit);
- expect(noteActionsProps.canReportAsAbuse).toBe(true);
- expect(noteActionsProps.canResolve).toBe(false);
- expect(noteActionsProps.reportAbusePath).toBe(note.report_abuse_path);
- expect(noteActionsProps.resolvable).toBe(false);
- expect(noteActionsProps.isResolved).toBe(false);
- expect(noteActionsProps.isResolving).toBe(false);
- expect(noteActionsProps.resolvedBy).toEqual({});
+ expect(vm.$el.querySelector('.note-actions')).toBeDefined();
});
it('should render issue body', () => {
- const noteBody = wrapper.find(NoteBody);
- const noteBodyProps = noteBody.props();
-
- expect(noteBodyProps.note).toEqual(note);
- expect(noteBodyProps.line).toBe(null);
- expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit);
- expect(noteBodyProps.isEditing).toBe(false);
- expect(noteBodyProps.helpPagePath).toBe('');
+ expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
});
it('prevents note preview xss', done => {
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
const alertSpy = spyOn(window, 'alert');
- store.hotUpdate({
- actions: {
- updateNote() {},
- },
- });
- const noteBodyComponent = wrapper.find(NoteBody);
+ vm.updateNote = () => new Promise($.noop);
- noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {});
+ vm.formUpdateHandler(noteBody, null, $.noop);
setTimeout(() => {
expect(alertSpy).not.toHaveBeenCalled();
- expect(wrapper.vm.note.note_html).toEqual(_.escape(noteBody));
+ expect(vm.note.note_html).toEqual(_.escape(noteBody));
done();
}, 0);
});
@@ -107,23 +66,17 @@ describe('issue_note', () => {
describe('cancel edit', () => {
it('restores content of updated note', done => {
const noteBody = 'updated note text';
- store.hotUpdate({
- actions: {
- updateNote() {},
- },
- });
- const noteBodyComponent = wrapper.find(NoteBody);
- noteBodyComponent.vm.resetAutoSave = () => {};
+ vm.updateNote = () => Promise.resolve();
- noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {});
+ vm.formUpdateHandler(noteBody, null, $.noop);
setTimeout(() => {
- expect(wrapper.vm.note.note_html).toEqual(noteBody);
+ expect(vm.note.note_html).toEqual(noteBody);
- noteBodyComponent.vm.$emit('cancelForm');
+ vm.formCancelHandler();
setTimeout(() => {
- expect(wrapper.vm.note.note_html).toEqual(noteBody);
+ expect(vm.note.note_html).toEqual(noteBody);
done();
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 7ae45c40c28..348743081eb 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -165,7 +165,6 @@ export const note = {
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/546',
- cached_markdown_version: 11,
};
export const discussionMock = {
diff --git a/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js b/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js
new file mode 100644
index 00000000000..994d6255324
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js
@@ -0,0 +1,85 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
+
+const localVue = createLocalVue();
+const testCommitMessage = 'Test commit message';
+const testLabel = 'Test label';
+const testInputId = 'test-input-id';
+
+describe('Commits edit component', () => {
+ let wrapper;
+
+ const createComponent = (slots = {}) => {
+ wrapper = shallowMount(localVue.extend(CommitEdit), {
+ localVue,
+ sync: false,
+ propsData: {
+ value: testCommitMessage,
+ label: testLabel,
+ inputId: testInputId,
+ },
+ slots: {
+ ...slots,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTextarea = () => wrapper.find('.form-control');
+
+ it('has a correct label', () => {
+ const labelElement = wrapper.find('.col-form-label');
+
+ expect(labelElement.text()).toBe(testLabel);
+ });
+
+ describe('textarea', () => {
+ it('has a correct ID', () => {
+ expect(findTextarea().attributes('id')).toBe(testInputId);
+ });
+
+ it('has a correct value', () => {
+ expect(findTextarea().element.value).toBe(testCommitMessage);
+ });
+
+ it('emits an input event and receives changed value', () => {
+ const changedCommitMessage = 'Changed commit message';
+
+ findTextarea().element.value = changedCommitMessage;
+ findTextarea().trigger('input');
+
+ expect(wrapper.emitted().input[0]).toEqual([changedCommitMessage]);
+ expect(findTextarea().element.value).toBe(changedCommitMessage);
+ });
+ });
+
+ describe('when slots are present', () => {
+ beforeEach(() => {
+ createComponent({
+ header: `<div class="test-header">${testCommitMessage}</div>`,
+ checkbox: `<label slot="checkbox" class="test-checkbox">${testLabel}</label >`,
+ });
+ });
+
+ it('renders header slot correctly', () => {
+ const headerSlotElement = wrapper.find('.test-header');
+
+ expect(headerSlotElement.exists()).toBe(true);
+ expect(headerSlotElement.text()).toBe(testCommitMessage);
+ });
+
+ it('renders checkbox slot correctly', () => {
+ const checkboxSlotElement = wrapper.find('.test-checkbox');
+
+ expect(checkboxSlotElement.exists()).toBe(true);
+ expect(checkboxSlotElement.text()).toBe(testLabel);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
new file mode 100644
index 00000000000..daf1cc2d98b
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
@@ -0,0 +1,61 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
+
+const localVue = createLocalVue();
+const commits = [
+ {
+ title: 'Commit 1',
+ short_id: '78d5b7',
+ message: 'Update test.txt',
+ },
+ {
+ title: 'Commit 2',
+ short_id: '34cbe28b',
+ message: 'Fixed test',
+ },
+ {
+ title: 'Commit 3',
+ short_id: 'fa42932a',
+ message: 'Added changelog',
+ },
+];
+
+describe('Commits message dropdown component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(localVue.extend(CommitMessageDropdown), {
+ localVue,
+ sync: false,
+ propsData: {
+ commits,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
+ const findFirstDropdownElement = () => findDropdownElements().at(0);
+
+ it('should have 3 elements in dropdown list', () => {
+ expect(findDropdownElements().length).toBe(3);
+ });
+
+ it('should have correct message for the first dropdown list element', () => {
+ expect(findFirstDropdownElement().text()).toBe('78d5b7 Commit 1');
+ });
+
+ it('should emit a commit title on selecting commit', () => {
+ findFirstDropdownElement().vm.$emit('click');
+
+ expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']);
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
new file mode 100644
index 00000000000..5cf6408cf34
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
@@ -0,0 +1,110 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+const localVue = createLocalVue();
+
+describe('Commits header component', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ wrapper = shallowMount(localVue.extend(CommitsHeader), {
+ localVue,
+ sync: false,
+ propsData: {
+ isSquashEnabled: false,
+ targetBranch: 'master',
+ commitsCount: 5,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count');
+ const findCommitToggle = () => wrapper.find('.commit-edit-toggle');
+ const findIcon = () => wrapper.find(Icon);
+ const findCommitsCountMessage = () => wrapper.find('.commits-count-message');
+ const findTargetBranchMessage = () => wrapper.find('.label-branch');
+ const findModifyButton = () => wrapper.find('.modify-message-button');
+
+ describe('when collapsed', () => {
+ it('toggle has aria-label equal to Expand', () => {
+ createComponent();
+
+ expect(findCommitToggle().attributes('aria-label')).toBe('Expand');
+ });
+
+ it('has a chevron-right icon', () => {
+ createComponent();
+ wrapper.setData({ expanded: false });
+
+ expect(findIcon().props('name')).toBe('chevron-right');
+ });
+
+ describe('when squash is disabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('has commits count message showing correct amount of commits', () => {
+ expect(findCommitsCountMessage().text()).toBe('5 commits');
+ });
+
+ it('has button with modify merge commit message', () => {
+ expect(findModifyButton().text()).toBe('Modify merge commit');
+ });
+ });
+
+ describe('when squash is enabled', () => {
+ beforeEach(() => {
+ createComponent({ isSquashEnabled: true });
+ });
+
+ it('has commits count message showing one commit when squash is enabled', () => {
+ expect(findCommitsCountMessage().text()).toBe('1 commit');
+ });
+
+ it('has button with modify commit messages text', () => {
+ expect(findModifyButton().text()).toBe('Modify commit messages');
+ });
+ });
+
+ it('has correct target branch displayed', () => {
+ createComponent();
+
+ expect(findTargetBranchMessage().text()).toBe('master');
+ });
+ });
+
+ describe('when expanded', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.setData({ expanded: true });
+ });
+
+ it('toggle has aria-label equal to collapse', done => {
+ wrapper.vm.$nextTick(() => {
+ expect(findCommitToggle().attributes('aria-label')).toBe('Collapse');
+ done();
+ });
+ });
+
+ it('has a chevron-down icon', done => {
+ wrapper.vm.$nextTick(() => {
+ expect(findIcon().props('name')).toBe('chevron-down');
+ done();
+ });
+ });
+
+ it('has a collapse text', done => {
+ wrapper.vm.$nextTick(() => {
+ expect(findHeaderWrapper().text()).toBe('Collapse');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index e387367d1a2..631da202d1d 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -1,10 +1,14 @@
import Vue from 'vue';
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
+import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
+import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
+import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import { createLocalVue, shallowMount } from '@vue/test-utils';
const commitMessage = 'This is the commit message';
+const squashCommitMessage = 'This is the squash commit message';
const commitMessageWithDescription = 'This is the commit message description';
const createTestMr = customConfig => {
const mr = {
@@ -19,9 +23,11 @@ const createTestMr = customConfig => {
sha: '12345678',
squash: false,
commitMessage,
+ squashCommitMessage,
commitMessageWithDescription,
shouldRemoveSourceBranch: true,
canRemoveSourceBranch: false,
+ targetBranch: 'master',
};
Object.assign(mr, customConfig.mr);
@@ -98,21 +104,6 @@ describe('ReadyToMerge', () => {
});
});
- describe('commitMessageLinkTitle', () => {
- const withDesc = 'Include description in commit message';
- const withoutDesc = "Don't include description in commit message";
-
- it('should return message with description', () => {
- expect(vm.commitMessageLinkTitle).toEqual(withDesc);
- });
-
- it('should return message without description', () => {
- vm.useCommitMessageWithDescription = true;
-
- expect(vm.commitMessageLinkTitle).toEqual(withoutDesc);
- });
- });
-
describe('status', () => {
it('defaults to success', () => {
vm.mr.pipeline = true;
@@ -279,55 +270,43 @@ describe('ReadyToMerge', () => {
vm.mr.isMergeAllowed = false;
vm.mr.isPipelineActive = false;
- expect(vm.shouldShowMergeControls()).toBeFalsy();
+ expect(vm.shouldShowMergeControls).toBeFalsy();
});
it('should return true when the build succeeded or build not required to succeed', () => {
vm.mr.isMergeAllowed = true;
vm.mr.isPipelineActive = false;
- expect(vm.shouldShowMergeControls()).toBeTruthy();
+ expect(vm.shouldShowMergeControls).toBeTruthy();
});
it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => {
vm.mr.isMergeAllowed = false;
vm.mr.isPipelineActive = true;
- expect(vm.shouldShowMergeControls()).toBeTruthy();
+ expect(vm.shouldShowMergeControls).toBeTruthy();
});
it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => {
vm.mr.isMergeAllowed = true;
vm.mr.isPipelineActive = true;
- expect(vm.shouldShowMergeControls()).toBeTruthy();
+ expect(vm.shouldShowMergeControls).toBeTruthy();
});
});
- describe('updateCommitMessage', () => {
+ describe('updateMergeCommitMessage', () => {
it('should revert flag and change commitMessage', () => {
- expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage);
- vm.updateCommitMessage();
+ vm.updateMergeCommitMessage(true);
- expect(vm.useCommitMessageWithDescription).toBeTruthy();
expect(vm.commitMessage).toEqual(commitMessageWithDescription);
- vm.updateCommitMessage();
+ vm.updateMergeCommitMessage(false);
- expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage);
});
});
- describe('toggleCommitMessageEditor', () => {
- it('should toggle showCommitMessageEditor flag', () => {
- expect(vm.showCommitMessageEditor).toBeFalsy();
- vm.toggleCommitMessageEditor();
-
- expect(vm.showCommitMessageEditor).toBeTruthy();
- });
- });
-
describe('handleMergeButtonClick', () => {
const returnPromise = status =>
new Promise(resolve => {
@@ -623,7 +602,7 @@ describe('ReadyToMerge', () => {
});
});
- describe('Squash checkbox component', () => {
+ describe('render children components', () => {
let wrapper;
const localVue = createLocalVue();
@@ -642,25 +621,101 @@ describe('ReadyToMerge', () => {
});
const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
+ const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
+ const findCommitEditElements = () => wrapper.findAll(CommitEdit);
+ const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
+
+ describe('squash checkbox', () => {
+ it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: { commitsCount: 2, enableSquashBeforeMerge: true },
+ });
- it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
- createLocalComponent({
- mr: { commitsCount: 2, enableSquashBeforeMerge: true },
+ expect(findCheckboxElement().exists()).toBeTruthy();
});
- expect(findCheckboxElement().exists()).toBeTruthy();
+ it('should not be rendered when squash before merge is disabled', () => {
+ createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } });
+
+ expect(findCheckboxElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered when there is only 1 commit', () => {
+ createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } });
+
+ expect(findCheckboxElement().exists()).toBeFalsy();
+ });
});
- it('should not be rendered when squash before merge is disabled', () => {
- createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } });
+ describe('commits count collapsible header', () => {
+ it('should be rendered if fast-forward is disabled', () => {
+ createLocalComponent();
- expect(findCheckboxElement().exists()).toBeFalsy();
+ expect(findCommitsHeaderElement().exists()).toBeTruthy();
+ });
+
+ it('should not be rendered if fast-forward is enabled', () => {
+ createLocalComponent({ mr: { ffOnlyEnabled: true } });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
});
- it('should not be rendered when there is only 1 commit', () => {
- createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } });
+ describe('commits edit components', () => {
+ it('should have one edit component when squash is disabled', () => {
+ createLocalComponent();
+
+ expect(findCommitEditElements().length).toBe(1);
+ });
- expect(findCheckboxElement().exists()).toBeFalsy();
+ const findFirstCommitEditLabel = () =>
+ findCommitEditElements()
+ .at(0)
+ .props('label');
+
+ it('should have two edit components when squash is enabled', () => {
+ createLocalComponent({
+ mr: {
+ commitsCount: 2,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(2);
+ });
+
+ it('should have correct edit merge commit label', () => {
+ createLocalComponent();
+
+ expect(findFirstCommitEditLabel()).toBe('Merge commit message');
+ });
+
+ it('should have correct edit squash commit label', () => {
+ createLocalComponent({
+ mr: {
+ commitsCount: 2,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ },
+ });
+
+ expect(findFirstCommitEditLabel()).toBe('Squash commit message');
+ });
+ });
+
+ describe('commits dropdown', () => {
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent();
+
+ expect(findCommitDropdownElement().exists()).toBeFalsy();
+ });
+
+ it('should be rendered if squash is enabled', () => {
+ createLocalComponent({ mr: { squash: true } });
+
+ expect(findCommitDropdownElement().exists()).toBeTruthy();
+ });
});
});
@@ -696,10 +751,6 @@ describe('ReadyToMerge', () => {
expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull();
});
- it('does not show modify commit message button', () => {
- expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
- });
-
it('shows message to resolve all items before being allowed to merge', () => {
expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined();
});
@@ -712,7 +763,7 @@ describe('ReadyToMerge', () => {
mr: { ffOnlyEnabled: false },
});
- expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeNull();
+ expect(customVm.$el.querySelector('.mr-fast-forward-message')).toBeNull();
expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined();
});
@@ -721,7 +772,7 @@ describe('ReadyToMerge', () => {
mr: { ffOnlyEnabled: true },
});
- expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeDefined();
+ expect(customVm.$el.querySelector('.mr-fast-forward-message')).toBeDefined();
expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
});
});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 75b197fb2ba..6ef07f81705 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -215,12 +215,14 @@ export default {
project_archived: false,
default_merge_commit_message_with_description:
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
+ default_squash_commit_message: 'Test squash commit message',
diverged_commits_count: 0,
only_allow_merge_if_pipeline_succeeds: false,
commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
+ squash: true,
};
export const mockStore = {
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 209a547c3b3..3b52f6666d0 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do
)
end
- let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
+ let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) }
describe '#render' do
context 'with cache' do
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 236808c0b69..a4a6338961e 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid]
+ expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid profile email]
end
context 'registry_scopes' do
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index afd8f5da39f..a07c5371134 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -61,7 +61,7 @@ describe ::Gitlab::BareRepositoryImport::Repository do
let(:wiki_path) { File.join(root_path, "#{hashed_path}.wiki.git") }
before do
- gitlab_shell.create_repository(repository_storage, hashed_path)
+ gitlab_shell.create_repository(repository_storage, hashed_path, 'group/project')
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
repository = Rugged::Repository.new(repo_path)
repository.config['gitlab.fullpath'] = 'to/repo'
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 0def685f177..c432cc223b9 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -218,7 +218,7 @@ describe Gitlab::BitbucketImport::Importer do
describe 'wiki import' do
it 'is skipped when the wiki exists' do
expect(project.wiki).to receive(:repository_exists?) { true }
- expect(importer.gitlab_shell).not_to receive(:import_repository)
+ expect(importer.gitlab_shell).not_to receive(:import_wiki_repository)
importer.execute
@@ -227,11 +227,7 @@ describe Gitlab::BitbucketImport::Importer do
it 'imports to the project disk_path' do
expect(project.wiki).to receive(:repository_exists?) { false }
- expect(importer.gitlab_shell).to receive(:import_repository).with(
- project.repository_storage,
- project.wiki.disk_path,
- project.import_url + '/wiki'
- )
+ expect(importer.gitlab_shell).to receive(:import_wiki_repository)
importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb b/spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb
new file mode 100644
index 00000000000..795fd069ab2
--- /dev/null
+++ b/spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::BitbucketImport::WikiFormatter do
+ let(:project) do
+ create(:project,
+ namespace: create(:namespace, path: 'gitlabhq'),
+ import_url: 'https://xxx@bitbucket.org/gitlabhq/sample.gitlabhq.git')
+ end
+
+ subject(:wiki) { described_class.new(project) }
+
+ describe '#disk_path' do
+ it 'appends .wiki to disk path' do
+ expect(wiki.disk_path).to eq project.wiki.disk_path
+ end
+ end
+
+ describe '#full_path' do
+ it 'appends .wiki to project path' do
+ expect(wiki.full_path).to eq project.wiki.full_path
+ end
+ end
+
+ describe '#import_url' do
+ it 'returns URL of the wiki repository' do
+ expect(wiki.import_url).to eq 'https://xxx@bitbucket.org/gitlabhq/sample.gitlabhq.git/wiki'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index e704d1c673c..0010c0304eb 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
describe Gitlab::Git::Blame, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:blame) do
Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md")
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 1bcec04d28f..a1b5cea88c0 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
describe Gitlab::Git::Blob, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:rugged) do
Rugged::Repository.new(File.join(TestEnv.repos_path, TEST_REPO_PATH))
end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index 0df282d0ae3..0764e525ede 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Branch, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:rugged) do
Rugged::Repository.new(File.join(TestEnv.repos_path, repository.relative_path))
end
@@ -64,7 +64,7 @@ describe Gitlab::Git::Branch, :seed_helper do
context 'with active, stale and future branches' do
let(:repository) do
- Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project')
end
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index db68062e433..2611ebed25b 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -3,7 +3,7 @@ require "spec_helper"
describe Gitlab::Git::Commit, :seed_helper do
include GitHelpers
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:rugged_repo) do
Rugged::Repository.new(File.join(TestEnv.repos_path, TEST_REPO_PATH))
end
@@ -146,7 +146,7 @@ describe Gitlab::Git::Commit, :seed_helper do
end
context 'with broken repo' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '', 'group/project') }
it 'returns nil' do
expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil
diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
index 771c71a16a9..65dfb93d0db 100644
--- a/spec/lib/gitlab/git/compare_spec.rb
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Compare, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) }
let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) }
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 8a4415506c4..1d22329b670 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Diff, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:gitaly_diff) do
Gitlab::GitalyClient::Diff.new(
from_path: '.gitmodules',
diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb
index 53ed7c5a13a..e166628d4ca 100644
--- a/spec/lib/gitlab/git/remote_repository_spec.rb
+++ b/spec/lib/gitlab/git/remote_repository_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe Gitlab::Git::RemoteRepository, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
subject { described_class.new(repository) }
describe '#empty?' do
using RSpec::Parameterized::TableSyntax
where(:repository, :result) do
- Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') | false
- Gitlab::Git::Repository.new('default', 'does-not-exist.git', '') | true
+ Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') | false
+ Gitlab::Git::Repository.new('default', 'does-not-exist.git', '', 'group/project') | true
end
with_them do
@@ -44,11 +44,11 @@ describe Gitlab::Git::RemoteRepository, :seed_helper do
using RSpec::Parameterized::TableSyntax
where(:other_repository, :result) do
- repository | true
- Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '') | true
- Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '') | false
- Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '') | false
- Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '') | false
+ repository | true
+ Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '', 'group/project') | true
+ Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '', 'group/project') | false
+ Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '', 'group/project') | false
+ Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '', 'group/project') | false
end
with_them do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index a19e3e84f83..cf9e0cccc71 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -19,8 +19,10 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
- let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') }
+ let(:mutable_repository_path) { File.join(TestEnv.repos_path, mutable_repository.relative_path) }
+ let(:mutable_repository_rugged) { Rugged::Repository.new(mutable_repository_path) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
let(:repository_rugged) { Rugged::Repository.new(repository_path) }
let(:storage_path) { TestEnv.repos_path }
@@ -434,13 +436,13 @@ describe Gitlab::Git::Repository, :seed_helper do
describe '#fetch_repository_as_mirror' do
let(:new_repository) do
- Gitlab::Git::Repository.new('default', 'my_project.git', '')
+ Gitlab::Git::Repository.new('default', 'my_project.git', '', 'group/project')
end
subject { new_repository.fetch_repository_as_mirror(repository) }
before do
- Gitlab::Shell.new.create_repository('default', 'my_project')
+ Gitlab::Shell.new.create_repository('default', 'my_project', 'group/project')
end
after do
@@ -497,6 +499,48 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#search_files_by_content' do
+ let(:repository) { mutable_repository }
+ let(:repository_rugged) { mutable_repository_rugged }
+
+ before do
+ repository.create_branch('search-files-by-content-branch', 'master')
+ new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'search-files-by-content-branch', 'committing something', 'search-files-by-content change')
+ new_commit_edit_new_file_on_branch(repository_rugged, 'anotherfile', 'search-files-by-content-branch', 'committing something', 'search-files-by-content change')
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ shared_examples 'search files by content' do
+ it 'should have 2 items' do
+ expect(search_results.size).to eq(2)
+ end
+
+ it 'should have the correct matching line' do
+ expect(search_results).to contain_exactly("search-files-by-content-branch:encoding/CHANGELOG\u00001\u0000search-files-by-content change\n",
+ "search-files-by-content-branch:anotherfile\u00001\u0000search-files-by-content change\n")
+ end
+ end
+
+ it_should_behave_like 'search files by content' do
+ let(:search_results) do
+ repository.search_files_by_content('search-files-by-content', 'search-files-by-content-branch')
+ end
+ end
+
+ it_should_behave_like 'search files by content' do
+ let(:search_results) do
+ repository.gitaly_repository_client.search_files_by_content(
+ 'search-files-by-content-branch',
+ 'search-files-by-content',
+ chunked_response: false
+ )
+ end
+ end
+ end
+
describe '#find_remote_root_ref' do
it 'gets the remote root ref from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
@@ -544,7 +588,7 @@ describe Gitlab::Git::Repository, :seed_helper do
# Add new commits so that there's a renamed file in the commit history
@commit_with_old_name_id = new_commit_edit_old_file(repository_rugged).oid
@rename_commit_id = new_commit_move_file(repository_rugged).oid
- @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged).oid
+ @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged, "encoding/CHANGELOG", "Edit encoding/CHANGELOG", "I'm a new changelog with different text").oid
end
after do
@@ -1230,7 +1274,7 @@ describe Gitlab::Git::Repository, :seed_helper do
end
describe '#gitattribute' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') }
after do
ensure_seeds
@@ -1249,7 +1293,7 @@ describe Gitlab::Git::Repository, :seed_helper do
end
context 'without gitattributes file' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
it 'returns nil' do
expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil)
@@ -1513,7 +1557,7 @@ describe Gitlab::Git::Repository, :seed_helper do
context 'repository does not exist' do
it 'raises NoRepository and does not call Gitaly WriteConfig' do
- repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '')
+ repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project')
expect(repository.gitaly_repository_client).not_to receive(:write_config)
@@ -1803,7 +1847,7 @@ describe Gitlab::Git::Repository, :seed_helper do
out: '/dev/null',
err: '/dev/null')
- empty_repo = described_class.new('default', 'empty-repo.git', '')
+ empty_repo = described_class.new('default', 'empty-repo.git', '', 'group/empty-repo')
expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
end
@@ -1818,13 +1862,13 @@ describe Gitlab::Git::Repository, :seed_helper do
File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
- non_valid = described_class.new('default', 'non-valid.git', '')
+ non_valid = described_class.new('default', 'non-valid.git', '', 'a/non-valid')
expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
end
it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
- broken_repo = described_class.new('default', 'a/path.git', '')
+ broken_repo = described_class.new('default', 'a/path.git', '', 'a/path')
expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
@@ -1964,7 +2008,7 @@ describe Gitlab::Git::Repository, :seed_helper do
end
# Build the options hash that's passed to Rugged::Commit#create
- def commit_options(repo, index, message)
+ def commit_options(repo, index, target, ref, message)
options = {}
options[:tree] = index.write_tree(repo)
options[:author] = {
@@ -1978,8 +2022,8 @@ describe Gitlab::Git::Repository, :seed_helper do
time: Time.gm(2014, "mar", 3, 20, 15, 1)
}
options[:message] ||= message
- options[:parents] = repo.empty? ? [] : [repo.head.target].compact
- options[:update_ref] = "HEAD"
+ options[:parents] = repo.empty? ? [] : [target].compact
+ options[:update_ref] = ref
options
end
@@ -1995,6 +2039,8 @@ describe Gitlab::Git::Repository, :seed_helper do
options = commit_options(
repo,
index,
+ repo.head.target,
+ "HEAD",
"Edit CHANGELOG in its original location"
)
@@ -2003,19 +2049,24 @@ describe Gitlab::Git::Repository, :seed_helper do
end
# Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of encoding/CHANGELOG with new text.
- def new_commit_edit_new_file(repo)
- oid = repo.write("I'm a new changelog with different text", :blob)
+ # contents of the specified file_path with new text.
+ def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
+ oid = repo.write(text, :blob)
index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
-
- options = commit_options(repo, index, "Edit encoding/CHANGELOG")
-
+ index.read_tree(branch.target.tree)
+ index.add(path: file_path, oid: oid, mode: 0100644)
+ options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of encoding/CHANGELOG with new text.
+ def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
+ branch = repo.branches[branch_name]
+ new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
+ end
+
# Writes a new commit to the repo and returns a Rugged::Commit. Moves the
# CHANGELOG file to the encoding/ directory.
def new_commit_move_file(repo)
@@ -2027,7 +2078,7 @@ describe Gitlab::Git::Repository, :seed_helper do
index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
index.remove("CHANGELOG")
- options = commit_options(repo, index, "Move CHANGELOG to encoding/")
+ options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index b51e3879f49..4c0291f64f0 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Tag, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
describe '#tags' do
describe 'first tag' do
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index bec875fb03d..4a4d69490a3 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Tree, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
context :repo do
let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index aff47599ad6..d5508dbff5d 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::GitalyClient::RemoteService do
end
describe '#fetch_internal_remote' do
- let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+ let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') }
it 'sends an fetch_internal_remote message and returns the result value' do
expect_any_instance_of(Gitaly::RemoteService::Stub)
diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb
index 550db6db6d9..78a5e195ad1 100644
--- a/spec/lib/gitlab/gitaly_client/util_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/util_spec.rb
@@ -7,6 +7,7 @@ describe Gitlab::GitalyClient::Util do
let(:gl_repository) { 'project-1' }
let(:git_object_directory) { '.git/objects' }
let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] }
+ let(:gl_project_path) { 'namespace/myproject' }
let(:git_env) do
{
'GIT_OBJECT_DIRECTORY_RELATIVE' => git_object_directory,
@@ -15,7 +16,7 @@ describe Gitlab::GitalyClient::Util do
end
subject do
- described_class.repository(repository_storage, relative_path, gl_repository)
+ described_class.repository(repository_storage, relative_path, gl_repository, gl_project_path)
end
it 'creates a Gitaly::Repository with the given data' do
@@ -27,6 +28,7 @@ describe Gitlab::GitalyClient::Util do
expect(subject.gl_repository).to eq(gl_repository)
expect(subject.git_object_directory).to eq(git_object_directory)
expect(subject.git_alternate_object_directories).to eq(git_alternate_object_directory)
+ expect(subject.gl_project_path).to eq(gl_project_path)
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 77f5b2ffa37..47233ea6ee2 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -5,6 +5,14 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
let(:import_state) { double(:import_state) }
let(:client) { double(:client) }
+ let(:wiki) do
+ double(
+ :wiki,
+ disk_path: 'foo.wiki',
+ full_path: 'group/foo.wiki'
+ )
+ end
+
let(:project) do
double(
:project,
@@ -15,7 +23,9 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
repository: repository,
create_wiki: true,
import_state: import_state,
- lfs_enabled?: true
+ full_path: 'group/foo',
+ lfs_enabled?: true,
+ wiki: wiki
)
end
@@ -195,7 +205,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
it 'imports the wiki repository' do
expect(importer.gitlab_shell)
.to receive(:import_repository)
- .with('foo', 'foo.wiki', 'foo.wiki.git')
+ .with('foo', 'foo.wiki', 'foo.wiki.git', 'group/foo.wiki')
expect(importer.import_wiki_repository).to eq(true)
end
diff --git a/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb
index 7723533aee2..7519707293c 100644
--- a/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb
@@ -10,11 +10,17 @@ describe Gitlab::LegacyGithubImport::WikiFormatter do
subject(:wiki) { described_class.new(project) }
describe '#disk_path' do
- it 'appends .wiki to project path' do
+ it 'appends .wiki to disk path' do
expect(wiki.disk_path).to eq project.wiki.disk_path
end
end
+ describe '#full_path' do
+ it 'appends .wiki to project path' do
+ expect(wiki.full_path).to eq project.wiki.full_path
+ end
+ end
+
describe '#import_url' do
it 'returns URL of the wiki repository' do
expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git'
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index 771b633a2b9..4b03f3c2532 100644
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -37,7 +37,7 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
end
it 'updates metrics type unix and with addr' do
- labels = { type: 'unix', address: socket_address }
+ labels = { socket_type: 'unix', socket_address: socket_address }
expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
@@ -69,7 +69,7 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
end
it 'updates metrics type unix and with addr' do
- labels = { type: 'tcp', address: tcp_socket_address }
+ labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 6ce9d515a0f..033e1bf81a1 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -412,7 +412,7 @@ describe Gitlab::Shell do
end
it 'creates a repository' do
- expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy
+ expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_truthy
expect(File.stat(created_path).mode & 0o777).to eq(0o770)
@@ -427,7 +427,7 @@ describe Gitlab::Shell do
# should cause #create_repository to fail.
FileUtils.touch(created_path)
- expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy
+ expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_falsy
end
end
@@ -474,13 +474,10 @@ describe Gitlab::Shell do
end
describe '#fork_repository' do
+ let(:target_project) { create(:project) }
+
subject do
- gitlab_shell.fork_repository(
- project.repository_storage,
- project.disk_path,
- 'nfs-file05',
- 'fork/path'
- )
+ gitlab_shell.fork_repository(project, target_project)
end
it 'returns true when the command succeeds' do
@@ -505,7 +502,7 @@ describe Gitlab::Shell do
it 'returns true when the command succeeds' do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository).with(import_url)
- result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
+ result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url, project.full_path)
expect(result).to be_truthy
end
@@ -516,7 +513,7 @@ describe Gitlab::Shell do
expect_any_instance_of(Gitlab::Shell::GitalyGitlabProjects).to receive(:output) { 'error'}
expect do
- gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
+ gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url, project.full_path)
end.to raise_error(Gitlab::Shell::Error, "error")
end
end
diff --git a/spec/migrations/clean_up_for_members_spec.rb b/spec/migrations/clean_up_for_members_spec.rb
index 7876536cb3e..1a79f94cf0d 100644
--- a/spec/migrations/clean_up_for_members_spec.rb
+++ b/spec/migrations/clean_up_for_members_spec.rb
@@ -2,6 +2,10 @@ require 'spec_helper'
require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb')
describe CleanUpForMembers, :migration do
+ before do
+ stub_feature_flags(enforced_sso: false)
+ end
+
let(:migration) { described_class.new }
let(:groups) { table(:namespaces) }
let!(:group_member) { create_group_member }
diff --git a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
new file mode 100644
index 00000000000..2ffc0e65fee
--- /dev/null
+++ b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb')
+
+describe MigrateAutoDevOpsDomainToClusterDomain, :migration do
+ include MigrationHelpers::ClusterHelpers
+
+ let(:migration) { described_class.new }
+ let(:project_auto_devops_table) { table(:project_auto_devops) }
+ let(:clusters_table) { table(:clusters) }
+ let(:cluster_projects_table) { table(:cluster_projects) }
+
+ # Following lets are needed by MigrationHelpers::ClusterHelpers
+ let(:cluster_kubernetes_namespaces_table) { table(:clusters_kubernetes_namespaces) }
+ let(:projects_table) { table(:projects) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:provider_gcp_table) { table(:cluster_providers_gcp) }
+ let(:platform_kubernetes_table) { table(:cluster_platforms_kubernetes) }
+
+ before do
+ setup_cluster_projects_with_domain(quantity: 20, domain: domain)
+ end
+
+ context 'with ProjectAutoDevOps with no domain' do
+ let(:domain) { nil }
+
+ it 'should not update cluster project' do
+ migrate!
+
+ expect(clusters_without_domain.count).to eq(clusters_table.count)
+ end
+ end
+
+ context 'with ProjectAutoDevOps with domain' do
+ let(:domain) { 'example-domain.com' }
+
+ it 'should update all cluster projects' do
+ migrate!
+
+ expect(clusters_with_domain.count).to eq(clusters_table.count)
+ end
+ end
+
+ context 'when only some ProjectAutoDevOps have domain set' do
+ let(:domain) { 'example-domain.com' }
+
+ before do
+ setup_cluster_projects_with_domain(quantity: 25, domain: nil)
+ end
+
+ it 'should only update specific cluster projects' do
+ migrate!
+
+ expect(clusters_with_domain.count).to eq(20)
+
+ project_auto_devops_with_domain.each do |project_auto_devops|
+ cluster_project = Clusters::Project.find_by(project_id: project_auto_devops.project_id)
+ cluster = Clusters::Cluster.find(cluster_project.cluster_id)
+
+ expect(cluster.domain).to be_present
+ end
+
+ expect(clusters_without_domain.count).to eq(25)
+
+ project_auto_devops_without_domain.each do |project_auto_devops|
+ cluster_project = Clusters::Project.find_by(project_id: project_auto_devops.project_id)
+ cluster = Clusters::Cluster.find(cluster_project.cluster_id)
+
+ expect(cluster.domain).not_to be_present
+ end
+ end
+ end
+
+ def setup_cluster_projects_with_domain(quantity:, domain:)
+ create_cluster_project_list(quantity)
+
+ cluster_projects = cluster_projects_table.last(quantity)
+
+ cluster_projects.each do |cluster_project|
+ specific_domain = "#{cluster_project.id}-#{domain}" if domain
+
+ project_auto_devops_table.create(
+ project_id: cluster_project.project_id,
+ enabled: true,
+ domain: specific_domain
+ )
+ end
+ end
+
+ def project_auto_devops_with_domain
+ project_auto_devops_table.where.not("domain IS NULL OR domain = ''")
+ end
+
+ def project_auto_devops_without_domain
+ project_auto_devops_table.where("domain IS NULL OR domain = ''")
+ end
+
+ def clusters_with_domain
+ clusters_table.where.not("domain IS NULL OR domain = ''")
+ end
+
+ def clusters_without_domain
+ clusters_table.where("domain IS NULL OR domain = ''")
+ end
+end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index ca23f581fdc..fd25132ed3a 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -11,7 +11,7 @@ describe ApplicationRecord do
end
end
- describe '#safe_find_or_create_by' do
+ describe '.safe_find_or_create_by' do
it 'creates the user avoiding race conditions' do
expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
allow(Suggestion).to receive(:find_or_create_by).and_call_original
@@ -20,4 +20,17 @@ describe ApplicationRecord do
.to change { Suggestion.count }.by(1)
end
end
+
+ describe '.safe_find_or_create_by!' do
+ it 'creates a record using safe_find_or_create_by' do
+ expect(Suggestion).to receive(:find_or_create_by).and_call_original
+
+ expect(Suggestion.safe_find_or_create_by!(build(:suggestion).attributes))
+ .to be_a(Suggestion)
+ end
+
+ it 'raises a validation error if the record was not persisted' do
+ expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 96aa9a82b71..789e14e8a20 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -70,6 +70,13 @@ describe ApplicationSetting do
.is_greater_than(0)
end
+ it do
+ is_expected.to validate_numericality_of(:local_markdown_version)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ .is_less_than(65536)
+ end
+
context 'key restrictions' do
it 'supports all key types' do
expect(described_class::SUPPORTED_KEY_TYPES).to contain_exactly(:rsa, :dsa, :ecdsa, :ed25519)
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 0161db740ee..92ce2b0999a 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -30,6 +30,7 @@ describe Clusters::Cluster do
it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix }
+ it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix }
it { is_expected.to respond_to :project }
@@ -514,4 +515,108 @@ describe Clusters::Cluster do
it { is_expected.to be_falsey }
end
end
+
+ describe '#kube_ingress_domain' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { cluster.kube_ingress_domain }
+
+ context 'with domain set in cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :with_domain) }
+
+ it { is_expected.to eq(cluster.domain) }
+ end
+
+ context 'with no domain on cluster' do
+ context 'with a project cluster' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ context 'with domain set at instance level' do
+ before do
+ stub_application_setting(auto_devops_domain: 'global_domain.com')
+
+ it { is_expected.to eq('global_domain.com') }
+ end
+ end
+
+ context 'with domain set on ProjectAutoDevops' do
+ before do
+ auto_devops = project.build_auto_devops(domain: 'legacy-ado-domain.com')
+ auto_devops.save
+ end
+
+ it { is_expected.to eq('legacy-ado-domain.com') }
+ end
+
+ context 'with domain set as environment variable on project' do
+ before do
+ variable = project.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'project-ado-domain.com')
+ variable.save
+ end
+
+ it { is_expected.to eq('project-ado-domain.com') }
+ end
+
+ context 'with domain set as environment variable on the group project' do
+ let(:group) { create(:group) }
+
+ before do
+ project.update(parent_id: group.id)
+ variable = group.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'group-ado-domain.com')
+ variable.save
+ end
+
+ it { is_expected.to eq('group-ado-domain.com') }
+ end
+ end
+
+ context 'with a group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+
+ context 'with domain set as environment variable for the group' do
+ let(:group) { cluster.group }
+
+ before do
+ variable = group.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'group-ado-domain.com')
+ variable.save
+ end
+
+ it { is_expected.to eq('group-ado-domain.com') }
+ end
+ end
+ end
+ end
+
+ describe '#predefined_variables' do
+ subject { cluster.predefined_variables }
+
+ context 'with an instance domain' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ before do
+ stub_application_setting(auto_devops_domain: 'global_domain.com')
+ end
+
+ it 'should include KUBE_INGRESS_BASE_DOMAIN' do
+ expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'global_domain.com')
+ end
+ end
+
+ context 'with a cluster domain' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, domain: 'example.com') }
+
+ it 'should include KUBE_INGRESS_BASE_DOMAIN' do
+ expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'example.com')
+ end
+ end
+
+ context 'with no domain' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :project) }
+
+ it 'should return an empty array' do
+ expect(subject.to_hash).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 6c8a223092e..c273fa7e164 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -297,6 +297,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
end
end
+
+ context 'with a domain' do
+ let!(:cluster) do
+ create(:cluster, :provided_by_gcp, :with_domain,
+ platform_kubernetes: kubernetes)
+ end
+
+ it 'sets KUBE_INGRESS_BASE_DOMAIN' do
+ expect(subject).to include(
+ { key: 'KUBE_INGRESS_BASE_DOMAIN', value: cluster.domain, public: true }
+ )
+ end
+ end
end
describe '#terminals' do
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 29197ef372e..447279f19a8 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -60,6 +60,10 @@ describe CacheMarkdownField do
changes_applied
end
end
+
+ def has_attribute?(attr_name)
+ attribute_names.include?(attr_name)
+ end
end
def thing_subclass(new_attr)
@@ -72,8 +76,8 @@ describe CacheMarkdownField do
let(:updated_markdown) { '`Bar`' }
let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
- let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION }
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+ let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
before do
stub_commonmark_sourcepos_disabled
@@ -94,11 +98,11 @@ describe CacheMarkdownField do
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.foo_html_changed?).not_to be_truthy }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
context 'a changed markdown field' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
before do
thing.foo = updated_markdown
@@ -139,7 +143,7 @@ describe CacheMarkdownField do
end
context 'a non-markdown field changed' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
before do
thing.bar = 'OK'
@@ -160,7 +164,7 @@ describe CacheMarkdownField do
end
it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
describe '#cached_html_up_to_date?' do
@@ -174,21 +178,35 @@ describe CacheMarkdownField do
is_expected.to be_falsy
end
- it 'returns false when the version is too early' do
- thing.cached_markdown_version -= 1
+ it 'returns false when the cached version is too old' do
+ thing.cached_markdown_version = cache_version - 1
is_expected.to be_falsy
end
- it 'returns false when the version is too late' do
- thing.cached_markdown_version += 1
+ it 'returns false when the cached version is in future' do
+ thing.cached_markdown_version = cache_version + 1
is_expected.to be_falsy
end
- it 'returns true when the version is just right' do
+ it 'returns false when the local version was bumped' do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
thing.cached_markdown_version = cache_version
+ is_expected.to be_falsy
+ end
+
+ it 'returns true when the local version is default' do
+ thing.cached_markdown_version = cache_version
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns true when the cached version is just right' do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
+ thing.cached_markdown_version = cache_version + 2
+
is_expected.to be_truthy
end
@@ -221,14 +239,9 @@ describe CacheMarkdownField do
describe '#latest_cached_markdown_version' do
subject { thing.latest_cached_markdown_version }
- it 'returns commonmark version' do
- thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + 1
- is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
- end
-
- it 'returns default version when version is nil' do
+ it 'returns default version' do
thing.cached_markdown_version = nil
- is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
+ is_expected.to eq(cache_version)
end
end
@@ -255,7 +268,7 @@ describe CacheMarkdownField do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
+ expect(thing.cached_markdown_version).to eq(cache_version)
end
end
@@ -336,7 +349,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
+ expect(thing.cached_markdown_version).to eq(cache_version)
end
end
@@ -356,7 +369,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
+ expect(thing.cached_markdown_version).to eq(cache_version)
end
end
end
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index cdd7dea2064..e90319c39b1 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -23,6 +23,41 @@ RSpec.describe GpgSignature do
it { is_expected.to validate_presence_of(:gpg_key_primary_keyid) }
end
+ describe '.safe_create!' do
+ let(:attributes) do
+ {
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key_primary_keyid: gpg_key.keyid
+ }
+ end
+
+ it 'finds a signature by commit sha if it existed' do
+ gpg_signature
+
+ expect(described_class.safe_create!(commit_sha: commit_sha)).to eq(gpg_signature)
+ end
+
+ it 'creates a new signature if it was not found' do
+ expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1)
+ end
+
+ it 'assigns the correct attributes when creating' do
+ signature = described_class.safe_create!(attributes)
+
+ expect(signature.project).to eq(project)
+ expect(signature.commit_sha).to eq(commit_sha)
+ expect(signature.gpg_key_primary_keyid).to eq(gpg_key.keyid)
+ end
+
+ it 'does not raise an error in case of a race condition' do
+ expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
+ allow(described_class).to receive(:find_or_create_by).and_call_original
+
+ described_class.safe_create!(attributes)
+ end
+ end
+
describe '#commit' do
it 'fetches the commit through the project' do
expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ae137aa7b78..c1767ed0535 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1765,7 +1765,7 @@ describe Project do
context 'using a regular repository' do
it 'creates the repository' do
expect(shell).to receive(:create_repository)
- .with(project.repository_storage, project.disk_path)
+ .with(project.repository_storage, project.disk_path, project.full_path)
.and_return(true)
expect(project.repository).to receive(:after_create)
@@ -1775,7 +1775,7 @@ describe Project do
it 'adds an error if the repository could not be created' do
expect(shell).to receive(:create_repository)
- .with(project.repository_storage, project.disk_path)
+ .with(project.repository_storage, project.disk_path, project.full_path)
.and_return(false)
expect(project.repository).not_to receive(:after_create)
@@ -1808,7 +1808,7 @@ describe Project do
.and_return(false)
allow(shell).to receive(:create_repository)
- .with(project.repository_storage, project.disk_path)
+ .with(project.repository_storage, project.disk_path, project.full_path)
.and_return(true)
expect(project).to receive(:create_repository).with(force: true)
@@ -1832,7 +1832,7 @@ describe Project do
.and_return(false)
expect(shell).to receive(:create_repository)
- .with(project.repository_storage, project.disk_path)
+ .with(project.repository_storage, project.disk_path, project.full_path)
.and_return(true)
project.ensure_repository
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 48a43801b9f..3ccc706edf2 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -7,7 +7,7 @@ describe ProjectWiki do
let(:repository) { project.repository }
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project_wiki) { described_class.new(project, user) }
- let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo') }
+ let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo', 'group/project.wiki') }
let(:commit) { project_wiki.repository.head_commit }
subject { project_wiki }
@@ -75,7 +75,7 @@ describe ProjectWiki do
# Create a fresh project which will not have a wiki
project_wiki = described_class.new(create(:project), user)
gitlab_shell = double(:gitlab_shell)
- allow(gitlab_shell).to receive(:create_repository)
+ allow(gitlab_shell).to receive(:create_wiki_repository)
allow(project_wiki).to receive(:gitlab_shell).and_return(gitlab_shell)
expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 4978c43c9b5..f78760bf567 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2291,6 +2291,7 @@ describe Repository do
expect(subject).to be_a(Gitlab::Git::Repository)
expect(subject.relative_path).to eq(project.disk_path + '.git')
expect(subject.gl_repository).to eq("project-#{project.id}")
+ expect(subject.gl_project_path).to eq(project.full_path)
end
context 'with a wiki repository' do
@@ -2300,6 +2301,7 @@ describe Repository do
expect(subject).to be_a(Gitlab::Git::Repository)
expect(subject.relative_path).to eq(project.disk_path + '.wiki.git')
expect(subject.gl_repository).to eq("wiki-#{project.id}")
+ expect(subject.gl_project_path).to eq(project.full_path)
end
end
end
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index 3797960ac3d..7eeb2fae57d 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -81,14 +81,14 @@ RSpec.describe ResourceLabelEvent, type: :model do
expect(subject.outdated_markdown?).to be true
end
- it 'returns true markdown is outdated' do
- subject.attributes = { cached_markdown_version: 0 }
+ it 'returns true if markdown is outdated' do
+ subject.attributes = { cached_markdown_version: ((CacheMarkdownField::CACHE_COMMONMARK_VERSION - 1) << 16) | 0 }
expect(subject.outdated_markdown?).to be true
end
it 'returns false if label and reference are set' do
- subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION }
+ subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
expect(subject.outdated_markdown?).to be false
end
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index e85e7a41017..bb1db9a3d51 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe BlobPresenter, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:git_blob) do
Gitlab::Git::Blob.find(
diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb
new file mode 100644
index 00000000000..3769f8b78e4
--- /dev/null
+++ b/spec/requests/api/group_labels_spec.rb
@@ -0,0 +1,258 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::GroupLabels do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+ let!(:label1) { create(:group_label, title: 'feature', group: group) }
+ let!(:label2) { create(:group_label, title: 'bug', group: group) }
+
+ describe 'GET :id/labels' do
+ it 'returns all available labels for the group' do
+ get api("/groups/#{group.id}/labels", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/group_labels')
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug')
+ end
+ end
+
+ describe 'POST /groups/:id/labels' do
+ it 'returns created label when all params are given' do
+ post api("/groups/#{group.id}/labels", user),
+ params: {
+ name: 'Foo',
+ color: '#FFAABB',
+ description: 'test'
+ }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq('Foo')
+ expect(json_response['color']).to eq('#FFAABB')
+ expect(json_response['description']).to eq('test')
+ end
+
+ it 'returns created label when only required params are given' do
+ post api("/groups/#{group.id}/labels", user),
+ params: {
+ name: 'Foo & Bar',
+ color: '#FFAABB'
+ }
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('Foo & Bar')
+ expect(json_response['color']).to eq('#FFAABB')
+ expect(json_response['description']).to be_nil
+ end
+
+ it 'returns a 400 bad request if name not given' do
+ post api("/groups/#{group.id}/labels", user), params: { color: '#FFAABB' }
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns a 400 bad request if color is not given' do
+ post api("/groups/#{group.id}/labels", user), params: { name: 'Foobar' }
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 409 if label already exists' do
+ post api("/groups/#{group.id}/labels", user),
+ params: {
+ name: label1.name,
+ color: '#FFAABB'
+ }
+
+ expect(response).to have_gitlab_http_status(409)
+ expect(json_response['message']).to eq('Label already exists')
+ end
+ end
+
+ describe 'DELETE /groups/:id/labels' do
+ it 'returns 204 for existing label' do
+ delete api("/groups/#{group.id}/labels", user), params: { name: label1.name }
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ it 'returns 404 for non existing label' do
+ delete api("/groups/#{group.id}/labels", user), params: { name: 'label2' }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Label Not Found')
+ end
+
+ it 'returns 400 for wrong parameters' do
+ delete api("/groups/#{group.id}/labels", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "does not delete parent's group labels", :nested_groups do
+ subgroup = create(:group, parent: group)
+ subgroup_label = create(:group_label, title: 'feature', group: subgroup)
+
+ delete api("/groups/#{subgroup.id}/labels", user), params: { name: subgroup_label.name }
+
+ expect(response).to have_gitlab_http_status(204)
+ expect(subgroup.labels.size).to eq(0)
+ expect(group.labels).to include(label1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/groups/#{group.id}/labels", user) }
+ let(:params) { { name: label1.name } }
+ end
+ end
+
+ describe 'PUT /groups/:id/labels' do
+ it 'returns 200 if name and colors and description are changed' do
+ put api("/groups/#{group.id}/labels", user),
+ params: {
+ name: label1.name,
+ new_name: 'New Label',
+ color: '#FFFFFF',
+ description: 'test'
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['name']).to eq('New Label')
+ expect(json_response['color']).to eq('#FFFFFF')
+ expect(json_response['description']).to eq('test')
+ end
+
+ it "does not update parent's group label", :nested_groups do
+ subgroup = create(:group, parent: group)
+ subgroup_label = create(:group_label, title: 'feature', group: subgroup)
+
+ put api("/groups/#{subgroup.id}/labels", user),
+ params: {
+ name: subgroup_label.name,
+ new_name: 'New Label'
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(subgroup.labels[0].name).to eq('New Label')
+ expect(label1.name).to eq('feature')
+ end
+
+ it 'returns 404 if label does not exist' do
+ put api("/groups/#{group.id}/labels", user),
+ params: {
+ name: 'label2',
+ new_name: 'label3'
+ }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 400 if no label name given' do
+ put api("/groups/#{group.id}/labels", user), params: { new_name: label1.name }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('name is missing')
+ end
+
+ it 'returns 400 if no new parameters given' do
+ put api("/groups/#{group.id}/labels", user), params: { name: label1.name }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('new_name, color, description are missing, '\
+ 'at least one parameter must be provided')
+ end
+ end
+
+ describe 'POST /groups/:id/labels/:label_id/subscribe' do
+ context 'when label_id is a label title' do
+ it 'subscribes to the label' do
+ post api("/groups/#{group.id}/labels/#{label1.title}/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq(label1.title)
+ expect(json_response['subscribed']).to be_truthy
+ end
+ end
+
+ context 'when label_id is a label ID' do
+ it 'subscribes to the label' do
+ post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq(label1.title)
+ expect(json_response['subscribed']).to be_truthy
+ end
+ end
+
+ context 'when user is already subscribed to label' do
+ before do
+ label1.subscribe(user)
+ end
+
+ it 'returns 304' do
+ post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(304)
+ end
+ end
+
+ context 'when label ID is not found' do
+ it 'returns 404 error' do
+ post api("/groups/#{group.id}/labels/1234/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /groups/:id/labels/:label_id/unsubscribe' do
+ before do
+ label1.subscribe(user)
+ end
+
+ context 'when label_id is a label title' do
+ it 'unsubscribes from the label' do
+ post api("/groups/#{group.id}/labels/#{label1.title}/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq(label1.title)
+ expect(json_response['subscribed']).to be_falsey
+ end
+ end
+
+ context 'when label_id is a label ID' do
+ it 'unsubscribes from the label' do
+ post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq(label1.title)
+ expect(json_response['subscribed']).to be_falsey
+ end
+ end
+
+ context 'when user is already unsubscribed from label' do
+ before do
+ label1.unsubscribe(user)
+ end
+
+ it 'returns 304' do
+ post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(304)
+ end
+ end
+
+ context 'when label ID is not found' do
+ it 'returns 404 error' do
+ post api("/groups/#{group.id}/labels/1234/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 51343287a13..0f5f6e38819 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -951,6 +951,29 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(404)
end
+
+ describe "the squash_commit_message param" do
+ let(:squash_commit) do
+ project.repository.commits_between(json_response['diff_refs']['start_sha'], json_response['merge_commit_sha']).first
+ end
+
+ it "results in a specific squash commit message when set" do
+ squash_commit_message = 'My custom squash commit message'
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: {
+ squash: true,
+ squash_commit_message: squash_commit_message
+ }
+
+ expect(squash_commit.message.chomp).to eq(squash_commit_message)
+ end
+
+ it "results in a default squash commit message when not set" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: { squash: true }
+
+ expect(squash_commit.message).to eq(merge_request.default_squash_commit_message)
+ end
+ end
end
describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 45fb1562e84..f33eb5b9e02 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -64,7 +64,8 @@ describe API::Settings, 'Settings' do
performance_bar_allowed_group_path: group.full_path,
instance_statistics_visibility_private: true,
diff_max_patch_bytes: 150_000,
- default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE
+ default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
+ local_markdown_version: 3
}
expect(response).to have_gitlab_http_status(200)
@@ -90,6 +91,7 @@ describe API::Settings, 'Settings' do
expect(json_response['instance_statistics_visibility_private']).to be(true)
expect(json_response['diff_max_patch_bytes']).to eq(150_000)
expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+ expect(json_response['local_markdown_version']).to eq(3)
end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 2b148c1b563..2a455523e2c 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -35,7 +35,7 @@ describe 'OpenID Connect requests' do
'name' => 'Alice',
'nickname' => 'alice',
'email' => 'public@example.com',
- 'email_verified' => true,
+ 'email_verified' => false,
'website' => 'https://example.com',
'profile' => 'http://localhost/alice',
'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
@@ -111,6 +111,18 @@ describe 'OpenID Connect requests' do
it 'does not include any unknown claims' do
expect(json_response.keys).to eq %w[sub sub_legacy] + user_info_claims.keys
end
+
+ it 'includes email and email_verified claims' do
+ expect(json_response.keys).to include('email', 'email_verified')
+ end
+
+ it 'has public email in email claim' do
+ expect(json_response['email']).to eq(user.public_email)
+ end
+
+ it 'has false in email_verified claim' do
+ expect(json_response['email_verified']).to eq(false)
+ end
end
context 'ID token payload' do
@@ -175,7 +187,35 @@ describe 'OpenID Connect requests' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to eq(%w[api read_user sudo read_repository openid])
+ expect(json_response['scopes_supported']).to eq(%w[api read_user sudo read_repository openid profile email])
+ end
+ end
+
+ context 'Application with OpenID and email scopes' do
+ let(:application) { create :oauth_application, scopes: 'openid email' }
+
+ it 'token response includes an ID token' do
+ request_access_token!
+
+ expect(json_response).to include 'id_token'
+ end
+
+ context 'UserInfo payload' do
+ before do
+ request_user_info!
+ end
+
+ it 'includes the email and email_verified claims' do
+ expect(json_response.keys).to include('email', 'email_verified')
+ end
+
+ it 'has private email in email claim' do
+ expect(json_response['email']).to eq(user.email)
+ end
+
+ it 'has true in email_verified claim' do
+ expect(json_response['email_verified']).to eq(true)
+ end
end
end
end
diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb
new file mode 100644
index 00000000000..ee9c59e3f65
--- /dev/null
+++ b/spec/services/error_tracking/list_projects_service_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ErrorTracking::ListProjectsService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:token) { 'test-token' }
+ let(:new_api_host) { 'https://gitlab.com/' }
+ let(:new_token) { 'new-token' }
+ let(:params) { ActionController::Parameters.new(api_host: new_api_host, token: new_token) }
+
+ let(:error_tracking_setting) do
+ create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
+ end
+
+ subject { described_class.new(project, user, params) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ describe '#execute' do
+ let(:result) { subject.execute }
+
+ context 'with authorized user' do
+ before do
+ expect(project).to receive(:error_tracking_setting).at_least(:once)
+ .and_return(error_tracking_setting)
+ end
+
+ context 'set model attributes to new values' do
+ let(:new_api_url) { new_api_host + 'api/0/projects/' }
+
+ before do
+ expect(error_tracking_setting).to receive(:list_sentry_projects)
+ .and_return({ projects: [] })
+ end
+
+ it 'uses new api_url and token' do
+ subject.execute
+
+ expect(error_tracking_setting.api_url).to eq(new_api_url)
+ expect(error_tracking_setting.token).to eq(new_token)
+ error_tracking_setting.reload
+ expect(error_tracking_setting.api_url).to eq(sentry_url)
+ expect(error_tracking_setting.token).to eq(token)
+ end
+ end
+
+ context 'sentry client raises exception' do
+ before do
+ expect(error_tracking_setting).to receive(:list_sentry_projects)
+ .and_raise(Sentry::Client::Error, 'Sentry response error: 500')
+ end
+
+ it 'returns error response' do
+ expect(result[:message]).to eq('Sentry response error: 500')
+ expect(result[:http_status]).to eq(:bad_request)
+ end
+ end
+
+ context 'with invalid url' do
+ let(:params) do
+ ActionController::Parameters.new(
+ api_host: 'https://localhost',
+ token: new_token
+ )
+ end
+
+ before do
+ error_tracking_setting.enabled = false
+ end
+
+ it 'returns error' do
+ expect(result[:message]).to start_with('Api url is blocked')
+ expect(error_tracking_setting).not_to be_valid
+ end
+ end
+
+ context 'when list_sentry_projects returns projects' do
+ let(:projects) { [:list, :of, :projects] }
+
+ before do
+ expect(error_tracking_setting)
+ .to receive(:list_sentry_projects).and_return(projects: projects)
+ end
+
+ it 'returns the projects' do
+ expect(result).to eq(status: :success, projects: projects)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns error' do
+ expect(result).to include(status: :error, message: 'access denied')
+ end
+ end
+
+ context 'with error tracking disabled' do
+ before do
+ expect(project).to receive(:error_tracking_setting).at_least(:once)
+ .and_return(error_tracking_setting)
+ expect(error_tracking_setting)
+ .to receive(:list_sentry_projects).and_return(projects: [])
+
+ error_tracking_setting.enabled = false
+ end
+
+ it 'ignores enabled flag' do
+ expect(result).to include(status: :success, projects: [])
+ end
+ end
+
+ context 'error_tracking_setting is nil' do
+ let(:error_tracking_setting) { build(:project_error_tracking_setting) }
+ let(:new_api_url) { new_api_host + 'api/0/projects/' }
+
+ before do
+ expect(project).to receive(:build_error_tracking_setting).once
+ .and_return(error_tracking_setting)
+
+ expect(error_tracking_setting).to receive(:list_sentry_projects)
+ .and_return(projects: [:project1, :project2])
+ end
+
+ it 'builds a new error_tracking_setting' do
+ expect(project.error_tracking_setting).to be_nil
+
+ expect(result[:projects]).to eq([:project1, :project2])
+
+ expect(error_tracking_setting.api_url).to eq(new_api_url)
+ expect(error_tracking_setting.token).to eq(new_token)
+ expect(error_tracking_setting.enabled).to be true
+ expect(error_tracking_setting.persisted?).to be false
+ expect(error_tracking_setting.project_id).not_to be_nil
+
+ expect(project.error_tracking_setting).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index ef76e2311b1..931e47d3a77 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -471,6 +471,8 @@ describe Issues::UpdateService, :mailer do
it { expect(issue.tasks?).to eq(true) }
+ it_behaves_like 'updating a single task'
+
context 'when tasks are marked as completed' do
before do
update_issue(description: "- [x] Task 1\n- [X] Task 2")
@@ -543,76 +545,6 @@ describe Issues::UpdateService, :mailer do
end
end
- context 'when updating a single task' do
- before do
- update_issue(description: "- [ ] Task 1\n- [ ] Task 2")
- end
-
- it { expect(issue.tasks?).to eq(true) }
-
- context 'when a task is marked as completed' do
- before do
- update_issue(update_task: { index: 1, checked: true, line_source: '- [ ] Task 1', line_number: 1 })
- end
-
- it 'creates system note about task status change' do
- note1 = find_note('marked the task **Task 1** as completed')
-
- expect(note1).not_to be_nil
-
- description_notes = find_notes('description')
- expect(description_notes.length).to eq(1)
- end
- end
-
- context 'when a task is marked as incomplete' do
- before do
- update_issue(description: "- [x] Task 1\n- [X] Task 2")
- update_issue(update_task: { index: 2, checked: false, line_source: '- [X] Task 2', line_number: 2 })
- end
-
- it 'creates system note about task status change' do
- note1 = find_note('marked the task **Task 2** as incomplete')
-
- expect(note1).not_to be_nil
-
- description_notes = find_notes('description')
- expect(description_notes.length).to eq(1)
- end
- end
-
- context 'when the task position has been modified' do
- before do
- update_issue(description: "- [ ] Task 1\n- [ ] Task 3\n- [ ] Task 2")
- end
-
- it 'raises an exception' do
- expect(Note.count).to eq(2)
- expect do
- update_issue(update_task: { index: 2, checked: true, line_source: '- [ ] Task 2', line_number: 2 })
- end.to raise_error(ActiveRecord::StaleObjectError)
- expect(Note.count).to eq(2)
- end
- end
-
- context 'when the content changes but not task line number' do
- before do
- update_issue(description: "Paragraph\n\n- [ ] Task 1\n- [x] Task 2")
- update_issue(description: "Paragraph with more words\n\n- [ ] Task 1\n- [x] Task 2")
- update_issue(update_task: { index: 2, checked: false, line_source: '- [x] Task 2', line_number: 4 })
- end
-
- it 'creates system note about task status change' do
- note1 = find_note('marked the task **Task 2** as incomplete')
-
- expect(note1).not_to be_nil
-
- description_notes = find_notes('description')
- expect(description_notes.length).to eq(2)
- end
- end
- end
-
context 'updating labels' do
let(:label3) { create(:label, project: project) }
let(:result) { described_class.new(project, user, params).execute(issue).reload }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index be5ad849ba7..20580bf14b9 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -466,6 +466,8 @@ describe MergeRequests::UpdateService, :mailer do
it { expect(@merge_request.tasks?).to eq(true) }
+ it_behaves_like 'updating a single task'
+
context 'when tasks are marked as completed' do
before do
update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" })
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 54ce33dd103..d1b110b9806 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -116,7 +116,7 @@ describe Projects::CreateService, '#execute' do
def wiki_repo(project)
relative_path = ProjectWiki.new(project).disk_path + '.git'
- Gitlab::Git::Repository.new(project.repository_storage, relative_path, 'foobar')
+ Gitlab::Git::Repository.new(project.repository_storage, relative_path, 'foobar', project.full_path)
end
end
@@ -198,7 +198,7 @@ describe Projects::CreateService, '#execute' do
context 'with legacy storage' do
before do
- gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing")
+ gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing", 'group/project')
end
after do
@@ -234,7 +234,7 @@ describe Projects::CreateService, '#execute' do
end
before do
- gitlab_shell.create_repository(repository_storage, hashed_path)
+ gitlab_shell.create_repository(repository_storage, hashed_path, 'group/project')
end
after do
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 26e8d829345..23ec29cce7b 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -119,7 +119,7 @@ describe Projects::ForkService do
let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
before do
- gitlab_shell.create_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ gitlab_shell.create_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}", "#{@to_user.namespace.full_path}/#{@from_project.path}")
end
after do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 766276fdba3..aae50d5307f 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -201,7 +201,7 @@ describe Projects::TransferService do
before do
group.add_owner(user)
- unless gitlab_shell.create_repository(repository_storage, "#{group.full_path}/#{project.path}")
+ unless gitlab_shell.create_repository(repository_storage, "#{group.full_path}/#{project.path}", project.full_path)
raise 'failed to add repository'
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 8adfc63222e..90eaea9c872 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -232,7 +232,7 @@ describe Projects::UpdateService do
let(:project) { create(:project, :legacy_storage, :repository, creator: user, namespace: user.namespace) }
before do
- gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing")
+ gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing", user.namespace.full_path)
end
after do
diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb
index cc64dd25085..7c5480d382f 100644
--- a/spec/services/task_list_toggle_service_spec.rb
+++ b/spec/services/task_list_toggle_service_spec.rb
@@ -67,6 +67,17 @@ describe TaskListToggleService do
expect(toggler.execute).to be_falsey
end
+ it 'tolerates \r\n line endings' do
+ rn_markdown = markdown.gsub("\n", "\r\n")
+ toggler = described_class.new(rn_markdown, markdown_html,
+ toggle_as_checked: true,
+ line_source: '* [ ] Task 1', line_number: 1)
+
+ expect(toggler.execute).to be_truthy
+ expect(toggler.updated_markdown.lines[0]).to eq "* [x] Task 1\r\n"
+ expect(toggler.updated_markdown_html).to include('disabled checked> Task 1')
+ end
+
it 'returns false if markdown is nil' do
toggler = described_class.new(nil, markdown_html,
toggle_as_checked: false,
diff --git a/spec/support/shared_examples/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb
index 42f3b4db23c..c3d40c5b231 100644
--- a/spec/support/shared_examples/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/issuable_shared_examples.rb
@@ -36,3 +36,76 @@ shared_examples 'system notes for milestones' do
end
end
end
+
+shared_examples 'updating a single task' do
+ def update_issuable(opts)
+ issuable = try(:issue) || try(:merge_request)
+ described_class.new(project, user, opts).execute(issuable)
+ end
+
+ before do
+ update_issuable(description: "- [ ] Task 1\n- [ ] Task 2")
+ end
+
+ context 'when a task is marked as completed' do
+ before do
+ update_issuable(update_task: { index: 1, checked: true, line_source: '- [ ] Task 1', line_number: 1 })
+ end
+
+ it 'creates system note about task status change' do
+ note1 = find_note('marked the task **Task 1** as completed')
+
+ expect(note1).not_to be_nil
+
+ description_notes = find_notes('description')
+ expect(description_notes.length).to eq(1)
+ end
+ end
+
+ context 'when a task is marked as incomplete' do
+ before do
+ update_issuable(description: "- [x] Task 1\n- [X] Task 2")
+ update_issuable(update_task: { index: 2, checked: false, line_source: '- [X] Task 2', line_number: 2 })
+ end
+
+ it 'creates system note about task status change' do
+ note1 = find_note('marked the task **Task 2** as incomplete')
+
+ expect(note1).not_to be_nil
+
+ description_notes = find_notes('description')
+ expect(description_notes.length).to eq(1)
+ end
+ end
+
+ context 'when the task position has been modified' do
+ before do
+ update_issuable(description: "- [ ] Task 1\n- [ ] Task 3\n- [ ] Task 2")
+ end
+
+ it 'raises an exception' do
+ expect(Note.count).to eq(2)
+ expect do
+ update_issuable(update_task: { index: 2, checked: true, line_source: '- [ ] Task 2', line_number: 2 })
+ end.to raise_error(ActiveRecord::StaleObjectError)
+ expect(Note.count).to eq(2)
+ end
+ end
+
+ context 'when the content changes but not task line number' do
+ before do
+ update_issuable(description: "Paragraph\n\n- [ ] Task 1\n- [x] Task 2")
+ update_issuable(description: "Paragraph with more words\n\n- [ ] Task 1\n- [x] Task 2")
+ update_issuable(update_task: { index: 2, checked: false, line_source: '- [x] Task 2', line_number: 4 })
+ end
+
+ it 'creates system note about task status change' do
+ note1 = find_note('marked the task **Task 2** as incomplete')
+
+ expect(note1).not_to be_nil
+
+ description_notes = find_notes('description')
+ expect(description_notes.length).to eq(2)
+ end
+ end
+end
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index cb1b9e6f5fb..2a2539c80b5 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -7,56 +7,9 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
assign :project, project
end
- context 'when kubernetes is not active' do
- context 'when auto devops domain is not defined' do
- it 'shows warning message' do
- render
+ it 'shows a warning message about Kubernetes cluster' do
+ render
- expect(rendered).to have_css('.auto-devops-warning-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a')
- expect(rendered).to have_link('Kubernetes cluster')
- end
- end
-
- context 'when auto devops domain is defined' do
- before do
- project.build_auto_devops(domain: 'example.com')
- end
-
- it 'shows warning message' do
- render
-
- expect(rendered).to have_css('.auto-devops-warning-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a')
- expect(rendered).to have_link('Kubernetes cluster')
- end
- end
- end
-
- context 'when kubernetes is active' do
- before do
- create(:kubernetes_service, project: project)
- end
-
- context 'when auto devops domain is not defined' do
- it 'shows warning message' do
- render
-
- expect(rendered).to have_css('.auto-devops-warning-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
- end
- end
-
- context 'when auto devops domain is defined' do
- before do
- project.build_auto_devops(domain: 'example.com')
- end
-
- it 'does not show warning message' do
- render
-
- expect(rendered).not_to have_css('.auto-devops-warning-message')
- end
- end
+ expect(rendered).to have_text('You must add a Kubernetes cluster integration to this project with a domain in order for your deployment strategy to work correctly.')
end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 9176eb12b12..caae46a3175 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -141,11 +141,18 @@ describe PostReceive do
let(:gl_repository) { "wiki-#{project.id}" }
it 'updates project activity' do
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ # Force Project#set_timestamps_for_create to initialize timestamps
+ project
- expect { project.reload }
- .to change(project, :last_activity_at)
- .and change(project, :last_repository_updated_at)
+ # MySQL drops milliseconds in the timestamps, so advance at least
+ # a second to ensure we see changes.
+ Timecop.freeze(1.second.from_now) do
+ expect do
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ project.reload
+ end.to change(project, :last_activity_at)
+ .and change(project, :last_repository_updated_at)
+ end
end
end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 781f91ac9ca..31bfe88d0bd 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -24,12 +24,7 @@ describe RepositoryForkWorker do
end
def expect_fork_repository
- expect(shell).to receive(:fork_repository).with(
- 'default',
- project.disk_path,
- forked_project.repository_storage,
- forked_project.disk_path
- )
+ expect(shell).to receive(:fork_repository).with(project, forked_project)
end
describe 'when a worker was reset without cleanup' do