summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml10
-rw-r--r--.rubocop.yml1
-rw-r--r--.rubocop_todo.yml4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/images/illustrations/pending_job_empty.svg1
-rw-r--r--app/assets/javascripts/api.js2
-rw-r--r--app/assets/javascripts/behaviors/secret_values.js21
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue4
-rw-r--r--app/assets/javascripts/dispatcher.js5
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js4
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue110
-rw-r--r--app/assets/javascripts/pages/milestones/shared/event_hub.js3
-rw-r--r--app/assets/javascripts/pages/milestones/shared/index.js88
-rw-r--r--app/assets/javascripts/pages/milestones/shared/init_milestones_show.js (renamed from app/assets/javascripts/pages/init_milestones_show.js)0
-rw-r--r--app/assets/javascripts/pages/projects/milestones/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/milestones/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js8
-rw-r--r--app/assets/javascripts/render_math.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue57
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/modal.scss1
-rw-r--r--app/assets/stylesheets/pages/commits.scss8
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/helpers/webpack_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/deployment.rb9
-rw-r--r--app/models/merge_request.rb8
-rw-r--r--app/models/repository.rb83
-rw-r--r--app/views/profiles/show.html.haml4
-rw-r--r--app/views/projects/_readme.html.haml8
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml13
-rw-r--r--app/views/projects/commit/_other_user_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/_same_user_different_email_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/_unverified_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/_verified_signature_badge.html.haml2
-rw-r--r--app/views/projects/empty.html.haml9
-rw-r--r--app/views/projects/jobs/show.html.haml4
-rw-r--r--app/views/projects/milestones/index.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml14
-rw-r--r--app/views/shared/icons/_icon_status_notfound_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_success_borderless.svg1
-rw-r--r--app/views/shared/milestones/_milestone.html.haml13
-rw-r--r--app/views/users/show.html.haml6
-rw-r--r--app/workers/repository_check/single_repository_worker.rb5
-rw-r--r--changelogs/unreleased/40028-special-characters-on-issuable-templates.yml5
-rw-r--r--changelogs/unreleased/42220-add-pending-empty-state.yml5
-rw-r--r--changelogs/unreleased/42285-not-found-status-icon.yml5
-rw-r--r--changelogs/unreleased/default-to-https-for-gravatar-urls.yml5
-rw-r--r--changelogs/unreleased/disable-throwOnError-in-katex.yml5
-rw-r--r--changelogs/unreleased/feat-add-section-headers-to-project-repo-buttons.yml5
-rw-r--r--changelogs/unreleased/gitaly-repo-exists.yml5
-rw-r--r--changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml5
-rw-r--r--changelogs/unreleased/update-node-docs.yml5
-rw-r--r--changelogs/unreleased/ux-guide-deprecation.yml6
-rw-r--r--changelogs/unreleased/winh-delete-milestone-modal.yml5
-rw-r--r--config/gitlab.yml.example8
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/initializers/rugged_use_gitlab_git_attributes.rb28
-rw-r--r--config/routes/project.rb2
-rw-r--r--doc/development/ux_guide/index.md2
-rw-r--r--doc/update/10.2-to-10.3.md7
-rw-r--r--doc/update/10.3-to-10.4.md7
-rw-r--r--features/project/issues/milestones.feature7
-rw-r--r--features/steps/project/issues/milestones.rb9
-rw-r--r--features/steps/user.rb38
-rw-r--r--features/support/capybara_helpers.rb10
-rw-r--r--features/user.feature86
-rw-r--r--lib/api/entities.rb11
-rw-r--r--lib/gitlab/bare_repository_import/repository.rb2
-rw-r--r--lib/gitlab/git/attributes_at_ref_parser.rb14
-rw-r--r--lib/gitlab/git/attributes_parser.rb (renamed from lib/gitlab/git/attributes.rb)53
-rw-r--r--lib/gitlab/git/blob.rb4
-rw-r--r--lib/gitlab/git/info_attributes.rb49
-rw-r--r--lib/gitlab/git/repository.rb157
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb20
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb25
-rw-r--r--lib/gitlab/github_import/importer/pull_requests_importer.rb5
-rw-r--r--lib/gitlab/import_export/command_line_util.rb5
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb2
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--package.json2
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock2
-rw-r--r--qa/qa.rb3
-rw-r--r--qa/qa/factory/resource/deploy_key.rb6
-rw-r--r--qa/qa/factory/resource/secret_variable.rb41
-rw-r--r--qa/qa/page/base.rb16
-rw-r--r--qa/qa/page/menu/side.rb4
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb7
-rw-r--r--qa/qa/page/project/settings/common.rb2
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb22
-rw-r--r--qa/qa/page/project/settings/secret_variables.rb57
-rw-r--r--qa/qa/runtime/rsa_key.rb21
-rw-r--r--qa/qa/runtime/user.rb11
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb8
-rw-r--r--qa/qa/specs/features/project/add_secret_variable_spec.rb19
-rw-r--r--qa/spec/runtime/rsa_key.rb9
-rwxr-xr-xscripts/lint-rugged36
-rwxr-xr-xscripts/static-analysis3
-rw-r--r--spec/features/user_page_spec.rb107
-rw-r--r--spec/fixtures/emails/attachment.eml28
-rw-r--r--spec/helpers/application_helper_spec.rb2
-rw-r--r--spec/initializers/settings_spec.rb2
-rw-r--r--spec/javascripts/api_spec.js4
-rw-r--r--spec/javascripts/behaviors/secret_values_spec.js110
-rw-r--r--spec/javascripts/environments/environment_item_spec.js4
-rw-r--r--spec/javascripts/fixtures/projects.json2
-rw-r--r--spec/javascripts/helpers/user_mock_data_helper.js2
-rw-r--r--spec/javascripts/jobs/mock_data.js8
-rw-r--r--spec/javascripts/notes/mock_data.js2
-rw-r--r--spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js95
-rw-r--r--spec/javascripts/sidebar/mock_data.js34
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js44
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js33
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js34
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js10
-rw-r--r--spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb28
-rw-r--r--spec/lib/gitlab/git/attributes_parser_spec.rb (renamed from spec/lib/gitlab/git/attributes_spec.rb)53
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb15
-rw-r--r--spec/lib/gitlab/git/info_attributes_spec.rb43
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb79
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb14
-rw-r--r--spec/models/repository_spec.rb28
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--yarn.lock6
154 files changed, 1693 insertions, 679 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 349ea49fe8f..c1d78ef2d48 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -388,7 +388,6 @@ spinach-mysql 2 3: *spinach-metadata-mysql
# Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis
- <<: *pull-cache
variables:
SIMPLECOV: "false"
SETUP_DB: "false"
@@ -409,6 +408,12 @@ static-analysis:
stage: test
script:
- scripts/static-analysis
+ cache:
+ key: "ruby-2.3.6-with-yarn-and-rubocop"
+ paths:
+ - vendor/ruby
+ - .yarn-cache/
+ - tmp/rubocop_cache
# Documentation checks:
# - Check validity of relative links
@@ -717,8 +722,6 @@ pages:
cache gems:
<<: *dedicated-runner
<<: *pull-cache
- only:
- - tags
variables:
SETUP_DB: "false"
script:
@@ -729,6 +732,7 @@ cache gems:
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
+ - tags
gitlab_git_test:
<<: *dedicated-runner
diff --git a/.rubocop.yml b/.rubocop.yml
index 9adc2fae7a8..563a00db6c0 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -17,6 +17,7 @@ AllCops:
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
+ CacheRootDirectory: tmp
# Gitlab ###################################################################
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 8d2276f71be..442d61bcf4f 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -342,10 +342,6 @@ RSpec/SharedContext:
Exclude:
- 'spec/features/admin/admin_groups_spec.rb'
-# Offense count: 90
-RSpec/SingleLineHook:
- Enabled: false
-
# Offense count: 5
RSpec/VoidExpect:
Exclude:
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 31b648bd6fa..b7c0622b4f4 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.73.0
+0.74.0
diff --git a/Gemfile b/Gemfile
index 346182b3852..ee576c53fe9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -406,7 +406,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.76.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.78.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 1cbeab8d6b5..5532888d179 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -285,7 +285,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.76.0)
+ gitaly-proto (0.78.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -304,7 +304,7 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.3)
- gitlab-styles (2.3.0)
+ gitlab-styles (2.3.1)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
@@ -1056,7 +1056,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.76.0)
+ gitaly-proto (~> 0.78.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
diff --git a/app/assets/images/illustrations/pending_job_empty.svg b/app/assets/images/illustrations/pending_job_empty.svg
new file mode 100644
index 00000000000..8de695afa18
--- /dev/null
+++ b/app/assets/images/illustrations/pending_job_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="430" height="200" viewBox="0 0 430 200"><g fill="none" fill-rule="evenodd"><g transform="translate(138 65)"><path fill="#E5E5E5" fill-rule="nonzero" d="M35 70a2 2 0 1 1 0-4c2.542 0 5.042-.305 7.463-.904a2 2 0 1 1 .96 3.884A35.075 35.075 0 0 1 35 70zm18.21-5.105a2 2 0 1 1-2.083-3.414 31.143 31.143 0 0 0 5.896-4.664 2 2 0 1 1 2.842 2.815 35.143 35.143 0 0 1-6.654 5.263zM66.106 51.06a2 2 0 0 1-3.552-1.838 30.77 30.77 0 0 0 2.612-7.042 2 2 0 1 1 3.892.922 34.77 34.77 0 0 1-2.952 7.958zm3.816-18.433a2 2 0 1 1-3.991.268 30.873 30.873 0 0 0-1.407-7.38 2 2 0 0 1 3.808-1.223 34.873 34.873 0 0 1 1.59 8.335zm-6.346-17.842a2 2 0 0 1-3.264 2.312 31.188 31.188 0 0 0-5.054-5.564 2 2 0 0 1 2.615-3.027 35.188 35.188 0 0 1 5.703 6.279zM48.895 2.867a2 2 0 0 1-1.59 3.67 30.758 30.758 0 0 0-7.206-2.12 2 2 0 1 1 .653-3.946 34.758 34.758 0 0 1 8.143 2.396zM30.263.318a2 2 0 0 1 .537 3.964c-2.505.339-4.94.98-7.266 1.907a2 2 0 1 1-1.48-3.716A34.774 34.774 0 0 1 30.263.318zM12.907 7.853a2 2 0 0 1 2.527 3.1 31.196 31.196 0 0 0-5.213 5.416 2 2 0 0 1-3.196-2.406 35.196 35.196 0 0 1 5.882-6.11zM1.99 23.343a2 2 0 0 1 3.772 1.331 30.82 30.82 0 0 0-1.619 7.337 2 2 0 1 1-3.982-.38 34.82 34.82 0 0 1 1.829-8.289zM.719 42.086a2 2 0 1 1 3.917-.806 30.757 30.757 0 0 0 2.4 7.118 2 2 0 1 1-3.605 1.73 34.757 34.757 0 0 1-2.713-8.042zM9.393 58.86a2 2 0 0 1 2.926-2.728 31.167 31.167 0 0 0 5.751 4.841 2 2 0 1 1-2.187 3.349 35.167 35.167 0 0 1-6.49-5.462zm16.245 9.873a2 2 0 1 1 1.067-3.855 30.979 30.979 0 0 0 7.434 1.11 2 2 0 1 1-.11 3.998 34.979 34.979 0 0 1-8.391-1.253z"/><circle cx="35" cy="35" r="16" stroke="#E1DBF1" stroke-width="4"/><path fill="#6B4FBB" d="M37 33h5a2 2 0 1 1 0 4h-7a2 2 0 0 1-2-2v-8a2 2 0 1 1 4 0v6z"/></g><g transform="translate(247 30)"><rect width="116" height="135" y="5" fill="#F9F9F9" rx="10"/><rect width="116" height="134" x="5" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="10"/><g transform="translate(23 23)"><rect width="16" height="4" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="32" y="12" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="44" fill="#EEE" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="32" y="36" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="48" fill="#E1DBF1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#FC6D26" rx="2"/><rect width="4" height="4" x="56" y="36" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="64" y="60" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="72" y="60" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="28" height="4" y="36" fill="#EEE" rx="2"/><rect width="28" height="4" x="44" y="48" fill="#EEE" rx="2"/><rect width="28" height="4" x="32" y="60" fill="#EFEDF8" rx="2"/><rect width="28" height="4" y="12" fill="#6B4FBB" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#C3B8E3" rx="2"/><rect width="8" height="4" y="24" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6B4FBB" rx="2"/><rect width="12" height="4" y="48" fill="#FC6D26" rx="2"/><rect width="12" height="4" y="60" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="60" fill="#FEF0E8" rx="2"/></g><g transform="translate(23 95)"><rect width="16" height="4" fill="#EFEDF8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#FC6D26" rx="2"/><rect width="16" height="4" x="44" fill="#6B4FBB" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="38" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="14" height="4" y="12" fill="#EEE" rx="2"/></g></g><path fill="#FC6D26" fill-rule="nonzero" d="M81 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15zm-5-20a2 2 0 0 1 2 2v6a2 2 0 1 1-4 0v-6a2 2 0 0 1 2-2zm10 0a2 2 0 0 1 2 2v6a2 2 0 1 1-4 0v-6a2 2 0 0 1 2-2z"/><path fill="#E5E5E5" fill-rule="nonzero" d="M108 102c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm93 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 38c67b5f04e..7cb81bf4d5b 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -178,7 +178,7 @@ const Api = {
issueTemplate(namespacePath, projectPath, key, type, callback) {
const url = Api.buildUrl(Api.issuableTemplatePath)
- .replace(':key', key)
+ .replace(':key', encodeURIComponent(key))
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js
index 1cf0b960eb0..7f70fce913a 100644
--- a/app/assets/javascripts/behaviors/secret_values.js
+++ b/app/assets/javascripts/behaviors/secret_values.js
@@ -2,18 +2,19 @@ import { n__ } from '../locale';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
export default class SecretValues {
- constructor(container) {
+ constructor({
+ container,
+ valueSelector = '.js-secret-value',
+ placeholderSelector = '.js-secret-value-placeholder',
+ }) {
this.container = container;
+ this.valueSelector = valueSelector;
+ this.placeholderSelector = placeholderSelector;
}
init() {
- this.values = this.container.querySelectorAll('.js-secret-value');
- this.placeholders = this.container.querySelectorAll('.js-secret-value-placeholder');
this.revealButton = this.container.querySelector('.js-secret-value-reveal-button');
- this.revealText = n__('Reveal value', 'Reveal values', this.values.length);
- this.hideText = n__('Hide value', 'Hide values', this.values.length);
-
const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus);
this.updateDom(isRevealed);
@@ -28,15 +29,17 @@ export default class SecretValues {
}
updateDom(isRevealed) {
- this.values.forEach((value) => {
+ const values = this.container.querySelectorAll(this.valueSelector);
+ values.forEach((value) => {
value.classList.toggle('hide', !isRevealed);
});
- this.placeholders.forEach((placeholder) => {
+ const placeholders = this.container.querySelectorAll(this.placeholderSelector);
+ placeholders.forEach((placeholder) => {
placeholder.classList.toggle('hide', isRevealed);
});
- this.revealButton.textContent = isRevealed ? this.hideText : this.revealText;
+ this.revealButton.textContent = isRevealed ? n__('Hide value', 'Hide values', values.length) : n__('Reveal value', 'Reveal values', values.length);
this.revealButton.dataset.secretRevealStatus = isRevealed;
}
}
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 843564ce016..c6091efd62f 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -53,10 +53,10 @@
</i>
</div>
<div class="deploy-key-content key-list-item-info">
- <strong class="title">
+ <strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
- <div class="description">
+ <div class="description qa-key-fingerprint">
{{ deployKey.fingerprint }}
</div>
</div>
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 89fc002d4ce..262ed3783fb 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -89,6 +89,11 @@ import SearchAutocomplete from './search_autocomplete';
.then(callDefault)
.catch(fail);
break;
+ case 'projects:milestones:index':
+ import('./pages/projects/milestones/index')
+ .then(callDefault)
+ .catch(fail);
+ break;
case 'projects:milestones:show':
import('./pages/projects/milestones/show')
.then(callDefault)
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index 0c3ce848e3d..c9a18353f2e 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,3 +1,3 @@
-import initMilestonesShow from '~/pages/init_milestones_show';
+import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
export default initMilestonesShow;
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index c4691cd367c..f26c7360fbe 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -3,7 +3,9 @@ import SecretValues from '~/behaviors/secret_values';
export default () => {
const secretVariableTable = document.querySelector('.js-secret-variable-table');
if (secretVariableTable) {
- const secretVariableTableValues = new SecretValues(secretVariableTable);
+ const secretVariableTableValues = new SecretValues({
+ container: secretVariableTable,
+ });
secretVariableTableValues.init();
}
};
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
new file mode 100644
index 00000000000..c43e0a0490f
--- /dev/null
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -0,0 +1,110 @@
+<script>
+ import axios from '~/lib/utils/axios_utils';
+
+ import Flash from '~/flash';
+ import modal from '~/vue_shared/components/modal.vue';
+ import { n__, s__, sprintf } from '~/locale';
+ import { redirectTo } from '~/lib/utils/url_utility';
+ import eventHub from '../event_hub';
+
+ export default {
+ components: {
+ modal,
+ },
+ props: {
+ issueCount: {
+ type: Number,
+ required: true,
+ },
+ mergeRequestCount: {
+ type: Number,
+ required: true,
+ },
+ milestoneId: {
+ type: Number,
+ required: true,
+ },
+ milestoneTitle: {
+ type: String,
+ required: true,
+ },
+ milestoneUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ text() {
+ const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle });
+
+ if (this.issueCount === 0 && this.mergeRequestCount === 0) {
+ return sprintf(
+ s__(`Milestones|
+You’re about to permanently delete the milestone %{milestoneTitle} from this project.
+%{milestoneTitle} is not currently used in any issues or merge requests.`),
+ {
+ milestoneTitle,
+ },
+ false,
+ );
+ }
+
+ return sprintf(
+ s__(`Milestones|
+You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
+Once deleted, it cannot be undone or recovered.`),
+ {
+ milestoneTitle,
+ issuesWithCount: n__('%d issue', '%d issues', this.issueCount),
+ mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount),
+ },
+ false,
+ );
+ },
+ title() {
+ return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle });
+ },
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl);
+
+ return axios.delete(this.milestoneUrl)
+ .then((response) => {
+ eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true });
+
+ // follow the rediect to milestones overview page
+ redirectTo(response.request.responseURL);
+ })
+ .catch((error) => {
+ eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false });
+
+ if (error.response && error.response.status === 404) {
+ Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle }));
+ } else {
+ Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle }));
+ }
+ throw error;
+ });
+ },
+ },
+ };
+</script>
+
+<template>
+ <modal
+ id="delete-milestone-modal"
+ :title="title"
+ :text="text"
+ kind="danger"
+ :primary-button-label="s__('Milestones|Delete milestone')"
+ @submit="onSubmit">
+
+ <template
+ slot="body"
+ slot-scope="props">
+ <p v-html="props.text"></p>
+ </template>
+
+ </modal>
+</template>
diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/pages/milestones/shared/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/pages/milestones/shared/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js
new file mode 100644
index 00000000000..327e2cf569c
--- /dev/null
+++ b/app/assets/javascripts/pages/milestones/shared/index.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+
+import Translate from '~/vue_shared/translate';
+
+import deleteMilestoneModal from './components/delete_milestone_modal.vue';
+import eventHub from './event_hub';
+
+export default () => {
+ Vue.use(Translate);
+
+ const onRequestFinished = ({ milestoneUrl, successful }) => {
+ const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+
+ if (!successful) {
+ button.removeAttribute('disabled');
+ }
+
+ button.querySelector('.js-loading-icon').classList.add('hidden');
+ };
+
+ const onRequestStarted = (milestoneUrl) => {
+ const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+ button.setAttribute('disabled', '');
+ button.querySelector('.js-loading-icon').classList.remove('hidden');
+ eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
+ };
+
+ const onDeleteButtonClick = (event) => {
+ const button = event.currentTarget;
+ const modalProps = {
+ milestoneId: parseInt(button.dataset.milestoneId, 10),
+ milestoneTitle: button.dataset.milestoneTitle,
+ milestoneUrl: button.dataset.milestoneUrl,
+ issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
+ mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
+ };
+ eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
+ eventHub.$emit('deleteMilestoneModal.props', modalProps);
+ };
+
+ const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
+ for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
+ const button = deleteMilestoneButtons[i];
+ button.addEventListener('click', onDeleteButtonClick);
+ }
+
+ eventHub.$once('deleteMilestoneModal.mounted', () => {
+ for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
+ const button = deleteMilestoneButtons[i];
+ button.removeAttribute('disabled');
+ }
+ });
+
+ return new Vue({
+ el: '#delete-milestone-modal',
+ components: {
+ deleteMilestoneModal,
+ },
+ data() {
+ return {
+ modalProps: {
+ milestoneId: -1,
+ milestoneTitle: '',
+ milestoneUrl: '',
+ issueCount: -1,
+ mergeRequestCount: -1,
+ },
+ };
+ },
+ mounted() {
+ eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
+ eventHub.$emit('deleteMilestoneModal.mounted');
+ },
+ beforeDestroy() {
+ eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
+ },
+ methods: {
+ setModalProps(modalProps) {
+ this.modalProps = modalProps;
+ },
+ },
+ render(createElement) {
+ return createElement(deleteMilestoneModal, {
+ props: this.modalProps,
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/init_milestones_show.js b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js
index 7aa5be0d5b9..7aa5be0d5b9 100644
--- a/app/assets/javascripts/pages/init_milestones_show.js
+++ b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js
diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js
new file mode 100644
index 00000000000..8fb4d83d8a3
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/milestones/index/index.js
@@ -0,0 +1,3 @@
+import milestones from '~/pages/milestones/shared';
+
+export default milestones;
diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js
index 0c3ce848e3d..35b5c9c2ced 100644
--- a/app/assets/javascripts/pages/projects/milestones/show/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/show/index.js
@@ -1,3 +1,7 @@
-import initMilestonesShow from '~/pages/init_milestones_show';
+import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
+import milestones from '~/pages/milestones/shared';
-export default initMilestonesShow;
+export default () => {
+ initMilestonesShow();
+ milestones();
+};
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 94b927a1548..18dc1dc03a5 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -6,13 +6,17 @@ export default function () {
initSettingsPanels();
const runnerToken = document.querySelector('.js-secret-runner-token');
if (runnerToken) {
- const runnerTokenSecretValue = new SecretValues(runnerToken);
+ const runnerTokenSecretValue = new SecretValues({
+ container: runnerToken,
+ });
runnerTokenSecretValue.init();
}
const secretVariableTable = document.querySelector('.js-secret-variable-table');
if (secretVariableTable) {
- const secretVariableTableValues = new SecretValues(secretVariableTable);
+ const secretVariableTableValues = new SecretValues({
+ container: secretVariableTable,
+ });
secretVariableTableValues.init();
}
}
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index a759992cd54..15205d8a4e2 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -18,7 +18,7 @@ function renderWithKaTeX(elements) {
const display = $this.attr('data-math-style') === 'display';
try {
- katex.render($this.text(), mathNode.get(0), { displayMode: display });
+ katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false });
mathNode.insertAfter($this);
$this.remove();
} catch (err) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index d48f3a01420..d174a900f63 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -2,7 +2,7 @@ import { getTimeago } from '~/lib/utils/datetime_utility';
import { visitUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
-import StatusIcon from './mr_widget_status_icon';
+import StatusIcon from './mr_widget_status_icon.vue';
import MRWidgetService from '../services/mr_widget_service';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
deleted file mode 100644
index eeb990908f6..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import ciIcon from '../../vue_shared/components/ci_icon.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-
-export default {
- props: {
- status: { type: String, required: true },
- showDisabledButton: { type: Boolean, required: false },
- },
- components: {
- ciIcon,
- loadingIcon,
- },
- computed: {
- statusObj() {
- return {
- group: this.status,
- icon: `status_${this.status}`,
- };
- },
- },
- template: `
- <div class="space-children flex-container-block append-right-10">
- <div v-if="status === 'loading'" class="mr-widget-icon">
- <loading-icon />
- </div>
- <ci-icon v-else :status="statusObj" />
- <button
- v-if="showDisabledButton"
- type="button"
- class="js-disabled-merge-button btn btn-success btn-sm"
- disabled="true">
- Merge
- </button>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
new file mode 100644
index 00000000000..1fdc3218671
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -0,0 +1,57 @@
+<script>
+ import ciIcon from '../../vue_shared/components/ci_icon.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+ export default {
+ components: {
+ ciIcon,
+ loadingIcon,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ showDisabledButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.status === 'loading';
+ },
+ statusObj() {
+ return {
+ group: this.status,
+ icon: `status_${this.status}`,
+ };
+ },
+ },
+ };
+</script>
+<template>
+ <div class="space-children flex-container-block append-right-10">
+ <div
+ v-if="isLoading"
+ class="mr-widget-icon"
+ >
+ <loading-icon />
+ </div>
+
+ <ci-icon
+ v-else
+ :status="statusObj"
+ />
+
+ <button
+ v-if="showDisabledButton"
+ type="button"
+ class="js-disabled-merge-button btn btn-success btn-sm"
+ disabled="true"
+ >
+ {{ s__("mrWidget|Merge") }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
index afa9cc57544..cfbd44d41b2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
@@ -1,5 +1,5 @@
<script>
- import statusIcon from '../mr_widget_status_icon';
+ import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetArchived',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index 77dd243d617..40c3cb500bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -1,7 +1,7 @@
<script>
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import eventHub from '../../event_hub';
- import statusIcon from '../mr_widget_status_icon';
+ import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetAutoMergeFailed',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
index 04e1766b8c7..caeaac75b45 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
@@ -1,5 +1,5 @@
<script>
- import statusIcon from '../mr_widget_status_icon';
+ import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetChecking',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 8c0ce43c76c..71bfdaf801e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,6 +1,6 @@
<script>
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
- import statusIcon from '../mr_widget_status_icon';
+ import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetClosed',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 13b07f82330..dad4b0fe49d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,5 +1,5 @@
<script>
- import statusIcon from '../mr_widget_status_icon';
+ import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetConflicts',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
index fc5f18695b7..76b0235af1b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index bd349111bbd..357485b9e78 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -1,5 +1,5 @@
import Flash from '../../../flash';
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index ba9681680ef..7f8d78cab73 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -2,7 +2,7 @@ import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js
deleted file mode 100644
index f6d1a4feeb2..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import statusIcon from '../mr_widget_status_icon';
-
-export default {
- name: 'MRWidgetMerging',
- props: {
- mr: { type: Object, required: true },
- },
- components: {
- statusIcon,
- },
- template: `
- <div class="mr-widget-body mr-state-locked media">
- <status-icon status="loading" />
- <div class="media-body">
- <h4>
- This merge request is in the process of being merged
- </h4>
- <section class="mr-info-list">
- <p>
- The changes will be merged into
- <span class="label-branch">
- <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
- </span>
- </p>
- </section>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
new file mode 100644
index 00000000000..953ddf40a51
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
@@ -0,0 +1,35 @@
+<script>
+ import statusIcon from '../mr_widget_status_icon.vue';
+
+ export default {
+ name: 'MRWidgetMerging',
+ components: {
+ statusIcon,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+ };
+</script>
+<template>
+ <div class="mr-widget-body mr-state-locked media">
+ <status-icon status="loading" />
+ <div class="media-body">
+ <h4>
+ {{ s__("mrWidget|This merge request is in the process of being merged") }}
+ </h4>
+ <section class="mr-info-list">
+ <p>
+ {{ s__("mrWidget|The changes will be merged into") }}
+ <span class="label-branch">
+ <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
+ </span>
+ </p>
+ </section>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 16ff1109e3f..303877d6fbf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
index 00047718201..cea3d97fa88 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
index 2c84f423ee2..e66ce071ab4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
index cbaa73deffa..4d9a2ca530f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index e51eef07093..7ba6c29006a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -3,7 +3,7 @@ import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 52dd0245ff0..2968af0d5cb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,7 +1,7 @@
<script>
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
- import statusIcon from '../mr_widget_status_icon';
+ import statusIcon from '../mr_widget_status_icon.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Flash from '../../../flash';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
index 46687cc85e1..142ddf477f1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetSHAMismatch',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
index 97b1940f4be..67b271c69ca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetUnresolvedDiscussions',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index b4b0f00445c..bbca641f65e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -1,4 +1,4 @@
-import statusIcon from '../mr_widget_status_icon';
+import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 5e8e251428a..8651945a3da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -19,7 +19,7 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li
export { default as MergedState } from './components/states/mr_widget_merged';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as ClosedState } from './components/states/mr_widget_closed.vue';
-export { default as MergingState } from './components/states/mr_widget_merging';
+export { default as MergingState } from './components/states/mr_widget_merging.vue';
export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index fd5c3c81a53..2d015ef086b 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -20,7 +20,7 @@
width: 100%;
}
- $image-widths: 250 306 394;
+ $image-widths: 250 306 394 430;
@each $width in $image-widths {
&.svg-#{$width} {
img,
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 51ae09777fd..32b9894ae04 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -9,6 +9,7 @@
.modal-body {
background-color: $modal-body-bg;
+ line-height: $line-height-base;
min-height: $modal-body-height;
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 0cba223c6a6..aeaa33bd3bd 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -310,8 +310,8 @@
}
&.invalid {
- @include status-color($gray-dark, $gray, $common-gray-dark);
- border-color: $common-gray-light;
+ @include status-color($gray-dark, $gray, $gray-darkest);
+ border-color: $gray-darkest;
}
}
@@ -335,8 +335,8 @@
&.invalid {
svg {
- border: 1px solid $common-gray-light;
- fill: $common-gray-light;
+ border: 1px solid $gray-darkest;
+ fill: $gray-darkest;
}
}
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 0f70efbce40..75b17d05e22 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -83,7 +83,7 @@ class Projects::MilestonesController < Projects::ApplicationController
Milestones::DestroyService.new(project, current_user).execute(milestone)
respond_to do |format|
- format.html { redirect_to namespace_project_milestones_path, status: 302 }
+ format.html { redirect_to namespace_project_milestones_path, status: 303 }
format.js { head :ok }
end
end
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 94887c2cbd2..77433acb92a 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -2,7 +2,7 @@ require 'webpack/rails/manifest'
module WebpackHelper
def webpack_bundle_tag(bundle, force_same_domain: false)
- javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
+ javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain))
end
# override webpack-rails gem helper until changes can make it upstream
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d7153d7b816..f84bf132854 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -524,7 +524,7 @@ module Ci
return unless sha
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
- rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal
+ rescue GRPC::NotFound, GRPC::Internal
nil
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 7bcded5b5e1..3aed071dd49 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -45,14 +45,7 @@ class Deployment < ActiveRecord::Base
def includes_commit?(commit)
return false unless commit
- # Before 8.10, deployments didn't have keep-around refs. Any deployment
- # created before then could have a `sha` referring to a commit that no
- # longer exists in the repository, so just ignore those.
- begin
- project.repository.ancestor?(commit.id, sha)
- rescue Rugged::OdbError
- false
- end
+ project.repository.ancestor?(commit.id, sha)
end
def update_merge_request_metrics!
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 8028ff3875b..4accb08eaf9 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -989,8 +989,14 @@ class MergeRequest < ActiveRecord::Base
merged_at = metrics&.merged_at
notes_association = notes_with_associations
+ # It is not guaranteed that Note#created_at will be strictly later than
+ # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
+ # comparison, as will a HA environment if clocks are not *precisely*
+ # synchronized. Add a minute's leeway to compensate for both possibilities
+ cutoff = merged_at - 1.minute
+
if merged_at
- notes_association = notes_association.where('created_at > ?', merged_at)
+ notes_association = notes_association.where('created_at >= ?', cutoff)
end
!merge_commit.has_been_reverted?(current_user, notes_association)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 824e18bec78..5b06dc5a39b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -20,7 +20,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
- delegate :bundle_to_disk, to: :raw_repository
+ delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository
CreateTreeError = Class.new(StandardError)
@@ -166,16 +166,10 @@ class Repository
return []
end
- raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled|
- commits =
- if is_enabled
- find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- else
- find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- end
-
- CommitCollection.new(project, commits, ref)
+ commits = raw_repository.find_commits_by_message(query, ref, path, limit, offset).map do |c|
+ commit(c)
end
+ CommitCollection.new(project, commits, ref)
end
def find_branch(name, fresh_repo: true)
@@ -740,23 +734,6 @@ class Repository
Commit.order_by(collection: commits, order_by: order_by, sort: sort)
end
- def refs_contains_sha(ref_type, sha)
- args = %W(#{ref_type} --contains #{sha})
- names = run_git(args).first
-
- if names.respond_to?(:split)
- names = names.split("\n").map(&:strip)
-
- names.each do |name|
- name.slice! '* '
- end
-
- names
- else
- []
- end
- end
-
def branch_names_contains(sha)
refs_contains_sha('branch', sha)
end
@@ -921,25 +898,6 @@ class Repository
end
end
- def search_files_by_content(query, ref)
- return [] if empty? || query.blank?
-
- offset = 2
- args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
-
- run_git(args).first.scrub.split(/^--$/)
- end
-
- def search_files_by_name(query, ref)
- safe_query = Regexp.escape(query.sub(/^\/*/, ""))
-
- return [] if empty? || safe_query.blank?
-
- args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{safe_query})
-
- run_git(args).first.lines.map(&:strip)
- end
-
def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil)
unless remote_name
remote_name = "tmp-#{SecureRandom.hex}"
@@ -973,6 +931,18 @@ class Repository
raw_repository.ls_files(actual_ref)
end
+ def search_files_by_content(query, ref)
+ return [] if empty? || query.blank?
+
+ raw_repository.search_files_by_content(query, ref)
+ end
+
+ def search_files_by_name(query, ref)
+ return [] if empty?
+
+ raw_repository.search_files_by_name(query, ref)
+ end
+
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
@@ -1133,25 +1103,4 @@ class Repository
def rugged_can_be_merged?(their_commit, our_commit)
!rugged.merge_commits(our_commit, their_commit).conflicts?
end
-
- def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- ref ||= root_ref
-
- args = %W(
- log #{ref} --pretty=%H --skip #{offset}
- --max-count #{limit} --grep=#{query} --regexp-ignore-case
- )
- args = args.concat(%W(-- #{path})) if path.present?
-
- git_log_results = run_git(args).first.lines
-
- git_log_results.map { |c| commit(c.chomp) }.compact
- end
-
- def find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- raw_repository
- .gitaly_commit_client
- .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
- .map { |c| commit(c) }
- end
end
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 0f773933ac2..5c76d2d8f51 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -13,11 +13,11 @@
- if @user.avatar?
You can change your avatar here
- if gravatar_enabled?
- or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host}
+ or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
- else
You can upload an avatar here
- if gravatar_enabled?
- or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host}
+ or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
.col-lg-8
.clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 32901d30b96..aebdfbc8218 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -9,15 +9,15 @@
- else
.row-content-block.second-block.center
- %h3.page-title
+ %h4
This project does not have a README yet
+
- if can?(current_user, :push_code, @project)
%p
A
%code README
file contains information about other files in a repository and is commonly
distributed with computer software, forming part of its documentation.
+ GitLab will render it here instead of this message.
%p
- We recommend you to
- = link_to "add a README", add_special_file_path(@project, file_name: 'README.md')
- file to the repository and GitLab will render it here instead of this message.
+ = link_to "Add Readme", add_special_file_path(@project, file_name: 'README.md'), class: 'btn btn-new'
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index dab94d10bb1..18e948ce35a 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -6,7 +6,10 @@
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- - can_create_snippet = can?(current_user, :create_snippet, @project)
+ - can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
+
+ - if can_create_issue || merge_project || can_create_project_snippet
+ %li.dropdown-header= _('This project')
- if can_create_issue
%li= link_to _('New issue'), new_project_issue_path(@project)
@@ -14,11 +17,11 @@
- if merge_project
%li= link_to _('New merge request'), project_new_merge_request_path(merge_project)
- - if can_create_snippet
+ - if can_create_project_snippet
%li= link_to _('New snippet'), new_project_snippet_path(@project)
- - if can_create_issue || merge_project || can_create_snippet
- %li.divider
+ - if can?(current_user, :push_code, @project)
+ %li.dropdown-header= _('This repository')
- if can?(current_user, :push_code, @project)
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
@@ -31,5 +34,5 @@
- continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
%li= link_to _('New file'), fork_path, method: :post
diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml
index 80eca96f7ce..d7bf2dc0cb6 100644
--- a/app/views/projects/commit/_other_user_signature_badge.html.haml
+++ b/app/views/projects/commit/_other_user_signature_badge.html.haml
@@ -1,6 +1,6 @@
- title = capture do
This commit was signed with a different user's verified signature.
-- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true }
+- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
index e737de48e22..22ffd66ff8e 100644
--- a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
+++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
@@ -2,6 +2,6 @@
This commit was signed with a verified signature, but the committer email
is <strong>not verified</strong> to belong to the same user.
-- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true }
+- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 44aa8002f12..aac020b42c5 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -10,7 +10,7 @@
- title = capture do
.gpg-popover-status
.gpg-popover-icon{ class: css_class }
- = render "shared/icons/#{icon}.svg"
+ = sprite_icon(icon)
%div
= title
diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml
index 1af58027b83..00e1efe0582 100644
--- a/app/views/projects/commit/_unverified_signature_badge.html.haml
+++ b/app/views/projects/commit/_unverified_signature_badge.html.haml
@@ -1,6 +1,6 @@
- title = capture do
This commit was signed with an <strong>unverified</strong> signature.
-- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' }
+- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless' }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml
index 423beba2120..31408806be7 100644
--- a/app/views/projects/commit/_verified_signature_badge.html.haml
+++ b/app/views/projects/commit/_verified_signature_badge.html.haml
@@ -2,6 +2,6 @@
This commit was signed with a <strong>verified</strong> signature and the
committer email is verified to belong to the same user.
-- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true }
+- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'status_success_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 58e89a481a9..ab225796b12 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -6,8 +6,9 @@
= render "home_panel"
.row-content-block.second-block.center
- %h3.page-title
+ %h4
The repository for this project is empty
+
- if can?(current_user, :push_code, @project)
%p
If you already have files you can push them using command line instructions below.
@@ -28,8 +29,8 @@
%p
- link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
= s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link }
- %p
- = s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+ %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+ %p= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master'), class: 'btn btn-new'
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
@@ -79,4 +80,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index eb0773f2d4e..93efa7e8e86 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -103,8 +103,8 @@
content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered')
- else
= render 'empty_state',
- illustration: 'illustrations/job_not_triggered.svg',
- illustration_size: 'svg-306',
+ illustration: 'illustrations/pending_job_empty.svg',
+ illustration_size: 'svg-430',
title: _('This job has not started yet'),
content: _('This job is in pending state and is waiting to be picked by a runner')
= render "sidebar"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index fcbf7cb802b..6a7bc4b1888 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -12,6 +12,8 @@
New milestone
.milestones
+ #delete-milestone-modal
+
%ul.content-list
= render @milestones
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 5dd4d2c949c..623c42ba88e 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -35,8 +35,18 @@
- else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
- Delete
+ %button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal',
+ target: '#delete-milestone-modal',
+ milestone_id: @milestone.id,
+ milestone_title: markdown_field(@milestone, :title),
+ milestone_url: project_milestone_path(@project, @milestone),
+ milestone_issue_count: @milestone.issues.count,
+ milestone_merge_request_count: @milestone.merge_requests.count },
+ disabled: true }
+ = _('Delete')
+ = icon('spin spinner', class: 'js-loading-icon hidden' )
+
+ #delete-milestone-modal
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
diff --git a/app/views/shared/icons/_icon_status_notfound_borderless.svg b/app/views/shared/icons/_icon_status_notfound_borderless.svg
deleted file mode 100644
index e58bd264ef8..00000000000
--- a/app/views/shared/icons/_icon_status_notfound_borderless.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0V11.78a5.9 5.9 0 0 0 .827-.492z" fill-rule="nonzero"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></svg>
diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg
deleted file mode 100644
index 8ee5be7ab78..00000000000
--- a/app/views/shared/icons/_icon_status_success_borderless.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg>
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 50f4901a2dd..e08a49b4e59 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -56,6 +56,13 @@
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
- = link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do
- Delete
-
+ %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal',
+ target: '#delete-milestone-modal',
+ milestone_id: milestone.id,
+ milestone_title: markdown_field(milestone, :title),
+ milestone_url: project_milestone_path(milestone.project, milestone),
+ milestone_issue_count: milestone.issues.count,
+ milestone_merge_request_count: milestone.merge_requests.count },
+ disabled: true }
+ = _('Delete')
+ = icon('spin spinner', class: 'js-loading-icon hidden' )
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 4f4e81c705f..90aa1be30ac 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -58,15 +58,15 @@
= icon('skype')
- unless @user.linkedin.blank?
.profile-link-holder.middle-dot-divider
- = link_to linkedin_url(@user), title: "LinkedIn" do
+ = link_to linkedin_url(@user), title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('linkedin-square')
- unless @user.twitter.blank?
.profile-link-holder.middle-dot-divider
- = link_to twitter_url(@user), title: "Twitter" do
+ = link_to twitter_url(@user), title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('twitter-square')
- unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url, class: 'text-link'
+ = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'noopener noreferrer nofollow'
- unless @user.location.blank?
.profile-link-holder.middle-dot-divider
= icon('map-marker')
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 4e3c691e8da..116bc185b38 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -20,10 +20,7 @@ module RepositoryCheck
# Historically some projects never had their wiki repos initialized;
# this happens on project creation now. Let's initialize an empty repo
# if it is not already there.
- begin
- project.create_wiki
- rescue Rugged::RepositoryError
- end
+ project.create_wiki
git_fsck(project.wiki.repository)
else
diff --git a/changelogs/unreleased/40028-special-characters-on-issuable-templates.yml b/changelogs/unreleased/40028-special-characters-on-issuable-templates.yml
new file mode 100644
index 00000000000..ffab28acbd5
--- /dev/null
+++ b/changelogs/unreleased/40028-special-characters-on-issuable-templates.yml
@@ -0,0 +1,5 @@
+---
+title: Handle special characters on API request of issuable templates
+merge_request: 15323
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/42220-add-pending-empty-state.yml b/changelogs/unreleased/42220-add-pending-empty-state.yml
new file mode 100644
index 00000000000..ad39578f2d9
--- /dev/null
+++ b/changelogs/unreleased/42220-add-pending-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Adds empty state illustration for pending job
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/42285-not-found-status-icon.yml b/changelogs/unreleased/42285-not-found-status-icon.yml
new file mode 100644
index 00000000000..ea7ff9d6ae7
--- /dev/null
+++ b/changelogs/unreleased/42285-not-found-status-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Replace verified badge icons and uniform colors
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/default-to-https-for-gravatar-urls.yml b/changelogs/unreleased/default-to-https-for-gravatar-urls.yml
new file mode 100644
index 00000000000..544c34fe31d
--- /dev/null
+++ b/changelogs/unreleased/default-to-https-for-gravatar-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Default to HTTPS for all Gravatar URLs
+merge_request: 16666
+author:
+type: fixed
diff --git a/changelogs/unreleased/disable-throwOnError-in-katex.yml b/changelogs/unreleased/disable-throwOnError-in-katex.yml
new file mode 100644
index 00000000000..0cd17bb29fe
--- /dev/null
+++ b/changelogs/unreleased/disable-throwOnError-in-katex.yml
@@ -0,0 +1,5 @@
+---
+title: Disable throwOnError in KaTeX to reveal user where is the problem
+merge_request: 16684
+author: Jakub Jirutka
+type: other
diff --git a/changelogs/unreleased/feat-add-section-headers-to-project-repo-buttons.yml b/changelogs/unreleased/feat-add-section-headers-to-project-repo-buttons.yml
new file mode 100644
index 00000000000..8f3459a7381
--- /dev/null
+++ b/changelogs/unreleased/feat-add-section-headers-to-project-repo-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Improve empty project overview
+merge_request: 16617
+author: George Tsiolis
+type: added
diff --git a/changelogs/unreleased/gitaly-repo-exists.yml b/changelogs/unreleased/gitaly-repo-exists.yml
new file mode 100644
index 00000000000..a9eb42a2038
--- /dev/null
+++ b/changelogs/unreleased/gitaly-repo-exists.yml
@@ -0,0 +1,5 @@
+---
+title: Make Gitaly RepositoryExists opt-out
+merge_request: 16680
+author:
+type: other
diff --git a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml
deleted file mode 100644
index 3854985e576..00000000000
--- a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Return more consistent values for merge_status on MR APIs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/update-node-docs.yml b/changelogs/unreleased/update-node-docs.yml
new file mode 100644
index 00000000000..a1d9d12f0ca
--- /dev/null
+++ b/changelogs/unreleased/update-node-docs.yml
@@ -0,0 +1,5 @@
+---
+title: fix documentation about node version
+merge_request: 16720
+author: Tobias Gurtzick
+type: other
diff --git a/changelogs/unreleased/ux-guide-deprecation.yml b/changelogs/unreleased/ux-guide-deprecation.yml
new file mode 100644
index 00000000000..16477f59abf
--- /dev/null
+++ b/changelogs/unreleased/ux-guide-deprecation.yml
@@ -0,0 +1,6 @@
+---
+title: Add note within ux documentation that further changes should be made within
+ the design.gitlab project
+merge_request:
+author:
+type: deprecated
diff --git a/changelogs/unreleased/winh-delete-milestone-modal.yml b/changelogs/unreleased/winh-delete-milestone-modal.yml
new file mode 100644
index 00000000000..6517fbd5f63
--- /dev/null
+++ b/changelogs/unreleased/winh-delete-milestone-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Add modal for deleting a milestone
+merge_request: 16229
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index f2f05b3eeb2..238e1583770 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -175,10 +175,12 @@ production: &base
host: 'https://mattermost.example.com'
## Gravatar
- ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
+ ## If using gravatar.com, there's nothing to change here. For Libravatar
+ ## you'll need to provide the custom URLs. For more information,
+ ## see: https://docs.gitlab.com/ee/customization/libravatar.html
gravatar:
- # gravatar urls: possible placeholders: %{hash} %{size} %{email} %{username}
- # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
+ # Gravatar/Libravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
+ # plain_url: "http://..." # default: https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
## Auxiliary jobs
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index abc992e49dc..899e612ffbd 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -350,7 +350,7 @@ Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
#
Settings['gravatar'] ||= Settingslogic.new({})
Settings.gravatar['enabled'] = true if Settings.gravatar['enabled'].nil?
-Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
+Settings.gravatar['plain_url'] ||= 'https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
Settings.gravatar['host'] = Settings.host_without_www(Settings.gravatar['plain_url'])
diff --git a/config/initializers/rugged_use_gitlab_git_attributes.rb b/config/initializers/rugged_use_gitlab_git_attributes.rb
deleted file mode 100644
index 1cfb3bcb4bd..00000000000
--- a/config/initializers/rugged_use_gitlab_git_attributes.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# We don't want to ever call Rugged::Repository#fetch_attributes, because it has
-# a lot of I/O overhead:
-# <https://gitlab.com/gitlab-org/gitlab_git/commit/340e111e040ae847b614d35b4d3173ec48329015>
-#
-# While we don't do this from within the GitLab source itself, the Linguist gem
-# has a dependency on Rugged and uses the gitattributes file when calculating
-# repository-wide language statistics:
-# <https://github.com/github/linguist/blob/v4.7.0/lib/linguist/lazy_blob.rb#L33-L36>
-#
-# The options passed by Linguist are those assumed by Gitlab::Git::Attributes
-# anyway, and there is no great efficiency gain from just fetching the listed
-# attributes with our implementation, so we ignore the additional arguments.
-#
-module Rugged
- class Repository
- module UseGitlabGitAttributes
- def fetch_attributes(name, *)
- attributes.attributes(name)
- end
-
- def attributes
- @attributes ||= Gitlab::Git::Attributes.new(path)
- end
- end
-
- prepend UseGitlabGitAttributes
- end
-end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 43ada9ba145..0496bd85b4e 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -40,7 +40,7 @@ constraints(ProjectUrlConstrainer.new) do
#
# Templates
#
- get '/templates/:template_type/:key' => 'templates#show', as: :template
+ get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: /[^\/]+/ }
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md
index 42bcf234e12..c59e7b72a1a 100644
--- a/doc/development/ux_guide/index.md
+++ b/doc/development/ux_guide/index.md
@@ -1,3 +1,5 @@
+> We are in the process of transferring UX documentation to the [design.gitlab.com](https://gitlab.com/gitlab-org/design.gitlab.com) project. Any updates to these docs should be made in that project. If documentation does not yet exist within [design.gitlab.com](https://gitlab.com/gitlab-org/design.gitlab.com), [create an issue](https://gitlab.com/gitlab-org/design.gitlab.com/issues) and merge request to add your new changes.
+
# GitLab UX Guide
The goal of this guide is to provide standards, principles and in-depth information to design beautiful and effective GitLab features. This will be a living document, and we welcome contributions, feedback and suggestions.
diff --git a/doc/update/10.2-to-10.3.md b/doc/update/10.2-to-10.3.md
index d6e2db8a353..f8fe4a4b6bf 100644
--- a/doc/update/10.2-to-10.3.md
+++ b/doc/update/10.2-to-10.3.md
@@ -54,17 +54,16 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Update Node
-GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
-it has a minimum requirement of node v4.3.0.
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets.
+We require a minimum version of node v6.0.0.
You can check which version you are running with `node -v`. If you are running
-a version older than `v4.3.0` you will need to update to a newer version. You
+a version older than `v6.0.0` you will need to update to a newer version. You
can find instructions to install from community maintained packages or compile
from source at the nodejs.org website.
<https://nodejs.org/en/download/>
-
Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies.
diff --git a/doc/update/10.3-to-10.4.md b/doc/update/10.3-to-10.4.md
index 67b7e634c94..083f6090a8a 100644
--- a/doc/update/10.3-to-10.4.md
+++ b/doc/update/10.3-to-10.4.md
@@ -56,17 +56,16 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Update Node
-GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
-it has a minimum requirement of node v4.3.0.
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets.
+We require a minimum version of node v6.0.0.
You can check which version you are running with `node -v`. If you are running
-a version older than `v4.3.0` you will need to update to a newer version. You
+a version older than `v6.0.0` you will need to update to a newer version. You
can find instructions to install from community maintained packages or compile
from source at the nodejs.org website.
<https://nodejs.org/en/download/>
-
Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies.
diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature
index 1af05b3c326..d121222308d 100644
--- a/features/project/issues/milestones.feature
+++ b/features/project/issues/milestones.feature
@@ -18,12 +18,15 @@ Feature: Project Issues Milestones
Given I click link "New Milestone"
And I submit new milestone "v2.3"
Then I should see milestone "v2.3"
- Given I click link to remove milestone
+ Given I click button to remove milestone
+ And I confirm in modal
When I visit project "Shop" activity page
Then I should see deleted milestone activity
+ @javascript
Scenario: I delete new milestone
- Given I click link to remove milestone
+ Given I click button to remove milestone
+ And I confirm in modal
And I should see no milestones
@javascript
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index 33a24e8913a..4ce67aa651c 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -3,7 +3,6 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
include SharedProject
include SharedPaths
include SharedMarkdown
- include CapybaraHelpers
step 'I should see milestone "v2.2"' do
milestone = @project.milestones.find_by(title: "v2.2")
@@ -65,8 +64,12 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
expect(page).to have_selector('#tab-issues li.issuable-row', count: 4)
end
- step 'I click link to remove milestone' do
- confirm_modal_if_present { click_link 'Delete' }
+ step 'I click button to remove milestone' do
+ click_button 'Delete'
+ end
+
+ step 'I confirm in modal' do
+ click_button 'Delete milestone'
end
step 'I should see no milestones' do
diff --git a/features/steps/user.rb b/features/steps/user.rb
deleted file mode 100644
index 321c1e942d5..00000000000
--- a/features/steps/user.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-class Spinach::Features::User < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedUser
- include SharedProject
-
- step 'I should see user "John Doe" page' do
- expect(title).to match(/^\s*John Doe/)
- end
-
- step '"John Doe" has contributions' do
- user = User.find_by(name: 'John Doe')
- project = contributed_project
-
- # Issue contribution
- issue_params = { title: 'Bug in old browser' }
- Issues::CreateService.new(project, user, issue_params).execute
-
- # Push code contribution
- event = create(:push_event, project: project, author: user)
-
- create(:push_event_payload, event: event, commit_count: 3)
- end
-
- step 'I should see contributed projects' do
- page.within '#contributed' do
- expect(page).to have_content(@contributed_project.name)
- end
- end
-
- step 'I should see contributions calendar' do
- expect(page).to have_css('.js-contrib-calendar')
- end
-
- def contributed_project
- @contributed_project ||= create(:project, :public, :empty_repo)
- end
-end
diff --git a/features/support/capybara_helpers.rb b/features/support/capybara_helpers.rb
deleted file mode 100644
index 647f8d087c3..00000000000
--- a/features/support/capybara_helpers.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module CapybaraHelpers
- def confirm_modal_if_present
- if Capybara.current_driver == Capybara.javascript_driver
- accept_confirm { yield }
- return
- end
-
- yield
- end
-end
diff --git a/features/user.feature b/features/user.feature
deleted file mode 100644
index e0cadba30a1..00000000000
--- a/features/user.feature
+++ /dev/null
@@ -1,86 +0,0 @@
-Feature: User
- Background:
- Given User "John Doe" exists
- And "John Doe" owns private project "Enterprise"
-
- # Signed out
-
- @javascript
- Scenario: I visit user "John Doe" page while not signed in when he owns a public project
- Given "John Doe" owns internal project "Internal"
- And "John Doe" owns public project "Community"
- When I visit user "John Doe" page
- And I click on "Personal projects" tab
- Then I should see user "John Doe" page
- And I should not see project "Enterprise"
- And I should not see project "Internal"
- And I should see project "Community"
-
- # Signed in as someone else
-
- @javascript
- Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project
- Given "John Doe" owns public project "Community"
- And "John Doe" owns internal project "Internal"
- And I sign in as a user
- When I visit user "John Doe" page
- And I click on "Personal projects" tab
- Then I should see user "John Doe" page
- And I should not see project "Enterprise"
- And I should see project "Internal"
- And I should see project "Community"
-
- @javascript
- Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project
- Given "John Doe" owns internal project "Internal"
- And I sign in as a user
- When I visit user "John Doe" page
- And I click on "Personal projects" tab
- Then I should see user "John Doe" page
- And I should not see project "Enterprise"
- And I should see project "Internal"
- And I should not see project "Community"
-
- @javascript
- Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see
- Given I sign in as a user
- When I visit user "John Doe" page
- And I click on "Personal projects" tab
- Then I should see user "John Doe" page
- And I should not see project "Enterprise"
- And I should not see project "Internal"
- And I should not see project "Community"
-
- # Signed in as the user himself
-
- @javascript
- Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project
- Given "John Doe" owns internal project "Internal"
- And "John Doe" owns public project "Community"
- And I sign in as "John Doe"
- When I visit user "John Doe" page
- And I click on "Personal projects" tab
- Then I should see user "John Doe" page
- And I should see project "Enterprise"
- And I should see project "Internal"
- And I should see project "Community"
-
- @javascript
- Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project
- Given I sign in as "John Doe"
- When I visit user "John Doe" page
- And I click on "Personal projects" tab
- Then I should see user "John Doe" page
- And I should see project "Enterprise"
- And I should not see project "Internal"
- And I should not see project "Community"
-
- @javascript
- Scenario: "John Doe" contribution profile
- Given I sign in as a user
- And "John Doe" has contributions
- When I visit user "John Doe" page
- And I click on "Contributed projects" tab
- Then I should see user "John Doe" page
- And I should see contributed projects
- And I should see contributions calendar
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5b470bd3479..7b9a80a234b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -507,16 +507,7 @@ module API
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
expose :merge_when_pipeline_succeeds
-
- # Ideally we should deprecate `MergeRequest#merge_status` exposure and
- # use `MergeRequest#mergeable?` instead (boolean).
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more
- # information.
- expose :merge_status do |merge_request|
- # In order to avoid having a breaking change for users, we keep returning the
- # expected values from MergeRequest#merge_status state machine.
- merge_request.mergeable? ? 'can_be_merged' : 'cannot_be_merged'
- end
+ expose :merge_status
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
expose :user_notes_count
diff --git a/lib/gitlab/bare_repository_import/repository.rb b/lib/gitlab/bare_repository_import/repository.rb
index c0c666dfb7b..fe267248275 100644
--- a/lib/gitlab/bare_repository_import/repository.rb
+++ b/lib/gitlab/bare_repository_import/repository.rb
@@ -1,3 +1,5 @@
+# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/953
+#
module Gitlab
module BareRepositoryImport
class Repository
diff --git a/lib/gitlab/git/attributes_at_ref_parser.rb b/lib/gitlab/git/attributes_at_ref_parser.rb
new file mode 100644
index 00000000000..26b5bd520d5
--- /dev/null
+++ b/lib/gitlab/git/attributes_at_ref_parser.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Git
+ # Parses root .gitattributes file at a given ref
+ class AttributesAtRefParser
+ delegate :attributes, to: :@parser
+
+ def initialize(repository, ref)
+ blob = repository.blob_at(ref, '.gitattributes')
+
+ @parser = AttributesParser.new(blob&.data)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/attributes.rb b/lib/gitlab/git/attributes_parser.rb
index 2d20cd473a7..d8aeabb6cba 100644
--- a/lib/gitlab/git/attributes.rb
+++ b/lib/gitlab/git/attributes_parser.rb
@@ -1,42 +1,26 @@
-# Gitaly note: JV: not sure what to make of this class. Why does it use
-# the full disk path of the repository to look up attributes This is
-# problematic in Gitaly, because Gitaly hides the full disk path to the
-# repository from gitlab-ce.
-
module Gitlab
module Git
# Class for parsing Git attribute files and extracting the attributes for
# file patterns.
- #
- # Unlike Rugged this parser only needs a single IO call (a call to `open`),
- # vastly reducing the time spent in extracting attributes.
- #
- # This class _only_ supports parsing the attributes file located at
- # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
- # (`.gitattributes` is copied to this particular path).
- #
- # Basic usage:
- #
- # attributes = Gitlab::Git::Attributes.new(some_repo.path)
- #
- # attributes.attributes('README.md') # => { "eol" => "lf }
- class Attributes
- # path - The path to the Git repository.
- def initialize(path)
- @path = File.expand_path(path)
- @patterns = nil
+ class AttributesParser
+ def initialize(attributes_data)
+ @data = attributes_data || ""
+
+ if @data.is_a?(File)
+ @patterns = parse_file
+ end
end
# Returns all the Git attributes for the given path.
#
- # path - A path to a file for which to get the attributes.
+ # file_path - A path to a file for which to get the attributes.
#
# Returns a Hash.
- def attributes(path)
- full_path = File.join(@path, path)
+ def attributes(file_path)
+ absolute_path = File.join('/', file_path)
patterns.each do |pattern, attrs|
- return attrs if File.fnmatch?(pattern, full_path)
+ return attrs if File.fnmatch?(pattern, absolute_path)
end
{}
@@ -98,16 +82,10 @@ module Gitlab
# Iterates over every line in the attributes file.
def each_line
- full_path = File.join(@path, 'info/attributes')
+ @data.each_line do |line|
+ break unless line.valid_encoding?
- return unless File.exist?(full_path)
-
- File.open(full_path, 'r') do |handle|
- handle.each_line do |line|
- break unless line.valid_encoding?
-
- yield line.strip
- end
+ yield line.strip
end
end
@@ -125,7 +103,8 @@ module Gitlab
parsed = attrs ? parse_attributes(attrs) : {}
- pairs << [File.join(@path, pattern), parsed]
+ absolute_pattern = File.join('/', pattern)
+ pairs << [absolute_pattern, parsed]
end
# Newer entries take precedence over older entries.
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 81e46028752..13120120223 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -70,11 +70,9 @@ module Gitlab
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
def batch_lfs_pointers(repository, blob_ids)
- return [] if blob_ids.empty?
-
repository.gitaly_migrate(:batch_lfs_pointers) do |is_enabled|
if is_enabled
- repository.gitaly_blob_client.batch_lfs_pointers(blob_ids)
+ repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a)
else
blob_ids.lazy
.select { |sha| possible_lfs_blob?(repository, sha) }
diff --git a/lib/gitlab/git/info_attributes.rb b/lib/gitlab/git/info_attributes.rb
new file mode 100644
index 00000000000..e79a440950b
--- /dev/null
+++ b/lib/gitlab/git/info_attributes.rb
@@ -0,0 +1,49 @@
+# Gitaly note: JV: not sure what to make of this class. Why does it use
+# the full disk path of the repository to look up attributes This is
+# problematic in Gitaly, because Gitaly hides the full disk path to the
+# repository from gitlab-ce.
+
+module Gitlab
+ module Git
+ # Parses gitattributes at `$GIT_DIR/info/attributes`
+ #
+ # Unlike Rugged this parser only needs a single IO call (a call to `open`),
+ # vastly reducing the time spent in extracting attributes.
+ #
+ # This class _only_ supports parsing the attributes file located at
+ # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
+ # (`.gitattributes` is copied to this particular path).
+ #
+ # Basic usage:
+ #
+ # attributes = Gitlab::Git::InfoAttributes.new(some_repo.path)
+ #
+ # attributes.attributes('README.md') # => { "eol" => "lf }
+ class InfoAttributes
+ delegate :attributes, :patterns, to: :parser
+
+ # path - The path to the Git repository.
+ def initialize(path)
+ @repo_path = File.expand_path(path)
+ end
+
+ def parser
+ @parser ||= begin
+ if File.exist?(attributes_path)
+ File.open(attributes_path, 'r') do |file_handle|
+ AttributesParser.new(file_handle)
+ end
+ else
+ AttributesParser.new("")
+ end
+ end
+ end
+
+ private
+
+ def attributes_path
+ @attributes_path ||= File.join(@repo_path, 'info/attributes')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index d7c712e75c5..638d335b523 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -102,7 +102,7 @@ module Gitlab
)
@path = File.join(storage_path, @relative_path)
@name = @relative_path.split("/").last
- @attributes = Gitlab::Git::Attributes.new(path)
+ @attributes = Gitlab::Git::InfoAttributes.new(path)
end
def ==(other)
@@ -133,7 +133,7 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
+ Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_repository_client.exists?
else
@@ -490,7 +490,11 @@ module Gitlab
return []
end
- log_by_shell(sha, options)
+ if log_using_shell?(options)
+ log_by_shell(sha, options)
+ else
+ log_by_walk(sha, options)
+ end
end
def count_commits(options)
@@ -559,6 +563,8 @@ module Gitlab
return false if ancestor_id.nil? || descendant_id.nil?
merge_base_commit(ancestor_id, descendant_id) == ancestor_id
+ rescue Rugged::OdbError
+ false
end
# Returns true is +from+ is direct ancestor to +to+, otherwise false
@@ -991,6 +997,18 @@ module Gitlab
attributes(path)[name]
end
+ # Check .gitattributes for a given ref
+ #
+ # This only checks the root .gitattributes file,
+ # it does not traverse subfolders to find additional .gitattributes files
+ #
+ # This method is around 30 times slower than `attributes`,
+ # which uses `$GIT_DIR/info/attributes`
+ def attributes_at(ref, file_path)
+ parser = AttributesAtRefParser.new(self, ref)
+ parser.attributes(file_path)
+ end
+
def languages(ref = nil)
Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled|
if is_enabled
@@ -1110,23 +1128,6 @@ module Gitlab
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
- def run_git(args, chdir: path, env: {}, nice: false, &block)
- cmd = [Gitlab.config.git.bin_path, *args]
- cmd.unshift("nice") if nice
- circuit_breaker.perform do
- popen(cmd, chdir, env, &block)
- end
- end
-
- def run_git!(args, chdir: path, env: {}, nice: false, &block)
- output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block)
-
- raise GitError, output unless status.zero?
-
- output
- end
-
- # Refactoring aid; allows us to copy code from app/models/repository.rb
def run_git_with_timeout(args, timeout, env: {})
circuit_breaker.perform do
popen_with_timeout([Gitlab.config.git.bin_path, *args], timeout, path, env)
@@ -1187,6 +1188,19 @@ module Gitlab
end
end
+ def create_from_bundle(bundle_path)
+ gitaly_migrate(:create_repo_from_bundle) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.create_from_bundle(bundle_path)
+ else
+ run_git!(%W(clone --bare -- #{bundle_path} #{path}), chdir: nil)
+ self.class.create_hooks(path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
+ end
+ end
+
+ true
+ end
+
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
gitaly_migrate(:rebase) do |is_enabled|
if is_enabled
@@ -1349,6 +1363,52 @@ module Gitlab
raise CommandError.new(e)
end
+ def refs_contains_sha(ref_type, sha)
+ args = %W(#{ref_type} --contains #{sha})
+ names = run_git(args).first
+
+ if names.respond_to?(:split)
+ names = names.split("\n").map(&:strip)
+
+ names.each do |name|
+ name.slice! '* '
+ end
+
+ names
+ else
+ []
+ end
+ end
+
+ def search_files_by_content(query, ref)
+ return [] if empty? || query.blank?
+
+ offset = 2
+ args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
+
+ run_git(args).first.scrub.split(/^--$/)
+ end
+
+ def search_files_by_name(query, ref)
+ safe_query = Regexp.escape(query.sub(/^\/*/, ""))
+
+ return [] if empty? || safe_query.blank?
+
+ args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{safe_query})
+
+ run_git(args).first.lines.map(&:strip)
+ end
+
+ def find_commits_by_message(query, ref, path, limit, offset)
+ gitaly_migrate(:commits_by_message) do |is_enabled|
+ if is_enabled
+ find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
+ else
+ find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
+ end
+ end
+ end
+
private
def shell_write_ref(ref_path, ref, old_ref)
@@ -1370,6 +1430,22 @@ module Gitlab
Rails.logger.error "Unable to create #{ref_path} reference for repository #{path}: #{ex}"
end
+ def run_git(args, chdir: path, env: {}, nice: false, &block)
+ cmd = [Gitlab.config.git.bin_path, *args]
+ cmd.unshift("nice") if nice
+ circuit_breaker.perform do
+ popen(cmd, chdir, env, &block)
+ end
+ end
+
+ def run_git!(args, chdir: path, env: {}, nice: false, &block)
+ output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block)
+
+ raise GitError, output unless status.zero?
+
+ output
+ end
+
def fresh_worktree?(path)
File.exist?(path) && !clean_stuck_worktree(path)
end
@@ -1517,6 +1593,27 @@ module Gitlab
end
end
+ def log_using_shell?(options)
+ options[:path].present? ||
+ options[:disable_walk] ||
+ options[:skip_merges] ||
+ options[:after] ||
+ options[:before]
+ end
+
+ def log_by_walk(sha, options)
+ walk_options = {
+ show: sha,
+ sort: Rugged::SORT_NONE,
+ limit: options[:limit],
+ offset: options[:offset]
+ }
+ Rugged::Walker.walk(rugged, walk_options).to_a
+ end
+
+ # Gitaly note: JV: although #log_by_shell shells out to Git I think the
+ # complexity is such that we should migrate it as Ruby before trying to
+ # do it in Go.
def log_by_shell(sha, options)
limit = options[:limit].to_i
offset = options[:offset].to_i
@@ -2124,6 +2221,26 @@ module Gitlab
def gitlab_projects_error
raise CommandError, @gitlab_projects.output
end
+
+ def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
+ ref ||= root_ref
+
+ args = %W(
+ log #{ref} --pretty=%H --skip #{offset}
+ --max-count #{limit} --grep=#{query} --regexp-ignore-case
+ )
+ args = args.concat(%W(-- #{path})) if path.present?
+
+ git_log_results = run_git(args).first.lines
+
+ git_log_results.map { |c| commit(c.chomp) }.compact
+ end
+
+ def find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
+ gitaly_commit_client
+ .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
+ .map { |c| commit(c) }
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index ee36684197b..d70a1a7665e 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -34,6 +34,8 @@ module Gitlab
end
def batch_lfs_pointers(blob_ids)
+ return [] if blob_ids.empty?
+
request = Gitaly::GetLFSPointersRequest.new(
repository: @gitaly_repo,
blob_ids: blob_ids
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 33a8d3e5612..cadc7149301 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -38,19 +38,27 @@ module Gitlab
from_id = case from
when NilClass
EMPTY_TREE_ID
- when Rugged::Commit
- from.oid
else
- from
+ if from.respond_to?(:oid)
+ # This is meant to match a Rugged::Commit. This should be impossible in
+ # the future.
+ from.oid
+ else
+ from
+ end
end
to_id = case to
when NilClass
EMPTY_TREE_ID
- when Rugged::Commit
- to.oid
else
- to
+ if to.respond_to?(:oid)
+ # This is meant to match a Rugged::Commit. This should be impossible in
+ # the future.
+ to.oid
+ else
+ to
+ end
end
request_params = diff_between_commits_request_params(from_id, to_id, options)
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 654a3c314f1..b0dbaf11598 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -3,6 +3,8 @@ module Gitlab
class RepositoryService
include Gitlab::EncodingHelper
+ MAX_MSG_SIZE = 128.kilobytes.freeze
+
def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository
@@ -178,6 +180,29 @@ module Gitlab
end
end
end
+
+ def create_from_bundle(bundle_path)
+ request = Gitaly::CreateRepositoryFromBundleRequest.new(repository: @gitaly_repo)
+ enum = Enumerator.new do |y|
+ File.open(bundle_path, 'rb') do |f|
+ while data = f.read(MAX_MSG_SIZE)
+ request.data = data
+
+ y.yield request
+
+ request = Gitaly::CreateRepositoryFromBundleRequest.new
+ end
+ end
+ end
+
+ GitalyClient.call(
+ @storage,
+ :repository_service,
+ :create_repository_from_bundle,
+ enum,
+ timeout: GitalyClient.default_timeout
+ )
+ end
end
end
end
diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb
index 5437e32e9f1..e70361c163b 100644
--- a/lib/gitlab/github_import/importer/pull_requests_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb
@@ -57,10 +57,7 @@ module Gitlab
end
def commit_exists?(sha)
- project.repository.lookup(sha)
- true
- rescue Rugged::Error
- false
+ project.repository.commit(sha).present?
end
def collection_method
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index 25399f307f2..2f163db936b 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -11,11 +11,6 @@ module Gitlab
untar_with_options(archive: archive, dir: dir, options: 'zxf')
end
- def git_clone_bundle(repo_path:, bundle_path:)
- execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path}))
- Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
- end
-
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index d0e5cfcfd3e..5a9bbceac67 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -13,7 +13,7 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- git_clone_bundle(repo_path: @project.repository.path_to_repo, bundle_path: @path_to_bundle)
+ @project.repository.create_from_bundle(@path_to_bundle)
rescue => e
@shared.error(e)
false
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 04d56509ac6..ab601b0d66b 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -1,3 +1,5 @@
+# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/954
+#
namespace :gitlab do
namespace :cleanup do
HASHED_REPOSITORY_NAME = '@hashed'.freeze
diff --git a/package.json b/package.json
index ef58f71ef8b..c68cf648932 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,7 @@
"worker-loader": "^1.1.0"
},
"devDependencies": {
- "@gitlab-org/gitlab-svgs": "^1.6.0",
+ "@gitlab-org/gitlab-svgs": "^1.7.0",
"axios-mock-adapter": "^1.10.0",
"babel-plugin-istanbul": "^4.1.5",
"eslint": "^3.18.0",
diff --git a/qa/Gemfile b/qa/Gemfile
index d69c71003ae..c3e61568f3d 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -6,4 +6,5 @@ gem 'capybara-screenshot', '~> 1.0.18'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.8.0'
+gem 'net-ssh', require: false
gem 'airborne', '~> 0.2.13'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 565adac7499..51d2e4d7a10 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -46,6 +46,7 @@ GEM
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.11.1)
+ net-ssh (4.1.0)
netrc (0.11.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
@@ -97,6 +98,7 @@ DEPENDENCIES
airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
+ net-ssh
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
diff --git a/qa/qa.rb b/qa/qa.rb
index ac51bb445f2..8f200049bf6 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -11,6 +11,7 @@ module QA
autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
+ autoload :RSAKey, 'qa/runtime/rsa_key'
autoload :Address, 'qa/runtime/address'
autoload :API, 'qa/runtime/api'
end
@@ -28,6 +29,7 @@ module QA
autoload :Group, 'qa/factory/resource/group'
autoload :Project, 'qa/factory/resource/project'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
+ autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
end
@@ -111,6 +113,7 @@ module QA
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
+ autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners'
end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
index 25d2af6e321..ff0b4a46b77 100644
--- a/qa/qa/factory/resource/deploy_key.rb
+++ b/qa/qa/factory/resource/deploy_key.rb
@@ -10,6 +10,12 @@ module QA
end
end
+ product :fingerprint do
+ Page::Project::Settings::Repository.act do
+ expand_deploy_keys(&:key_fingerprint)
+ end
+ end
+
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-deploy'
project.description = 'project for adding deploy key test'
diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb
new file mode 100644
index 00000000000..54ef4d8d964
--- /dev/null
+++ b/qa/qa/factory/resource/secret_variable.rb
@@ -0,0 +1,41 @@
+module QA
+ module Factory
+ module Resource
+ class SecretVariable < Factory::Base
+ attr_accessor :key, :value
+
+ product :key do
+ Page::Project::Settings::CICD.act do
+ expand_secret_variables(&:variable_key)
+ end
+ end
+
+ product :value do
+ Page::Project::Settings::CICD.act do
+ expand_secret_variables(&:variable_value)
+ end
+ end
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-with-secret-variables'
+ project.description = 'project for adding secret variable test'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Menu::Side.act { click_ci_cd_settings }
+
+ Page::Project::Settings::CICD.perform do |setting|
+ setting.expand_secret_variables do |page|
+ page.fill_variable_key(key)
+ page.fill_variable_value(value)
+
+ page.add_variable
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index ea4c920c82c..81ba80cdbaf 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -41,7 +41,21 @@ module QA
end
def click_element(name)
- find(Page::Element.new(name).selector_css).click
+ find_element(name).click
+ end
+
+ def find_element(name)
+ find(element_selector_css(name))
+ end
+
+ def within_element(name)
+ page.within(element_selector_css(name)) do
+ yield
+ end
+ end
+
+ def element_selector_css(name)
+ Page::Element.new(name).selector_css
end
def self.path
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
index 7f0f924c5e8..239f2872228 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/menu/side.rb
@@ -9,6 +9,10 @@ module QA
element :top_level_items, '.sidebar-top-level-items'
end
+ view 'app/assets/javascripts/fly_out_nav.js' do
+ element :fly_out, "classList.add('fly-out-list')"
+ end
+
def click_repository_settings
hover_settings do
within_submenu do
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 5270dde7411..99be21bbe89 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -7,6 +7,7 @@ module QA
view 'app/views/projects/settings/ci_cd/show.html.haml' do
element :runners_settings, 'Runners settings'
+ element :secret_variables, 'Secret variables'
end
def expand_runners_settings(&block)
@@ -14,6 +15,12 @@ module QA
Settings::Runners.perform(&block)
end
end
+
+ def expand_secret_variables(&block)
+ expand_section('Secret variables') do
+ Settings::SecretVariables.perform(&block)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb
index 1357bf031d5..0a2a3488b9a 100644
--- a/qa/qa/page/project/settings/common.rb
+++ b/qa/qa/page/project/settings/common.rb
@@ -14,7 +14,7 @@ module QA
def expand_section(name)
page.within('#content-body') do
page.within('section', text: name) do
- click_button 'Expand'
+ click_button 'Expand' unless first('button', text: 'Collapse')
yield
end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index f9e40bf4252..332e84724c7 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -14,8 +14,8 @@ module QA
end
view 'app/assets/javascripts/deploy_keys/components/key.vue' do
- element :key_title, /class=".*title.*"/
- element :key_title_field, '{{ deployKey.title }}'
+ element :key_title, /class=".*qa-key-title.*"/
+ element :key_fingerprint, /class=".*qa-key-fingerprint.*"/
end
def fill_key_title(title)
@@ -31,8 +31,22 @@ module QA
end
def key_title
- page.within('.qa-project-deploy-keys') do
- page.find('.title').text
+ within_project_deploy_keys do
+ find_element(:key_title).text
+ end
+ end
+
+ def key_fingerprint
+ within_project_deploy_keys do
+ find_element(:key_fingerprint).text
+ end
+ end
+
+ private
+
+ def within_project_deploy_keys
+ within_element(:project_deploy_keys) do
+ yield
end
end
end
diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/secret_variables.rb
new file mode 100644
index 00000000000..e3bfbfcf080
--- /dev/null
+++ b/qa/qa/page/project/settings/secret_variables.rb
@@ -0,0 +1,57 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class SecretVariables < Page::Base
+ include Common
+
+ view 'app/views/ci/variables/_table.html.haml' do
+ element :variable_key, '.variable-key'
+ element :variable_value, '.variable-value'
+ end
+
+ view 'app/views/ci/variables/_index.html.haml' do
+ element :add_new_variable, 'btn_text: "Add new variable"'
+ end
+
+ view 'app/assets/javascripts/behaviors/secret_values.js' do
+ element :reveal_value, 'Reveal value'
+ element :hide_value, 'Hide value'
+ end
+
+ def fill_variable_key(key)
+ fill_in 'variable_key', with: key
+ end
+
+ def fill_variable_value(value)
+ fill_in 'variable_value', with: value
+ end
+
+ def add_variable
+ click_on 'Add new variable'
+ end
+
+ def variable_key
+ page.find('.variable-key').text
+ end
+
+ def variable_value
+ reveal_value do
+ page.find('.variable-value').text
+ end
+ end
+
+ private
+
+ def reveal_value
+ click_button('Reveal value')
+
+ yield.tap do
+ click_button('Hide value')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/rsa_key.rb b/qa/qa/runtime/rsa_key.rb
new file mode 100644
index 00000000000..d456062bce7
--- /dev/null
+++ b/qa/qa/runtime/rsa_key.rb
@@ -0,0 +1,21 @@
+require 'net/ssh'
+require 'forwardable'
+
+module QA
+ module Runtime
+ class RSAKey
+ extend Forwardable
+
+ attr_reader :key
+ def_delegators :@key, :fingerprint
+
+ def initialize(bits = 4096)
+ @key = OpenSSL::PKey::RSA.new(bits)
+ end
+
+ def public_key
+ @public_key ||= "#{key.ssh_type} #{[key.to_blob].pack('m0')}"
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
index 2832439d9e0..60027c89ab1 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -10,17 +10,6 @@ module QA
def password
ENV['GITLAB_PASSWORD'] || '5iveL!fe'
end
-
- def ssh_key
- <<~KEY.delete("\n")
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
- 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
- /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
- M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
- rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
- 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
- KEY
- end
end
end
end
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
index 7a123e539e1..b9998dda895 100644
--- a/qa/qa/specs/features/project/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -1,18 +1,20 @@
module QA
feature 'deploy keys support', :core do
- given(:deploy_key_title) { 'deploy key title' }
- given(:deploy_key_value) { Runtime::User.ssh_key }
-
scenario 'user adds a deploy key' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
+ key = Runtime::RSAKey.new
+ deploy_key_title = 'deploy key title'
+ deploy_key_value = key.public_key
+
deploy_key = Factory::Resource::DeployKey.fabricate! do |resource|
resource.title = deploy_key_title
resource.key = deploy_key_value
end
expect(deploy_key.title).to eq(deploy_key_title)
+ expect(deploy_key.fingerprint).to eq(key.fingerprint)
end
end
end
diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb
new file mode 100644
index 00000000000..36422a92afc
--- /dev/null
+++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb
@@ -0,0 +1,19 @@
+module QA
+ feature 'secret variables support', :core do
+ scenario 'user adds a secret variable' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ variable_key = 'VARIABLE_KEY'
+ variable_value = 'variable value'
+
+ variable = Factory::Resource::SecretVariable.fabricate! do |resource|
+ resource.key = variable_key
+ resource.value = variable_value
+ end
+
+ expect(variable.key).to eq(variable_key)
+ expect(variable.value).to eq(variable_value)
+ end
+ end
+end
diff --git a/qa/spec/runtime/rsa_key.rb b/qa/spec/runtime/rsa_key.rb
new file mode 100644
index 00000000000..ff277b9077b
--- /dev/null
+++ b/qa/spec/runtime/rsa_key.rb
@@ -0,0 +1,9 @@
+describe QA::Runtime::RSAKey do
+ describe '#public_key' do
+ subject { described_class.new.public_key }
+
+ it 'generates a public RSA key' do
+ expect(subject).to match(/\Assh\-rsa AAAA[0-9A-Za-z+\/]+={0,3}\z/)
+ end
+ end
+end
diff --git a/scripts/lint-rugged b/scripts/lint-rugged
new file mode 100755
index 00000000000..3f8fcb558e3
--- /dev/null
+++ b/scripts/lint-rugged
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+
+ALLOWED = [
+ # Can be deleted (?) once rugged is no longer used in production. Doesn't make Rugged calls.
+ 'config/initializers/8_metrics.rb',
+
+ # Can be deleted once wiki's are fully (mandatory) migrated
+ 'config/initializers/gollum.rb',
+
+ # Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/953
+ 'lib/gitlab/bare_repository_import/repository.rb',
+
+ # Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/954
+ 'lib/tasks/gitlab/cleanup.rake',
+
+ # https://gitlab.com/gitlab-org/gitaly/issues/961
+ 'app/models/repository.rb',
+
+ # The only place where Rugged code is still allowed in production
+ 'lib/gitlab/git/'
+].freeze
+
+rugged_lines = IO.popen(%w[git grep -i -n rugged -- app config lib], &:read).lines
+rugged_lines = rugged_lines.reject { |l| l.start_with?(*ALLOWED) }
+rugged_lines = rugged_lines.reject do |line|
+ code, _comment = line.split('# ', 2)
+ code !~ /rugged/i
+end
+
+exit if rugged_lines.empty?
+
+puts "Using Rugged is only allowed in test and #{ALLOWED}\n\n"
+
+puts rugged_lines
+
+exit(false)
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 9690b42c788..96d08287ded 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -13,7 +13,8 @@ tasks = [
%w[bundle exec rake gettext:lint],
%w[bundle exec rake lint:static_verification],
%w[scripts/lint-changelog-yaml],
- %w[scripts/lint-conflicts.sh]
+ %w[scripts/lint-conflicts.sh],
+ %w[scripts/lint-rugged]
]
failed_tasks = tasks.reduce({}) do |failures, task|
diff --git a/spec/features/user_page_spec.rb b/spec/features/user_page_spec.rb
new file mode 100644
index 00000000000..19c587e53c8
--- /dev/null
+++ b/spec/features/user_page_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe 'User page', :js do
+ let!(:user) { create :user }
+ let!(:private_project) do
+ create :project, :private, name: 'private', namespace: user.namespace do |project|
+ project.add_master(user)
+ end
+ end
+
+ let!(:internal_project) do
+ create :project, :internal, name: 'internal', namespace: user.namespace do |project|
+ project.add_master(user)
+ end
+ end
+
+ let!(:public_project) do
+ create :project, :public, name: 'public', namespace: user.namespace do |project|
+ project.add_master(user)
+ end
+ end
+
+ def click_nav_link(name)
+ page.within '.nav-links' do
+ click_link name
+ end
+ end
+
+ context 'when not signed in' do
+ it 'renders user public project' do
+ visit user_path(user)
+ click_nav_link('Personal projects')
+
+ expect(page).to have_css('.tab-content #projects.active')
+ expect(title).to start_with(user.name)
+
+ expect(page).to have_content(public_project.name)
+ expect(page).not_to have_content(private_project.name)
+ expect(page).not_to have_content(internal_project.name)
+ end
+ end
+
+ context 'when signed in as another user' do
+ let(:another_user) { create :user }
+
+ before do
+ sign_in(another_user)
+ end
+
+ it 'renders user public and internal projects' do
+ visit user_path(user)
+ click_nav_link('Personal projects')
+
+ expect(title).to start_with(user.name)
+
+ expect(page).not_to have_content(private_project.name)
+ expect(page).to have_content(public_project.name)
+ expect(page).to have_content(internal_project.name)
+ end
+ end
+
+ context 'when signed in as user' do
+ before do
+ sign_in(user)
+ end
+
+ describe 'personal projects' do
+ it 'renders all user projects' do
+ visit user_path(user)
+ click_nav_link('Personal projects')
+
+ expect(title).to start_with(user.name)
+
+ expect(page).to have_content(private_project.name)
+ expect(page).to have_content(public_project.name)
+ expect(page).to have_content(internal_project.name)
+ end
+ end
+
+ describe 'contributed projects' do
+ context 'when user has contributions' do
+ let(:contributed_project) do
+ create :project, :public, :empty_repo
+ end
+
+ before do
+ Issues::CreateService.new(contributed_project, user, { title: 'Bug in old browser' }).execute
+ event = create(:push_event, project: contributed_project, author: user)
+ create(:push_event_payload, event: event, commit_count: 3)
+ end
+
+ it 'renders contributed project' do
+ visit user_path(user)
+
+ expect(title).to start_with(user.name)
+ expect(page).to have_css('.js-contrib-calendar')
+
+ click_nav_link('Contributed projects')
+
+ page.within '#contributed' do
+ expect(page).to have_content(contributed_project.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/emails/attachment.eml b/spec/fixtures/emails/attachment.eml
index f25c3d1a449..b3a30b3221b 100644
--- a/spec/fixtures/emails/attachment.eml
+++ b/spec/fixtures/emails/attachment.eml
@@ -91,7 +91,7 @@ x #ccc solid;padding-left:1ex"><div>
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
+ <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m=
ax-width:694px" width=3D"45" height=3D"45">
</td>
@@ -121,7 +121,7 @@ nk">@eviltrout</a> Any idea why it showed up in suggested topics? </p>
<div style=3D"color:#666">
<p>To respond, reply to this email or visit <a href=3D"http://meta.disc=
ourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"co=
-lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back=
+lor:#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back=
-up-in-suggested-topics/11005/5</a> in your browser.</p>
</div>
@@ -132,12 +132,12 @@ lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back=
lpadding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
+ <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m=
ax-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/neil" style=3D"font-size=
+ <a href=3D"https://meta.discourse.org/users/neil" style=3D"font-size=
:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif;c=
olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<=
/a><br>
@@ -155,12 +155,12 @@ vember 19</span>
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
+ <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D=
"max-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si=
+ <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si=
ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif=
;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik=
ing</a><br>
@@ -173,7 +173,7 @@ vember 19</span>
<td style=3D"padding-top:5px" colspan=3D"2">
<p style=3D"margin-top:0"><u></u></p><div>
<div></div>
-<img width=3D"20" height=3D"20" src=3D"http://www.gravatar.com/avatar/51d62=
+<img width=3D"20" height=3D"20" src=3D"https://www.gravatar.com/avatar/51d62=
3f33f8b83095db84ff35e15dbe8.png?s=3D40&amp;r=3Dpg&amp;d=3Didenticon" style=
=3D"max-width:694px">codinghorror:</div>
<blockquote><p style=3D"margin-top:0">I can&#39;t even find that topic by n=
@@ -193,12 +193,12 @@ uld be invisible to me, and not showing up in Suggested Topics.</p>
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/51d623f33f8b83095db84ff3=
+ <img src=3D"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff3=
5e15dbe8.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"codinghorror" st=
yle=3D"max-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/codinghorror" style=3D"f=
+ <a href=3D"https://meta.discourse.org/users/codinghorror" style=3D"f=
ont-size:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans=
-serif;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blan=
k">codinghorror</a><br>
@@ -219,12 +219,12 @@ rout" target=3D"_blank">@eviltrout</a>? I can&#39;t even find that topic by=
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
+ <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D=
"max-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si=
+ <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si=
ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif=
;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik=
ing</a><br>
@@ -241,7 +241,7 @@ lar spam post, and it was promptly deleted/hidden, but it just popped up in=
<p style=3D"margin-top:0"></p>
<div><a href=3D"//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557c=
-b249e.png" target=3D"_blank"><img src=3D"http://cdn.discourse.org/uploads/m=
+b249e.png" target=3D"_blank"><img src=3D"https://cdn.discourse.org/uploads/m=
eta_discourse/_optimized/ab1/c92/acd2c33402_584x134.png" width=3D"584" heig=
ht=3D"134" style=3D"max-width:694px"><div>
@@ -257,12 +257,12 @@ ht=3D"134" style=3D"max-width:694px"><div>
<div style=3D"color:#666">
<p>To respond, reply to this email or visit <a href=3D"http://meta.discours=
e.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"color:=
-#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back-up-=
+#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back-up-=
in-suggested-topics/11005/5</a> in your browser.</p>
</div>
<div style=3D"color:#666">
-<p>To unsubscribe from these emails, visit your <a href=3D"http://meta.disc=
+<p>To unsubscribe from these emails, visit your <a href=3D"https://meta.disc=
ourse.org/user_preferences" style=3D"color:#666" target=3D"_blank">user pre=
ferences</a>.</p>
</div>
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 5c5d53877a6..da0343588ef 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -117,7 +117,7 @@ describe ApplicationHelper do
stub_config_setting(https: false)
expect(helper.gravatar_icon(user_email))
- .to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
+ .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
end
it 'uses HTTPs when configured' do
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index a11824d0ac5..838ca9fabef 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -24,7 +24,7 @@ describe Settings do
expect(described_class.host_without_www('http://foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('http://www.foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com'
- expect(described_class.host_without_www('http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
+ expect(described_class.host_without_www('https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
expect(described_class.host_without_www('https://foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('https://www.foo.com')).to eq 'foo.com'
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 2aa4fb1f6c6..cc5fa42aafe 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -262,9 +262,9 @@ describe('Api', () => {
it('fetches an issue template', (done) => {
const namespace = 'some namespace';
const project = 'some project';
- const templateKey = 'template key';
+ const templateKey = ' template #%?.key ';
const templateType = 'template type';
- const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${templateKey}`;
+ const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`;
spyOn(jQuery, 'ajax').and.callFake((request) => {
expect(request.url).toEqual(expectedUrl);
return sendDummyResponse();
diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js
index 9eeae474e7d..38d9bba6868 100644
--- a/spec/javascripts/behaviors/secret_values_spec.js
+++ b/spec/javascripts/behaviors/secret_values_spec.js
@@ -1,16 +1,24 @@
import SecretValues from '~/behaviors/secret_values';
-function generateFixtureMarkup(secrets, isRevealed) {
+function generateValueMarkup(
+ secret,
+ valueClass = 'js-secret-value',
+ placeholderClass = 'js-secret-value-placeholder',
+) {
+ return `
+ <div class="${placeholderClass}">
+ ***
+ </div>
+ <div class="hide ${valueClass}">
+ ${secret}
+ </div>
+ `;
+}
+
+function generateFixtureMarkup(secrets, isRevealed, valueClass, placeholderClass) {
return `
<div class="js-secret-container">
- ${secrets.map(secret => `
- <div class="js-secret-value-placeholder">
- ***
- </div>
- <div class="hide js-secret-value">
- ${secret}
- </div>
- `).join('')}
+ ${secrets.map(secret => generateValueMarkup(secret, valueClass, placeholderClass)).join('')}
<button
class="js-secret-value-reveal-button"
data-secret-reveal-status="${isRevealed}"
@@ -21,11 +29,25 @@ function generateFixtureMarkup(secrets, isRevealed) {
`;
}
-function setupSecretFixture(secrets, isRevealed) {
+function setupSecretFixture(
+ secrets,
+ isRevealed,
+ valueClass = 'js-secret-value',
+ placeholderClass = 'js-secret-value-placeholder',
+) {
const wrapper = document.createElement('div');
- wrapper.innerHTML = generateFixtureMarkup(secrets, isRevealed);
-
- const secretValues = new SecretValues(wrapper.querySelector('.js-secret-container'));
+ wrapper.innerHTML = generateFixtureMarkup(
+ secrets,
+ isRevealed,
+ valueClass,
+ placeholderClass,
+ );
+
+ const secretValues = new SecretValues({
+ container: wrapper.querySelector('.js-secret-container'),
+ valueSelector: `.${valueClass}`,
+ placeholderSelector: `.${placeholderClass}`,
+ });
secretValues.init();
return wrapper;
@@ -49,7 +71,7 @@ describe('setupSecretValues', () => {
expect(revealButton.textContent).toEqual('Hide value');
});
- it('should value hidden initially', () => {
+ it('should have value hidden initially', () => {
const wrapper = setupSecretFixture(secrets, false);
const values = wrapper.querySelectorAll('.js-secret-value');
const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
@@ -143,4 +165,64 @@ describe('setupSecretValues', () => {
});
});
});
+
+ describe('with dynamic secrets', () => {
+ const secrets = ['mysecret123', 'happygoat456', 'tanuki789'];
+
+ it('should toggle values and placeholders', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ // Insert the new dynamic row
+ wrapper.querySelector('.js-secret-container').insertAdjacentHTML('afterbegin', generateValueMarkup('foobarbazdynamic'));
+
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+ const values = wrapper.querySelectorAll('.js-secret-value');
+ const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
+
+ revealButton.click();
+
+ expect(values.length).toEqual(4);
+ values.forEach((value) => {
+ expect(value.classList.contains('hide')).toEqual(false);
+ });
+ expect(placeholders.length).toEqual(4);
+ placeholders.forEach((placeholder) => {
+ expect(placeholder.classList.contains('hide')).toEqual(true);
+ });
+
+ revealButton.click();
+
+ expect(values.length).toEqual(4);
+ values.forEach((value) => {
+ expect(value.classList.contains('hide')).toEqual(true);
+ });
+ expect(placeholders.length).toEqual(4);
+ placeholders.forEach((placeholder) => {
+ expect(placeholder.classList.contains('hide')).toEqual(false);
+ });
+ });
+ });
+
+ describe('selector options', () => {
+ const secrets = ['mysecret123'];
+
+ it('should respect `valueSelector` and `placeholderSelector` options', () => {
+ const valueClass = 'js-some-custom-placeholder-selector';
+ const placeholderClass = 'js-some-custom-value-selector';
+
+ const wrapper = setupSecretFixture(secrets, false, valueClass, placeholderClass);
+ const values = wrapper.querySelectorAll(`.${valueClass}`);
+ const placeholders = wrapper.querySelectorAll(`.${placeholderClass}`);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+
+ expect(values.length).toEqual(1);
+ expect(placeholders.length).toEqual(1);
+
+ revealButton.click();
+
+ expect(values.length).toEqual(1);
+ expect(values[0].classList.contains('hide')).toEqual(false);
+ expect(placeholders.length).toEqual(1);
+ expect(placeholders[0].classList.contains('hide')).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index 0e141adb628..7a34126eef7 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -68,7 +68,7 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
@@ -84,7 +84,7 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 1339ee00870..68a150f602a 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -14,7 +14,7 @@
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"name": "test",
diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js
index a9783ea065c..323fee3767e 100644
--- a/spec/javascripts/helpers/user_mock_data_helper.js
+++ b/spec/javascripts/helpers/user_mock_data_helper.js
@@ -4,7 +4,7 @@ export default {
for (let i = 0; i < numberUsers; i = i += 1) {
users.push(
{
- avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: (i + 1),
name: `GitLab User ${i}`,
username: `gitlab${i}`,
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 43532275121..43589d54be4 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -37,7 +37,7 @@ export default {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
erase_path: '/root/ci-mock/-/jobs/4757/erase',
@@ -54,7 +54,7 @@ export default {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
active: false,
@@ -107,10 +107,10 @@ export default {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
- author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ author_gravatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
},
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index b020a1020df..f0c800c759d 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -107,7 +107,7 @@ export const note = {
"name": "Administrator",
"username": "root",
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"path": "/root"
},
"created_at": "2017-08-10T15:24:03.087Z",
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
new file mode 100644
index 00000000000..3cd33a3e900
--- /dev/null
+++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+
+import axios from '~/lib/utils/axios_utils';
+import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
+import eventHub from '~/pages/milestones/shared/event_hub';
+import * as urlUtility from '~/lib/utils/url_utility';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+describe('delete_milestone_modal.vue', () => {
+ const Component = Vue.extend(deleteMilestoneModal);
+ const props = {
+ issueCount: 1,
+ mergeRequestCount: 2,
+ milestoneId: 3,
+ milestoneTitle: 'my milestone title',
+ milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`,
+ };
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('onSubmit', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ spyOn(eventHub, '$emit');
+ });
+
+ it('deletes milestone and redirects to overview page', (done) => {
+ const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
+ spyOn(axios, 'delete').and.callFake((url) => {
+ expect(url).toBe(props.milestoneUrl);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl);
+ eventHub.$emit.calls.reset();
+ return Promise.resolve({
+ request: {
+ responseURL,
+ },
+ });
+ });
+ const redirectSpy = spyOn(urlUtility, 'redirectTo');
+
+ vm.onSubmit()
+ .then(() => {
+ expect(redirectSpy).toHaveBeenCalledWith(responseURL);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: true });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays error if deleting milestone failed', (done) => {
+ const dummyError = new Error('deleting milestone failed');
+ dummyError.response = { status: 418 };
+ spyOn(axios, 'delete').and.callFake((url) => {
+ expect(url).toBe(props.milestoneUrl);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl);
+ eventHub.$emit.calls.reset();
+ return Promise.reject(dummyError);
+ });
+ const redirectSpy = spyOn(urlUtility, 'redirectTo');
+
+ vm.onSubmit()
+ .catch((error) => {
+ expect(error).toBe(dummyError);
+ expect(redirectSpy).not.toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: false });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('text', () => {
+ it('contains the issue and milestone count', () => {
+ vm = mountComponent(Component, props);
+ const value = vm.text;
+
+ expect(value).toContain('remove it from 1 issue and 2 merge requests');
+ });
+
+ it('contains neither issue nor milestone count', () => {
+ vm = mountComponent(Component, { ...props,
+ issueCount: 0,
+ mergeRequestCount: 0,
+ });
+
+ const value = vm.text;
+
+ expect(value).toContain('is not currently used');
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 7bc591d2d47..d9e84e35f69 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -27,7 +27,7 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0',
},
{
@@ -35,7 +35,7 @@ const RESPONSE_MAP = {
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana',
},
{
@@ -43,7 +43,7 @@ const RESPONSE_MAP = {
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will',
},
],
@@ -72,24 +72,24 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/user0',
+ avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/user0',
},
{
name: 'Marguerite Bartell',
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/tajuana',
+ avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/tajuana',
},
{
name: 'Laureen Ritchie',
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/michaele.will',
+ avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/michaele.will',
},
],
human_time_estimate: null,
@@ -100,24 +100,24 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/user0',
+ avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/user0',
},
{
name: 'Marguerite Bartell',
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/tajuana',
+ avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/tajuana',
},
{
name: 'Laureen Ritchie',
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/michaele.will',
+ avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/michaele.will',
},
],
subscribed: true,
@@ -182,7 +182,7 @@ const mockData = {
id: 1,
name: 'Administrator',
username: 'root',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
rootPath: '/',
fullPath: '/gitlab-org/gitlab-shell',
@@ -194,7 +194,7 @@ const mockData = {
human_total_time_spent: null,
},
user: {
- avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 1,
name: 'Administrator',
username: 'root',
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index ea4eae1e23f..3591f96ff87 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -6,14 +6,14 @@ const ASSIGNEE = {
id: 2,
name: 'gitlab user 2',
username: 'gitlab2',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
const ANOTHER_ASSINEE = {
id: 3,
name: 'gitlab user 3',
username: 'gitlab3',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
const PARTICIPANT = {
@@ -38,7 +38,7 @@ describe('Sidebar store', () => {
id: 1,
name: 'Administrator',
username: 'root',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
editable: true,
rootPath: '/',
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
new file mode 100644
index 00000000000..c39fcda0071
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('MR widget status icon component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(mrStatusIcon);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('while loading', () => {
+ it('renders loading icon', () => {
+ vm = mountComponent(Component, { status: 'loading' });
+ expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner');
+ });
+ });
+
+ describe('with status icon', () => {
+ it('renders ci status icon', () => {
+ vm = mountComponent(Component, { status: 'failed' });
+ expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull();
+ });
+ });
+
+ describe('with disabled button', () => {
+ it('renders a disabled button', () => {
+ vm = mountComponent(Component, { status: 'failed', showDisabledButton: true });
+ expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge');
+ });
+ });
+
+ describe('without disabled button', () => {
+ it('does not render a disabled button', () => {
+ vm = mountComponent(Component, { status: 'failed' });
+ expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
deleted file mode 100644
index 237035648cf..00000000000
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from 'vue';
-import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging';
-
-describe('MRWidgetMerging', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = mergingComponent.props;
-
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
- });
-
- describe('template', () => {
- it('should have correct elements', () => {
- const Component = Vue.extend(mergingComponent);
- const mr = {
- targetBranchPath: '/branch-path',
- targetBranch: 'branch',
- };
- const el = new Component({
- el: document.createElement('div'),
- propsData: { mr },
- }).$el;
-
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('This merge request is in the process of being merged');
- expect(el.innerText).toContain('changes will be merged into');
- expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath);
- expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch);
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js
new file mode 100644
index 00000000000..0b2ed2d4086
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('MRWidgetMerging', () => {
+ let vm;
+ beforeEach(() => {
+ const Component = Vue.extend(mergingComponent);
+
+ vm = mountComponent(Component, { mr: {
+ targetBranchPath: '/branch-path',
+ targetBranch: 'branch',
+ } });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders information about merge request being merged', () => {
+ expect(
+ vm.$el.querySelector('.media-body').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '),
+ ).toContain('This merge request is in the process of being merged');
+ });
+
+ it('renders branch information', () => {
+ expect(
+ vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '),
+ ).toEqual('The changes will be merged into branch');
+ expect(
+ vm.$el.querySelector('a').getAttribute('href'),
+ ).toEqual('/branch-path');
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index ae494267659..3dd75307484 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -38,7 +38,7 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"merged_at": "2017-04-07T15:39:25.696Z",
@@ -50,7 +50,7 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"merge_user": null,
@@ -64,7 +64,7 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"active": false,
@@ -159,10 +159,10 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
- "author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d",
"commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d"
},
diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
new file mode 100644
index 00000000000..5d22dcfb508
--- /dev/null
+++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::Git::AttributesAtRefParser, seed_helper: true do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ subject { described_class.new(repository, 'lfs') }
+
+ it 'loads .gitattributes blob' do
+ repository.raw # Initialize repository in advance since this also checks attributes
+
+ expected_filter = 'filter=lfs diff=lfs merge=lfs'
+ receive_blob = receive(:new).with(a_string_including(expected_filter))
+ expect(Gitlab::Git::AttributesParser).to receive_blob.and_call_original
+
+ subject
+ end
+
+ it 'handles missing blobs' do
+ expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error
+ end
+
+ describe '#attributes' do
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.lfs')['filter']).to eq('lfs')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb
index b715fc3410a..323334e99a5 100644
--- a/spec/lib/gitlab/git/attributes_spec.rb
+++ b/spec/lib/gitlab/git/attributes_parser_spec.rb
@@ -1,11 +1,10 @@
require 'spec_helper'
-describe Gitlab::Git::Attributes, seed_helper: true do
- let(:path) do
- File.join(SEED_STORAGE_PATH, 'with-git-attributes.git')
- end
+describe Gitlab::Git::AttributesParser, seed_helper: true do
+ let(:attributes_path) { File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info', 'attributes') }
+ let(:data) { File.read(attributes_path) }
- subject { described_class.new(path) }
+ subject { described_class.new(data) }
describe '#attributes' do
context 'using a path with attributes' do
@@ -66,6 +65,26 @@ describe Gitlab::Git::Attributes, seed_helper: true do
expect(subject.attributes('test.foo')).to eq({})
end
end
+
+ context 'when attributes data is a file handle' do
+ subject do
+ File.open(attributes_path, 'r') do |file_handle|
+ described_class.new(file_handle)
+ end
+ end
+
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.txt')).to eq({ 'text' => true })
+ end
+ end
+
+ context 'when attributes data is nil' do
+ let(:data) { nil }
+
+ it 'returns an empty Hash' do
+ expect(subject.attributes('test.foo')).to eq({})
+ end
+ end
end
describe '#patterns' do
@@ -74,14 +93,14 @@ describe Gitlab::Git::Attributes, seed_helper: true do
end
it 'parses an entry that uses a tab to separate the pattern and attributes' do
- expect(subject.patterns[File.join(path, '*.md')])
+ expect(subject.patterns[File.join('/', '*.md')])
.to eq({ 'gitlab-language' => 'markdown' })
end
it 'stores patterns in reverse order' do
first = subject.patterns.to_a[0]
- expect(first[0]).to eq(File.join(path, 'bla/bla.txt'))
+ expect(first[0]).to eq(File.join('/', 'bla/bla.txt'))
end
# It's a bit hard to test for something _not_ being processed. As such we'll
@@ -89,14 +108,6 @@ describe Gitlab::Git::Attributes, seed_helper: true do
it 'ignores any comments and empty lines' do
expect(subject.patterns.length).to eq(10)
end
-
- it 'does not parse anything when the attributes file does not exist' do
- expect(File).to receive(:exist?)
- .with(File.join(path, 'info/attributes'))
- .and_return(false)
-
- expect(subject.patterns).to eq({})
- end
end
describe '#parse_attributes' do
@@ -132,17 +143,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do
expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
end
- it 'does not yield when the attributes file does not exist' do
- expect(File).to receive(:exist?)
- .with(File.join(path, 'info/attributes'))
- .and_return(false)
-
- expect { |b| subject.each_line(&b) }.not_to yield_control
- end
-
it 'does not yield when the attributes file has an unsupported encoding' do
- path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git')
- attrs = described_class.new(path)
+ path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info', 'attributes')
+ attrs = described_class.new(File.read(path))
expect { |b| attrs.each_line(&b) }.not_to yield_control
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 168207552ff..8ac960133c5 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -268,6 +268,21 @@ describe Gitlab::Git::Blob, seed_helper: true do
expect(blobs).to all( be_a(Gitlab::Git::Blob) )
end
+ it 'accepts blob IDs as a lazy enumerator' do
+ blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy)
+
+ expect(blobs.count).to eq(1)
+ expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ end
+
+ it 'handles empty list of IDs gracefully' do
+ blobs_1 = described_class.batch_lfs_pointers(repository, [].lazy)
+ blobs_2 = described_class.batch_lfs_pointers(repository, [])
+
+ expect(blobs_1).to eq([])
+ expect(blobs_2).to eq([])
+ end
+
it 'silently ignores tree objects' do
blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
diff --git a/spec/lib/gitlab/git/info_attributes_spec.rb b/spec/lib/gitlab/git/info_attributes_spec.rb
new file mode 100644
index 00000000000..ea84909c3e0
--- /dev/null
+++ b/spec/lib/gitlab/git/info_attributes_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::Git::InfoAttributes, seed_helper: true do
+ let(:path) do
+ File.join(SEED_STORAGE_PATH, 'with-git-attributes.git')
+ end
+
+ subject { described_class.new(path) }
+
+ describe '#attributes' do
+ context 'using a path with attributes' do
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.txt')).to eq({ 'text' => true })
+ end
+
+ it 'returns an empty Hash for a defined path without attributes' do
+ expect(subject.attributes('bla/bla.txt')).to eq({})
+ end
+ end
+ end
+
+ describe '#parser' do
+ it 'parses a file with entries' do
+ expect(subject.patterns).to be_an_instance_of(Hash)
+ expect(subject.patterns["/*.txt"]).to eq({ 'text' => true })
+ end
+
+ it 'does not parse anything when the attributes file does not exist' do
+ expect(File).to receive(:exist?)
+ .with(File.join(path, 'info/attributes'))
+ .and_return(false)
+
+ expect(subject.patterns).to eq({})
+ end
+
+ it 'does not parse attributes files with unsupported encoding' do
+ path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git')
+ subject = described_class.new(path)
+
+ expect(subject.patterns).to eq({})
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 36ca3980de9..3db04d99855 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -899,6 +899,44 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ context "compare results between log_by_walk and log_by_shell" do
+ let(:options) { { ref: "master" } }
+ let(:commits_by_walk) { repository.log(options).map(&:id) }
+ let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:id) }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+
+ context "with limit" do
+ let(:options) { { ref: "master", limit: 1 } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with offset" do
+ let(:options) { { ref: "master", offset: 1 } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with skip_merges" do
+ let(:options) { { ref: "master", skip_merges: true } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with path" do
+ let(:options) { { ref: "master", path: "encoding" } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+
+ context "with follow" do
+ let(:options) { { ref: "master", path: "encoding", follow: true } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+ end
+ end
+
context "where provides 'after' timestamp" do
options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
@@ -1954,6 +1992,47 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#create_from_bundle' do
+ shared_examples 'creating repo from bundle' do
+ let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
+ let(:project) { create(:project) }
+ let(:imported_repo) { project.repository.raw }
+
+ before do
+ expect(repository.bundle_to_disk(bundle_path)).to be true
+ end
+
+ after do
+ FileUtils.rm_rf(bundle_path)
+ end
+
+ it 'creates a repo from a bundle file' do
+ expect(imported_repo).not_to exist
+
+ result = imported_repo.create_from_bundle(bundle_path)
+
+ expect(result).to be true
+ expect(imported_repo).to exist
+ expect { imported_repo.fsck }.not_to raise_exception
+ end
+
+ it 'creates a symlink to the global hooks dir' do
+ imported_repo.create_from_bundle(bundle_path)
+ hooks_path = File.join(imported_repo.path, 'hooks')
+
+ expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
+ end
+ end
+
+ context 'when Gitaly create_repo_from_bundle feature is enabled' do
+ it_behaves_like 'creating repo from bundle'
+ end
+
+ context 'when Gitaly create_repo_from_bundle feature is disabled', :disable_gitaly do
+ it_behaves_like 'creating repo from bundle'
+ end
+ end
+
context 'gitlab_projects commands' do
let(:gitlab_projects) { repository.gitlab_projects }
let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index d72572cd510..44695acbe7d 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -244,7 +244,7 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
it 'returns true when a commit exists' do
expect(project.repository)
- .to receive(:lookup)
+ .to receive(:commit)
.with('123')
.and_return(double(:commit))
@@ -253,9 +253,9 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
it 'returns false when a commit does not exist' do
expect(project.repository)
- .to receive(:lookup)
+ .to receive(:commit)
.with('123')
- .and_raise(Rugged::OdbError)
+ .and_return(nil)
expect(importer.commit_exists?('123')).to eq(false)
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c76f32b3989..429b6615131 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1127,9 +1127,19 @@ describe MergeRequest do
end
end
- context 'when the revert commit is mentioned in a note before the MR was merged' do
+ context 'when the revert commit is mentioned in a note just before the MR was merged' do
before do
- subject.notes.last.update!(created_at: subject.metrics.merged_at - 1.second)
+ subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds)
+ end
+
+ it 'returns false' do
+ expect(subject.can_be_reverted?(current_user)).to be_falsey
+ end
+ end
+
+ context 'when the revert commit is mentioned in a note long before the MR was merged' do
+ before do
+ subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes)
end
it 'returns true' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 8f406253f39..7c61c6b7299 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2356,7 +2356,7 @@ describe Repository do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
- context 'with Gitaly enabled' do
+ shared_examples '#ancestor?' do
it 'it is an ancestor' do
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
end
@@ -2370,27 +2370,19 @@ describe Repository do
expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
expect(repository.ancestor?(nil, nil)).to eq(false)
end
- end
-
- context 'with Gitaly disabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false)
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false)
- end
- it 'it is an ancestor' do
- expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
+ it 'returns false for invalid commit IDs' do
+ expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false)
+ expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
end
+ end
- it 'it is not an ancestor' do
- expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
- end
+ context 'with Gitaly enabled' do
+ it_behaves_like('#ancestor?')
+ end
- it 'returns false on nil-values' do
- expect(repository.ancestor?(nil, commit.id)).to eq(false)
- expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
- expect(repository.ancestor?(nil, nil)).to eq(false)
- end
+ context 'with Gitaly disabled', :skip_gitaly_mock do
+ it_behaves_like('#ancestor?')
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index fd6368e7b40..9e5f08fbc51 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -20,7 +20,7 @@ module TestEnv
'improve/awesome' => '5937ac0',
'merged-target' => '21751bf',
'markdown' => '0ed8c6c',
- 'lfs' => 'be93687',
+ 'lfs' => '55bc176',
'master' => 'b83d6e3',
'merge-test' => '5937ac0',
"'test'" => 'e56497b',
diff --git a/yarn.lock b/yarn.lock
index e66affe2bd4..d10a4372a40 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,9 +54,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.6.0":
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.6.0.tgz#08fa5f2e80b7ac4e4713f71fe8a684bd34430c9d"
+"@gitlab-org/gitlab-svgs@^1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.7.0.tgz#dbb1330a1b1ee478378dddab53fe1a881e810f5d"
"@types/jquery@^2.0.40":
version "2.0.48"