Commit 1c75e4e6 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'dm-auxiliary-viewers' into 'master'

Implement auxiliary blob viewers

See merge request !11195
parents 4067dd4f 90997505
/* global Flash */ /* global Flash */
export default class BlobViewer { export default class BlobViewer {
constructor() { constructor() {
BlobViewer.initAuxiliaryViewer();
this.initMainViewers();
}
static initAuxiliaryViewer() {
const auxiliaryViewer = document.querySelector('.blob-viewer[data-type="auxiliary"]');
if (!auxiliaryViewer) return;
BlobViewer.loadViewer(auxiliaryViewer);
}
initMainViewers() {
this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return;
this.switcher = document.querySelector('.js-blob-viewer-switcher'); this.switcher = document.querySelector('.js-blob-viewer-switcher');
this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn'); this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn'); this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
this.$fileHolder = $('.file-holder');
const initialViewer = document.querySelector('.blob-viewer:not(.hidden)'); this.simpleViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="simple"]');
if (!initialViewer) return; this.richViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="rich"]');
let initialViewerName = initialViewer.getAttribute('data-type');
this.initBindings(); this.initBindings();
this.switchToInitialViewer();
}
switchToInitialViewer() {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
if (this.switcher && location.hash.indexOf('#L') === 0) { if (this.switcher && location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple'; initialViewerName = 'simple';
} }
...@@ -64,40 +82,13 @@ export default class BlobViewer { ...@@ -64,40 +82,13 @@ export default class BlobViewer {
$(this.copySourceBtn).tooltip('fixTitle'); $(this.copySourceBtn).tooltip('fixTitle');
} }
loadViewer(viewerParam) {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
return;
}
viewer.setAttribute('data-loading', 'true');
$.ajax({
url,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading source view'))
.done((data) => {
viewer.innerHTML = data.html;
$(viewer).syntaxHighlight();
viewer.setAttribute('data-loaded', 'true');
this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState();
});
}
switchToViewer(name) { switchToViewer(name) {
const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`); const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return; if (this.activeViewer === newViewer) return;
const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active'); const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`); const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`); const oldViewer = this.$fileHolder[0].querySelector(`.blob-viewer:not([data-type='${name}'])`);
if (oldButton) { if (oldButton) {
oldButton.classList.remove('active'); oldButton.classList.remove('active');
...@@ -118,6 +109,40 @@ export default class BlobViewer { ...@@ -118,6 +109,40 @@ export default class BlobViewer {
this.toggleCopyButtonState(); this.toggleCopyButtonState();
this.loadViewer(newViewer); BlobViewer.loadViewer(newViewer)
.then((viewer) => {
$(viewer).syntaxHighlight();
this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState();
})
.catch(() => new Flash('Error loading viewer'));
}
static loadViewer(viewerParam) {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
return new Promise((resolve, reject) => {
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
resolve(viewer);
return;
}
viewer.setAttribute('data-loading', 'true');
$.ajax({
url,
dataType: 'JSON',
})
.fail(reject)
.done((data) => {
viewer.innerHTML = data.html;
viewer.setAttribute('data-loaded', 'true');
resolve(viewer);
});
});
} }
} }
...@@ -3,8 +3,11 @@ module RendersBlob ...@@ -3,8 +3,11 @@ module RendersBlob
def render_blob_json(blob) def render_blob_json(blob)
viewer = viewer =
if params[:viewer] == 'rich' case params[:viewer]
when 'rich'
blob.rich_viewer blob.rich_viewer
when 'auxiliary'
blob.auxiliary_viewer
else else
blob.simple_viewer blob.simple_viewer
end end
......
...@@ -110,11 +110,8 @@ module ProjectsHelper ...@@ -110,11 +110,8 @@ module ProjectsHelper
end end
def license_short_name(project) def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil? license = project.repository.license
license&.nickname || license&.name || 'LICENSE'
license = Licensee::License.new(project.repository.license_key)
license.nickname || license.name
end end
def last_push_event def last_push_event
......
...@@ -34,10 +34,13 @@ class Blob < SimpleDelegator ...@@ -34,10 +34,13 @@ class Blob < SimpleDelegator
BlobViewer::BinarySTL, BlobViewer::BinarySTL,
BlobViewer::TextSTL BlobViewer::TextSTL
].freeze ].sort_by { |v| v.binary? ? 0 : 1 }.freeze
BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze AUXILIARY_VIEWERS = [
TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze BlobViewer::GitlabCiYml,
BlobViewer::RouteMap,
BlobViewer::License
].freeze
attr_reader :project attr_reader :project
...@@ -154,6 +157,12 @@ class Blob < SimpleDelegator ...@@ -154,6 +157,12 @@ class Blob < SimpleDelegator
@rich_viewer = rich_viewer_class&.new(self) @rich_viewer = rich_viewer_class&.new(self)
end end
def auxiliary_viewer
return @auxiliary_viewer if defined?(@auxiliary_viewer)
@auxiliary_viewer = auxiliary_viewer_class&.new(self)
end
def rendered_as_text?(ignore_errors: true) def rendered_as_text?(ignore_errors: true)
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?) simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end end
...@@ -180,17 +189,18 @@ class Blob < SimpleDelegator ...@@ -180,17 +189,18 @@ class Blob < SimpleDelegator
end end
def rich_viewer_class def rich_viewer_class
viewer_class_from(RICH_VIEWERS)
end
def auxiliary_viewer_class
viewer_class_from(AUXILIARY_VIEWERS)
end
def viewer_class_from(classes)
return if empty? || external_storage_error? return if empty? || external_storage_error?
classes = verify_binary = !stored_externally?
if stored_externally?
BINARY_VIEWERS + TEXT_VIEWERS
elsif binary?
BINARY_VIEWERS
else # text
TEXT_VIEWERS
end
classes.find { |viewer_class| viewer_class.can_render?(self) } classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
end end
end end
module BlobViewer
module Auxiliary
extend ActiveSupport::Concern
included do
self.loading_partial_name = 'loading_auxiliary'
self.type = :auxiliary
self.max_size = 100.kilobytes
self.absolute_max_size = 100.kilobytes
end
end
end
module BlobViewer module BlobViewer
class Base class Base
class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze
delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_type, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
self.loading_partial_name = 'loading'
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
attr_reader :blob attr_reader :blob
attr_accessor :override_max_size attr_accessor :override_max_size
...@@ -12,7 +16,11 @@ module BlobViewer ...@@ -12,7 +16,11 @@ module BlobViewer
end end
def self.partial_path def self.partial_path
"projects/blob/viewers/#{partial_name}" File.join(PARTIAL_PATH_PREFIX, partial_name)
end
def self.loading_partial_path
File.join(PARTIAL_PATH_PREFIX, loading_partial_name)
end end
def self.rich? def self.rich?
...@@ -23,6 +31,10 @@ module BlobViewer ...@@ -23,6 +31,10 @@ module BlobViewer
type == :simple type == :simple
end end
def self.auxiliary?
type == :auxiliary
end
def self.client_side? def self.client_side?
client_side client_side
end end
...@@ -39,8 +51,12 @@ module BlobViewer ...@@ -39,8 +51,12 @@ module BlobViewer
!binary? !binary?
end end
def self.can_render?(blob) def self.can_render?(blob, verify_binary: true)
!extensions || extensions.include?(blob.extension) return false if verify_binary && binary? != blob.binary?
return true if extensions&.include?(blob.extension)
return true if file_type && Gitlab::FileDetector.type_of(blob.path) == file_type
false
end end
def too_large? def too_large?
...@@ -83,9 +99,7 @@ module BlobViewer ...@@ -83,9 +99,7 @@ module BlobViewer
end end
def prepare! def prepare!
if server_side? && blob.project # To be overridden by subclasses
blob.load_all_data!(blob.project.repository)
end
end end
private private
......
module BlobViewer
class GitlabCiYml < Base
include ServerSide
include Auxiliary
self.partial_name = 'gitlab_ci_yml'
self.loading_partial_name = 'gitlab_ci_yml_loading'
self.file_type = :gitlab_ci
self.binary = false
def validation_message
return @validation_message if defined?(@validation_message)
prepare!
@validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data)
end
def valid?
validation_message.blank?
end
end
end
module BlobViewer
class License < Base
# We treat the License viewer as if it renders the content client-side,
# so that it doesn't attempt to load the entire blob contents and is
# rendered synchronously instead of loaded asynchronously.
include ClientSide
include Auxiliary
self.partial_name = 'license'
self.file_type = :license
self.binary = false
def license
blob.project.repository.license
end
def render_error
return if license
:unknown_license
end
end
end
module BlobViewer
class RouteMap < Base
include ServerSide
include Auxiliary
self.partial_name = 'route_map'
self.loading_partial_name = 'route_map_loading'
self.file_type = :route_map
self.binary = false
def validation_message
return @validation_message if defined?(@validation_message)
prepare!
@validation_message =
begin
Gitlab::RouteMap.new(blob.data)
nil
rescue Gitlab::RouteMap::FormatError => e
e.message
end
end
def valid?
validation_message.blank?
end
end
end
...@@ -7,5 +7,11 @@ module BlobViewer ...@@ -7,5 +7,11 @@ module BlobViewer
self.max_size = 2.megabytes self.max_size = 2.megabytes
self.absolute_max_size = 5.megabytes self.absolute_max_size = 5.megabytes
end end
def prepare!
if blob.project
blob.load_all_data!(blob.project.repository)
end
end
end end
end end
...@@ -30,7 +30,7 @@ class Repository ...@@ -30,7 +30,7 @@ class Repository
METHOD_CACHES_FOR_FILE_TYPES = { METHOD_CACHES_FOR_FILE_TYPES = {
readme: :rendered_readme, readme: :rendered_readme,
changelog: :changelog, changelog: :changelog,
license: %i(license_blob license_key), license: %i(license_blob license_key license),
contributing: :contribution_guide, contributing: :contribution_guide,
gitignore: :gitignore, gitignore: :gitignore,
koding: :koding_yml, koding: :koding_yml,
...@@ -42,13 +42,13 @@ class Repository ...@@ -42,13 +42,13 @@ class Repository
# variable. # variable.
# #
# This only works for methods that do not take any arguments. # This only works for methods that do not take any arguments.
def self.cache_method(name, fallback: nil) def self.cache_method(name, fallback: nil, memoize_only: false)
original = :"_uncached_#{name}" original = :"_uncached_#{name}"
alias_method(original, name) alias_method(original, name)
define_method(name) do define_method(name) do
cache_method_output(name, fallback: fallback) { __send__(original) } cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) }
end end
end end
...@@ -549,6 +549,13 @@ class Repository ...@@ -549,6 +549,13 @@ class Repository
end end
cache_method :license_key cache_method :license_key
def license
return unless license_key
Licensee::License.new(license_key)
end
cache_method :license, memoize_only: true
def gitignore def gitignore
file_on_head(:gitignore) file_on_head(:gitignore)
end end
...@@ -1061,14 +1068,20 @@ class Repository ...@@ -1061,14 +1068,20 @@ class Repository
# #
# key - The name of the key to cache the data in. # key - The name of the key to cache the data in.
# fallback - A value to fall back to in the event of a Git error. # fallback - A value to fall back to in the event of a Git error.
def cache_method_output(key, fallback: nil, &block) def cache_method_output(key, fallback: nil, memoize_only: false, &block)
ivar = cache_instance_variable_name(key) ivar = cache_instance_variable_name(key)
if instance_variable_defined?(ivar) if instance_variable_defined?(ivar)
instance_variable_get(ivar) instance_variable_get(ivar)
else else
begin begin
instance_variable_set(ivar, cache.fetch(key, &block)) value =
if memoize_only
yield
else
cache.fetch(key, &block)
end
instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to # if e.g. HEAD or the entire repository doesn't exist we want to
# gracefully handle this and not cache anything. # gracefully handle this and not cache anything.
...@@ -1083,8 +1096,8 @@ class Repository ...@@ -1083,8 +1096,8 @@ class Repository
def file_on_head(type) def file_on_head(type)
if head = tree(:head) if head = tree(:head)
head.blobs.find do |file| head.blobs.find do |blob|
Gitlab::FileDetector.type_of(file.name) == type Gitlab::FileDetector.type_of(blob.path) == type
end end
end end
end end
......
...@@ -6,6 +6,11 @@ ...@@ -6,6 +6,11 @@
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref = render blob_commit, project: @project, ref: @ref
- auxiliary_viewer = blob.auxiliary_viewer
- if auxiliary_viewer && !auxiliary_viewer.render_error
.well-segment.blob-auxiliary-viewer
= render 'projects/blob/viewer', viewer: auxiliary_viewer
#blob-content-holder.blob-content-holder #blob-content-holder.blob-content-holder
%article.file-holder %article.file-holder
= render "projects/blob/header", blob: blob = render "projects/blob/header", blob: blob
......
...@@ -5,8 +5,7 @@ ...@@ -5,8 +5,7 @@
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_asynchronously - viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_asynchronously
.blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) } .blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
- if load_asynchronously - if load_asynchronously
.text-center.prepend-top-default.append-bottom-default = render viewer.loading_partial_path, viewer: viewer
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
- elsif render_error - elsif render_error
= render 'projects/blob/render_error', viewer: viewer = render 'projects/blob/render_error', viewer: viewer
- else - else
......
- if viewer.valid?
= icon('check fw')
This GitLab CI configuration is valid.
- else
= icon('warning fw')
This GitLab CI configuration is invalid:
= viewer.validation_message
= link_to 'Learn more', help_page_path('ci/yaml/README')
= icon('spinner spin fw')
Validating GitLab CI configuration…
= link_to 'Learn more', help_page_path('ci/yaml/README')
- license = viewer.license
= icon('balance-scale fw')
This project is licensed under the
= succeed '.' do
%strong= license.name
= link_to 'Learn more about this license', license.url, target: '_blank', rel: 'noopener noreferrer'
.text-center.prepend-top-default.append-bottom-default
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…')
- if viewer.valid?
= icon('check fw')
This Route Map is valid.
- else
= icon('warning fw')
This Route Map is invalid:
= viewer.validation_message
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
= icon('spinner spin fw')
Validating Route Map…
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
---
title: Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and
LICENSE blob pages
merge_request:
author:
...@@ -442,7 +442,8 @@ and/or `production`) you can see this information in the merge request itself. ...@@ -442,7 +442,8 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png) ![Environment URLs in merge request](img/environments_link_url_mr.png)
### Go directly from source files to public pages on the environment ### <a name="route-map"></a>Go directly from source files to public pages on the environment
> Introduced in GitLab 8.17. > Introduced in GitLab 8.17.
......
...@@ -13,7 +13,8 @@ module Gitlab ...@@ -13,7 +13,8 @@ module Gitlab
gitignore: '.gitignore', gitignore: '.gitignore',
koding: '.koding.yml', koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml', gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/ avatar: /\Alogo\.(png|jpg|gif)\z/,
route_map: 'route-map.yml'
}.freeze }.freeze
# Returns an Array of file types based on the given paths. # Returns an Array of file types based on the given paths.
......
...@@ -5,13 +5,13 @@ feature 'File blob', :js, feature: true do ...@@ -5,13 +5,13 @@ feature 'File blob', :js, feature: true do
def visit_blob(path, fragment = nil) def visit_blob(path, fragment = nil)
visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment) visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment)
wait_for_ajax
end end
context 'Ruby file' do context 'Ruby file' do
before do before do
visit_blob('files/ruby/popen.rb') visit_blob('files/ruby/popen.rb')
wait_for_ajax
end end
it 'displays the blob' do it 'displays the blob' do
...@@ -35,8 +35,6 @@ feature 'File blob', :js, feature: true do ...@@ -35,8 +35,6 @@ feature 'File blob', :js, feature: true do
context 'visiting directly' do context 'visiting directly' do
before do before do
visit_blob('files/markdown/ruby-style-guide.md') visit_blob('files/markdown/ruby-style-guide.md')
wait_for_ajax
end end
it 'displays the blob using the rich viewer' do it 'displays the blob using the rich viewer' do
...@@ -104,8 +102,6 @@ feature 'File blob', :js, feature: true do ...@@ -104,8 +102,6 @@ feature 'File blob', :js, feature: true do
context 'visiting with a line number anchor' do context 'visiting with a line number anchor' do
before do before do
visit_blob('files/markdown/ruby-style-guide.md', 'L1') visit_blob('files/markdown/ruby-style-guide.md', 'L1')
wait_for_ajax
end end
it 'displays the blob using the simple viewer' do it 'displays the blob using the simple viewer' do
...@@ -148,8 +144,6 @@ feature 'File blob', :js, feature: true do ...@@ -148,8 +144,6 @@ feature 'File blob', :js, feature: true do
project.update_attribute(:lfs_enabled, true) project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/file.md') visit_blob('files/lfs/file.md')
wait_for_ajax
end end
it 'displays an error' do it 'displays an error' do
...@@ -198,8 +192,6 @@ feature 'File blob', :js, feature: true do ...@@ -198,8 +192,6 @@ feature 'File blob', :js, feature: true do
context 'when LFS is disabled on the project' do context 'when LFS is disabled on the project' do
before do before do
visit_blob('files/lfs/file.md') visit_blob('files/lfs/file.md')
wait_for_ajax
end end
it 'displays the blob' do it 'displays the blob' do
...@@ -235,8 +227,6 @@ feature 'File blob', :js, feature: true do ...@@ -235,8 +227,6 @@ feature 'File blob', :js, feature: true do
).execute ).execute
visit_blob('files/test.pdf') visit_blob('files/test.pdf')
wait_for_ajax
end end
it 'displays the blob' do it 'displays the blob' do
...@@ -263,8 +253,6 @@ feature 'File blob', :js, feature: true do ...@@ -263,8 +253,6 @@ feature 'File blob', :js, feature: true do
project.update_attribute(:lfs_enabled, true) project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/lfs_object.iso') visit_blob('files/lfs/lfs_object.iso')
wait_for_ajax
end end
it 'displays the blob' do it 'displays the blob' do
...@@ -287,8 +275,6 @@ feature 'File blob', :js, feature: true do ...@@ -287,8 +275,6 @@ feature 'File blob', :js, feature: true do
context 'when LFS is disabled on the project' do context 'when LFS is disabled on the project' do
before do before do
visit_blob('files/lfs/lfs_object.iso') visit_blob('files/lfs/lfs_object.iso')
wait_for_ajax
end end
it 'displays the blob' do it 'displays the blob' do
...@@ -312,8 +298,6 @@ feature 'File blob', :js, feature: true do ...@@ -312,8 +298,6 @@ feature 'File blob', :js, feature: true do
context 'ZIP file' do context 'ZIP file' do
before do before do
visit_blob('Gemfile.zip') visit_blob('Gemfile.zip')
wait_for_ajax
end end
it 'displays the blob' do it 'displays the blob' do
...@@ -348,8 +332,6 @@ feature 'File blob', :js, feature: true do ...@@ -348,8 +332,6 @@ feature 'File blob', :js, feature: true do
).execute ).execute
visit_blob('files/empty.md') visit_blob('files/empty.md')
wait_for_ajax
end end
it 'displays an error' do it 'displays an error' do
...@@ -369,4 +351,80 @@ feature 'File blob', :js, feature: true do ...@@ -369,4 +351,80 @@ feature 'File blob', :js, feature: true do
end end
end end
end end
context '.gitlab-ci.yml' do
before do
project.add_master(project.creator)
Files::CreateService.new(
project,
project.creator,
start_branch: 'master',
branch_name: 'master',
commit_message: "Add .gitlab-ci.yml",
file_path: '.gitlab-ci.yml',
file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
).execute
visit_blob('.gitlab-ci.yml')
end
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows that configuration is valid
expect(page).to have_content('This GitLab CI configuration is valid.')
# shows a learn more link
expect(page).to have_link('Learn more')
end
end
end
context '.gitlab/route-map.yml' do
before do
project.add_master(project.creator)
Files::CreateService.new(
project,
project.creator,
start_branch: 'master',
branch_name: 'master',
commit_message: "Add .gitlab/route-map.yml",
file_path: '.gitlab/route-map.yml',
file_content: <<-MAP.strip_heredoc
# Team data
- source: 'data/team.yml'
public: 'team/'
MAP
).execute
visit_blob('.gitlab/route-map.yml')
end
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows that map is valid
expect(page).to have_content('This Route Map is valid.')
# shows a learn more link
expect(page).to have_link('Learn more')
end
end
end
context 'LICENSE' do
before do
visit_blob('LICENSE')
end
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows license
expect(page).to have_content('This project is licensed under the MIT License.')
# shows a learn more link
expect(page).to have_link('Learn more about this license', 'http://choosealicense.com/licenses/mit/')
end
end
end
end end
...@@ -271,6 +271,52 @@ describe Blob do ...@@ -271,6 +271,52 @@ describe Blob do
end end
end end
describe '#auxiliary_viewer' do
context 'when the blob has an external storage error' do
before do
project.lfs_enabled = false
end
it 'returns nil' do
blob = fake_blob(path: 'LICENSE', lfs: true)
expect(blob.auxiliary_viewer).to be_nil
end
end
context 'when the blob is empty' do
it 'returns nil' do
blob = fake_blob(data: '')
expect(blob.auxiliary_viewer).to be_nil
end
end
context 'when the blob is stored externally' do
it 'returns a matching viewer' do
blob = fake_blob(path: 'LICENSE', lfs: true)
expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
end
end
context 'when the blob is binary' do
it 'returns nil' do
blob = fake_blob(path: 'LICENSE', binary: true)
expect(blob.auxiliary_viewer).to be_nil
end
end
context 'when the blob is text-based' do
it 'returns a matching text-based viewer' do
blob = fake_blob(path: 'LICENSE')
expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
end
end
end
describe '#rendered_as_text?' do describe '#rendered_as_text?' do
context 'when ignoring errors' do context 'when ignoring errors' do
context 'when the simple viewer is text-based' do context 'when the simple viewer is text-based' do
......
...@@ -8,6 +8,7 @@ describe BlobViewer::Base, model: true do ...@@ -8,6 +8,7 @@ describe BlobViewer::Base, model: true do
let(:viewer_class) do let(:viewer_class) do
Class.new(described_class) do Class.new(described_class) do
self.extensions = %w(pdf) self.extensions = %w(pdf)
self.binary = true
self.max_size = 1.megabyte self.max_size = 1.megabyte
self.absolute_max_size = 5.megabytes self.absolute_max_size = 5.megabytes
self.client_side = false self.client_side = false
...@@ -18,14 +19,47 @@ describe BlobViewer::Base, model: true do ...@@ -18,14 +19,47 @@ describe BlobViewer::Base, model: true do
describe '.can_render?' do describe '.can_render?' do
context 'when the extension is supported' do context 'when the extension is supported' do
let(:blob) { fake_blob(path: 'file.pdf') } context 'when the binaryness matches' do
let(:blob) { fake_blob(path: 'file.pdf', binary: true) }
it 'returns true' do it 'returns true' do
expect(viewer_class.can_render?(blob)).to be_truthy expect(viewer_class.can_render?(blob)).to be_truthy
end
end
context 'when the binaryness does not match' do
let(:blob) { fake_blob(path: 'file.pdf', binary: false) }
it 'returns false' do
expect(viewer_class.can_render?(blob)).to be_falsey
end
end
end
context 'when the file type is supported' do
before do
viewer_class.file_type = :license
viewer_class.binary = false
end
context 'when the binaryness matches' do
let(:blob) { fake_blob(path: 'LICENSE', binary: false) }
it 'returns true' do
expect(viewer_class.can_render?(blob)).to be_truthy
end
end
context 'when the binaryness does not match' do
let(:blob) { fake_blob(path: 'LICENSE', binary: true) }
it 'returns false' do
expect(viewer_class.can_render?(blob)).to be_falsey
end
end end
end end
context 'when the extension is not supported' do context 'when the extension and file type are not supported' do
let(:blob) { fake_blob(path: 'file.txt') } let(:blob) { fake_blob(path: 'file.txt') }
it 'returns false' do it 'returns false' do
...@@ -153,34 +187,4 @@ describe BlobViewer::Base, model: true do ...@@ -153,34 +187,4 @@ describe BlobViewer::Base, model: true do
end end
end end
end end
describe '#prepare!' do
context 'when the viewer is server side' do
let(:blob) { fake_blob(path: 'file.md') }
before do
viewer_class.client_side = false
end
it 'loads all blob data' do
expect(blob).to receive(:load_all_data!)
viewer.prepare!
end
end
context 'when the viewer is client side' do
let(:blob) { fake_blob(path: 'file.md') }
before do
viewer_class.client_side = true
end
it "doesn't load all blob data" do
expect(blob).not_to receive(:load_all_data!)
viewer.prepare!
end
end
end
end end
require 'spec_helper'
describe BlobViewer::GitlabCiYml, model: true do
include FakeBlobHelpers
let(:project) { build(:project) }
let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) }
subject { described_class.new(blob) }
describe '#validation_message' do
it 'calls prepare! on the viewer' do
expect(subject).to receive(:prepare!)
subject.validation_message
end
context 'when the configuration is valid' do
it 'returns nil' do
expect(subject.validation_message).to be_nil
end
end
context 'when the configuration is invalid' do
let(:data) { 'oof' }
it 'returns the error message' do
expect(subject.validation_message).to eq('Invalid configuration format')
end
end
end
end
require 'spec_helper'
describe BlobViewer::License, model: true do
include FakeBlobHelpers
let(:project) { create(:project, :repository) }
let(:blob) { fake_blob(path: 'LICENSE') }
subject { described_class.new(blob) }
describe '#license' do
it 'returns the blob project repository license' do
expect(subject.license).not_to be_nil
expect(subject.license).to eq(project.repository.license)
end
end
describe '#render_error' do
context 'when there is no license' do
before do
allow(project.repository).to receive(:license).and_return(nil)
end
it 'returns :unknown_license' do
expect(subject.render_error).to eq(:unknown_license)
end
end
context 'when there is a license' do
it 'returns nil' do
expect(subject.render_error).to be_nil
end
end
end
end
require 'spec_helper'
describe BlobViewer::RouteMap, model: true do
include FakeBlobHelpers
let(:project) { build(:project) }
let(:data) do
<<-MAP.strip_heredoc
# Team data
- source: 'data/team.yml'
public: 'team/'
MAP
end
let(:blob) { fake_blob(path: '.gitlab/route-map.yml', data: data) }
subject { described_class.new(blob) }
describe '#validation_message' do
it 'calls prepare! on the viewer' do
expect(subject).to receive(:prepare!)
subject.validation_message
end
context 'when the configuration is valid' do
it 'returns nil' do
expect(subject.validation_message).to be_nil
end
end
context 'when the configuration is invalid' do
let(:data) { 'oof' }
it 'returns the error message' do
expect(subject.validation_message).to eq('Route map is not an array')
end
end
end
end
require 'spec_helper'
describe BlobViewer::ServerSide, model: true do
include FakeBlobHelpers
let(:project) { build(:empty_project) }
let(:viewer_class) do
Class.new(BlobViewer::Base) do
include BlobViewer::ServerSide
end
end
subject { viewer_class.new(blob) }
describe '#prepare!' do
let(:blob) { fake_blob(path: 'file.txt') }
it 'loads all blob data' do
expect(blob).to receive(:load_all_data!)
subject.prepare!
end
end
end
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe Repository, models: true do describe Repository, models: true do
include RepoHelpers include RepoHelpers
TestBlob = Struct.new(:name) TestBlob = Struct.new(:path)
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:repository) { project.repository } let(:repository) { project.repository }
...@@ -565,31 +565,31 @@ describe Repository, models: true do ...@@ -565,31 +565,31 @@ describe Repository, models: true do
it 'accepts changelog' do it 'accepts changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')]) expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
expect(repository.changelog.name).to eq('changelog') expect(repository.changelog.path).to eq('changelog')
end end
it 'accepts news instead of changelog' do it 'accepts news instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')]) expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')])
expect(repository.changelog.name).to eq('news') expect(repository.changelog.path).to eq('news')
end end
it 'accepts history instead of changelog' do it 'accepts history instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')]) expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')])
expect(repository.changelog.name).to eq('history') expect(repository.changelog.path).to eq('history')
end end
it 'accepts changes instead of changelog' do it 'accepts changes instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')]) expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')])
expect(repository.changelog.name).to eq('changes') expect(repository.changelog.path).to eq('changes')
end end
it 'is case-insensitive' do it 'is case-insensitive' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')]) expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')])
expect(repository.changelog.name).to eq('CHANGELOG') expect(repository.changelog.path).to eq('CHANGELOG')
end end
end end
...@@ -624,7 +624,7 @@ describe Repository, models: true do ...@@ -624,7 +624,7 @@ describe Repository, models: true do
repository.create_file(user, 'LICENSE', 'Copyright!', repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master') message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_blob.name).to eq('LICENSE') expect(repository.license_blob.path).to eq('LICENSE')
end end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
...@@ -654,7 +654,7 @@ describe Repository, models: true do ...@@ -654,7 +654,7 @@ describe Repository, models: true do
expect(repository.license_key).to be_nil expect(repository.license_key).to be_nil
end end
it 'detects license file with no recognizable open-source license content' do it 'returns nil when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Copyright!', repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master') message: 'Add LICENSE', branch_name: 'master')
...@@ -670,12 +670,45 @@ describe Repository, models: true do ...@@ -670,12 +670,45 @@ describe Repository, models: true do
end end
end end
describe '#license' do
before do
repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
end
it 'returns nil when no license is detected' do
expect(repository.license).to be_nil
end
it 'returns nil when the repository does not exist' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.license).to be_nil
end
it 'returns nil when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license).to be_nil
end
it 'returns the license' do
license = Licensee::License.new('mit')
repository.create_file(user, 'LICENSE',
license.content,
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license).to eq(license)
end
end
describe "#gitlab_ci_yml", caching: true do describe "#gitlab_ci_yml", caching: true do
it 'returns valid file' do it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')] files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files) expect(repository.tree).to receive(:blobs).and_return(files)
expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml') expect(repository.gitlab_ci_yml.path).to eq('.gitlab-ci.yml')
end end
it 'returns nil if not exists' do it 'returns nil if not exists' do
...@@ -1825,11 +1858,12 @@ describe Repository, models: true do ...@@ -1825,11 +1858,12 @@ describe Repository, models: true do
describe '#refresh_method_caches' do describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches). expect(repository).to receive(:expire_method_caches).
with(%i(rendered_readme license_blob license_key)) with(%i(rendered_readme license_blob license_key license))
expect(repository).to receive(:rendered_readme) expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:license_blob) expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key) expect(repository).to receive(:license_key)
expect(repository).to receive(:license)
repository.refresh_method_caches(%i(readme license)) repository.refresh_method_caches(%i(readme license))
end end
......
...@@ -47,10 +47,10 @@ describe 'projects/blob/_viewer.html.haml', :view do ...@@ -47,10 +47,10 @@ describe 'projects/blob/_viewer.html.haml', :view do
expect(rendered).to have_css('.blob-viewer[data-url]') expect(rendered).to have_css('.blob-viewer[data-url]')
end end
it 'displays a spinner' do it 'renders the loading indicator' do
render_view render_view
expect(rendered).to have_css('i[aria-label="Loading content"]') expect(view).to render_template('projects/blob/viewers/_loading')
end end
end end
......
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