diff options
46 files changed, 813 insertions, 518 deletions
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index c198c4eea4a..f9f4948277d 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -116,6 +116,9 @@ export default class ProjectFindFile { html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); results.push(this.element.find('.tree-table > tbody').append(html)); } + + this.element.find('.empty-state').toggleClass('hidden', Boolean(results.length)); + return results; } diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 2dacd8549ad..32bf05a7629 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -42,6 +42,12 @@ export default { commit() { return this.release.commit || {}; }, + commitUrl() { + return this.release.commit_path; + }, + tagUrl() { + return this.release.tag_path; + }, assets() { return this.release.assets || {}; }, @@ -81,12 +87,18 @@ export default { <div class="card-subtitle d-flex flex-wrap text-secondary"> <div class="append-right-8"> <icon name="commit" class="align-middle" /> - <span v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span> + <gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl"> + {{ commit.short_id }} + </gl-link> + <span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span> </div> <div class="append-right-8"> <icon name="tag" class="align-middle" /> - <span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span> + <gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl"> + {{ release.tag_name }} + </gl-link> + <span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span> </div> <milestone-list diff --git a/app/models/blob.rb b/app/models/blob.rb index a590536d5fe..137dfb484e0 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -176,7 +176,7 @@ class Blob < SimpleDelegator end def video? - UploaderHelper::VIDEO_EXT.include?(extension) + UploaderHelper::SAFE_VIDEO_EXT.include?(extension) end def readable_text? diff --git a/app/models/blob_viewer/image.rb b/app/models/blob_viewer/image.rb index 56e27839fca..cbebef46c60 100644 --- a/app/models/blob_viewer/image.rb +++ b/app/models/blob_viewer/image.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.partial_name = 'image' - self.extensions = UploaderHelper::IMAGE_EXT + self.extensions = UploaderHelper::SAFE_IMAGE_EXT self.binary = true self.switcher_icon = 'picture-o' self.switcher_title = 'image' diff --git a/app/models/blob_viewer/video.rb b/app/models/blob_viewer/video.rb index 48bb2a13518..d35b8e7342e 100644 --- a/app/models/blob_viewer/video.rb +++ b/app/models/blob_viewer/video.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.partial_name = 'video' - self.extensions = UploaderHelper::VIDEO_EXT + self.extensions = UploaderHelper::SAFE_VIDEO_EXT self.binary = true self.switcher_icon = 'film' self.switcher_title = 'video' diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 269145309fc..a98baeb0e3d 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -38,7 +38,7 @@ module Avatarable def avatar_type unless self.avatar.image? - errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::IMAGE_EXT.join(', ')}" + errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::SAFE_IMAGE_EXT.join(', ')}" end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index aa7286a9971..65e87bb08a7 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -75,6 +75,10 @@ class DiffNote < Note self.original_position.diff_refs == diff_refs end + # Checks if the current `position` line in the diff + # exists and is suggestible (not a deletion). + # + # Avoid using in iterations as it requests Gitaly. def supports_suggestion? return false unless noteable&.supports_suggestion? && on_text? # We don't want to trigger side-effects of `diff_file` call. diff --git a/app/models/diff_viewer/image.rb b/app/models/diff_viewer/image.rb index 350bef1d42a..cfda0058d81 100644 --- a/app/models/diff_viewer/image.rb +++ b/app/models/diff_viewer/image.rb @@ -6,7 +6,7 @@ module DiffViewer include ClientSide self.partial_name = 'image' - self.extensions = UploaderHelper::IMAGE_EXT + self.extensions = UploaderHelper::SAFE_IMAGE_EXT self.binary = true self.switcher_icon = 'picture-o' self.switcher_title = _('image diff') diff --git a/app/models/repository.rb b/app/models/repository.rb index 96b1b55e2b1..fbd84e90215 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -249,13 +249,13 @@ class Repository def branch_exists?(branch_name) return false unless raw_repository - branch_names.include?(branch_name) + branch_names_include?(branch_name) end def tag_exists?(tag_name) return false unless raw_repository - tag_names.include?(tag_name) + tag_names_include?(tag_name) end def ref_exists?(ref) @@ -559,10 +559,10 @@ class Repository end delegate :branch_names, to: :raw_repository - cache_method :branch_names, fallback: [] + cache_method_as_redis_set :branch_names, fallback: [] delegate :tag_names, to: :raw_repository - cache_method :tag_names, fallback: [] + cache_method_as_redis_set :tag_names, fallback: [] delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository cache_method :branch_count, fallback: 0 diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb index 22e2f11230d..96ffec90c00 100644 --- a/app/models/suggestion.rb +++ b/app/models/suggestion.rb @@ -41,7 +41,6 @@ class Suggestion < ApplicationRecord !applied? && noteable.opened? && !outdated?(cached: cached) && - note.supports_suggestion? && different_content? && note.active? end diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 82f035f24da..caaf164a763 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -15,4 +15,12 @@ .table-holder %table.table.files-slider{ class: "table_#{@hex_path} tree-table" } %tbody + .col-12.empty-state.hidden + .svg-250.svg-content + = image_tag('illustrations/profile-page/personal-projects.svg', alt: 'No files svg', lazy: true) + .text-center + %h4 + = _('There are no matching files') + %p.text-secondary + = _('Try using a different search term to find the file you are looking for.') = spinner nil, true diff --git a/changelogs/unreleased/61061-links-to-sha-commits-in-release-notes.yml b/changelogs/unreleased/61061-links-to-sha-commits-in-release-notes.yml new file mode 100644 index 00000000000..554d30c9dc5 --- /dev/null +++ b/changelogs/unreleased/61061-links-to-sha-commits-in-release-notes.yml @@ -0,0 +1,5 @@ +--- +title: Links on Releases page to commits and tags +merge_request: 16128 +author: +type: changed diff --git a/changelogs/unreleased/61078-empty-state-file-finder.yml b/changelogs/unreleased/61078-empty-state-file-finder.yml new file mode 100644 index 00000000000..694ac4a3b7c --- /dev/null +++ b/changelogs/unreleased/61078-empty-state-file-finder.yml @@ -0,0 +1,5 @@ +--- +title: Add empty state in file search +merge_request: 16851 +author: +type: changed diff --git a/changelogs/unreleased/64251-branch-name-set-cache.yml b/changelogs/unreleased/64251-branch-name-set-cache.yml new file mode 100644 index 00000000000..6ce4bdf5e43 --- /dev/null +++ b/changelogs/unreleased/64251-branch-name-set-cache.yml @@ -0,0 +1,5 @@ +--- +title: Cache branch and tag names as Redis sets +merge_request: 30476 +author: +type: performance diff --git a/changelogs/unreleased/fj-fix-smau-usage-counters.yml b/changelogs/unreleased/fj-fix-smau-usage-counters.yml new file mode 100644 index 00000000000..3bc4bb348ee --- /dev/null +++ b/changelogs/unreleased/fj-fix-smau-usage-counters.yml @@ -0,0 +1,5 @@ +--- +title: Move SMAU usage counters to the UsageData count field +merge_request: 17074 +author: +type: fixed diff --git a/changelogs/unreleased/osw-unnappliable-suggestion-on-expanded-lines.yml b/changelogs/unreleased/osw-unnappliable-suggestion-on-expanded-lines.yml new file mode 100644 index 00000000000..726ac455466 --- /dev/null +++ b/changelogs/unreleased/osw-unnappliable-suggestion-on-expanded-lines.yml @@ -0,0 +1,5 @@ +--- +title: Adjust unnapliable suggestions in expanded lines +merge_request: 17286 +author: +type: fixed diff --git a/config/brakeman.ignore b/config/brakeman.ignore deleted file mode 100644 index 0e4fef65781..00000000000 --- a/config/brakeman.ignore +++ /dev/null @@ -1,24 +0,0 @@ -{ - "ignored_warnings": [ - { - "warning_type": "Cross-Site Request Forgery", - "warning_code": 7, - "fingerprint": "dc562678129557cdb8b187217da304044547a3605f05fe678093dcb4b4d8bbe4", - "message": "'protect_from_forgery' should be called in Oauth::GeoAuthController", - "file": "app/controllers/oauth/geo_auth_controller.rb", - "line": 1, - "link": "http://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/", - "code": null, - "render_path": null, - "location": { - "type": "controller", - "controller": "Oauth::GeoAuthController" - }, - "user_input": null, - "confidence": "High", - "note": "" - } - ], - "updated": "2017-01-20 02:06:54 +0000", - "brakeman_version": "3.4.1" -} diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index bd51a3e18d7..61ea673071e 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -117,6 +117,35 @@ on adding these events into GitLab: - [Group settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/475) - [Instance-level settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/476) +### Disabled events + +#### Repository push + +The current architecture of audit events is not prepared to receive a very high amount of records. +It may make your project/admin audit logs UI very busy and the disk space consumed by the +`audit_events` Postgres table will increase considerably. Thus, it's disabled by default +to prevent performance degradations on GitLab instances with very high Git write traffic. + +In an upcoming release, Audit Logs for Git push events will be enabled +by default. Follow [#7865](https://gitlab.com/gitlab-org/gitlab/issues/7865) for updates. + +If you still wish to enable **Repository push** events in your instance, follow +the steps bellow. + +**In Omnibus installations:** + +1. Enter the Rails console: + + ```sh + sudo gitlab-rails console + ``` + +1. Flip the switch and enable the feature flag: + + ```ruby + Feature.enable(:repository_push_audit_event) + ``` + [ee-2336]: https://gitlab.com/gitlab-org/gitlab/issues/2336 [ee]: https://about.gitlab.com/pricing/ [permissions]: ../user/permissions.md diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index c856a06b57d..a29769708bb 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -85,6 +85,8 @@ Example response: "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" } ], + "commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a", + "tag_path":"/root/awesome-app/-/tags/v0.11.1", "assets":{ "count":6, "sources":[ @@ -261,6 +263,8 @@ Example response: "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" } ], + "commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a", + "tag_path":"/root/awesome-app/-/tags/v0.11.1", "assets":{ "count":4, "sources":[ @@ -379,6 +383,8 @@ Example response: "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" } ], + "commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a", + "tag_path":"/root/awesome-app/-/tags/v0.11.1", "assets":{ "count":5, "sources":[ @@ -483,6 +489,8 @@ Example response: "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/3" } ], + "commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a", + "tag_path":"/root/awesome-app/-/tags/v0.11.1", "assets":{ "count":4, "sources":[ @@ -563,6 +571,8 @@ Example response: "committer_email":"admin@example.com", "committed_date":"2019-01-03T01:53:28.000Z" }, + "commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a", + "tag_path":"/root/awesome-app/-/tags/v0.11.1", "assets":{ "count":4, "sources":[ diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 8ed941893d3..a0e11d20339 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -375,6 +375,7 @@ timestamps with timezones: - `add_timestamps_with_timezone` - `timestamps_with_timezone` +- `datetime_with_timezone` This ensures all timestamps have a time zone specified. This, in turn, means existing timestamps won't suddenly use a different timezone when the system's diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 1300c99622e..df1f39540d0 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -87,7 +87,7 @@ $ cat -- -l hello ``` -In the GitLab codebase, we avoid the option/argument ambiguity by _always_ using `--`. +In the GitLab codebase, we avoid the option/argument ambiguity by _always_ using `--` for commands that support it. ```ruby # Wrong diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 487ad005f28..43479aff526 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -64,8 +64,8 @@ The following quick actions are applicable to descriptions, discussions and thre | `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue | | `/relate #issue1 #issue2` | ✓ | | | Mark issues as related **(STARTER)** | | `/move <path/to/project>` | ✓ | | | Move this issue to another project | -| `/zoom <Zoom URL>` | ✓ | | | Add Zoom meeting to this issue. ([Introduced in GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/merge_requests/16609) enabled by feature flag `issue_zoom_integration`) | -| `/remove_zoom` | ✓ | | | Remove Zoom meeting from this issue. ([Introduced in GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/merge_requests/16609) enabled by feature flag `issue_zoom_integration`) | +| `/zoom <Zoom URL>` | ✓ | | | Add Zoom meeting to this issue. ([Introduced in GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/merge_requests/16609). Must be enabled by feature flag `issue_zoom_integration` for self-hosted. Feature flag to be removed and available by default in 12.4.) | +| `/remove_zoom` | ✓ | | | Remove Zoom meeting from this issue. ([Introduced in GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/merge_requests/16609). Must be enabled by feature flag `issue_zoom_integration` for self-hosted. Feature flag to be removed and available by default in 12.4.) | | `/target_branch <local branch name>` | | ✓ | | Set target branch | | `/wip` | | ✓ | | Toggle the Work In Progress status | | `/approve` | | ✓ | | Approve the merge request | diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 89951498489..94fa174d4dc 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1276,7 +1276,7 @@ module API class Release < Grape::Entity expose :name - expose :tag, as: :tag_name, if: lambda { |_, _| can_download_code? } + expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? } expose :description expose :description_html do |entity| MarkupHelper.markdown_field(entity, :description) @@ -1284,16 +1284,17 @@ module API expose :created_at expose :released_at expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } - expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? } + expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? } expose :upcoming_release?, as: :upcoming_release expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? } - + expose :commit_path, if: ->(_, _) { can_download_code? } + expose :tag_path, if: ->(_, _) { can_download_code? } expose :assets do expose :assets_count, as: :count do |release, _| assets_to_exclude = can_download_code? ? [] : [:sources] release.assets_count(except: assets_to_exclude) end - expose :sources, using: Entities::Releases::Source, if: lambda { |_, _| can_download_code? } + expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? } expose :links, using: Entities::Releases::Link do |release, options| release.links.sorted end @@ -1304,6 +1305,16 @@ module API def can_download_code? Ability.allowed?(options[:current_user], :download_code, object.project) end + + def commit_path + return unless object.commit + + Gitlab::Routing.url_helpers.project_commit_path(object.project, object.commit.id) + end + + def tag_path + Gitlab::Routing.url_helpers.project_tag_path(object.project, object.tag) + end end class Tag < Grape::Entity diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index c0390959269..ce0c4c5d974 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -127,7 +127,7 @@ module Backup end tar_file = if ENV['BACKUP'].present? - "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}" + File.basename(ENV['BACKUP']) + FILE_NAME_SUFFIX else backup_file_list.first end @@ -235,8 +235,8 @@ module Backup end def tar_file - @tar_file ||= if ENV['BACKUP'] - ENV['BACKUP'] + "#{FILE_NAME_SUFFIX}" + @tar_file ||= if ENV['BACKUP'].present? + File.basename(ENV['BACKUP']) + FILE_NAME_SUFFIX else "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" end diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb index a278fcfdb47..58006cc6c13 100644 --- a/lib/banzai/filter/video_link_filter.rb +++ b/lib/banzai/filter/video_link_filter.rb @@ -19,13 +19,13 @@ module Banzai def query @query ||= begin - src_query = UploaderHelper::VIDEO_EXT.map do |ext| + src_query = UploaderHelper::SAFE_VIDEO_EXT.map do |ext| "'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})" end if context[:asset_proxy_enabled].present? src_query.concat( - UploaderHelper::VIDEO_EXT.map do |ext| + UploaderHelper::SAFE_VIDEO_EXT.map do |ext| "'.#{ext}' = substring(@data-canonical-src, string-length(@data-canonical-src) - #{ext.size})" end ) diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index dfa80eb4a64..5fe06b9c5e6 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -118,8 +118,14 @@ module Gitlab path: file_path } + # Takes action when creating diff notes (multiple calls are + # submitted to this method). Gitlab::SafeRequestStore.fetch(key) { find_diff_file(repository) } end + + # We need to unfold diff lines according to the position in order + # to correctly calculate the line code and trace position changes. + @diff_file&.tap { |file| file.unfold_diff_lines(self) } end def diff_options @@ -152,13 +158,7 @@ module Gitlab return unless diff_refs.complete? return unless comparison = diff_refs.compare_in(repository.project) - file = comparison.diffs(diff_options).diff_files.first - - # We need to unfold diff lines according to the position in order - # to correctly calculate the line code and trace position changes. - file&.unfold_diff_lines(self) - - file + comparison.diffs(diff_options).diff_files.first end def get_formatter_class(type) diff --git a/lib/gitlab/file_markdown_link_builder.rb b/lib/gitlab/file_markdown_link_builder.rb index 180140e7da2..e9e5172e6f8 100644 --- a/lib/gitlab/file_markdown_link_builder.rb +++ b/lib/gitlab/file_markdown_link_builder.rb @@ -10,7 +10,7 @@ module Gitlab return unless name = markdown_name markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})" - markdown = "!#{markdown}" if image_or_video? || dangerous? + markdown = "!#{markdown}" if image_or_video? || dangerous_image_or_video? markdown end diff --git a/lib/gitlab/file_type_detection.rb b/lib/gitlab/file_type_detection.rb index 25ee07cf940..c2b9dfa562d 100644 --- a/lib/gitlab/file_type_detection.rb +++ b/lib/gitlab/file_type_detection.rb @@ -1,34 +1,59 @@ # frozen_string_literal: true -# File helpers methods. -# It needs the method filename to be defined. +# The method `filename` must be defined in classes that use this module. +# +# This module is intended to be used as a helper and not a security gate +# to validate that a file is safe, as it identifies files only by the +# file extension and not its actual contents. +# +# An example useage of this module is in `FileMarkdownLinkBuilder` that +# renders markdown depending on a file name. +# +# We use Workhorse to detect the real extension when we serve files with +# the `SendsBlob` helper methods, and ask Workhorse to set the content +# type when it serves the file: +# https://gitlab.com/gitlab-org/gitlab-ce/blob/33e5955/app/helpers/workhorse_helper.rb#L48. +# +# Because Workhorse has access to the content when it is downloaded, if +# the type/extension doesn't match the real type, we adjust the +# `Content-Type` and `Content-Disposition` to the one we get from the detection. module Gitlab module FileTypeDetection - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze + SAFE_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 + SAFE_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 + DANGEROUS_IMAGE_EXT = %w[svg].freeze + DANGEROUS_VIDEO_EXT = [].freeze # None, yet def image? - extension_match?(IMAGE_EXT) + extension_match?(SAFE_IMAGE_EXT) end def video? - extension_match?(VIDEO_EXT) + extension_match?(SAFE_VIDEO_EXT) end def image_or_video? image? || video? end - def dangerous? - extension_match?(DANGEROUS_EXT) + def dangerous_image? + extension_match?(DANGEROUS_IMAGE_EXT) + end + + def dangerous_video? + extension_match?(DANGEROUS_VIDEO_EXT) + end + + def dangerous_image_or_video? + dangerous_image? || dangerous_video? end private diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index ed2693aaedf..c5303dad558 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -17,7 +17,6 @@ module Gitlab .merge(features_usage_data) .merge(components_usage_data) .merge(cycle_analytics_usage_data) - .merge(usage_counters) end def to_json(force_refresh: false) @@ -99,6 +98,7 @@ module Gitlab web_hooks: count(WebHook) }.merge(services_usage) .merge(approximate_counts) + .merge(usage_counters) }.tap do |data| data[:counts][:user_preferences] = user_preferences_usage end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f41f0dad321..6658d2b67e5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -15647,6 +15647,9 @@ msgstr "" msgid "There are no labels yet" msgstr "" +msgid "There are no matching files" +msgstr "" + msgid "There are no open issues" msgstr "" @@ -16569,6 +16572,9 @@ msgstr "" msgid "Try to fork again" msgstr "" +msgid "Try using a different search term to find the file you are looking for." +msgstr "" + msgid "Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now." msgstr "" diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb index 4363b359038..3d26ff3ed94 100644 --- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb +++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb @@ -14,6 +14,10 @@ describe 'User comments on a diff', :js do expect(suggested_content).to eq(expected_suggested_content) end + def expect_appliable_suggestions(amount) + expect(all('button', text: 'Apply suggestion').size).to eq(amount) + end + let(:project) { create(:project, :repository) } let(:merge_request) do create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') @@ -89,6 +93,60 @@ describe 'User comments on a diff', :js do end end + context 'multiple suggestions in expanded lines' do + it 'suggestions are appliable' do + diff_file = merge_request.diffs(paths: ['files/ruby/popen.rb']).diff_files.first + hash = Digest::SHA1.hexdigest(diff_file.file_path) + + expanded_changes = [ + { + line_code: "#{hash}_1_1", + file_path: diff_file.file_path + }, + { + line_code: "#{hash}_5_5", + file_path: diff_file.file_path + } + ] + changes = sample_compare(expanded_changes).changes.last(expanded_changes.size) + + page.within("[id='#{hash}']") do + find("button[data-original-title='Show full file']").click + wait_for_requests + + click_diff_line(find("[id='#{changes.first[:line_code]}']")) + + page.within('.js-discussion-note-form') do + fill_in('note_note', with: "```suggestion\n# change to a comment\n```") + click_button('Comment') + wait_for_requests + end + + click_diff_line(find("[id='#{changes.last[:line_code]}']")) + + page.within('.js-discussion-note-form') do + fill_in('note_note', with: "```suggestion\n# 2nd change to a comment\n```") + click_button('Comment') + wait_for_requests + end + + expect_appliable_suggestions(2) + end + + # Making sure it's not a Front-end cache. + visit(diffs_project_merge_request_path(project, merge_request)) + + expect_appliable_suggestions(2) + + page.within("[id='#{hash}']") do + all('button', text: 'Apply suggestion').last.click + wait_for_requests + + expect(page).to have_content('Applied') + end + end + end + context 'multiple suggestions in a single note' do it 'suggestions are presented' do click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json index 662e61a9c06..3ca6167d0c6 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release.json +++ b/spec/fixtures/api/schemas/public_api/v4/release.json @@ -19,6 +19,9 @@ "type": "array", "items": { "$ref": "milestone.json" } }, + "commit_path": { "type": "string" }, + "tag_path": { "type": "string" }, + "name": { "type": "string" }, "assets": { "required": ["count", "links", "sources"], "properties": { diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json index 0c1e8fd5fb3..57814b8bf73 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json +++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json @@ -8,6 +8,12 @@ "created_at": { "type": "date" }, "released_at": { "type": "date" }, "upcoming_release": { "type": "boolean" }, + "milestones": { + "type": "array", + "items": { "$ref": "../milestone.json" } + }, + "commit_path": { "type": "string" }, + "tag_path": { "type": "string" }, "author": { "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }] }, diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js new file mode 100644 index 00000000000..83d797469ad --- /dev/null +++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js @@ -0,0 +1,175 @@ +import Vuex from 'vuex'; +import $ from 'jquery'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Dropdown from '~/ide/components/file_templates/dropdown.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('IDE file templates dropdown component', () => { + let wrapper; + let element; + let fetchTemplateTypesMock; + + const defaultProps = { + label: 'label', + }; + + const findItemButtons = () => wrapper.findAll('button'); + const findSearch = () => wrapper.find('input[type="search"]'); + const triggerDropdown = () => $(element).trigger('show.bs.dropdown'); + + const createComponent = ({ props, state } = {}) => { + fetchTemplateTypesMock = jest.fn(); + const fakeStore = new Vuex.Store({ + modules: { + fileTemplates: { + namespaced: true, + state: { + templates: [], + isLoading: false, + ...state, + }, + actions: { + fetchTemplateTypes: fetchTemplateTypesMock, + }, + }, + }, + }); + + wrapper = shallowMount(Dropdown, { + propsData: { + ...defaultProps, + ...props, + }, + store: fakeStore, + localVue, + sync: false, + }); + + ({ element } = wrapper); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('calls clickItem on click', () => { + const itemData = { name: 'test.yml ' }; + createComponent({ props: { data: [itemData] } }); + const item = findItemButtons().at(0); + item.trigger('click'); + + expect(wrapper.emitted().click[0][0]).toBe(itemData); + }); + + it('renders dropdown title', () => { + const title = 'Test title'; + createComponent({ props: { title } }); + + expect(wrapper.find('.dropdown-title').text()).toContain(title); + }); + + describe('in async mode', () => { + const defaultAsyncProps = { ...defaultProps, isAsyncData: true }; + + it('calls `fetchTemplateTypes` on dropdown event', () => { + createComponent({ props: defaultAsyncProps }); + + triggerDropdown(); + + expect(fetchTemplateTypesMock).toHaveBeenCalled(); + }); + + it('does not call `fetchTemplateTypes` on dropdown event if destroyed', () => { + createComponent({ props: defaultAsyncProps }); + wrapper.destroy(); + + triggerDropdown(); + + expect(fetchTemplateTypesMock).not.toHaveBeenCalled(); + }); + + it('shows loader when isLoading is true', () => { + createComponent({ props: defaultAsyncProps, state: { isLoading: true } }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('renders templates', () => { + const templates = [{ name: 'file-1' }, { name: 'file-2' }]; + createComponent({ + props: { ...defaultAsyncProps, data: [{ name: 'should-never-appear ' }] }, + state: { + templates, + }, + }); + const items = findItemButtons(); + + expect(items.wrappers.map(x => x.text())).toEqual(templates.map(x => x.name)); + }); + + it('searches template data', () => { + const templates = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }]; + const matches = ['match 1', 'match 2']; + createComponent({ + props: { ...defaultAsyncProps, data: matches, searchable: true }, + state: { templates }, + }); + findSearch().setValue('match'); + return wrapper.vm.$nextTick().then(() => { + const items = findItemButtons(); + + expect(items.length).toBe(matches.length); + expect(items.wrappers.map(x => x.text())).toEqual(matches); + }); + }); + + it('does not render input when `searchable` is true & `showLoading` is true', () => { + createComponent({ + props: { ...defaultAsyncProps, searchable: true }, + state: { isLoading: true }, + }); + + expect(findSearch().exists()).toBe(false); + }); + }); + + describe('in sync mode', () => { + it('renders props data', () => { + const data = [{ name: 'file-1' }, { name: 'file-2' }]; + createComponent({ + props: { data }, + state: { + templates: [{ name: 'should-never-appear ' }], + }, + }); + + const items = findItemButtons(); + + expect(items.length).toBe(data.length); + expect(items.wrappers.map(x => x.text())).toEqual(data.map(x => x.name)); + }); + + it('renders input when `searchable` is true', () => { + createComponent({ props: { searchable: true } }); + + expect(findSearch().exists()).toBe(true); + }); + + it('searches data', () => { + const data = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }]; + const matches = ['match 1', 'match 2']; + createComponent({ props: { searchable: true, data } }); + findSearch().setValue('match'); + return wrapper.vm.$nextTick().then(() => { + const items = findItemButtons(); + + expect(items.length).toBe(matches.length); + expect(items.wrappers.map(x => x.text())).toEqual(matches); + }); + }); + }); +}); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index 4be5d500fd9..229d3799ee1 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -12,7 +12,6 @@ describe('Release block', () => { propsData: { release: releaseProp, }, - sync: false, }); }; @@ -37,10 +36,16 @@ describe('Release block', () => { it('renders commit sha', () => { expect(wrapper.text()).toContain(release.commit.short_id); + + wrapper.setProps({ release: { ...release, commit_path: '/commit/example' } }); + expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true); }); it('renders tag name', () => { expect(wrapper.text()).toContain(release.tag_name); + + wrapper.setProps({ release: { ...release, tag_path: '/tag/example' } }); + expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true); }); it('renders release date', () => { diff --git a/spec/javascripts/ide/components/file_templates/dropdown_spec.js b/spec/javascripts/ide/components/file_templates/dropdown_spec.js deleted file mode 100644 index 898796f4fa0..00000000000 --- a/spec/javascripts/ide/components/file_templates/dropdown_spec.js +++ /dev/null @@ -1,201 +0,0 @@ -import $ from 'jquery'; -import Vue from 'vue'; -import { createStore } from '~/ide/stores'; -import Dropdown from '~/ide/components/file_templates/dropdown.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; - -describe('IDE file templates dropdown component', () => { - let Component; - let vm; - - beforeAll(() => { - Component = Vue.extend(Dropdown); - }); - - beforeEach(() => { - const store = createStore(); - - vm = createComponentWithStore(Component, store, { - label: 'Test', - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - resetStore(vm.$store); - }); - - describe('async', () => { - beforeEach(() => { - vm.isAsyncData = true; - }); - - it('calls async store method on Bootstrap dropdown event', () => { - spyOn(vm, 'fetchTemplateTypes').and.stub(); - - $(vm.$el).trigger('show.bs.dropdown'); - - expect(vm.fetchTemplateTypes).toHaveBeenCalled(); - }); - - it('renders templates when async', done => { - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test'); - - done(); - }); - }); - - it('renders loading icon when isLoading is true', done => { - vm.$store.state.fileTemplates.isLoading = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.loading-container')).not.toBe(null); - - done(); - }); - }); - - it('searches template data', () => { - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - vm.searchable = true; - vm.search = 'hello'; - - expect(vm.outputData).toEqual([]); - }); - - it('does not filter data is searchable is false', () => { - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - vm.search = 'hello'; - - expect(vm.outputData).toEqual([ - { - name: 'test', - }, - ]); - }); - - it('calls clickItem on click', done => { - spyOn(vm, 'clickItem').and.stub(); - - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - - vm.$nextTick(() => { - vm.$el.querySelector('.dropdown-content button').click(); - - expect(vm.clickItem).toHaveBeenCalledWith({ - name: 'test', - }); - - done(); - }); - }); - - it('renders input when searchable is true', done => { - vm.searchable = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); - - done(); - }); - }); - - it('does not render input when searchable is true & showLoading is true', done => { - vm.searchable = true; - vm.$store.state.fileTemplates.isLoading = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input')).toBe(null); - - done(); - }); - }); - }); - - describe('sync', () => { - beforeEach(done => { - vm.data = [ - { - name: 'test sync', - }, - ]; - - vm.$nextTick(done); - }); - - it('renders props data', () => { - expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync'); - }); - - it('renders input when searchable is true', done => { - vm.searchable = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); - - done(); - }); - }); - - it('calls clickItem on click', done => { - spyOn(vm, 'clickItem').and.stub(); - - vm.$nextTick(() => { - vm.$el.querySelector('.dropdown-content button').click(); - - expect(vm.clickItem).toHaveBeenCalledWith({ - name: 'test sync', - }); - - done(); - }); - }); - - it('searches template data', () => { - vm.searchable = true; - vm.search = 'hello'; - - expect(vm.outputData).toEqual([]); - }); - - it('does not filter data is searchable is false', () => { - vm.search = 'hello'; - - expect(vm.outputData).toEqual([ - { - name: 'test sync', - }, - ]); - }); - - it('renders dropdown title', done => { - vm.title = 'Test title'; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title'); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js deleted file mode 100644 index 11a385fa64d..00000000000 --- a/spec/javascripts/releases/components/release_block_spec.js +++ /dev/null @@ -1,170 +0,0 @@ -import Vue from 'vue'; -import component from '~/releases/components/release_block.vue'; -import timeagoMixin from '~/vue_shared/mixins/timeago'; - -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('Release block', () => { - const Component = Vue.extend(component); - - const release = { - name: 'Bionic Beaver', - tag_name: '18.04', - description: '## changelog\n\n* line 1\n* line2', - description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>', - author_name: 'Release bot', - author_email: 'release-bot@example.com', - released_at: '2012-05-28T05:00:00-07:00', - author: { - avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', - id: 482476, - name: 'John Doe', - path: '/johndoe', - state: 'active', - status_tooltip_html: null, - username: 'johndoe', - web_url: 'https://gitlab.com/johndoe', - }, - commit: { - id: '2695effb5807a22ff3d138d593fd856244e155e7', - short_id: '2695effb', - title: 'Initial commit', - created_at: '2017-07-26T11:08:53.000+02:00', - parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'], - message: 'Initial commit', - author_name: 'John Smith', - author_email: 'john@example.com', - authored_date: '2012-05-28T04:42:42-07:00', - committer_name: 'Jack Smith', - committer_email: 'jack@example.com', - committed_date: '2012-05-28T04:42:42-07:00', - }, - assets: { - count: 6, - sources: [ - { - format: 'zip', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip', - }, - { - format: 'tar.gz', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz', - }, - { - format: 'tar.bz2', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2', - }, - { - format: 'tar', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar', - }, - ], - links: [ - { - name: 'release-18.04.dmg', - url: 'https://my-external-hosting.example.com/scrambled-url/', - external: true, - }, - { - name: 'binary-linux-amd64', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50', - external: false, - }, - ], - }, - }; - let vm; - - const factory = props => mountComponent(Component, { release: props }); - - beforeEach(() => { - vm = factory(release); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it("renders the block with an id equal to the release's tag name", () => { - expect(vm.$el.id).toBe('18.04'); - }); - - it('renders release name', () => { - expect(vm.$el.textContent).toContain(release.name); - }); - - it('renders commit sha', () => { - expect(vm.$el.textContent).toContain(release.commit.short_id); - }); - - it('renders tag name', () => { - expect(vm.$el.textContent).toContain(release.tag_name); - }); - - it('renders release date', () => { - expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at)); - }); - - it('renders number of assets provided', () => { - expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count); - }); - - it('renders dropdown with the sources', () => { - expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual( - release.assets.sources.length, - ); - - expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual( - release.assets.sources[0].url, - ); - - expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain( - release.assets.sources[0].format, - ); - }); - - it('renders list with the links provided', () => { - expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual( - release.assets.links.length, - ); - - expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual( - release.assets.links[0].url, - ); - - expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain( - release.assets.links[0].name, - ); - }); - - it('renders author avatar', () => { - expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull(); - }); - - describe('external label', () => { - it('renders external label when link is external', () => { - expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain('external source'); - }); - - it('does not render external label when link is not external', () => { - expect(vm.$el.querySelector('.js-assets-list li:nth-child(2) a').textContent).not.toContain( - 'external source', - ); - }); - }); - - describe('with upcoming_release flag', () => { - beforeEach(() => { - vm = factory(Object.assign({}, release, { upcoming_release: true })); - }); - - it('renders upcoming release badge', () => { - expect(vm.$el.textContent).toContain('Upcoming Release'); - }); - }); -}); diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index fee7ffc60ee..35594cd2fb8 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -21,6 +21,49 @@ describe Backup::Manager do $progress = @old_progress # rubocop:disable Style/GlobalVars end + describe '#pack' do + let(:backup_contents) { ['backup_contents'] } + let(:tar_system_options) { { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] } } + let(:tar_cmdline) { ['tar', '-cf', '-', *backup_contents, tar_system_options] } + + let(:backup_information) do + { + backup_created_at: Time.zone.parse('2019-01-01'), + gitlab_version: '12.3' + } + end + + before do + allow(ActiveRecord::Base.connection).to receive(:reconnect!) + allow(Kernel).to receive(:system).and_return(true) + + allow(subject).to receive(:backup_contents).and_return(backup_contents) + allow(subject).to receive(:backup_information).and_return(backup_information) + allow(subject).to receive(:upload) + end + + context 'when BACKUP is not set' do + let(:tar_file) { '1546300800_2019_01_01_12.3_gitlab_backup.tar' } + + it 'uses the default tar file name' do + subject.pack + + expect(Kernel).to have_received(:system).with(*tar_cmdline) + end + end + + context 'when BACKUP is set' do + let(:tar_file) { 'custom_gitlab_backup.tar' } + + it 'uses the given value as tar file name' do + stub_env('BACKUP', '/ignored/path/custom') + subject.pack + + expect(Kernel).to have_received(:system).with(*tar_cmdline) + end + end + end + describe '#remove_old' do let(:files) do [ @@ -238,7 +281,7 @@ describe Backup::Manager do allow(Kernel).to receive(:system).and_return(true) allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) - stub_env('BACKUP', '1451606400_2016_01_01_1.2.3') + stub_env('BACKUP', '/ignored/path/1451606400_2016_01_01_1.2.3') end it 'unpacks the file' do diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb index cd932f502f3..b5be204d680 100644 --- a/spec/lib/banzai/filter/video_link_filter_spec.rb +++ b/spec/lib/banzai/filter/video_link_filter_spec.rb @@ -18,7 +18,7 @@ describe Banzai::Filter::VideoLinkFilter do let(:project) { create(:project, :repository) } context 'when the element src has a video extension' do - UploaderHelper::VIDEO_EXT.each do |ext| + UploaderHelper::SAFE_VIDEO_EXT.each do |ext| it "replaces the image tag 'path/video.#{ext}' with a video tag" do container = filter(link_to_image("/path/video.#{ext}")).children.first diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 399787635c0..839780b53fe 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -130,6 +130,26 @@ describe Gitlab::Diff::Position do expect(diff_file.new_path).to eq(subject.new_path) expect(diff_file.diff_refs).to eq(subject.diff_refs) end + + context 'different folded positions in the same diff file' do + def diff_file(args = {}) + described_class + .new(args_for_text.merge(args)) + .diff_file(project.repository) + end + + it 'expands the diff file', :request_store do + expect_any_instance_of(Gitlab::Diff::File) + .to receive(:unfold_diff_lines).and_call_original + + diff_file(old_line: 1, new_line: 1, diff_refs: commit.diff_refs) + + expect_any_instance_of(Gitlab::Diff::File) + .to receive(:unfold_diff_lines).and_call_original + + diff_file(old_line: 5, new_line: 5, diff_refs: commit.diff_refs) + end + end end describe "#diff_line" do diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb index 22ec7d414e8..1edf882afe2 100644 --- a/spec/lib/gitlab/file_type_detection_spec.rb +++ b/spec/lib/gitlab/file_type_detection_spec.rb @@ -2,38 +2,103 @@ require 'spec_helper' describe Gitlab::FileTypeDetection do - def upload_fixture(filename) - fixture_file_upload(File.join('spec', 'fixtures', filename)) - end + context 'when class is an uploader' do + shared_examples '#image? for an uploader' do + it 'returns true for an image file' do + uploader.store!(upload_fixture('dk.png')) - 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 + expect(uploader).to be_image + end - storage :file - end + it 'returns false if filename has a dangerous image extension' do + uploader.store!(upload_fixture('unsanitized.svg')) - example_uploader.new + expect(uploader).to be_dangerous_image + expect(uploader).not_to be_image end - it 'returns true for an image file' do + it 'returns false for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).not_to be_image + end + + it 'returns false if filename is blank' do uploader.store!(upload_fixture('dk.png')) - expect(uploader).to be_image_or_video + allow(uploader).to receive(:filename).and_return(nil) + + expect(uploader).not_to be_image end + end + shared_examples '#video? for an uploader' do it 'returns true for a video file' do uploader.store!(upload_fixture('video_sample.mp4')) - expect(uploader).to be_image_or_video + expect(uploader).to be_video + end + + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).not_to be_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_video + end + end + + shared_examples '#dangerous_image? for an uploader' do + it 'returns true if filename has a dangerous extension' do + uploader.store!(upload_fixture('unsanitized.svg')) + + expect(uploader).to be_dangerous_image + end + + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).not_to be_dangerous_image + end + + it 'returns false for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).not_to be_dangerous_image + 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_dangerous_image + end + end + + shared_examples '#dangerous_video? for an uploader' do + it 'returns false for a safe video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).not_to be_dangerous_video + end + + it 'returns false if filename is a dangerous image extension' do + uploader.store!(upload_fixture('unsanitized.svg')) + + expect(uploader).not_to be_dangerous_video end - it 'returns false for other extensions' do - uploader.store!(upload_fixture('doc_sample.txt')) + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) - expect(uploader).not_to be_image_or_video + expect(uploader).not_to be_dangerous_video end it 'returns false if filename is blank' do @@ -41,42 +106,190 @@ describe Gitlab::FileTypeDetection do allow(uploader).to receive(:filename).and_return(nil) - expect(uploader).not_to be_image_or_video + expect(uploader).not_to be_dangerous_video end end - context 'when class is a regular class' do - let(:custom_class) do - custom_class = Class.new do - include Gitlab::FileTypeDetection - end + let(:uploader) do + example_uploader = Class.new(CarrierWave::Uploader::Base) do + include Gitlab::FileTypeDetection - custom_class.new + storage :file end + example_uploader.new + end + + def upload_fixture(filename) + fixture_file_upload(File.join('spec', 'fixtures', filename)) + end + + describe '#image?' do + include_examples '#image? for an uploader' + end + + describe '#video?' do + include_examples '#video? for an uploader' + end + + describe '#image_or_video?' do + include_examples '#image? for an uploader' + include_examples '#video? for an uploader' + end + + describe '#dangerous_image?' do + include_examples '#dangerous_image? for an uploader' + end + + describe '#dangerous_video?' do + include_examples '#dangerous_video? for an uploader' + end + + describe '#dangerous_image_or_video?' do + include_examples '#dangerous_image? for an uploader' + include_examples '#dangerous_video? for an uploader' + end + end + + context 'when class is a regular class' do + shared_examples '#image? for a regular class' do 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 + expect(custom_class).to be_image end + it 'returns false if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') + + expect(custom_class).to be_dangerous_image + expect(custom_class).not_to be_image + end + + it 'returns false for any non image file' do + allow(custom_class).to receive(:filename).and_return('video_sample.mp4') + + expect(custom_class).not_to be_image + 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 + end + end + + shared_examples '#video? for a regular class' do 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 + expect(custom_class).to be_video + end + + it 'returns false for any non-video file' do + allow(custom_class).to receive(:filename).and_return('dk.png') + + expect(custom_class).not_to be_video + end + + it 'returns false if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') + + expect(custom_class).to be_dangerous_image + expect(custom_class).not_to be_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_video + end + end + + shared_examples '#dangerous_image? for a regular class' do + it 'returns true if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') + + expect(custom_class).to be_dangerous_image + end + + it 'returns false for an image file' do + allow(custom_class).to receive(:filename).and_return('dk.png') + + expect(custom_class).not_to be_dangerous_image + end + + it 'returns false for any non image file' do + allow(custom_class).to receive(:filename).and_return('video_sample.mp4') + + expect(custom_class).not_to be_dangerous_image + end + + it 'returns false if filename is blank' do + allow(custom_class).to receive(:filename).and_return(nil) + + expect(custom_class).not_to be_dangerous_image + end + end + + shared_examples '#dangerous_video? for a regular class' do + it 'returns false for a safe video file' do + allow(custom_class).to receive(:filename).and_return('video_sample.mp4') + + expect(custom_class).not_to be_dangerous_video + end + + it 'returns false for an image file' do + allow(custom_class).to receive(:filename).and_return('dk.png') + + expect(custom_class).not_to be_dangerous_video end - it 'returns false for other extensions' do - allow(custom_class).to receive(:filename).and_return('doc_sample.txt') + it 'returns false if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') - expect(custom_class).not_to be_image_or_video + expect(custom_class).not_to be_dangerous_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 + expect(custom_class).not_to be_dangerous_video end end + + let(:custom_class) do + custom_class = Class.new do + include Gitlab::FileTypeDetection + end + + custom_class.new + end + + describe '#image?' do + include_examples '#image? for a regular class' + end + + describe '#video?' do + include_examples '#video? for a regular class' + end + + describe '#image_or_video?' do + include_examples '#image? for a regular class' + include_examples '#video? for a regular class' + end + + describe '#dangerous_image?' do + include_examples '#dangerous_image? for a regular class' + end + + describe '#dangerous_video?' do + include_examples '#dangerous_video? for a regular class' + end + + describe '#dangerous_image_or_video?' do + include_examples '#dangerous_image? for a regular class' + include_examples '#dangerous_video? for a regular class' + end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 842dc4d511c..e09390a0047 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -64,31 +64,29 @@ describe Gitlab::UsageData do avg_cycle_analytics influxdb_metrics_enabled prometheus_metrics_enabled - cycle_analytics_views - productivity_analytics_views )) - - expect(subject).to include( - snippet_create: a_kind_of(Integer), - snippet_update: a_kind_of(Integer), - snippet_comment: a_kind_of(Integer), - merge_request_comment: a_kind_of(Integer), - merge_request_create: a_kind_of(Integer), - commit_comment: a_kind_of(Integer), - wiki_pages_create: a_kind_of(Integer), - wiki_pages_update: a_kind_of(Integer), - wiki_pages_delete: a_kind_of(Integer), - web_ide_views: a_kind_of(Integer), - web_ide_commits: a_kind_of(Integer), - web_ide_merge_requests: a_kind_of(Integer), - navbar_searches: a_kind_of(Integer), - cycle_analytics_views: a_kind_of(Integer), - productivity_analytics_views: a_kind_of(Integer), - source_code_pushes: a_kind_of(Integer) - ) end it 'gathers usage counts' do + smau_keys = %i( + snippet_create + snippet_update + snippet_comment + merge_request_comment + merge_request_create + commit_comment + wiki_pages_create + wiki_pages_update + wiki_pages_delete + web_ide_views + web_ide_commits + web_ide_merge_requests + navbar_searches + cycle_analytics_views + productivity_analytics_views + source_code_pushes + ) + expected_keys = %i( assignee_lists boards @@ -154,12 +152,13 @@ describe Gitlab::UsageData do uploads web_hooks user_preferences - ) + ).push(*smau_keys) count_data = subject[:counts] expect(count_data[:boards]).to eq(1) expect(count_data[:projects]).to eq(4) + expect(count_data.values_at(*smau_keys)).to all(be_an(Integer)) expect(count_data.keys).to include(*expected_keys) expect(expected_keys - count_data.keys).to be_empty end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 011b46c7f1a..28be8056993 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1223,36 +1223,66 @@ describe Repository do end describe '#branch_exists?' do - it 'uses branch_names' do - allow(repository).to receive(:branch_names).and_return(['foobar']) + let(:branch) { repository.root_ref } - expect(repository.branch_exists?('foobar')).to eq(true) - expect(repository.branch_exists?('master')).to eq(false) + subject { repository.branch_exists?(branch) } + + it 'delegates to branch_names when the cache is empty' do + repository.expire_branches_cache + + expect(repository).to receive(:branch_names).and_call_original + is_expected.to eq(true) + end + + it 'uses redis set caching when the cache is filled' do + repository.branch_names # ensure the branch name cache is filled + + expect(repository) + .to receive(:branch_names_include?) + .with(branch) + .and_call_original + + is_expected.to eq(true) end end describe '#tag_exists?' do - it 'uses tag_names' do - allow(repository).to receive(:tag_names).and_return(['foobar']) + let(:tag) { repository.tags.first.name } + + subject { repository.tag_exists?(tag) } + + it 'delegates to tag_names when the cache is empty' do + repository.expire_tags_cache + + expect(repository).to receive(:tag_names).and_call_original + is_expected.to eq(true) + end + + it 'uses redis set caching when the cache is filled' do + repository.tag_names # ensure the tag name cache is filled + + expect(repository) + .to receive(:tag_names_include?) + .with(tag) + .and_call_original - expect(repository.tag_exists?('foobar')).to eq(true) - expect(repository.tag_exists?('master')).to eq(false) + is_expected.to eq(true) end end - describe '#branch_names', :use_clean_rails_memory_store_caching do + describe '#branch_names', :clean_gitlab_redis_cache do let(:fake_branch_names) { ['foobar'] } it 'gets cached across Repository instances' do allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names) - expect(repository.branch_names).to eq(fake_branch_names) + expect(repository.branch_names).to match_array(fake_branch_names) fresh_repository = Project.find(project.id).repository expect(fresh_repository.object_id).not_to eq(repository.object_id) expect(fresh_repository.raw_repository).not_to receive(:branch_names) - expect(fresh_repository.branch_names).to eq(fake_branch_names) + expect(fresh_repository.branch_names).to match_array(fake_branch_names) end end diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb index 8d4e9070b19..2ac3ae0a5ad 100644 --- a/spec/models/suggestion_spec.rb +++ b/spec/models/suggestion_spec.rb @@ -38,16 +38,6 @@ describe Suggestion do end describe '#appliable?' do - context 'when note does not support suggestions' do - it 'returns false' do - expect_next_instance_of(DiffNote) do |note| - allow(note).to receive(:supports_suggestion?) { false } - end - - expect(suggestion).not_to be_appliable - end - end - context 'when patch is already applied' do let(:suggestion) { create(:suggestion, :applied) } diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 206e898381d..0bb238d08c0 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -54,6 +54,15 @@ describe API::Releases do expect(response).to match_response_schema('public_api/v4/releases') end + + it 'returns rendered helper paths' do + get api("/projects/#{project.id}/releases", maintainer) + + expect(json_response.first['commit_path']).to eq("/#{release_2.project.full_path}/commit/#{release_2.commit.id}") + expect(json_response.first['tag_path']).to eq("/#{release_2.project.full_path}/-/tags/#{release_2.tag}") + expect(json_response.second['commit_path']).to eq("/#{release_1.project.full_path}/commit/#{release_1.commit.id}") + expect(json_response.second['tag_path']).to eq("/#{release_1.project.full_path}/-/tags/#{release_1.tag}") + end end it 'returns an upcoming_release status for a future release' do @@ -103,11 +112,13 @@ describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end - it "does not expose tag, commit and source code" do + it "does not expose tag, commit, source code or helper paths" do get api("/projects/#{project.id}/releases", guest) expect(response).to match_response_schema('public_api/v4/release/releases_for_guest') expect(json_response[0]['assets']['count']).to eq(release.links.count) + expect(json_response[0]['commit_path']).to be_nil + expect(json_response[0]['tag_path']).to be_nil end context 'when project is public' do @@ -119,11 +130,13 @@ describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end - it "exposes tag, commit and source code" do + it "exposes tag, commit, source code and helper paths" do get api("/projects/#{project.id}/releases", guest) expect(response).to match_response_schema('public_api/v4/releases') - expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count) + expect(json_response.first['assets']['count']).to eq(release.links.count + release.sources.count) + expect(json_response.first['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}") + expect(json_response.first['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}") end end end @@ -172,6 +185,8 @@ describe API::Releases do expect(json_response['author']['name']).to eq(maintainer.name) expect(json_response['commit']['id']).to eq(commit.id) expect(json_response['assets']['count']).to eq(4) + expect(json_response['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}") + expect(json_response['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}") end it 'matches response schema' do diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb index b5defba332a..255a15b1ab0 100644 --- a/spec/support/helpers/repo_helpers.rb +++ b/spec/support/helpers/repo_helpers.rb @@ -92,7 +92,7 @@ eos ) end - def sample_compare + def sample_compare(extra_changes = []) changes = [ { line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20', @@ -102,7 +102,7 @@ eos line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6', file_path: '.gitmodules' } - ] + ] + extra_changes commits = %w( 5937ac0a7beb003549fc5fd26fc247adbce4a52e |