summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flayignore35
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock11
-rw-r--r--Gemfile.rails5.lock10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue42
-rw-r--r--app/assets/stylesheets/framework/mobile.scss5
-rw-r--r--app/controllers/concerns/issuable_collections.rb12
-rw-r--r--app/controllers/projects/application_controller.rb3
-rw-r--r--app/helpers/cookies_helper.rb9
-rw-r--r--app/helpers/wiki_helper.rb6
-rw-r--r--app/policies/issuable_policy.rb1
-rw-r--r--app/policies/issue_policy.rb4
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/serializers/build_details_entity.rb22
-rw-r--r--app/services/files/base_service.rb4
-rw-r--r--app/services/issues/reopen_service.rb2
-rw-r--r--app/services/wikis/create_attachment_service.rb71
-rw-r--r--app/uploaders/file_uploader.rb10
-rw-r--r--app/uploaders/uploader_helper.rb27
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml5
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml16
-rw-r--r--changelogs/unreleased/39665-restrict-issue-reopen.yml5
-rw-r--r--changelogs/unreleased/50101-add-artifact-information-to-job-api.yml5
-rw-r--r--changelogs/unreleased/50879-unused-css-container-fluid.yml5
-rw-r--r--changelogs/unreleased/_acet-disable-ide-button.yml5
-rw-r--r--changelogs/unreleased/fix-junit-parser.yml5
-rw-r--r--changelogs/unreleased/fj-33475-files-inside-wiki-repo.yml5
-rw-r--r--changelogs/unreleased/rails5-mysql-binary-column-index-length.yml5
-rw-r--r--changelogs/unreleased/sh-send-put-headers-object-storage.yml5
-rw-r--r--changelogs/unreleased/sh-set-secure-cookies.yml5
-rw-r--r--config/initializers/mysql_set_length_for_binary_indexes.rb28
-rw-r--r--danger/metadata/Dangerfile2
-rw-r--r--doc/administration/job_traces.md7
-rw-r--r--doc/api/commits.md2
-rw-r--r--doc/api/wikis.md44
-rw-r--r--doc/ci/caching/index.md115
-rw-r--r--doc/ci/junit_test_reports.md8
-rw-r--r--doc/install/installation.md8
-rw-r--r--doc/update/11.2-to-11-3.md378
-rw-r--r--doc/user/discussions/index.md2
-rw-r--r--doc/user/gitlab_com/index.md11
-rw-r--r--lib/api/entities.rb22
-rw-r--r--lib/api/wikis.rb31
-rw-r--r--lib/banzai/filter/wiki_link_filter.rb10
-rw-r--r--lib/banzai/filter/wiki_link_filter/rewriter.rb21
-rw-r--r--lib/gitlab/ci/parsers/junit.rb45
-rw-r--r--lib/gitlab/file_markdown_link_builder.rb21
-rw-r--r--lib/gitlab/file_type_detection.rb43
-rw-r--r--lib/object_storage/direct_upload.rb4
-rw-r--r--lib/tasks/flay.rake9
-rw-r--r--lib/tasks/lint.rake1
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa.rb7
-rw-r--r--qa/qa/page/component/clone_panel.rb (renamed from qa/qa/page/shared/clone_panel.rb)4
-rw-r--r--qa/qa/page/project/issue/show.rb2
-rw-r--r--qa/qa/page/project/show.rb2
-rw-r--r--qa/qa/page/project/wiki/show.rb4
-rw-r--r--qa/qa/scenario/test/integration/object_storage.rb2
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb28
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb56
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb26
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/ci_detailed_status.json24
-rw-r--r--spec/fixtures/api/schemas/http_method.json5
-rw-r--r--spec/fixtures/api/schemas/job/artifact.json11
-rw-r--r--spec/fixtures/api/schemas/job/job.json (renamed from spec/fixtures/api/schemas/job.json)13
-rw-r--r--spec/fixtures/api/schemas/job/job_details.json7
-rw-r--r--spec/fixtures/api/schemas/pipeline_stage.json2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js45
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/parsers/junit_spec.rb64
-rw-r--r--spec/lib/gitlab/file_markdown_link_builder_spec.rb80
-rw-r--r--spec/lib/gitlab/file_type_detection_spec.rb82
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb2
-rw-r--r--spec/policies/issue_policy_spec.rb42
-rw-r--r--spec/requests/api/wikis_spec.rb124
-rw-r--r--spec/services/wikis/create_attachment_service_spec.rb202
-rw-r--r--spec/support/shared_examples/wiki_file_attachments_examples.rb88
-rw-r--r--spec/uploaders/uploader_helper_spec.rb25
84 files changed, 1835 insertions, 325 deletions
diff --git a/.flayignore b/.flayignore
deleted file mode 100644
index 87411516a2a..00000000000
--- a/.flayignore
+++ /dev/null
@@ -1,35 +0,0 @@
-*.erb
-lib/gitlab/sanitizers/svg/whitelist.rb
-lib/gitlab/diff/position_tracer.rb
-app/controllers/projects/approver_groups_controller.rb
-app/controllers/projects/approvers_controller.rb
-app/controllers/projects/protected_branches/merge_access_levels_controller.rb
-app/controllers/projects/protected_branches/push_access_levels_controller.rb
-app/controllers/projects/protected_tags/create_access_levels_controller.rb
-app/helpers/system_note_helper.rb
-app/policies/project_policy.rb
-app/models/concerns/relative_positioning.rb
-app/workers/stuck_merge_jobs_worker.rb
-lib/gitlab/redis/*.rb
-lib/gitlab/gitaly_client/operation_service.rb
-app/models/project_services/packagist_service.rb
-lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
-lib/gitlab/background_migration/*
-app/models/project_services/kubernetes_service.rb
-lib/gitlab/workhorse.rb
-lib/gitlab/ci/trace/chunked_io.rb
-lib/gitlab/gitaly_client/ref_service.rb
-lib/gitlab/gitaly_client/commit_service.rb
-lib/gitlab/git/commit.rb
-lib/gitlab/git/tag.rb
-
-ee/db/**/*
-ee/app/serializers/ee/merge_request_widget_entity.rb
-ee/lib/api/epics.rb
-ee/lib/api/geo_nodes.rb
-ee/lib/ee/api/group_boards.rb
-ee/lib/ee/api/boards.rb
-ee/lib/ee/gitlab/ldap/sync/admin_users.rb
-ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
-ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
-ee/spec/**/*
diff --git a/.gitignore b/.gitignore
index 9a42a663fb4..eb0875a977f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,3 +77,4 @@ eslint-report.html
/plugins/*
/.gitlab_pages_secret
package-lock.json
+/junit_rspec.xml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index db1ce3e9054..1b4134282c9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -171,7 +171,7 @@ stages:
- '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn
- - knapsack rspec "--color --format documentation"
+ - knapsack rspec "--color --format documentation --format RspecJunitFormatter --out junit_rspec.xml"
artifacts:
expire_in: 31d
when: always
@@ -180,6 +180,8 @@ stages:
- knapsack/
- rspec_flaky/
- tmp/capybara/
+ reports:
+ junit: junit_rspec.xml
.rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata
diff --git a/Gemfile b/Gemfile
index 7d9d7a99c71..7b83c6d1178 100644
--- a/Gemfile
+++ b/Gemfile
@@ -363,7 +363,6 @@ group :development, :test do
gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
- gem 'flay', '~> 2.10.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false
@@ -390,6 +389,7 @@ group :test do
gem 'sham_rack', '~> 1.3.6'
gem 'concurrent-ruby', '~> 1.0.5'
gem 'test-prof', '~> 0.2.5'
+ gem 'rspec_junit_formatter'
end
gem 'octokit', '~> 4.9'
diff --git a/Gemfile.lock b/Gemfile.lock
index 11921a64900..b9fa9c74919 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -209,11 +209,6 @@ GEM
fast_gettext (1.6.0)
ffaker (2.4.0)
ffi (1.9.18)
- flay (2.10.0)
- erubis (~> 2.7.0)
- path_expander (~> 1.0)
- ruby_parser (~> 3.0)
- sexp_processor (~> 4.0)
flipper (0.13.0)
flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6)
@@ -588,7 +583,6 @@ GEM
parser (2.5.1.0)
ast (~> 2.4.0)
parslet (1.8.2)
- path_expander (1.0.2)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0)
@@ -776,6 +770,9 @@ GEM
rspec-core
rspec-set (0.1.3)
rspec-support (3.7.1)
+ rspec_junit_formatter (0.2.3)
+ builder (< 4)
+ rspec-core (>= 2, < 4, != 2.12.0)
rspec_profiling (0.0.5)
activerecord
pg
@@ -1024,7 +1021,6 @@ DEPENDENCIES
faraday (~> 0.12)
fast_blank
ffaker (~> 2.4)
- flay (~> 2.10.0)
flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
@@ -1150,6 +1146,7 @@ DEPENDENCIES
rspec-rails (~> 3.7.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
+ rspec_junit_formatter
rspec_profiling (~> 0.0.5)
rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 02f9e112300..0171c3564e3 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -212,11 +212,6 @@ GEM
fast_gettext (1.6.0)
ffaker (2.4.0)
ffi (1.9.18)
- flay (2.10.0)
- erubis (~> 2.7.0)
- path_expander (~> 1.0)
- ruby_parser (~> 3.0)
- sexp_processor (~> 4.0)
flipper (0.13.0)
flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6)
@@ -592,7 +587,6 @@ GEM
parser (2.5.1.0)
ast (~> 2.4.0)
parslet (1.8.2)
- path_expander (1.0.2)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0)
@@ -785,6 +779,8 @@ GEM
rspec-core
rspec-set (0.1.3)
rspec-support (3.7.1)
+ rspec_junit_formatter (0.4.1)
+ rspec-core (>= 2, < 4, != 2.12.0)
rspec_profiling (0.0.5)
activerecord
pg
@@ -1034,7 +1030,6 @@ DEPENDENCIES
faraday (~> 0.12)
fast_blank
ffaker (~> 2.4)
- flay (~> 2.10.0)
flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
@@ -1161,6 +1156,7 @@ DEPENDENCIES
rspec-rails (~> 3.7.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
+ rspec_junit_formatter
rspec_profiling (~> 0.0.5)
rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 72bd28ae03f..4c3f8dff3c4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -4,6 +4,7 @@ import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
@@ -13,6 +14,9 @@ export default {
clipboardButton,
TooltipOnTruncate,
},
+ directives: {
+ tooltip,
+ },
props: {
mr: {
type: Object,
@@ -40,10 +44,19 @@ export default {
});
},
webIdePath() {
- return mergeUrlParams({
- target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
- this.mr.targetProjectFullPath : '',
- }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
+ if (this.mr.canPushToSourceBranch) {
+ return mergeUrlParams({
+ target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
+ this.mr.targetProjectFullPath : '',
+ }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
+ }
+
+ return null;
+ },
+ ideButtonTitle() {
+ return !this.mr.canPushToSourceBranch
+ ? s__('mrWidget|You are not allowed to edit this project directly. Please fork to make changes.')
+ : '';
},
},
};
@@ -93,13 +106,22 @@ export default {
v-if="mr.isOpen"
class="branch-actions"
>
- <a
- v-if="!mr.sourceBranchRemoved"
- :href="webIdePath"
- class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ <span
+ v-tooltip
+ :title="ideButtonTitle"
+ data-placement="bottom"
+ tabindex="0"
>
- {{ s__("mrWidget|Open in Web IDE") }}
- </a>
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ :class="{ disabled: !mr.canPushToSourceBranch }"
+ class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ role="button"
+ >
+ {{ s__("mrWidget|Open in Web IDE") }}
+ </a>
+ </span>
<button
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 6244fb86fea..033e5e57177 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -4,11 +4,6 @@
margin-top: 20px;
}
- .container-fluid {
- padding-left: 5px;
- padding-right: 5px;
- }
-
.nav-links > li > a {
padding: 10px;
font-size: 12px;
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 22b39f47bf0..a2c96f5d635 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -1,5 +1,6 @@
module IssuableCollections
extend ActiveSupport::Concern
+ include CookiesHelper
include SortingHelper
include Gitlab::IssuableMetadata
include Gitlab::Utils::StrongMemoize
@@ -107,11 +108,14 @@ module IssuableCollections
end
def set_sort_order_from_cookie
- cookies[remember_sorting_key] = params[:sort] if params[:sort].present?
+ sort_param = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility
- cookies[remember_sorting_key] ||= cookies['issuable_sort']
- cookies[remember_sorting_key] = update_cookie_value(cookies[remember_sorting_key])
- params[:sort] = cookies[remember_sorting_key]
+ sort_param ||= cookies['issuable_sort']
+ sort_param ||= cookies[remember_sorting_key]
+
+ sort_value = update_cookie_value(sort_param)
+ set_secure_cookie(remember_sorting_key, sort_value)
+ params[:sort] = sort_value
end
def remember_sorting_key
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index b4f814fd3a4..695ffd90a85 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,4 +1,5 @@
class Projects::ApplicationController < ApplicationController
+ include CookiesHelper
include RoutableActions
include ChecksCollaboration
@@ -74,7 +75,7 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
- cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
+ set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present?
end
def require_pages_enabled!
diff --git a/app/helpers/cookies_helper.rb b/app/helpers/cookies_helper.rb
new file mode 100644
index 00000000000..3a7e9987190
--- /dev/null
+++ b/app/helpers/cookies_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module CookiesHelper
+ def set_secure_cookie(key, value, httponly: false, permanent: false)
+ cookie_jar = permanent ? cookies.permanent : cookies
+
+ cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly }
+ end
+end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 41f9eedd4bd..17940aeb900 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -1,4 +1,6 @@
module WikiHelper
+ include API::Helpers::RelatedResourcesHelpers
+
# Produces a pure text breadcrumb for a given page.
#
# page_slug - The slug of a WikiPage object.
@@ -39,4 +41,8 @@ module WikiHelper
end
end
end
+
+ def wiki_attachment_upload_url
+ expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
+ end
end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 198bb168d85..6d8b575102e 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -14,6 +14,7 @@ class IssuablePolicy < BasePolicy
rule { assignee_or_author }.policy do
enable :read_issue
enable :update_issue
+ enable :reopen_issue
enable :read_merge_request
enable :update_merge_request
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 94b5f37c682..a0706eaa46c 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -19,4 +19,8 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue
prevent :admin_issue
end
+
+ rule { locked }.policy do
+ prevent :reopen_issue
+ end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index fd6cc504a3b..273a93a1423 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -180,6 +180,7 @@ class ProjectPolicy < BasePolicy
enable :fork_project
enable :create_project_snippet
enable :update_issue
+ enable :reopen_issue
enable :admin_issue
enable :admin_label
enable :admin_list
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index b887b99d31c..271ff668eda 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -9,6 +9,28 @@ class BuildDetailsEntity < JobEntity
expose :metadata, using: BuildMetadataEntity
+ expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do
+ expose :download_path, if: -> (*) { build.artifacts? } do |build|
+ download_project_job_artifacts_path(project, build)
+ end
+
+ expose :browse_path, if: -> (*) { build.browsable_artifacts? } do |build|
+ browse_project_job_artifacts_path(project, build)
+ end
+
+ expose :keep_path, if: -> (*) { build.has_expiring_artifacts? && can?(current_user, :update_build, build) } do |build|
+ keep_project_job_artifacts_path(project, build)
+ end
+
+ expose :expire_at, if: -> (*) { build.artifacts_expire_at.present? } do |build|
+ build.artifacts_expire_at
+ end
+
+ expose :expired, if: -> (*) { build.artifacts_expire_at.present? } do |build|
+ build.artifacts_expired?
+ end
+ end
+
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 025f093a428..fc7b236f7da 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -7,8 +7,8 @@ module Files
def initialize(*args)
super
- @author_email = params[:author_email]
- @author_name = params[:author_name]
+ @author_email = params[:author_email] || current_user&.email
+ @author_name = params[:author_name] || current_user&.name
@commit_message = params[:commit_message]
@last_commit_sha = params[:last_commit_sha]
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 3bd53f9ccdc..56d59b235a7 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -3,7 +3,7 @@
module Issues
class ReopenService < Issues::BaseService
def execute(issue)
- return issue unless can?(current_user, :update_issue, issue)
+ return issue unless can?(current_user, :reopen_issue, issue)
if issue.reopen
event_service.reopen_issue(issue, current_user)
diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb
new file mode 100644
index 00000000000..30fe0e371a6
--- /dev/null
+++ b/app/services/wikis/create_attachment_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Wikis
+ class CreateAttachmentService < Files::CreateService
+ ATTACHMENT_PATH = 'uploads'.freeze
+ MAX_FILENAME_LENGTH = 255
+
+ delegate :wiki, to: :project
+ delegate :repository, to: :wiki
+
+ def initialize(*args)
+ super
+
+ @file_name = truncate_file_name(params[:file_name])
+ @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name
+ @commit_message ||= "Upload attachment #{@file_name}"
+ @branch_name ||= wiki.default_branch
+ end
+
+ def create_commit!
+ commit_result(create_transformed_commit(@file_content))
+ end
+
+ private
+
+ def truncate_file_name(file_name)
+ return unless file_name.present?
+ return file_name if file_name.length <= MAX_FILENAME_LENGTH
+
+ extension = File.extname(file_name)
+ truncate_at = MAX_FILENAME_LENGTH - extension.length - 1
+ base_name = File.basename(file_name, extension)[0..truncate_at]
+ base_name + extension
+ end
+
+ def validate!
+ validate_file_name!
+ validate_permissions!
+ end
+
+ def validate_file_name!
+ raise_error('The file name cannot be empty') unless @file_name
+ end
+
+ def validate_permissions!
+ unless can?(current_user, :create_wiki, project)
+ raise_error('You are not allowed to push to the wiki')
+ end
+ end
+
+ def create_transformed_commit(content)
+ repository.create_file(
+ current_user,
+ @file_path,
+ content,
+ message: @commit_message,
+ branch_name: @branch_name,
+ author_email: @author_email,
+ author_name: @author_name)
+ end
+
+ def commit_result(commit_id)
+ {
+ file_name: @file_name,
+ file_path: @file_path,
+ branch: @branch_name,
+ commit: commit_id
+ }
+ end
+ end
+end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index b1365659834..ffc1e5f75ca 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -122,12 +122,6 @@ class FileUploader < GitlabUploader
}
end
- def markdown_link
- markdown = +"[#{markdown_name}](#{secure_url})"
- markdown.prepend("!") if image_or_video? || dangerous?
- markdown
- end
-
def to_h
{
alt: markdown_name,
@@ -192,10 +186,6 @@ class FileUploader < GitlabUploader
storage.delete_dir!(store_dir) # only remove when empty
end
- def markdown_name
- (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
- end
-
def identifier
@identifier ||= filename
end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 2a2b54a9270..e8a2dce7755 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -2,32 +2,7 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
- # We recommend using the .mp4 format over .mov. Videos in .mov format can
- # still be used but you really need to make sure they are served with the
- # proper MIME type video/mp4 and not video/quicktime or your videos won't play
- # on IE >= 9.
- # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
- VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
- # These extension types can contain dangerous code and should only be embedded inline with
- # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
- DANGEROUS_EXT = %w[svg].freeze
-
- def image?
- extension_match?(IMAGE_EXT)
- end
-
- def video?
- extension_match?(VIDEO_EXT)
- end
-
- def image_or_video?
- image? || video?
- end
-
- def dangerous?
- extension_match?(DANGEROUS_EXT)
- end
+ include Gitlab::FileMarkdownLinkBuilder
private
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2d036bd4e3e..b81d1a188f0 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -6,6 +6,7 @@
- page_card_attributes @issue.card_attributes
- can_update_issue = can?(current_user, :update_issue, @issue)
+- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
@@ -40,6 +41,7 @@
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ - if can_reopen_issue
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
@@ -48,7 +50,7 @@
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
+ = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index a58179091ae..1bf42ded97a 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -39,4 +39,4 @@
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit"
- = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
+ = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_merge_request
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index d80d2957466..71359708022 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -41,3 +41,8 @@
= render 'sidebar'
#delete-wiki-modal.modal.fade
+
+- content_for :scripts_body do
+ -# haml-lint:disable InlineJavaScript
+ :javascript
+ window.uploads_path = "#{wiki_attachment_upload_url}";
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 933d4b2ea65..70e05eb1c8c 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -2,13 +2,15 @@
- display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable)
-- if can_update && is_current_user
- = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
- = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
-- elsif can_update && !is_current_user
- = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+- if can_update
+ - if is_current_user
+ = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ - else
+ = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+ - if can_reopen && is_current_user
+ = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/changelogs/unreleased/39665-restrict-issue-reopen.yml b/changelogs/unreleased/39665-restrict-issue-reopen.yml
new file mode 100644
index 00000000000..204baafb700
--- /dev/null
+++ b/changelogs/unreleased/39665-restrict-issue-reopen.yml
@@ -0,0 +1,5 @@
+---
+title: Restrict reopening locked issues for non authorized issue authors
+merge_request: 21299
+author:
+type: changed
diff --git a/changelogs/unreleased/50101-add-artifact-information-to-job-api.yml b/changelogs/unreleased/50101-add-artifact-information-to-job-api.yml
new file mode 100644
index 00000000000..f98d111a337
--- /dev/null
+++ b/changelogs/unreleased/50101-add-artifact-information-to-job-api.yml
@@ -0,0 +1,5 @@
+---
+title: Send artifact information in job API
+merge_request: 50460
+author:
+type: other
diff --git a/changelogs/unreleased/50879-unused-css-container-fluid.yml b/changelogs/unreleased/50879-unused-css-container-fluid.yml
new file mode 100644
index 00000000000..3f706472523
--- /dev/null
+++ b/changelogs/unreleased/50879-unused-css-container-fluid.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused CSS part in mobile framework
+merge_request: 21439
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/_acet-disable-ide-button.yml b/changelogs/unreleased/_acet-disable-ide-button.yml
new file mode 100644
index 00000000000..2fff3847052
--- /dev/null
+++ b/changelogs/unreleased/_acet-disable-ide-button.yml
@@ -0,0 +1,5 @@
+---
+title: Disable Web IDE button if user is not allowed to push the source branch.
+merge_request: 21288
+author:
+type: added
diff --git a/changelogs/unreleased/fix-junit-parser.yml b/changelogs/unreleased/fix-junit-parser.yml
new file mode 100644
index 00000000000..e0a9ad8f210
--- /dev/null
+++ b/changelogs/unreleased/fix-junit-parser.yml
@@ -0,0 +1,5 @@
+---
+title: Fix edge cases of JUnitParser
+merge_request: 21469
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-33475-files-inside-wiki-repo.yml b/changelogs/unreleased/fj-33475-files-inside-wiki-repo.yml
new file mode 100644
index 00000000000..8c1f0e3dbf2
--- /dev/null
+++ b/changelogs/unreleased/fj-33475-files-inside-wiki-repo.yml
@@ -0,0 +1,5 @@
+---
+title: Store wiki uploads inside git repository
+merge_request: 21362
+author:
+type: added
diff --git a/changelogs/unreleased/rails5-mysql-binary-column-index-length.yml b/changelogs/unreleased/rails5-mysql-binary-column-index-length.yml
new file mode 100644
index 00000000000..c4eb0ddac4c
--- /dev/null
+++ b/changelogs/unreleased/rails5-mysql-binary-column-index-length.yml
@@ -0,0 +1,5 @@
+---
+title: 'Rails 5: support schema t.index for mysql'
+merge_request: 21485
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/sh-send-put-headers-object-storage.yml b/changelogs/unreleased/sh-send-put-headers-object-storage.yml
new file mode 100644
index 00000000000..cbd8b6deb5b
--- /dev/null
+++ b/changelogs/unreleased/sh-send-put-headers-object-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Send back required object storage PUT headers in /uploads/authorize API
+merge_request: 21319
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-set-secure-cookies.yml b/changelogs/unreleased/sh-set-secure-cookies.yml
new file mode 100644
index 00000000000..da741288b42
--- /dev/null
+++ b/changelogs/unreleased/sh-set-secure-cookies.yml
@@ -0,0 +1,5 @@
+---
+title: Set issuable_sort, diff_view, and perf_bar_enabled cookies to secure when possible
+merge_request: 21442
+author:
+type: security
diff --git a/config/initializers/mysql_set_length_for_binary_indexes.rb b/config/initializers/mysql_set_length_for_binary_indexes.rb
index de0bc5322aa..1b16b39d517 100644
--- a/config/initializers/mysql_set_length_for_binary_indexes.rb
+++ b/config/initializers/mysql_set_length_for_binary_indexes.rb
@@ -2,6 +2,9 @@
# MySQL adapter apply a length of 20. Otherwise MySQL can't create an index on
# binary columns.
+# This module can be removed once a Rails 5 schema is used.
+# It can't be wrapped in a check that checks Gitlab.rails5? because
+# the old Rails 4 schema layout is still used
module MysqlSetLengthForBinaryIndex
def add_index(table_name, column_names, options = {})
Array(column_names).each do |column_name|
@@ -19,3 +22,28 @@ end
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:prepend, MysqlSetLengthForBinaryIndex)
end
+
+if Gitlab.rails5?
+ module MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema
+ # This method is used in Rails 5 schema loading as t.index
+ def index(column_names, options = {})
+ Array(column_names).each do |column_name|
+ column = columns.find { |c| c.name == column_name }
+
+ if column&.type == :binary
+ options[:length] = 20
+ end
+ end
+
+ # Ignore indexes that use opclasses,
+ # also see config/initializers/mysql_ignore_postgresql_options.rb
+ unless options[:opclasses]
+ super(column_names, options)
+ end
+ end
+ end
+
+ if defined?(ActiveRecord::ConnectionAdapters::MySQL::TableDefinition)
+ ActiveRecord::ConnectionAdapters::MySQL::TableDefinition.send(:prepend, MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema)
+ end
+end
diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile
index 3cfaa04e01b..51fc9e6bfca 100644
--- a/danger/metadata/Dangerfile
+++ b/danger/metadata/Dangerfile
@@ -21,5 +21,5 @@ end
has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('Pick into') }
if gitlab.branch_for_base != "master" && !has_pick_into_stable_label
- warn "Most of the time, all merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
+ warn "Most of the time, merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
end
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index 6e2f67f61bc..a792a5f2a97 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -12,8 +12,8 @@ In the following table you can see the phases a trace goes through.
| ----- | ----- | --------- | --------- | ----------- |
| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
-| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`|
-| 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`|
+| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
+| 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
The `ROOT_PATH` varies per your environment. For Omnibus GitLab it
would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source
@@ -88,6 +88,8 @@ To archive those legacy job traces, please follow the instruction below.
## How to migrate archived job traces to object storage
+> [Introduced][ce-21193] in GitLab 11.3.
+
If job traces have already been archived into local storage, and you want to migrate those traces to object storage, please follow the instruction below.
1. Ensure [Object storage integration for Job Artifacts](job_artifacts.md#object-storage-settings) is enabled
@@ -201,4 +203,5 @@ indicate that we have trace chunk. `UPDATE`s with 128KB of data is issued once w
receive multiple chunks.
[ce-18169]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
+[ce-21193]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21193
[ce-46097]: https://gitlab.com/gitlab-org/gitlab-ce/issues/46097
diff --git a/doc/api/commits.md b/doc/api/commits.md
index d07b9d5614a..624ed529009 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -464,7 +464,7 @@ Example response:
},
{
"started_at" : null,
- "name" : "flay",
+ "name" : "test",
"allow_failure" : false,
"status" : "pending",
"created_at" : "2016-01-19T08:40:25.832Z",
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
index 15ce5f96b60..fb0ec773da5 100644
--- a/doc/api/wikis.md
+++ b/doc/api/wikis.md
@@ -97,12 +97,12 @@ curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKE
Example response:
```json
-{
+{
"content" : "Hello world",
"format" : "markdown",
"slug" : "Hello",
"title" : "Hello"
-}
+}
```
## Edit an existing wiki page
@@ -154,6 +154,44 @@ DELETE /projects/:id/wikis/:slug
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo"
```
-On success the HTTP status code is `204` and no JSON response is expected.
+On success the HTTP status code is `204` and no JSON response is expected.
[ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372
+
+## Upload an attachment to the wiki repository
+
+Uploads a file to the attachment folder inside the wiki's repository. The
+ attachment folder is the `uploads` folder.
+
+```
+POST /projects/:id/wikis/attachments
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `file` | string | yes | The attachment to be uploaded |
+| `branch` | string | no | The name of the branch. Defaults to the wiki repository default branch |
+
+To upload a file from your filesystem, use the `--form` argument. This causes
+cURL to post data using the header `Content-Type: multipart/form-data`.
+The `file=` parameter must point to a file on your filesystem and be preceded
+by `@`. For example:
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v4/projects/1/wikis/attachments
+```
+
+Example response:
+
+```json
+{
+ "file_name" : "dk.png",
+ "file_path" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png",
+ "branch" : "master",
+ "link" : {
+ "url" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png",
+ "markdown" : "![dk](uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png)"
+ }
+}
+```
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index 881db84e916..01e95b54fc4 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -14,6 +14,64 @@ starting from GitLab 9.0.
Make sure you read the [`cache` reference](../yaml/README.md#cache) to learn
how it is defined in `.gitlab-ci.yml`.
+## Cache vs artifacts
+
+NOTE: **Note:**
+Be careful if you use cache and artifacts to store the same path in your jobs
+as **caches are restored before artifacts** and the content would be overwritten.
+
+Don't mix the caching with passing artifacts between stages. Caching is not
+designed to pass artifacts between stages. Cache is for runtime dependencies
+needed to compile the project:
+
+- `cache` - **Use for temporary storage for project dependencies.** Not useful
+ for keeping intermediate build results, like `jar` or `apk` files.
+ Cache was designed to be used to speed up invocations of subsequent runs of a
+ given job, by keeping things like dependencies (e.g., npm packages, Go vendor
+ packages, etc.) so they don't have to be re-fetched from the public internet.
+ While the cache can be abused to pass intermediate build results between stages,
+ there may be cases where artifacts are a better fit.
+- `artifacts` - **Use for stage results that will be passed between stages.**
+ Artifacts were designed to upload some compiled/generated bits of the build,
+ and they can be fetched by any number of concurrent Runners. They are
+ guaranteed to be available and are there to pass data between jobs. They are
+ also exposed to be downloaded from the UI. **Artifacts can only exist in
+ directories relative to the build directory** and specifying paths which don't
+ comply to this rule trigger an unintuitive and illogical error message (an
+ enhancement is discussed at
+ https://gitlab.com/gitlab-org/gitlab-ce/issues/15530). Artifacts need to be
+ uploaded to the GitLab instance (not only the GitLab runner) before the next
+ stage job(s) can start, so you need to evaluate carefully whether your
+ bandwidth allows you to profit from parallelization with stages and shared
+ artifacts before investing time in changes to the setup.
+
+It's sometimes confusing because the name artifact sounds like something that
+is only useful outside of the job, like for downloading a final image. But
+artifacts are also available in between stages within a pipeline. So if you
+build your application by downloading all the required modules, you might want
+to declare them as artifacts so that each subsequent stage can depend on them
+being there. There are some optimizations like declaring an
+[expiry time](../yaml/README.md#artifacts-expire_in) so you don't keep artifacts
+around too long, and using [dependencies](../yaml/README.md#dependencies) to
+control exactly where artifacts are passed around.
+
+In summary:
+
+- Caches are disabled if not defined globally or per job (using `cache:`)
+- Caches are available for all jobs in your `.gitlab-ci.yml` if enabled globally
+- Caches can be used by subsequent pipelines of that very same job (a script in
+ a stage) in which the cache was created (if not defined globally).
+- Caches are stored where the Runner is installed **and** uploaded to S3 if
+ [distributed cache is enabled](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching)
+- Caches defined per job are only used either a) for the next pipeline of that job,
+ or b) if that same cache is also defined in a subsequent job of the same pipeline
+- Artifacts are disabled if not defined per job (using `artifacts:`)
+- Artifacts can only be enabled per job, not globally
+- Artifacts are created during a pipeline and can be used by the subsequent
+ jobs of that currently active pipeline
+- Artifacts are always uploaded to GitLab (known as coordinator)
+- Artifacts can have an expiration value for controlling disk usage (30 days by default).
+
## Good caching practices
We have the cache from the perspective of the developers (who consume a cache
@@ -467,60 +525,3 @@ Behind the scenes, this works by increasing a counter in the database, and the
value of that counter is used to create the key for the cache by appending an
integer to it: `-1`, `-2`, etc. After a push, a new key is generated and the
old cache is not valid anymore.
-
-## Cache vs artifacts
-
-NOTE: **Note:**
-Be careful if you use cache and artifacts to store the same path in your jobs
-as **caches are restored before artifacts** and the content would be overwritten.
-
-Don't mix the caching with passing artifacts between stages. Caching is not
-designed to pass artifacts between stages. Cache is for runtime dependencies
-needed to compile the project:
-
-- `cache` - **Use for temporary storage for project dependencies.** Not useful
- for keeping intermediate build results, like `jar` or `apk` files.
- Cache was designed to be used to speed up invocations of subsequent runs of a
- given job, by keeping things like dependencies (e.g., npm packages, Go vendor
- packages, etc.) so they don't have to be re-fetched from the public internet.
- While the cache can be abused to pass intermediate build results between stages,
- there may be cases where artifacts are a better fit.
-- `artifacts` - **Use for stage results that will be passed between stages.**
- Artifacts were designed to upload some compiled/generated bits of the build,
- and they can be fetched by any number of concurrent Runners. They are
- guaranteed to be available and are there to pass data between jobs. They are
- also exposed to be downloaded from the UI. **Artifacts can only exist in
- directories relative to the build directory** and specifying paths which don't
- comply to this rule trigger an unintuitive and unlogical error message (an
- enhancement is discussed at
- https://gitlab.com/gitlab-org/gitlab-ce/issues/15530). Artifacts need to be
- uploaded to the GitLab instance (not only the GitLab runner) before the next
- stage job(s) can start, so you need to evaluate carefully whether your
- bandwidth allows you to profit from parallelization with stages and shared
- artifacts before investing time in changes to the setup.
-
-It's sometimes confusing because the name artifact sounds like something that
-is only useful outside of the job, like for downloading a final image. But
-artifacts are also available in between stages within a pipeline. So if you
-build your application by downloading all the required modules, you might want
-to declare them as artifacts so that each subsequent stage can depend on them
-being there. There are some optimizations like declaring an
-[expiry time](../yaml/README.md#artifacts-expire_in) so you don't keep artifacts
-around too long, and using [dependencies](../yaml/README.md#dependencies) to
-control exactly where artifacts are passed around.
-
-So, to sum up:
-- Caches are disabled if not defined globally or per job (using `cache:`)
-- Caches are available for all jobs in your `.gitlab-ci.yml` if enabled globally
-- Caches can be used by subsequent pipelines of that very same job (a script in
- a stage) in which the cache was created (if not defined globally).
-- Caches are stored where the Runner is installed **and** uploaded to S3 if
- [distributed cache is enabled](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching)
-- Caches defined per job are only used either a) for the next pipeline of that job,
- or b) if that same cache is also defined in a subsequent job of the same pipeline
-- Artifacts are disabled if not defined per job (using `artifacts:`)
-- Artifacts can only be enabled per job, not globally
-- Artifacts are created during a pipeline and can be used by the subsequent
- jobs of that currently active pipeline
-- Artifacts are always uploaded to GitLab (known as coordinator)
-- Artifacts can have an expiration value for controlling disk usage (30 days by default)
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index 6717dd2dad1..cf22450914c 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -139,3 +139,11 @@ java:
- target/surefire-reports/TEST-*.xml
- target/failsafe-reports/TEST-*.xml
```
+
+## Limitations
+
+Currently, the following tools might not work because their XML formats are unsupported in GitLab.
+
+|Case|Tool|Issue|
+|---|---|---|
+|`<testcase>` does not have `classname` attribute|ESlint, sass-lint|https://gitlab.com/gitlab-org/gitlab-ce/issues/50964|
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2d657163721..85431a80a81 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -12,7 +12,7 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
## Select Version to Install
-Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-2-stable`).
+Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-3-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the [GitLab Blog](https://about.gitlab.com/blog/) for installation guide links by version.
@@ -300,9 +300,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-2-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-3-stable gitlab
-**Note:** You can change `11-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `11-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -483,7 +483,7 @@ For more information about configuring Gitaly see
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
-
+
# or you can skip the question by adding force=yes
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes
diff --git a/doc/update/11.2-to-11-3.md b/doc/update/11.2-to-11-3.md
new file mode 100644
index 00000000000..d77f879ee57
--- /dev/null
+++ b/doc/update/11.2-to-11-3.md
@@ -0,0 +1,378 @@
+---
+comments: false
+---
+
+# From 11.2 to 11.3
+
+Make sure you view this update guide from the branch (version) of GitLab you would
+like to install (e.g., `11-3-stable`. You can select the branch in the version
+dropdown at the top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
+echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz
+cd ruby-2.4.4
+
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires 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 `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/>
+
+GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript
+dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 11.0 and higher only supports Go 1.9.x and newer, and dropped support for Go
+1.5.x through 1.8.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
+echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.10.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-3-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-3-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update gitlab-pages
+
+#### Only needed if you use GitLab Pages.
+
+Install and compile gitlab-pages. GitLab-Pages uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-pages
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
+sudo -u git -H make
+```
+
+### 11. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 12. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-1-stable:config/gitlab.yml.example origin/11-3-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/11-1-stable:lib/support/nginx/gitlab-ssl origin/11-3-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/11-1-stable:lib/support/nginx/gitlab origin/11-3-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-3-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-3-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-1-stable:lib/support/init.d/gitlab.default.example origin/11-3-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 13. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 14. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 15. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (11.2)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 11.1 to 11.2](11.1-to-11.2.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-3-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-3-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 9b0ff02f227..aff7898ebf2 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -271,6 +271,8 @@ edit existing comments. Non-team members are restricted from adding or editing c
| :-----------: | :----------: |
| ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) |
+Additionally locked issues can not be reopened.
+
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 20886faf418..de5d7d0a3a0 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -74,22 +74,17 @@ or over the size limit, you can [reduce your repository size with Git](../projec
## Shared Runners
Shared Runners on GitLab.com run in [autoscale mode] and powered by
-Google Cloud Platform and DigitalOcean. Autoscaling means reduced
+Google Cloud Platform. Autoscaling means reduced
waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security.
They're free to use for public open source projects and limited to 2000 CI
minutes per month per group for private projects. Read about all
[GitLab.com plans](https://about.gitlab.com/pricing/).
-In case of DigitalOcean based Runners, all your CI/CD jobs run on ephemeral
-instances with 2GB of RAM, CoreOS and the latest Docker Engine installed.
-Instances provide 2 vCPUs and 60GB of SSD disk space. The default region of the
-VMs is NYC1.
-
-In case of Google Cloud Platform based Runners, all your CI/CD jobs run on
-ephemeral instances with 3.75GB of RAM, CoreOS and the latest Docker Engine
+All your CI/CD jobs run on [n1-standard-1 instances](https://cloud.google.com/compute/docs/machine-types) with 3.75GB of RAM, CoreOS and the latest Docker Engine
installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
region of the VMs is US East1.
+Each instance is used only for one job, this ensures any sensitive data left on the system can't be accessed by other people their CI jobs.
Jobs handled by the shared Runners on GitLab.com (`shared-runners-manager-X.gitlab.com`),
**will be timed out after 3 hours**, regardless of the timeout configured in a
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 59042d2b568..624eda3f5dd 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -10,6 +10,28 @@ module API
expose :content
end
+ class WikiAttachment < Grape::Entity
+ include Gitlab::FileMarkdownLinkBuilder
+
+ expose :file_name
+ expose :file_path
+ expose :branch
+ expose :link do
+ expose :file_path, as: :url
+ expose :markdown do |_entity|
+ self.markdown_link
+ end
+ end
+
+ def filename
+ object.file_name
+ end
+
+ def secure_url
+ object.file_path
+ end
+ end
+
class UserSafe < Grape::Entity
expose :id, :name, :username
end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index b3fc4e876ad..e86ebc573f2 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -1,6 +1,14 @@
module API
class Wikis < Grape::API
helpers do
+ def commit_params(attrs)
+ {
+ file_name: attrs[:file][:filename],
+ file_content: File.read(attrs[:file][:tempfile]),
+ branch_name: attrs[:branch]
+ }
+ end
+
params :wiki_page_params do
requires :content, type: String, desc: 'Content of a wiki page'
requires :title, type: String, desc: 'Title of a wiki page'
@@ -84,6 +92,29 @@ module API
status 204
WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page)
end
+
+ desc 'Upload an attachment to the wiki repository' do
+ detail 'This feature was introduced in GitLab 11.3.'
+ success Entities::WikiAttachment
+ end
+ params do
+ requires :file, type: File, desc: 'The attachment file to be uploaded'
+ optional :branch, type: String, desc: 'The name of the branch'
+ end
+ post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ authorize! :create_wiki, user_project
+
+ result = ::Wikis::CreateAttachmentService.new(user_project,
+ current_user,
+ commit_params(declared_params(include_missing: false))).execute
+
+ if result[:status] == :success
+ status(201)
+ present OpenStruct.new(result[:result]), with: Entities::WikiAttachment
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
end
end
end
diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb
index 870721f895d..1728a442533 100644
--- a/lib/banzai/filter/wiki_link_filter.rb
+++ b/lib/banzai/filter/wiki_link_filter.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'uri'
-
module Banzai
module Filter
# HTML filter that "fixes" links to pages/files in a wiki.
@@ -13,8 +11,12 @@ module Banzai
def call
return doc unless project_wiki?
- doc.search('a:not(.gfm)').each do |el|
- process_link_attr el.attribute('href')
+ doc.search('a:not(.gfm)').each { |el| process_link_attr(el.attribute('href')) }
+ doc.search('video').each { |el| process_link_attr(el.attribute('src')) }
+ doc.search('img').each do |el|
+ attr = el.attribute('data-src') || el.attribute('src')
+
+ process_link_attr(attr)
end
doc
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
index 072d24e5a11..4bf80aff418 100644
--- a/lib/banzai/filter/wiki_link_filter/rewriter.rb
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -10,11 +10,16 @@ module Banzai
def apply_rules
# Special case: relative URLs beginning with `/uploads/` refer to
- # user-uploaded files and will be handled elsewhere.
- return @uri.to_s if @uri.relative? && @uri.path.starts_with?('/uploads/')
+ # user-uploaded files will be handled elsewhere.
+ return @uri.to_s if public_upload?
+
+ # Special case: relative URLs beginning with Wikis::CreateAttachmentService::ATTACHMENT_PATH
+ # refer to user-uploaded files to the wiki repository.
+ unless repository_upload?
+ apply_file_link_rules!
+ apply_hierarchical_link_rules!
+ end
- apply_file_link_rules!
- apply_hierarchical_link_rules!
apply_relative_link_rules!
@uri.to_s
end
@@ -39,6 +44,14 @@ module Banzai
@uri = Addressable::URI.parse(link)
end
end
+
+ def public_upload?
+ @uri.relative? && @uri.path.starts_with?('/uploads/')
+ end
+
+ def repository_upload?
+ @uri.relative? && @uri.path.starts_with?(Wikis::CreateAttachmentService::ATTACHMENT_PATH)
+ end
end
end
end
diff --git a/lib/gitlab/ci/parsers/junit.rb b/lib/gitlab/ci/parsers/junit.rb
index 3c4668ec13b..d1c136f2009 100644
--- a/lib/gitlab/ci/parsers/junit.rb
+++ b/lib/gitlab/ci/parsers/junit.rb
@@ -2,18 +2,14 @@ module Gitlab
module Ci
module Parsers
class Junit
- attr_reader :data
-
JunitParserError = Class.new(StandardError)
def parse!(xml_data, test_suite)
- @data = Hash.from_xml(xml_data)
+ root = Hash.from_xml(xml_data)
- each_suite do |testcases|
- testcases.each do |testcase|
- test_case = create_test_case(testcase)
- test_suite.add_test_case(test_case)
- end
+ all_cases(root) do |test_case|
+ test_case = create_test_case(test_case)
+ test_suite.add_test_case(test_case)
end
rescue REXML::ParseException => e
raise JunitParserError, "XML parsing failed: #{e.message}"
@@ -23,26 +19,27 @@ module Gitlab
private
- def each_suite
- testsuites.each do |testsuite|
- yield testcases(testsuite)
- end
- end
+ def all_cases(root, parent = nil, &blk)
+ return unless root.present?
- def testsuites
- if data['testsuites']
- data['testsuites']['testsuite']
- else
- [data['testsuite']]
+ [root].flatten.compact.map do |node|
+ next unless node.is_a?(Hash)
+
+ # we allow only one top-level 'testsuites'
+ all_cases(node['testsuites'], root, &blk) unless parent
+
+ # we require at least one level of testsuites or testsuite
+ each_case(node['testcase'], &blk) if parent
+
+ # we allow multiple nested 'testsuite' (eg. PHPUnit)
+ all_cases(node['testsuite'], root, &blk)
end
end
- def testcases(testsuite)
- if testsuite['testcase'].is_a?(Array)
- testsuite['testcase']
- else
- [testsuite['testcase']]
- end
+ def each_case(testcase, &blk)
+ return unless testcase.present?
+
+ [testcase].flatten.compact.map(&blk)
end
def create_test_case(data)
diff --git a/lib/gitlab/file_markdown_link_builder.rb b/lib/gitlab/file_markdown_link_builder.rb
new file mode 100644
index 00000000000..5386656efe7
--- /dev/null
+++ b/lib/gitlab/file_markdown_link_builder.rb
@@ -0,0 +1,21 @@
+# Builds the markdown link of a file
+# It needs the methods filename and secure_url (final destination url) to be defined.
+module Gitlab
+ module FileMarkdownLinkBuilder
+ include FileTypeDetection
+
+ def markdown_link
+ return unless name = markdown_name
+
+ markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})"
+ markdown.prepend("!") if image_or_video? || dangerous?
+ markdown
+ end
+
+ def markdown_name
+ return unless filename.present?
+
+ image_or_video? ? File.basename(filename, File.extname(filename)) : filename
+ end
+ end
+end
diff --git a/lib/gitlab/file_type_detection.rb b/lib/gitlab/file_type_detection.rb
new file mode 100644
index 00000000000..25ee07cf940
--- /dev/null
+++ b/lib/gitlab/file_type_detection.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+# File helpers methods.
+# It needs the method filename to be defined.
+module Gitlab
+ module FileTypeDetection
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
+ # We recommend using the .mp4 format over .mov. Videos in .mov format can
+ # still be used but you really need to make sure they are served with the
+ # proper MIME type video/mp4 and not video/quicktime or your videos won't play
+ # on IE >= 9.
+ # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
+ VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
+ # These extension types can contain dangerous code and should only be embedded inline with
+ # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
+ DANGEROUS_EXT = %w[svg].freeze
+
+ def image?
+ extension_match?(IMAGE_EXT)
+ end
+
+ def video?
+ extension_match?(VIDEO_EXT)
+ end
+
+ def image_or_video?
+ image? || video?
+ end
+
+ def dangerous?
+ extension_match?(DANGEROUS_EXT)
+ end
+
+ private
+
+ def extension_match?(extensions)
+ return false unless filename
+
+ extension = File.extname(filename).delete('.')
+ extensions.include?(extension.downcase)
+ end
+ end
+end
diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb
index 61a69e7ffe4..b372b4af090 100644
--- a/lib/object_storage/direct_upload.rb
+++ b/lib/object_storage/direct_upload.rb
@@ -41,7 +41,9 @@ module ObjectStorage
GetURL: get_url,
StoreURL: store_url,
DeleteURL: delete_url,
- MultipartUpload: multipart_upload_hash
+ MultipartUpload: multipart_upload_hash,
+ CustomPutHeaders: true,
+ PutHeaders: upload_options
}.compact
end
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
deleted file mode 100644
index 4bec013a141..00000000000
--- a/lib/tasks/flay.rake
+++ /dev/null
@@ -1,9 +0,0 @@
-desc 'Code duplication analyze via flay'
-task :flay do
- output = `bundle exec flay --mass 35 app/ lib/gitlab/ ee/ 2> #{File::NULL}`
-
- if output.include?("Similar code found") || output.include?("IDENTICAL code found")
- puts output
- exit 1
- end
-end
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 006fcdd31a4..5d673a1a285 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -34,7 +34,6 @@ unless Rails.env.production?
config_lint
lint:haml
scss_lint
- flay
gettext:lint
gettext:updated_check
lint:static_verification
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 394a226e5a5..5f9b02bd559 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6835,6 +6835,9 @@ msgstr ""
msgid "mrWidget|This project is archived, write access has been disabled"
msgstr ""
+msgid "mrWidget|You are not allowed to edit this project directly. Please fork to make changes."
+msgstr ""
+
msgid "mrWidget|You can merge this merge request manually using the"
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index c29d6ce5498..830a6aa17de 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
$: << File.expand_path(File.dirname(__FILE__))
Encoding.default_external = 'UTF-8'
@@ -213,10 +215,6 @@ module QA
end
end
- module Shared
- autoload :ClonePanel, 'qa/page/shared/clone_panel'
- end
-
module Profile
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
end
@@ -250,6 +248,7 @@ module QA
# Classes describing components that are used by several pages.
#
module Component
+ autoload :ClonePanel, 'qa/page/component/clone_panel'
autoload :Dropzone, 'qa/page/component/dropzone'
autoload :Select2, 'qa/page/component/select2'
end
diff --git a/qa/qa/page/shared/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
index 73e3dff956d..8e8ff4e3bb0 100644
--- a/qa/qa/page/shared/clone_panel.rb
+++ b/qa/qa/page/component/clone_panel.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module QA
module Page
- module Shared
+ module Component
module ClonePanel
def self.included(base)
base.view 'app/views/shared/_clone_panel.html.haml' do
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 5bc0598a524..587a02163b9 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module QA
module Page
module Project
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index c751b472535..07b4d0b745d 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -2,7 +2,7 @@ module QA
module Page
module Project
class Show < Page::Base
- include Page::Shared::ClonePanel
+ include Page::Component::ClonePanel
view 'app/views/projects/_last_push.html.haml' do
element :create_merge_request
diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb
index 044e514bab3..c47a715687f 100644
--- a/qa/qa/page/project/wiki/show.rb
+++ b/qa/qa/page/project/wiki/show.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module QA
module Page
module Project
module Wiki
class Show < Page::Base
- include Page::Shared::ClonePanel
+ include Page::Component::ClonePanel
view 'app/views/projects/wikis/pages.html.haml' do
element :clone_repository_link, 'Clone repository'
diff --git a/qa/qa/scenario/test/integration/object_storage.rb b/qa/qa/scenario/test/integration/object_storage.rb
index 874789db20d..2e028bbb5c6 100644
--- a/qa/qa/scenario/test/integration/object_storage.rb
+++ b/qa/qa/scenario/test/integration/object_storage.rb
@@ -4,7 +4,7 @@ module QA
module Scenario
module Test
module Integration
- class ObjectStorage < Test::Instance
+ class ObjectStorage < Test::Instance::All
tags :object_storage
end
end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index c1f42bbb9d7..d16a3464495 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -21,6 +21,34 @@ describe IssuableCollections do
controller
end
+ describe '#set_set_order_from_cookie' do
+ describe 'when sort param given' do
+ let(:cookies) { {} }
+ let(:params) { { sort: 'downvotes_asc' } }
+
+ it 'sets the cookie with the right values and flags' do
+ allow(controller).to receive(:cookies).and_return(cookies)
+
+ controller.send(:set_sort_order_from_cookie)
+
+ expect(cookies['issue_sort']).to eq({ value: 'popularity', secure: false, httponly: false })
+ end
+ end
+
+ describe 'when cookie exists' do
+ let(:cookies) { { 'issue_sort' => 'id_asc' } }
+ let(:params) { {} }
+
+ it 'sets the cookie with the right values and flags' do
+ allow(controller).to receive(:cookies).and_return(cookies)
+
+ controller.send(:set_sort_order_from_cookie)
+
+ expect(cookies['issue_sort']).to eq({ value: 'created_asc', secure: false, httponly: false })
+ end
+ end
+ end
+
describe '#page_count_for_relation' do
let(:params) { { state: 'opened' } }
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 1aca44c6e74..d9499d7e207 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -135,7 +135,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context 'when requesting JSON' do
+ context 'when requesting JSON with failed job' do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
@@ -149,10 +149,60 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
it 'exposes needed information' do
expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
+ expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z})
+ expect(json_response['new_issue_path']).to include('/issues/new')
+ end
+ end
+
+ context 'when request JSON for successful job' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request)
+
+ get_show(id: job.id, format: :json)
+ end
+
+ it 'exposes needed information' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['artifact']['download_path']).to match(%r{artifacts/download})
+ expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse})
+ expect(json_response['artifact']).not_to have_key(:expired)
+ expect(json_response['artifact']).not_to have_key(:expired_at)
expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
- expect(json_response['new_issue_path'])
- .to include('/issues/new')
+ end
+
+ context 'when request JSON for successful job with expired artifacts' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:job) { create(:ci_build, :success, :artifacts, :expired, pipeline: pipeline) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request)
+
+ get_show(id: job.id, format: :json)
+ end
+
+ it 'exposes needed information' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['artifact']).not_to have_key(:download_path)
+ expect(json_response['artifact']).not_to have_key(:browse_path)
+ expect(json_response['artifact']['expired']).to eq(true)
+ expect(json_response['artifact']['expire_at']).not_to be_empty
+ expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
+ expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
+ end
end
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 149eeb4f9ba..b30286e4446 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -146,6 +146,8 @@ describe "User creates wiki page" do
expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
end
end
+
+ it_behaves_like 'wiki file attachments'
end
context "in a group namespace", :js do
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 2840d28cf30..2ce5ee0e87d 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'User updates wiki page' do
shared_examples 'wiki page user update' do
let(:user) { create(:user) }
+
before do
project.add_maintainer(user)
sign_in(user)
@@ -55,6 +56,8 @@ describe 'User updates wiki page' do
expect(page).to have_content('Updated Wiki Content')
end
+
+ it_behaves_like 'wiki file attachments'
end
end
@@ -64,14 +67,14 @@ describe 'User updates wiki page' do
before do
visit(project_wikis_path(project))
+
+ click_link('Edit')
end
context 'in a user namespace' do
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it 'updates a page' do
- click_link('Edit')
-
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
@@ -84,8 +87,6 @@ describe 'User updates wiki page' do
end
it 'shows a validation error message' do
- click_link('Edit')
-
fill_in(:wiki_content, with: '')
click_button('Save changes')
@@ -97,8 +98,6 @@ describe 'User updates wiki page' do
end
it 'shows the emoji autocompletion dropdown', :js do
- click_link('Edit')
-
find('#wiki_content').native.send_keys('')
fill_in(:wiki_content, with: ':')
@@ -106,8 +105,6 @@ describe 'User updates wiki page' do
end
it 'shows the error message' do
- click_link('Edit')
-
wiki_page.update(content: 'Update')
click_button('Save changes')
@@ -116,30 +113,27 @@ describe 'User updates wiki page' do
end
it 'updates a page' do
- click_on('Edit')
fill_in('Content', with: 'Updated Wiki Content')
click_on('Save changes')
expect(page).to have_content('Updated Wiki Content')
end
- it 'cancels edititng of a page' do
- click_on('Edit')
-
+ it 'cancels editing of a page' do
page.within(:css, '.wiki-form .form-actions') do
click_on('Cancel')
end
expect(current_path).to eq(project_wiki_path(project, wiki_page))
end
+
+ it_behaves_like 'wiki file attachments'
end
context 'in a group namespace' do
let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it 'updates a page' do
- click_link('Edit')
-
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
@@ -151,6 +145,8 @@ describe 'User updates wiki page' do
expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
+
+ it_behaves_like 'wiki file attachments'
end
end
@@ -222,6 +218,8 @@ describe 'User updates wiki page' do
expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
end
+
+ it_behaves_like 'wiki file attachments'
end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 760324adacc..747406efc8b 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -93,7 +93,7 @@ describe 'User views a wiki page' do
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
- expect(page).to have_xpath('//img[@data-src="image.jpg"]')
+ expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/image.jpg']")
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
click_on('image')
diff --git a/spec/fixtures/api/schemas/ci_detailed_status.json b/spec/fixtures/api/schemas/ci_detailed_status.json
index 01e34249bf1..d74248eabef 100644
--- a/spec/fixtures/api/schemas/ci_detailed_status.json
+++ b/spec/fixtures/api/schemas/ci_detailed_status.json
@@ -18,7 +18,29 @@
"tooltip": { "type": "string" },
"has_details": { "type": "boolean" },
"details_path": { "type": "string" },
- "favicon": { "type": "string" }
+ "favicon": { "type": "string" },
+ "action": {
+ "type": "object",
+ "required": [
+ "icon",
+ "title",
+ "path",
+ "method"
+ ],
+ "properties": {
+ "icon": {
+ "type": "string",
+ "enum": [
+ "retry",
+ "play",
+ "cancel"
+ ]
+ },
+ "title": { "type": "string" },
+ "path": { "type": "string" },
+ "method": { "$ref": "http_method.json" }
+ }
+ }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/http_method.json b/spec/fixtures/api/schemas/http_method.json
new file mode 100644
index 00000000000..c0f8acc5781
--- /dev/null
+++ b/spec/fixtures/api/schemas/http_method.json
@@ -0,0 +1,5 @@
+{
+ "type": "string",
+ "description": "HTTP methods that the API can specify to send.",
+ "enum": [ "post", "get", "put", "patch" ]
+}
diff --git a/spec/fixtures/api/schemas/job/artifact.json b/spec/fixtures/api/schemas/job/artifact.json
new file mode 100644
index 00000000000..1812e69fbd6
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/artifact.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "properties": {
+ "download_path": { "type": "string"},
+ "browse_path": { "type": "string"},
+ "keep_path": { "type": "string"},
+ "expired": { "type": "boolean" },
+ "expire_at": { "type": "string", "format": "date-time" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/job.json b/spec/fixtures/api/schemas/job/job.json
index 7b92ab25bc1..c793d93c0f6 100644
--- a/spec/fixtures/api/schemas/job.json
+++ b/spec/fixtures/api/schemas/job/job.json
@@ -1,4 +1,5 @@
{
+ "description": "Basic job information",
"type": "object",
"required": [
"id",
@@ -13,12 +14,18 @@
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
- "started": { "type": "boolean" } ,
+ "started": {
+ "oneOf": [
+ { "type": "string", "format": "date-time" },
+ { "type": "boolean" }
+ ]
+ },
"build_path": { "type": "string" },
+ "retry_path": { "type": "string" },
"playable": { "type": "boolean" },
"created_at": { "type": "string" },
"updated_at": { "type": "string" },
- "status": { "$ref": "ci_detailed_status.json" }
+ "status": { "$ref": "../ci_detailed_status.json" }
},
- "additionalProperties": false
+ "additionalProperties": true
}
diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json
new file mode 100644
index 00000000000..73eca83d788
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/job_details.json
@@ -0,0 +1,7 @@
+{
+ "allOf": [{ "$ref": "job.json" }],
+ "description": "An extension of job.json with more detailed information",
+ "properties": {
+ "artifact": { "$ref": "artifact.json" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/pipeline_stage.json b/spec/fixtures/api/schemas/pipeline_stage.json
index 55454200bb3..eb2667295f0 100644
--- a/spec/fixtures/api/schemas/pipeline_stage.json
+++ b/spec/fixtures/api/schemas/pipeline_stage.json
@@ -13,7 +13,7 @@
"groups": { "optional": true },
"latest_statuses": {
"type": "array",
- "items": { "$ref": "job.json" },
+ "items": { "$ref": "job/job.json" },
"optional": true
},
"status": { "$ref": "ci_detailed_status.json" },
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 0e42537faed..237e2fa79f2 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -114,28 +114,31 @@ describe('MRWidgetHeader', () => {
});
describe('with an open merge request', () => {
+ const mrDefaultOptions = {
+ iid: 1,
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ canPushToSourceBranch: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ sourceProjectFullPath: 'root/gitlab-ce',
+ targetProjectFullPath: 'gitlab-org/gitlab-ce',
+ };
+
afterEach(() => {
vm.$destroy();
});
beforeEach(() => {
vm = mountComponent(Component, {
- mr: {
- iid: 1,
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- statusPath: 'abc',
- sourceProjectFullPath: 'root/gitlab-ce',
- targetProjectFullPath: 'gitlab-org/gitlab-ce',
- },
+ mr: Object.assign({}, mrDefaultOptions),
});
});
@@ -151,11 +154,21 @@ describe('MRWidgetHeader', () => {
const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Open in Web IDE');
+ expect(button.classList.contains('disabled')).toBe(false);
expect(button.getAttribute('href')).toEqual(
'/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=gitlab-org%2Fgitlab-ce',
);
});
+ it('renders web ide button in disabled state with no href', () => {
+ const mr = Object.assign({}, mrDefaultOptions, { canPushToSourceBranch: false });
+ vm = mountComponent(Component, { mr });
+
+ const link = vm.$el.querySelector('.js-web-ide');
+ expect(link.classList.contains('disabled')).toBe(true);
+ expect(link.getAttribute('href')).toBeNull();
+ });
+
it('renders web ide button with blank query string if target & source project branch', done => {
vm.mr.targetProjectFullPath = 'root/gitlab-ce';
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
index 50d053011b3..b9059b85fdc 100644
--- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -7,6 +7,7 @@ describe Banzai::Filter::WikiLinkFilter do
let(:project) { build_stubbed(:project, :public, name: "wiki_link_project", namespace: namespace) }
let(:user) { double }
let(:wiki) { ProjectWiki.new(project, user) }
+ let(:repository_upload_folder) { Wikis::CreateAttachmentService::ATTACHMENT_PATH }
it "doesn't rewrite absolute links" do
filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0]
@@ -20,6 +21,45 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('href').value).to eq('/uploads/a.test')
end
+ describe "when links point to the #{Wikis::CreateAttachmentService::ATTACHMENT_PATH} folder" do
+ context 'with an "a" html tag' do
+ it 'rewrites links' do
+ filtered_link = filter("<a href='#{repository_upload_folder}/a.test'>Link</a>", project_wiki: wiki).children[0]
+
+ expect(filtered_link.attribute('href').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.test")
+ end
+ end
+
+ context 'with "img" html tag' do
+ let(:path) { "#{wiki.wiki_base_path}/#{repository_upload_folder}/a.jpg" }
+
+ context 'inside an "a" html tag' do
+ it 'rewrites links' do
+ filtered_elements = filter("<a href='#{repository_upload_folder}/a.jpg'><img src='#{repository_upload_folder}/a.jpg'>example</img></a>", project_wiki: wiki)
+
+ expect(filtered_elements.search('img').first.attribute('src').value).to eq(path)
+ expect(filtered_elements.search('a').first.attribute('href').value).to eq(path)
+ end
+ end
+
+ context 'outside an "a" html tag' do
+ it 'rewrites links' do
+ filtered_link = filter("<img src='#{repository_upload_folder}/a.jpg'>example</img>", project_wiki: wiki).children[0]
+
+ expect(filtered_link.attribute('src').value).to eq(path)
+ end
+ end
+ end
+
+ context 'with "video" html tag' do
+ it 'rewrites links' do
+ filtered_link = filter("<video src='#{repository_upload_folder}/a.mp4'></video>", project_wiki: wiki).children[0]
+
+ expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4")
+ end
+ end
+ end
+
describe "invalid links" do
invalid_links = ["http://:8080", "http://", "http://:8080/path"]
diff --git a/spec/lib/gitlab/ci/parsers/junit_spec.rb b/spec/lib/gitlab/ci/parsers/junit_spec.rb
index f7ec86f5385..47497f06229 100644
--- a/spec/lib/gitlab/ci/parsers/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/junit_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Ci::Parsers::Junit do
describe '#parse!' do
@@ -8,21 +8,35 @@ describe Gitlab::Ci::Parsers::Junit do
let(:test_cases) { flattened_test_cases(test_suite) }
context 'when data is JUnit style XML' do
- context 'when there are no test cases' do
+ context 'when there are no <testcases> in <testsuite>' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuite></testsuite>
EOF
end
- it 'raises an error and does not add any test cases' do
- expect { subject }.to raise_error(described_class::JunitParserError)
+ it 'ignores the case' do
+ expect { subject }.not_to raise_error
+
+ expect(test_cases.count).to eq(0)
+ end
+ end
+
+ context 'when there are no <testcases> in <testsuites>' do
+ let(:junit) do
+ <<-EOF.strip_heredoc
+ <testsuites><testsuite /></testsuites>
+ EOF
+ end
+
+ it 'ignores the case' do
+ expect { subject }.not_to raise_error
expect(test_cases.count).to eq(0)
end
end
- context 'when there is a test case' do
+ context 'when there is only one <testcase> in <testsuite>' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuite>
@@ -40,6 +54,46 @@ describe Gitlab::Ci::Parsers::Junit do
end
end
+ context 'when there is only one <testsuite> in <testsuites>' do
+ let(:junit) do
+ <<-EOF.strip_heredoc
+ <testsuites>
+ <testsuite>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
+ </testsuite>
+ </testsuites>
+ EOF
+ end
+
+ it 'parses XML and adds a test case to a suite' do
+ expect { subject }.not_to raise_error
+
+ expect(test_cases[0].classname).to eq('Calculator')
+ expect(test_cases[0].name).to eq('sumTest1')
+ expect(test_cases[0].execution_time).to eq(0.01)
+ end
+ end
+
+ context 'PHPUnit' do
+ let(:junit) do
+ <<-EOF.strip_heredoc
+ <testsuites>
+ <testsuite name="Project Test Suite" tests="1" assertions="1" failures="0" errors="0" time="1.376748">
+ <testsuite name="XXX\\FrontEnd\\WebBundle\\Tests\\Controller\\LogControllerTest" file="/Users/mcfedr/projects/xxx/server/tests/XXX/FrontEnd/WebBundle/Tests/Controller/LogControllerTest.php" tests="1" assertions="1" failures="0" errors="0" time="1.376748">
+ <testcase name="testIndexAction" class="XXX\\FrontEnd\\WebBundle\\Tests\\Controller\\LogControllerTest" file="/Users/mcfedr/projects/xxx/server/tests/XXX/FrontEnd/WebBundle/Tests/Controller/LogControllerTest.php" line="9" assertions="1" time="1.376748"/>
+ </testsuite>
+ </testsuite>
+ </testsuites>
+ EOF
+ end
+
+ it 'parses XML and adds a test case to a suite' do
+ expect { subject }.not_to raise_error
+
+ expect(test_cases.count).to eq(1)
+ end
+ end
+
context 'when there are two test cases' do
let(:junit) do
<<-EOF.strip_heredoc
diff --git a/spec/lib/gitlab/file_markdown_link_builder_spec.rb b/spec/lib/gitlab/file_markdown_link_builder_spec.rb
new file mode 100644
index 00000000000..feb2776c5d0
--- /dev/null
+++ b/spec/lib/gitlab/file_markdown_link_builder_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+describe Gitlab::FileMarkdownLinkBuilder do
+ let(:custom_class) do
+ Class.new do
+ include Gitlab::FileMarkdownLinkBuilder
+ end.new
+ end
+
+ before do
+ allow(custom_class).to receive(:filename).and_return(filename)
+ end
+
+ describe 'markdown_link' do
+ let(:url) { "/uploads/#{filename}"}
+
+ before do
+ allow(custom_class).to receive(:secure_url).and_return(url)
+ end
+
+ context 'when file name has the character ]' do
+ let(:filename) { 'd]k.png' }
+
+ it 'escapes the character' do
+ expect(custom_class.markdown_link).to eq '![d\\]k](/uploads/d]k.png)'
+ end
+ end
+
+ context 'when file is an image or video' do
+ let(:filename) { 'dk.png' }
+
+ it 'returns preview markdown link' do
+ expect(custom_class.markdown_link).to eq '![dk](/uploads/dk.png)'
+ end
+ end
+
+ context 'when file is not an image or video' do
+ let(:filename) { 'dk.zip' }
+
+ it 'returns markdown link' do
+ expect(custom_class.markdown_link).to eq '[dk.zip](/uploads/dk.zip)'
+ end
+ end
+
+ context 'when file name is blank' do
+ let(:filename) { nil }
+
+ it 'returns nil' do
+ expect(custom_class.markdown_link).to eq nil
+ end
+ end
+ end
+
+ describe 'mardown_name' do
+ context 'when file is an image or video' do
+ let(:filename) { 'dk.png' }
+
+ it 'retrieves the name without the extension' do
+ expect(custom_class.markdown_name).to eq 'dk'
+ end
+ end
+
+ context 'when file is not an image or video' do
+ let(:filename) { 'dk.zip' }
+
+ it 'retrieves the name with the extesion' do
+ expect(custom_class.markdown_name).to eq 'dk.zip'
+ end
+ end
+
+ context 'when file name is blank' do
+ let(:filename) { nil }
+
+ it 'returns nil' do
+ expect(custom_class.markdown_name).to eq nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb
new file mode 100644
index 00000000000..5e9b8988cc8
--- /dev/null
+++ b/spec/lib/gitlab/file_type_detection_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+describe Gitlab::FileTypeDetection do
+ def upload_fixture(filename)
+ fixture_file_upload(File.join('spec', 'fixtures', filename))
+ end
+
+ describe '#image_or_video?' do
+ context 'when class is an uploader' do
+ let(:uploader) do
+ example_uploader = Class.new(CarrierWave::Uploader::Base) do
+ include Gitlab::FileTypeDetection
+
+ storage :file
+ end
+
+ example_uploader.new
+ end
+
+ it 'returns true for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).to be_image_or_video
+ end
+
+ it 'returns true for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).to be_image_or_video
+ end
+
+ it 'returns false for other extensions' do
+ uploader.store!(upload_fixture('doc_sample.txt'))
+
+ expect(uploader).not_to be_image_or_video
+ end
+
+ it 'returns false if filename is blank' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ allow(uploader).to receive(:filename).and_return(nil)
+
+ expect(uploader).not_to be_image_or_video
+ end
+ end
+
+ context 'when class is a regular class' do
+ let(:custom_class) do
+ custom_class = Class.new do
+ include Gitlab::FileTypeDetection
+ end
+
+ custom_class.new
+ end
+
+ it 'returns true for an image file' do
+ allow(custom_class).to receive(:filename).and_return('dk.png')
+
+ expect(custom_class).to be_image_or_video
+ end
+
+ it 'returns true for a video file' do
+ allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
+
+ expect(custom_class).to be_image_or_video
+ end
+
+ it 'returns false for other extensions' do
+ allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
+
+ expect(custom_class).not_to be_image_or_video
+ end
+
+ it 'returns false if filename is blank' do
+ allow(custom_class).to receive(:filename).and_return(nil)
+
+ expect(custom_class).not_to be_image_or_video
+ end
+ end
+ end
+end
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index e0569218d78..632acd6eb46 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -61,6 +61,8 @@ describe ObjectStorage::DirectUpload do
expect(subject[:GetURL]).to start_with(storage_url)
expect(subject[:StoreURL]).to start_with(storage_url)
expect(subject[:DeleteURL]).to start_with(storage_url)
+ expect(subject[:CustomPutHeaders]).to be_truthy
+ expect(subject[:PutHeaders]).to eq({ 'Content-Type' => 'application/octet-stream' })
end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 793b724bfca..93e85b3a6fa 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -112,6 +112,7 @@ describe IssuePolicy do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
let(:issue_no_assignee) { create(:issue, project: project) }
+ let(:issue_locked) { create(:issue, project: project, discussion_locked: true, author: author, assignees: [assignee]) }
before do
project.add_guest(guest)
@@ -124,36 +125,49 @@ describe IssuePolicy do
it 'allows guests to read issues' do
expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+
+ expect(permissions(guest, issue_locked)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(guest, issue_locked)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
end
- it 'allows reporters to read, update, and admin issues' do
- expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ it 'allows reporters to read, update, reopen, and admin issues' do
+ expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(reporter, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_locked)).to be_disallowed(:reopen_issue)
end
- it 'allows reporters from group links to read, update, and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ it 'allows reporters from group links to read, update, reopen and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue)
end
- it 'allows issue authors to read and update their issues' do
- expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ it 'allows issue authors to read, reopen and update their issues' do
+ expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
expect(permissions(author, issue)).to be_disallowed(:admin_issue)
expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+
+ expect(permissions(author, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(author, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue)
end
- it 'allows issue assignees to read and update their issues' do
- expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ it 'allows issue assignees to read, reopen and update their issues' do
+ expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
expect(permissions(assignee, issue)).to be_disallowed(:admin_issue)
expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+
+ expect(permissions(assignee, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(assignee, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue)
end
context 'with confidential issues' do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 489cb001b82..c40d01e1a14 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -139,6 +139,27 @@ describe API::Wikis do
end
end
+ shared_examples_for 'uploads wiki attachment' do
+ it 'pushes attachment to the wiki repository' do
+ allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
+
+ post(api(url, user), payload)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response).to eq result_hash.deep_stringify_keys
+ end
+
+ it 'responds with validation error on empty file' do
+ payload.delete(:file)
+
+ post(api(url, user), payload)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response.size).to eq(1)
+ expect(json_response['error']).to eq('file is missing')
+ end
+ end
+
describe 'GET /projects/:id/wikis' do
let(:url) { "/projects/#{project.id}/wikis" }
@@ -698,4 +719,107 @@ describe API::Wikis do
include_examples '204 No Content'
end
end
+
+ describe 'POST /projects/:id/wikis/attachments' do
+ let(:payload) { { file: fixture_file_upload('spec/fixtures/dk.png') } }
+ let(:url) { "/projects/#{project.id}/wikis/attachments" }
+ let(:file_path) { "#{Wikis::CreateAttachmentService::ATTACHMENT_PATH}/fixed_hex/dk.png" }
+ let(:result_hash) do
+ {
+ file_name: 'dk.png',
+ file_path: file_path,
+ branch: 'master',
+ link: {
+ url: file_path,
+ markdown: "![dk](#{file_path})"
+ }
+ }
+ end
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
+
+ context 'when user is guest' do
+ before do
+ post(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ post(api(url, user), payload)
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is maintainer' do
+ before do
+ project.add_maintainer(user)
+ post(api(url, user), payload)
+ end
+
+ include_examples '403 Forbidden'
+ end
+ end
+
+ context 'when wiki is available only for team members' do
+ let(:project) { create(:project, :wiki_private, :wiki_repo) }
+
+ context 'when user is guest' do
+ before do
+ post(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ include_examples 'uploads wiki attachment'
+ end
+
+ context 'when user is maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ include_examples 'uploads wiki attachment'
+ end
+ end
+
+ context 'when wiki is available for everyone with access' do
+ let(:project) { create(:project, :wiki_repo) }
+
+ context 'when user is guest' do
+ before do
+ post(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ include_examples 'uploads wiki attachment'
+ end
+
+ context 'when user is maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ include_examples 'uploads wiki attachment'
+ end
+ end
+ end
end
diff --git a/spec/services/wikis/create_attachment_service_spec.rb b/spec/services/wikis/create_attachment_service_spec.rb
new file mode 100644
index 00000000000..3f4da873ce4
--- /dev/null
+++ b/spec/services/wikis/create_attachment_service_spec.rb
@@ -0,0 +1,202 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Wikis::CreateAttachmentService do
+ let(:project) { create(:project, :wiki_repo) }
+ let(:user) { create(:user) }
+ let(:file_name) { 'filename.txt' }
+ let(:file_path_regex) { %r{#{described_class::ATTACHMENT_PATH}/\h{32}/#{file_name}} }
+
+ let(:file_opts) do
+ {
+ file_name: file_name,
+ file_content: 'Content of attachment'
+ }
+ end
+ let(:opts) { file_opts }
+
+ subject(:service) { described_class.new(project, user, opts) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe 'initialization' do
+ context 'author commit info' do
+ it 'does not raise error if user is nil' do
+ service = described_class.new(project, nil, opts)
+
+ expect(service.instance_variable_get(:@author_email)).to be_nil
+ expect(service.instance_variable_get(:@author_name)).to be_nil
+ end
+
+ it 'fills file_path from the repository uploads folder' do
+ expect(service.instance_variable_get(:@file_path)).to match(file_path_regex)
+ end
+
+ context 'when no author info provided' do
+ it 'fills author_email and author_name from current_user info' do
+ expect(service.instance_variable_get(:@author_email)).to eq user.email
+ expect(service.instance_variable_get(:@author_name)).to eq user.name
+ end
+ end
+
+ context 'when author info provided' do
+ let(:author_email) { 'author_email' }
+ let(:author_name) { 'author_name' }
+ let(:opts) { file_opts.merge(author_email: author_email, author_name: author_name) }
+
+ it 'fills author_email and author_name from params' do
+ expect(service.instance_variable_get(:@author_email)).to eq author_email
+ expect(service.instance_variable_get(:@author_name)).to eq author_name
+ end
+ end
+ end
+
+ context 'commit message' do
+ context 'when no commit message provided' do
+ it 'sets a default commit message' do
+ expect(service.instance_variable_get(:@commit_message)).to eq "Upload attachment #{opts[:file_name]}"
+ end
+ end
+
+ context 'when commit message provided' do
+ let(:commit_message) { 'whatever' }
+ let(:opts) { file_opts.merge(commit_message: commit_message) }
+
+ it 'use the commit message from params' do
+ expect(service.instance_variable_get(:@commit_message)).to eq commit_message
+ end
+ end
+ end
+
+ context 'branch name' do
+ context 'when no branch provided' do
+ it 'sets the branch from the wiki default_branch' do
+ expect(service.instance_variable_get(:@branch_name)).to eq project.wiki.default_branch
+ end
+ end
+
+ context 'when branch provided' do
+ let(:branch_name) { 'whatever' }
+ let(:opts) { file_opts.merge(branch_name: branch_name) }
+
+ it 'use the commit message from params' do
+ expect(service.instance_variable_get(:@branch_name)).to eq branch_name
+ end
+ end
+ end
+ end
+
+ describe 'validations' do
+ context 'when file_name' do
+ context 'is not present' do
+ let(:file_name) { nil }
+
+ it 'returns error' do
+ result = service.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'The file name cannot be empty'
+ end
+ end
+
+ context 'length' do
+ context 'is bigger than 255' do
+ let(:file_name) { "#{'0' * 256}.jpg" }
+
+ it 'truncates file name' do
+ result = service.execute
+
+ expect(result[:status]).to eq :success
+ expect(result[:result][:file_name].length).to eq 255
+ expect(result[:result][:file_name]).to match(/0{251}\.jpg/)
+ end
+ end
+
+ context 'is less or equal to 255 does not return error' do
+ let(:file_name) { '0' * 255 }
+
+ it 'does not return error' do
+ result = service.execute
+
+ expect(result[:status]).to eq :success
+ end
+ end
+ end
+ end
+
+ context 'when user' do
+ shared_examples 'wiki attachment user validations' do
+ it 'returns error' do
+ result = described_class.new(project, user2, opts).execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'You are not allowed to push to the wiki'
+ end
+ end
+
+ context 'does not have permission' do
+ let(:user2) { create(:user) }
+
+ it_behaves_like 'wiki attachment user validations'
+ end
+
+ context 'is nil' do
+ let(:user2) { nil }
+
+ it_behaves_like 'wiki attachment user validations'
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:wiki) { project.wiki }
+ subject(:service_execute) { service.execute[:result] }
+
+ context 'creates branch if it does not exists' do
+ let(:branch_name) { 'new_branch' }
+ let(:opts) { file_opts.merge(branch_name: branch_name) }
+
+ it do
+ expect(wiki.repository.branches).to be_empty
+ expect { service.execute }.to change { wiki.repository.branches.count }.by(1)
+ expect(wiki.repository.branches.first.name).to eq branch_name
+ end
+ end
+
+ it 'adds file to the repository' do
+ expect(wiki.repository.ls_files('HEAD')).to be_empty
+
+ service.execute
+
+ files = wiki.repository.ls_files('HEAD')
+ expect(files.count).to eq 1
+ expect(files.first).to match(file_path_regex)
+ end
+
+ context 'returns' do
+ before do
+ allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
+
+ service_execute
+ end
+
+ it 'returns the file name' do
+ expect(service_execute[:file_name]).to eq file_name
+ end
+
+ it 'returns the path where file was stored' do
+ expect(service_execute[:file_path]).to eq 'uploads/fixed_hex/filename.txt'
+ end
+
+ it 'returns the branch where the file was pushed' do
+ expect(service_execute[:branch]).to eq wiki.default_branch
+ end
+
+ it 'returns the commit id' do
+ expect(service_execute[:commit]).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb
new file mode 100644
index 00000000000..b6fb2a66b0e
--- /dev/null
+++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# project
+
+shared_examples 'wiki file attachments' do
+ include DropzoneHelper
+
+ context 'uploading attachments', :js do
+ let(:wiki) { project.wiki }
+
+ def attach_with_dropzone(wait = false)
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, wait)
+ end
+
+ context 'before uploading' do
+ it 'shows "Attach a file" button' do
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+ end
+
+ context 'uploading is in progress' do
+ it 'cancels uploading on clicking to "Cancel" button' do
+ slow_requests do
+ attach_with_dropzone
+
+ click_button 'Cancel'
+ end
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_button('Cancel')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+
+ it 'shows "Attaching a file" message on uploading 1 file' do
+ slow_requests do
+ attach_with_dropzone
+
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ end
+ end
+ end
+
+ context 'uploading is complete' do
+ it 'shows "Attach a file" button on uploading complete' do
+ attach_with_dropzone
+ wait_for_requests
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+
+ it 'the markdown link is added to the page' do
+ fill_in(:wiki_content, with: '')
+ attach_with_dropzone(true)
+ wait_for_requests
+
+ expect(page.find('#wiki_content').value)
+ .to match(%r{\!\[dk\]\(uploads/\h{32}/dk\.png\)$})
+ end
+
+ it 'the links point to the wiki root url' do
+ attach_with_dropzone(true)
+ wait_for_requests
+
+ find('.js-md-preview-button').click
+ file_path = page.find('input[name="files[]"]', visible: :hidden).value
+ link = page.find('a.no-attachment-icon')['href']
+ img_link = page.find('a.no-attachment-icon img')['src']
+
+ expect(link).to eq img_link
+ expect(URI.parse(link).path).to eq File.join(wiki.wiki_base_path, file_path)
+ end
+
+ it 'the file has been added to the wiki repository' do
+ expect do
+ attach_with_dropzone(true)
+ wait_for_requests
+ end.to change { wiki.repository.ls_files('HEAD').count }.by(1)
+
+ file_path = page.find('input[name="files[]"]', visible: :hidden).value
+
+ expect(wiki.find_file(file_path, 'HEAD').path).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb
index 33da93cc9d0..fd6712d4645 100644
--- a/spec/uploaders/uploader_helper_spec.rb
+++ b/spec/uploaders/uploader_helper_spec.rb
@@ -11,27 +11,10 @@ describe UploaderHelper do
example_uploader.new
end
- def upload_fixture(filename)
- fixture_file_upload(File.join('spec', 'fixtures', filename))
- end
-
- describe '#image_or_video?' do
- it 'returns true for an image file' do
- uploader.store!(upload_fixture('dk.png'))
-
- expect(uploader).to be_image_or_video
- end
-
- it 'it returns true for a video file' do
- uploader.store!(upload_fixture('video_sample.mp4'))
-
- expect(uploader).to be_image_or_video
- end
-
- it 'returns false for other extensions' do
- uploader.store!(upload_fixture('doc_sample.txt'))
-
- expect(uploader).not_to be_image_or_video
+ describe '#extension_match?' do
+ it 'returns false if file does not exists' do
+ expect(uploader.file).to be_nil
+ expect(uploader.send(:extension_match?, 'jpg')).to eq false
end
end
end