Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/project_find_file.js3
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue16
-rw-r--r--app/models/blob.rb2
-rw-r--r--app/models/blob_viewer/image.rb2
-rw-r--r--app/models/blob_viewer/video.rb2
-rw-r--r--app/models/concerns/avatarable.rb2
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/diff_viewer/image.rb2
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/suggestion.rb1
-rw-r--r--app/views/projects/find_file/show.html.haml8
-rw-r--r--changelogs/unreleased/61061-links-to-sha-commits-in-release-notes.yml5
-rw-r--r--changelogs/unreleased/61078-empty-state-file-finder.yml5
-rw-r--r--changelogs/unreleased/64251-branch-name-set-cache.yml5
-rw-r--r--changelogs/unreleased/fj-fix-smau-usage-counters.yml5
-rw-r--r--changelogs/unreleased/osw-unnappliable-suggestion-on-expanded-lines.yml5
-rw-r--r--config/brakeman.ignore24
-rw-r--r--doc/administration/audit_events.md29
-rw-r--r--doc/api/releases/index.md10
-rw-r--r--doc/development/migration_style_guide.md1
-rw-r--r--doc/development/shell_commands.md2
-rw-r--r--doc/user/project/quick_actions.md4
-rw-r--r--lib/api/entities.rb19
-rw-r--r--lib/backup/manager.rb6
-rw-r--r--lib/banzai/filter/video_link_filter.rb4
-rw-r--r--lib/gitlab/diff/position.rb14
-rw-r--r--lib/gitlab/file_markdown_link_builder.rb2
-rw-r--r--lib/gitlab/file_type_detection.rb43
-rw-r--r--lib/gitlab/usage_data.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb58
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json6
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js175
-rw-r--r--spec/frontend/releases/components/release_block_spec.js7
-rw-r--r--spec/javascripts/ide/components/file_templates/dropdown_spec.js201
-rw-r--r--spec/javascripts/releases/components/release_block_spec.js170
-rw-r--r--spec/lib/backup/manager_spec.rb45
-rw-r--r--spec/lib/banzai/filter/video_link_filter_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb20
-rw-r--r--spec/lib/gitlab/file_type_detection_spec.rb273
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb43
-rw-r--r--spec/models/repository_spec.rb52
-rw-r--r--spec/models/suggestion_spec.rb10
-rw-r--r--spec/requests/api/releases_spec.rb21
-rw-r--r--spec/support/helpers/repo_helpers.rb4
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