Commit c792263e authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 6f9edd1a
...@@ -116,6 +116,9 @@ export default class ProjectFindFile { ...@@ -116,6 +116,9 @@ export default class ProjectFindFile {
html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html)); results.push(this.element.find('.tree-table > tbody').append(html));
} }
this.element.find('.empty-state').toggleClass('hidden', Boolean(results.length));
return results; return results;
} }
......
...@@ -42,6 +42,12 @@ export default { ...@@ -42,6 +42,12 @@ export default {
commit() { commit() {
return this.release.commit || {}; return this.release.commit || {};
}, },
commitUrl() {
return this.release.commit_path;
},
tagUrl() {
return this.release.tag_path;
},
assets() { assets() {
return this.release.assets || {}; return this.release.assets || {};
}, },
...@@ -81,12 +87,18 @@ export default { ...@@ -81,12 +87,18 @@ export default {
<div class="card-subtitle d-flex flex-wrap text-secondary"> <div class="card-subtitle d-flex flex-wrap text-secondary">
<div class="append-right-8"> <div class="append-right-8">
<icon name="commit" class="align-middle" /> <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>
<div class="append-right-8"> <div class="append-right-8">
<icon name="tag" class="align-middle" /> <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> </div>
<milestone-list <milestone-list
......
...@@ -176,7 +176,7 @@ class Blob < SimpleDelegator ...@@ -176,7 +176,7 @@ class Blob < SimpleDelegator
end end
def video? def video?
UploaderHelper::VIDEO_EXT.include?(extension) UploaderHelper::SAFE_VIDEO_EXT.include?(extension)
end end
def readable_text? def readable_text?
......
...@@ -6,7 +6,7 @@ module BlobViewer ...@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide include ClientSide
self.partial_name = 'image' self.partial_name = 'image'
self.extensions = UploaderHelper::IMAGE_EXT self.extensions = UploaderHelper::SAFE_IMAGE_EXT
self.binary = true self.binary = true
self.switcher_icon = 'picture-o' self.switcher_icon = 'picture-o'
self.switcher_title = 'image' self.switcher_title = 'image'
......
...@@ -6,7 +6,7 @@ module BlobViewer ...@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide include ClientSide
self.partial_name = 'video' self.partial_name = 'video'
self.extensions = UploaderHelper::VIDEO_EXT self.extensions = UploaderHelper::SAFE_VIDEO_EXT
self.binary = true self.binary = true
self.switcher_icon = 'film' self.switcher_icon = 'film'
self.switcher_title = 'video' self.switcher_title = 'video'
......
...@@ -38,7 +38,7 @@ module Avatarable ...@@ -38,7 +38,7 @@ module Avatarable
def avatar_type def avatar_type
unless self.avatar.image? 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
end end
......
...@@ -75,6 +75,10 @@ class DiffNote < Note ...@@ -75,6 +75,10 @@ class DiffNote < Note
self.original_position.diff_refs == diff_refs self.original_position.diff_refs == diff_refs
end 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? def supports_suggestion?
return false unless noteable&.supports_suggestion? && on_text? return false unless noteable&.supports_suggestion? && on_text?
# We don't want to trigger side-effects of `diff_file` call. # We don't want to trigger side-effects of `diff_file` call.
......
...@@ -6,7 +6,7 @@ module DiffViewer ...@@ -6,7 +6,7 @@ module DiffViewer
include ClientSide include ClientSide
self.partial_name = 'image' self.partial_name = 'image'
self.extensions = UploaderHelper::IMAGE_EXT self.extensions = UploaderHelper::SAFE_IMAGE_EXT
self.binary = true self.binary = true
self.switcher_icon = 'picture-o' self.switcher_icon = 'picture-o'
self.switcher_title = _('image diff') self.switcher_title = _('image diff')
......
...@@ -249,13 +249,13 @@ class Repository ...@@ -249,13 +249,13 @@ class Repository
def branch_exists?(branch_name) def branch_exists?(branch_name)
return false unless raw_repository return false unless raw_repository
branch_names.include?(branch_name) branch_names_include?(branch_name)
end end
def tag_exists?(tag_name) def tag_exists?(tag_name)
return false unless raw_repository return false unless raw_repository
tag_names.include?(tag_name) tag_names_include?(tag_name)
end end
def ref_exists?(ref) def ref_exists?(ref)
...@@ -559,10 +559,10 @@ class Repository ...@@ -559,10 +559,10 @@ class Repository
end end
delegate :branch_names, to: :raw_repository 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 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 delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository
cache_method :branch_count, fallback: 0 cache_method :branch_count, fallback: 0
......
...@@ -41,7 +41,6 @@ class Suggestion < ApplicationRecord ...@@ -41,7 +41,6 @@ class Suggestion < ApplicationRecord
!applied? && !applied? &&
noteable.opened? && noteable.opened? &&
!outdated?(cached: cached) && !outdated?(cached: cached) &&
note.supports_suggestion? &&
different_content? && different_content? &&
note.active? note.active?
end end
......
...@@ -15,4 +15,12 @@ ...@@ -15,4 +15,12 @@
.table-holder .table-holder
%table.table.files-slider{ class: "table_#{@hex_path} tree-table" } %table.table.files-slider{ class: "table_#{@hex_path} tree-table" }
%tbody %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 = spinner nil, true
---
title: Links on Releases page to commits and tags
merge_request: 16128
author:
type: changed
---
title: Add empty state in file search
merge_request: 16851
author:
type: changed
---
title: Cache branch and tag names as Redis sets
merge_request: 30476
author:
type: performance
---
title: Move SMAU usage counters to the UsageData count field
merge_request: 17074
author:
type: fixed
---
title: Adjust unnapliable suggestions in expanded lines
merge_request: 17286
author:
type: fixed
{
"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"
}
...@@ -117,6 +117,35 @@ on adding these events into GitLab: ...@@ -117,6 +117,35 @@ on adding these events into GitLab:
- [Group settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/475) - [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) - [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-2336]: https://gitlab.com/gitlab-org/gitlab/issues/2336
[ee]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/
[permissions]: ../user/permissions.md [permissions]: ../user/permissions.md
...@@ -85,6 +85,8 @@ Example response: ...@@ -85,6 +85,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" "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":{ "assets":{
"count":6, "count":6,
"sources":[ "sources":[
...@@ -261,6 +263,8 @@ Example response: ...@@ -261,6 +263,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" "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":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
...@@ -379,6 +383,8 @@ Example response: ...@@ -379,6 +383,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" "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":{ "assets":{
"count":5, "count":5,
"sources":[ "sources":[
...@@ -483,6 +489,8 @@ Example response: ...@@ -483,6 +489,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/3" "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":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
...@@ -563,6 +571,8 @@ Example response: ...@@ -563,6 +571,8 @@ Example response:
"committer_email":"admin@example.com", "committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:53:28.000Z" "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":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
......
...@@ -375,6 +375,7 @@ timestamps with timezones: ...@@ -375,6 +375,7 @@ timestamps with timezones:
- `add_timestamps_with_timezone` - `add_timestamps_with_timezone`
- `timestamps_with_timezone` - `timestamps_with_timezone`
- `datetime_with_timezone`
This ensures all timestamps have a time zone specified. This, in turn, means 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 existing timestamps won't suddenly use a different timezone when the system's
......
...@@ -87,7 +87,7 @@ $ cat -- -l ...@@ -87,7 +87,7 @@ $ cat -- -l
hello 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 ```ruby
# Wrong # Wrong
......
...@@ -64,8 +64,8 @@ The following quick actions are applicable to descriptions, discussions and thre ...@@ -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 | | `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue |
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related **(STARTER)** | | `/relate #issue1 #issue2` | ✓ | | | Mark issues as related **(STARTER)** |
| `/move <path/to/project>` | ✓ | | | Move this issue to another project | | `/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`) | | `/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) 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). 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 | | `/target_branch <local branch name>` | | ✓ | | Set target branch |
| `/wip` | | ✓ | | Toggle the Work In Progress status | | `/wip` | | ✓ | | Toggle the Work In Progress status |
| `/approve` | | ✓ | | Approve the merge request | | `/approve` | | ✓ | | Approve the merge request |
......
...@@ -1276,7 +1276,7 @@ module API ...@@ -1276,7 +1276,7 @@ module API
class Release < Grape::Entity class Release < Grape::Entity
expose :name 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
expose :description_html do |entity| expose :description_html do |entity|
MarkupHelper.markdown_field(entity, :description) MarkupHelper.markdown_field(entity, :description)
...@@ -1284,16 +1284,17 @@ module API ...@@ -1284,16 +1284,17 @@ module API
expose :created_at expose :created_at
expose :released_at expose :released_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } 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 :upcoming_release?, as: :upcoming_release
expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? } 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 do
expose :assets_count, as: :count do |release, _| expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources] assets_to_exclude = can_download_code? ? [] : [:sources]
release.assets_count(except: assets_to_exclude) release.assets_count(except: assets_to_exclude)
end 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| expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted release.links.sorted
end end
...@@ -1304,6 +1305,16 @@ module API ...@@ -1304,6 +1305,16 @@ module API
def can_download_code? def can_download_code?
Ability.allowed?(options[:current_user], :download_code, object.project) Ability.allowed?(options[:current_user], :download_code, object.project)
end 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 end
class Tag < Grape::Entity class Tag < Grape::Entity
......
...@@ -127,7 +127,7 @@ module Backup ...@@ -127,7 +127,7 @@ module Backup
end end
tar_file = if ENV['BACKUP'].present? tar_file = if ENV['BACKUP'].present?
"#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}" File.basename(ENV['BACKUP']) + FILE_NAME_SUFFIX
else else
backup_file_list.first backup_file_list.first
end end
...@@ -235,8 +235,8 @@ module Backup ...@@ -235,8 +235,8 @@ module Backup
end end
def tar_file def tar_file
@tar_file ||= if ENV['BACKUP'] @tar_file ||= if ENV['BACKUP'].present?
ENV['BACKUP'] + "#{FILE_NAME_SUFFIX}" File.basename(ENV['BACKUP']) + FILE_NAME_SUFFIX
else else
"#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}"
end end
......
...@@ -19,13 +19,13 @@ module Banzai ...@@ -19,13 +19,13 @@ module Banzai
def query def query
@query ||= begin @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})" "'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})"
end end
if context[:asset_proxy_enabled].present? if context[:asset_proxy_enabled].present?
src_query.concat( 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})" "'.#{ext}' = substring(@data-canonical-src, string-length(@data-canonical-src) - #{ext.size})"
end end
) )
......
...@@ -118,8 +118,14 @@ module Gitlab ...@@ -118,8 +118,14 @@ module Gitlab
path: file_path path: file_path
} }
# Takes action when creating diff notes (multiple calls are
# submitted to this method).
Gitlab::SafeRequestStore.fetch(key) { find_diff_file(repository) } Gitlab::SafeRequestStore.fetch(key) { find_diff_file(repository) }
end 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 end
def diff_options def diff_options
...@@ -152,13 +158,7 @@ module Gitlab ...@@ -152,13 +158,7 @@ module Gitlab
return unless diff_refs.complete? return unless diff_refs.complete?
return unless comparison = diff_refs.compare_in(repository.project) return unless comparison = diff_refs.compare_in(repository.project)
file = comparison.diffs(diff_options).diff_files.first 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
end end
def get_formatter_class(type) def get_formatter_class(type)
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
return unless name = markdown_name return unless name = markdown_name
markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})" markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})"
markdown = "!#{markdown}" if image_or_video? || dangerous? markdown = "!#{markdown}" if image_or_video? || dangerous_image_or_video?
markdown markdown
end end
......
# frozen_string_literal: true # frozen_string_literal: true
# File helpers methods. # The method `filename` must be defined in classes that use this module.
# It needs the method filename to be defined. #
# 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 Gitlab
module FileTypeDetection 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 # 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 # 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 # proper MIME type video/mp4 and not video/quicktime or your videos won't play
# on IE >= 9. # on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html # 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 # 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". # 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? def image?
extension_match?(IMAGE_EXT) extension_match?(SAFE_IMAGE_EXT)
end end
def video? def video?
extension_match?(VIDEO_EXT) extension_match?(SAFE_VIDEO_EXT)
end end
def image_or_video? def image_or_video?
image? || video? image? || video?
end end
def dangerous? def dangerous_image?
extension_match?(DANGEROUS_EXT) 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 end
private private
......
...@@ -17,7 +17,6 @@ module Gitlab ...@@ -17,7 +17,6 @@ module Gitlab
.merge(features_usage_data) .merge(features_usage_data)
.merge(components_usage_data) .merge(components_usage_data)
.merge(cycle_analytics_usage_data) .merge(cycle_analytics_usage_data)
.merge(usage_counters)
end end
def to_json(force_refresh: false) def to_json(force_refresh: false)
...@@ -99,6 +98,7 @@ module Gitlab ...@@ -99,6 +98,7 @@ module Gitlab
web_hooks: count(WebHook) web_hooks: count(WebHook)
}.merge(services_usage) }.merge(services_usage)
.merge(approximate_counts) .merge(approximate_counts)
.merge(usage_counters)
}.tap do |data| }.tap do |data|
data[:counts][:user_preferences] = user_preferences_usage data[:counts][:user_preferences] = user_preferences_usage
end end
......
...@@ -15647,6 +15647,9 @@ msgstr "" ...@@ -15647,6 +15647,9 @@ msgstr ""
msgid "There are no labels yet" msgid "There are no labels yet"
msgstr "" msgstr ""
msgid "There are no matching files"
msgstr ""
msgid "There are no open issues" msgid "There are no open issues"
msgstr "" msgstr ""
...@@ -16569,6 +16572,9 @@ msgstr "" ...@@ -16569,6 +16572,9 @@ msgstr ""
msgid "Try to fork again" msgid "Try to fork again"
msgstr "" 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." msgid "Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now."
msgstr "" msgstr ""
......
...@@ -14,6 +14,10 @@ describe 'User comments on a diff', :js do ...@@ -14,6 +14,10 @@ describe 'User comments on a diff', :js do
expect(suggested_content).to eq(expected_suggested_content) expect(suggested_content).to eq(expected_suggested_content)
end end
def expect_appliable_suggestions(amount)
expect(all('button', text: 'Apply suggestion').size).to eq(amount)
end
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:merge_request) do let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') 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 ...@@ -89,6 +93,60 @@ describe 'User comments on a diff', :js do
end end
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 context 'multiple suggestions in a single note' do
it 'suggestions are presented' do it 'suggestions are presented' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
......
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
"type": "array", "type": "array",
"items": { "$ref": "milestone.json" } "items": { "$ref": "milestone.json" }
}, },
"commit_path": { "type": "string" },
"tag_path": { "type": "string" },
"name": { "type": "string" },
"assets": { "assets": {
"required": ["count", "links", "sources"], "required": ["count", "links", "sources"],
"properties": { "properties": {
......
...@@ -8,6 +8,12 @@ ...@@ -8,6 +8,12 @@
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"released_at": { "type": "date" }, "released_at": { "type": "date" },
"upcoming_release": { "type": "boolean" }, "upcoming_release": { "type": "boolean" },
"milestones": {
"type": "array",
"items": { "$ref": "../milestone.json" }
},
"commit_path": { "type": "string" },
"tag_path": { "type": "string" },
"author": { "author": {
"oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }] "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
}, },
......
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);
});
});
});
});
...@@ -12,7 +12,6 @@ describe('Release block', () => { ...@@ -12,7 +12,6 @@ describe('Release block', () => {
propsData: { propsData: {
release: releaseProp, release: releaseProp,
}, },
sync: false,
}); });
}; };
...@@ -37,10 +36,16 @@ describe('Release block', () => { ...@@ -37,10 +36,16 @@ describe('Release block', () => {
it('renders commit sha', () => { it('renders commit sha', () => {
expect(wrapper.text()).toContain(release.commit.short_id); 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', () => { it('renders tag name', () => {
expect(wrapper.text()).toContain(release.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', () => { it('renders release date', () => {
......
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();
});
});
});
});
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');
});
});
});
...@@ -21,6 +21,49 @@ describe Backup::Manager do ...@@ -21,6 +21,49 @@ describe Backup::Manager do
$progress = @old_progress # rubocop:disable Style/GlobalVars $progress = @old_progress # rubocop:disable Style/GlobalVars
end 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 describe '#remove_old' do
let(:files) do let(:files) do
[ [
...@@ -238,7 +281,7 @@ describe Backup::Manager do ...@@ -238,7 +281,7 @@ describe Backup::Manager do
allow(Kernel).to receive(:system).and_return(true) allow(Kernel).to receive(:system).and_return(true)
allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) 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 end
it 'unpacks the file' do it 'unpacks the file' do
......
...@@ -18,7 +18,7 @@ describe Banzai::Filter::VideoLinkFilter do ...@@ -18,7 +18,7 @@ describe Banzai::Filter::VideoLinkFilter do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
context 'when the element src has a video extension' do 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 it "replaces the image tag 'path/video.#{ext}' with a video tag" do
container = filter(link_to_image("/path/video.#{ext}")).children.first container = filter(link_to_image("/path/video.#{ext}")).children.first
......
...@@ -130,6 +130,26 @@ describe Gitlab::Diff::Position do ...@@ -130,6 +130,26 @@ describe Gitlab::Diff::Position do
expect(diff_file.new_path).to eq(subject.new_path) expect(diff_file.new_path).to eq(subject.new_path)
expect(diff_file.diff_refs).to eq(subject.diff_refs) expect(diff_file.diff_refs).to eq(subject.diff_refs)
end 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 end
describe "#diff_line" do describe "#diff_line" do
......
...@@ -2,38 +2,103 @@ ...@@ -2,38 +2,103 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::FileTypeDetection do describe Gitlab::FileTypeDetection do
def upload_fixture(filename) context 'when class is an uploader' do
fixture_file_upload(File.join('spec', 'fixtures', filename)) shared_examples '#image? for an uploader' do
end it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png'))
describe '#image_or_video?' do expect(uploader).to be_image
context 'when class is an uploader' do end
let(:uploader) do
example_uploader = Class.new(CarrierWave::Uploader::Base) do
include Gitlab::FileTypeDetection
storage :file it 'returns false if filename has a dangerous image extension' do
end uploader.store!(upload_fixture('unsanitized.svg'))
example_uploader.new expect(uploader).to be_dangerous_image
expect(uploader).not_to be_image
end 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')) 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
end
shared_examples '#video? for an uploader' do
it 'returns true for a video file' do it 'returns true for a video file' do
uploader.store!(upload_fixture('video_sample.mp4')) 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 end
it 'returns false for other extensions' do it 'returns false for an image file' do
uploader.store!(upload_fixture('doc_sample.txt')) uploader.store!(upload_fixture('dk.png'))
expect(uploader).not_to be_image_or_video expect(uploader).not_to be_dangerous_video
end end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
...@@ -41,42 +106,190 @@ describe Gitlab::FileTypeDetection do ...@@ -41,42 +106,190 @@ describe Gitlab::FileTypeDetection do
allow(uploader).to receive(:filename).and_return(nil) 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
end end
context 'when class is a regular class' do let(:uploader) do
let(:custom_class) do example_uploader = Class.new(CarrierWave::Uploader::Base) do
custom_class = Class.new do include Gitlab::FileTypeDetection
include Gitlab::FileTypeDetection
end
custom_class.new storage :file
end 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 it 'returns true for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png') 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 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 it 'returns true for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4') 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 end
it 'returns false for other extensions' do it 'returns false if file has a dangerous image extension' do
allow(custom_class).to receive(:filename).and_return('doc_sample.txt') 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 end
it 'returns false if filename is blank' do it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil) 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
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
end end
...@@ -64,31 +64,29 @@ describe Gitlab::UsageData do ...@@ -64,31 +64,29 @@ describe Gitlab::UsageData do
avg_cycle_analytics avg_cycle_analytics
influxdb_metrics_enabled influxdb_metrics_enabled
prometheus_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 end
it 'gathers usage counts' do 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( expected_keys = %i(
assignee_lists assignee_lists
boards boards
...@@ -154,12 +152,13 @@ describe Gitlab::UsageData do ...@@ -154,12 +152,13 @@ describe Gitlab::UsageData do
uploads uploads
web_hooks web_hooks
user_preferences user_preferences
) ).push(*smau_keys)
count_data = subject[:counts] count_data = subject[:counts]
expect(count_data[:boards]).to eq(1) expect(count_data[:boards]).to eq(1)
expect(count_data[:projects]).to eq(4) 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(count_data.keys).to include(*expected_keys)
expect(expected_keys - count_data.keys).to be_empty expect(expected_keys - count_data.keys).to be_empty
end end
......
...@@ -1223,36 +1223,66 @@ describe Repository do ...@@ -1223,36 +1223,66 @@ describe Repository do
end end
describe '#branch_exists?' do describe '#branch_exists?' do
it 'uses branch_names' do let(:branch) { repository.root_ref }
allow(repository).to receive(:branch_names).and_return(['foobar'])
expect(repository.branch_exists?('foobar')).to eq(true) subject { repository.branch_exists?(branch) }
expect(repository.branch_exists?('master')).to eq(false)
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
end end
describe '#tag_exists?' do describe '#tag_exists?' do
it 'uses tag_names' do let(:tag) { repository.tags.first.name }
allow(repository).to receive(:tag_names).and_return(['foobar'])
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) is_expected.to eq(true)
expect(repository.tag_exists?('master')).to eq(false)
end end
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'] } let(:fake_branch_names) { ['foobar'] }
it 'gets cached across Repository instances' do it 'gets cached across Repository instances' do
allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names) 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 fresh_repository = Project.find(project.id).repository
expect(fresh_repository.object_id).not_to eq(repository.object_id) expect(fresh_repository.object_id).not_to eq(repository.object_id)
expect(fresh_repository.raw_repository).not_to receive(:branch_names) 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
end end
......
...@@ -38,16 +38,6 @@ describe Suggestion do ...@@ -38,16 +38,6 @@ describe Suggestion do
end end
describe '#appliable?' do 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 context 'when patch is already applied' do
let(:suggestion) { create(:suggestion, :applied) } let(:suggestion) { create(:suggestion, :applied) }
......
...@@ -54,6 +54,15 @@ describe API::Releases do ...@@ -54,6 +54,15 @@ describe API::Releases do
expect(response).to match_response_schema('public_api/v4/releases') expect(response).to match_response_schema('public_api/v4/releases')
end 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 end
it 'returns an upcoming_release status for a future release' do it 'returns an upcoming_release status for a future release' do
...@@ -103,11 +112,13 @@ describe API::Releases do ...@@ -103,11 +112,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end 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) get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/release/releases_for_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]['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 end
context 'when project is public' do context 'when project is public' do
...@@ -119,11 +130,13 @@ describe API::Releases do ...@@ -119,11 +130,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end 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) get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/releases') 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 end
end end
...@@ -172,6 +185,8 @@ describe API::Releases do ...@@ -172,6 +185,8 @@ describe API::Releases do
expect(json_response['author']['name']).to eq(maintainer.name) expect(json_response['author']['name']).to eq(maintainer.name)
expect(json_response['commit']['id']).to eq(commit.id) expect(json_response['commit']['id']).to eq(commit.id)
expect(json_response['assets']['count']).to eq(4) 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 end
it 'matches response schema' do it 'matches response schema' do
......
...@@ -92,7 +92,7 @@ eos ...@@ -92,7 +92,7 @@ eos
) )
end end
def sample_compare def sample_compare(extra_changes = [])
changes = [ changes = [
{ {
line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20', line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20',
...@@ -102,7 +102,7 @@ eos ...@@ -102,7 +102,7 @@ eos
line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6', line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6',
file_path: '.gitmodules' file_path: '.gitmodules'
} }
] ] + extra_changes
commits = %w( commits = %w(
5937ac0a7beb003549fc5fd26fc247adbce4a52e 5937ac0a7beb003549fc5fd26fc247adbce4a52e
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment