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

Implement auxiliary blob viewers

See merge request !11195
This commit is contained in:
Sean McGivern 2017-05-11 08:26:05 +00:00
commit 1c75e4e6a0
32 changed files with 616 additions and 140 deletions

View file

@ -1,20 +1,38 @@
/* global Flash */
export default class BlobViewer {
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.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-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)');
if (!initialViewer) return;
let initialViewerName = initialViewer.getAttribute('data-type');
this.simpleViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="rich"]');
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) {
initialViewerName = 'simple';
}
@ -64,40 +82,13 @@ export default class BlobViewer {
$(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) {
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;
const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
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) {
oldButton.classList.remove('active');
@ -118,6 +109,40 @@ export default class BlobViewer {
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);
});
});
}
}

View file

@ -3,8 +3,11 @@ module RendersBlob
def render_blob_json(blob)
viewer =
if params[:viewer] == 'rich'
case params[:viewer]
when 'rich'
blob.rich_viewer
when 'auxiliary'
blob.auxiliary_viewer
else
blob.simple_viewer
end

View file

@ -110,11 +110,8 @@ module ProjectsHelper
end
def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil?
license = Licensee::License.new(project.repository.license_key)
license.nickname || license.name
license = project.repository.license
license&.nickname || license&.name || 'LICENSE'
end
def last_push_event

View file

@ -34,10 +34,13 @@ class Blob < SimpleDelegator
BlobViewer::BinarySTL,
BlobViewer::TextSTL
].freeze
].sort_by { |v| v.binary? ? 0 : 1 }.freeze
BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
AUXILIARY_VIEWERS = [
BlobViewer::GitlabCiYml,
BlobViewer::RouteMap,
BlobViewer::License
].freeze
attr_reader :project
@ -154,6 +157,12 @@ class Blob < SimpleDelegator
@rich_viewer = rich_viewer_class&.new(self)
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)
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end
@ -180,17 +189,18 @@ class Blob < SimpleDelegator
end
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?
classes =
if stored_externally?
BINARY_VIEWERS + TEXT_VIEWERS
elsif binary?
BINARY_VIEWERS
else # text
TEXT_VIEWERS
end
verify_binary = !stored_externally?
classes.find { |viewer_class| viewer_class.can_render?(self) }
classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
end
end

View file

@ -0,0 +1,12 @@
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

View file

@ -1,8 +1,12 @@
module BlobViewer
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_accessor :override_max_size
@ -12,7 +16,11 @@ module BlobViewer
end
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
def self.rich?
@ -23,6 +31,10 @@ module BlobViewer
type == :simple
end
def self.auxiliary?
type == :auxiliary
end
def self.client_side?
client_side
end
@ -39,8 +51,12 @@ module BlobViewer
!binary?
end
def self.can_render?(blob)
!extensions || extensions.include?(blob.extension)
def self.can_render?(blob, verify_binary: true)
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
def too_large?
@ -83,9 +99,7 @@ module BlobViewer
end
def prepare!
if server_side? && blob.project
blob.load_all_data!(blob.project.repository)
end
# To be overridden by subclasses
end
private

View file

@ -0,0 +1,23 @@
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

View file

@ -0,0 +1,23 @@
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

View file

@ -0,0 +1,30 @@
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

View file

@ -7,5 +7,11 @@ module BlobViewer
self.max_size = 2.megabytes
self.absolute_max_size = 5.megabytes
end
def prepare!
if blob.project
blob.load_all_data!(blob.project.repository)
end
end
end
end

View file

@ -30,7 +30,7 @@ class Repository
METHOD_CACHES_FOR_FILE_TYPES = {
readme: :rendered_readme,
changelog: :changelog,
license: %i(license_blob license_key),
license: %i(license_blob license_key license),
contributing: :contribution_guide,
gitignore: :gitignore,
koding: :koding_yml,
@ -42,13 +42,13 @@ class Repository
# variable.
#
# 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}"
alias_method(original, name)
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
@ -549,6 +549,13 @@ class Repository
end
cache_method :license_key
def license
return unless license_key
Licensee::License.new(license_key)
end
cache_method :license, memoize_only: true
def gitignore
file_on_head(:gitignore)
end
@ -1061,14 +1068,20 @@ class Repository
#
# 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.
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)
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
else
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
# if e.g. HEAD or the entire repository doesn't exist we want to
# gracefully handle this and not cache anything.
@ -1083,8 +1096,8 @@ class Repository
def file_on_head(type)
if head = tree(:head)
head.blobs.find do |file|
Gitlab::FileDetector.type_of(file.name) == type
head.blobs.find do |blob|
Gitlab::FileDetector.type_of(blob.path) == type
end
end
end

View file

@ -6,6 +6,11 @@
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= 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
%article.file-holder
= render "projects/blob/header", blob: blob

View file

@ -5,8 +5,7 @@
- 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) }
- if load_asynchronously
.text-center.prepend-top-default.append-bottom-default
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
= render viewer.loading_partial_path, viewer: viewer
- elsif render_error
= render 'projects/blob/render_error', viewer: viewer
- else

View file

@ -0,0 +1,9 @@
- 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')

View file

@ -0,0 +1,4 @@
= icon('spinner spin fw')
Validating GitLab CI configuration…
= link_to 'Learn more', help_page_path('ci/yaml/README')

View file

@ -0,0 +1,8 @@
- 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'

View file

@ -0,0 +1,2 @@
.text-center.prepend-top-default.append-bottom-default
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…')

View file

@ -0,0 +1,2 @@
= icon('spinner spin fw')
Loading…

View file

@ -0,0 +1,9 @@
- 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')

View file

@ -0,0 +1,4 @@
= icon('spinner spin fw')
Validating Route Map…
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')

View file

@ -0,0 +1,5 @@
---
title: Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and
LICENSE blob pages
merge_request:
author:

View file

@ -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)
### 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.

View file

@ -13,7 +13,8 @@ module Gitlab
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/
avatar: /\Alogo\.(png|jpg|gif)\z/,
route_map: 'route-map.yml'
}.freeze
# Returns an Array of file types based on the given paths.

View file

@ -5,13 +5,13 @@ feature 'File blob', :js, feature: true do
def visit_blob(path, fragment = nil)
visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment)
wait_for_ajax
end
context 'Ruby file' do
before do
visit_blob('files/ruby/popen.rb')
wait_for_ajax
end
it 'displays the blob' do
@ -35,8 +35,6 @@ feature 'File blob', :js, feature: true do
context 'visiting directly' do
before do
visit_blob('files/markdown/ruby-style-guide.md')
wait_for_ajax
end
it 'displays the blob using the rich viewer' do
@ -104,8 +102,6 @@ feature 'File blob', :js, feature: true do
context 'visiting with a line number anchor' do
before do
visit_blob('files/markdown/ruby-style-guide.md', 'L1')
wait_for_ajax
end
it 'displays the blob using the simple viewer' do
@ -148,8 +144,6 @@ feature 'File blob', :js, feature: true do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/file.md')
wait_for_ajax
end
it 'displays an error' do
@ -198,8 +192,6 @@ feature 'File blob', :js, feature: true do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/file.md')
wait_for_ajax
end
it 'displays the blob' do
@ -235,8 +227,6 @@ feature 'File blob', :js, feature: true do
).execute
visit_blob('files/test.pdf')
wait_for_ajax
end
it 'displays the blob' do
@ -263,8 +253,6 @@ feature 'File blob', :js, feature: true do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/lfs_object.iso')
wait_for_ajax
end
it 'displays the blob' do
@ -287,8 +275,6 @@ feature 'File blob', :js, feature: true do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/lfs_object.iso')
wait_for_ajax
end
it 'displays the blob' do
@ -312,8 +298,6 @@ feature 'File blob', :js, feature: true do
context 'ZIP file' do
before do
visit_blob('Gemfile.zip')
wait_for_ajax
end
it 'displays the blob' do
@ -348,8 +332,6 @@ feature 'File blob', :js, feature: true do
).execute
visit_blob('files/empty.md')
wait_for_ajax
end
it 'displays an error' do
@ -369,4 +351,80 @@ feature 'File blob', :js, feature: true do
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

View file

@ -271,6 +271,52 @@ describe Blob do
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
context 'when ignoring errors' do
context 'when the simple viewer is text-based' do

View file

@ -8,6 +8,7 @@ describe BlobViewer::Base, model: true do
let(:viewer_class) do
Class.new(described_class) do
self.extensions = %w(pdf)
self.binary = true
self.max_size = 1.megabyte
self.absolute_max_size = 5.megabytes
self.client_side = false
@ -18,14 +19,47 @@ describe BlobViewer::Base, model: true do
describe '.can_render?' 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
expect(viewer_class.can_render?(blob)).to be_truthy
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: 'file.pdf', binary: false) }
it 'returns false' do
expect(viewer_class.can_render?(blob)).to be_falsey
end
end
end
context 'when the extension is not supported' do
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
context 'when the extension and file type are not supported' do
let(:blob) { fake_blob(path: 'file.txt') }
it 'returns false' do
@ -153,34 +187,4 @@ describe BlobViewer::Base, model: true do
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

View file

@ -0,0 +1,32 @@
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

View file

@ -0,0 +1,34 @@
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

View file

@ -0,0 +1,38 @@
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

View file

@ -0,0 +1,25 @@
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

View file

@ -2,7 +2,7 @@ require 'spec_helper'
describe Repository, models: true do
include RepoHelpers
TestBlob = Struct.new(:name)
TestBlob = Struct.new(:path)
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
@ -565,31 +565,31 @@ describe Repository, models: true do
it 'accepts changelog' do
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
it 'accepts news instead of changelog' do
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
it 'accepts history instead of changelog' do
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
it 'accepts changes instead of changelog' do
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
it 'is case-insensitive' do
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
@ -624,7 +624,7 @@ describe Repository, models: true do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_blob.name).to eq('LICENSE')
expect(repository.license_blob.path).to eq('LICENSE')
end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
@ -654,7 +654,7 @@ describe Repository, models: true do
expect(repository.license_key).to be_nil
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!',
message: 'Add LICENSE', branch_name: 'master')
@ -670,12 +670,45 @@ describe Repository, models: true do
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
it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
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
it 'returns nil if not exists' do
@ -1825,11 +1858,12 @@ describe Repository, models: true do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
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(:license_blob)
expect(repository).to receive(:license_key)
expect(repository).to receive(:license)
repository.refresh_method_caches(%i(readme license))
end

View file

@ -47,10 +47,10 @@ describe 'projects/blob/_viewer.html.haml', :view do
expect(rendered).to have_css('.blob-viewer[data-url]')
end
it 'displays a spinner' do
it 'renders the loading indicator' do
render_view
expect(rendered).to have_css('i[aria-label="Loading content"]')
expect(view).to render_template('projects/blob/viewers/_loading')
end
end