Merge branch 'master' into balsalmiq-support
This commit is contained in:
commit
cde3760bb4
117 changed files with 2582 additions and 508 deletions
|
@ -403,13 +403,6 @@ docs:check:links:
|
|||
# Check the internal links
|
||||
- bundle exec nanoc check internal_links
|
||||
|
||||
bundler:check:
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *ruby-static-analysis
|
||||
script:
|
||||
- bundle check
|
||||
|
||||
bundler:audit:
|
||||
stage: test
|
||||
<<: *ruby-static-analysis
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
|
||||
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
|
||||
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
|
||||
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
## Test coverage
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ export default () => {
|
|||
},
|
||||
},
|
||||
template: `
|
||||
<div class="container-fluid md prepend-top-default append-bottom-default">
|
||||
<div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default">
|
||||
<div
|
||||
class="text-center loading"
|
||||
v-if="loading && !error">
|
||||
|
|
120
app/assets/javascripts/blob/viewer/index.js
Normal file
120
app/assets/javascripts/blob/viewer/index.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/* global Flash */
|
||||
export default class BlobViewer {
|
||||
constructor() {
|
||||
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.$blobContentHolder = $('#blob-content-holder');
|
||||
|
||||
let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
|
||||
|
||||
this.initBindings();
|
||||
|
||||
if (this.switcher && location.hash.indexOf('#L') === 0) {
|
||||
initialViewerName = 'simple';
|
||||
}
|
||||
|
||||
this.switchToViewer(initialViewerName);
|
||||
}
|
||||
|
||||
initBindings() {
|
||||
if (this.switcherBtns.length) {
|
||||
Array.from(this.switcherBtns)
|
||||
.forEach((el) => {
|
||||
el.addEventListener('click', this.switchViewHandler.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
if (this.copySourceBtn) {
|
||||
this.copySourceBtn.addEventListener('click', () => {
|
||||
if (this.copySourceBtn.classList.contains('disabled')) return;
|
||||
|
||||
this.switchToViewer('simple');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switchViewHandler(e) {
|
||||
const target = e.currentTarget;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.switchToViewer(target.getAttribute('data-viewer'));
|
||||
}
|
||||
|
||||
toggleCopyButtonState() {
|
||||
if (!this.copySourceBtn) return;
|
||||
|
||||
if (this.simpleViewer.getAttribute('data-loaded')) {
|
||||
this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
|
||||
this.copySourceBtn.classList.remove('disabled');
|
||||
} else if (this.activeViewer === this.simpleViewer) {
|
||||
this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard');
|
||||
this.copySourceBtn.classList.add('disabled');
|
||||
} else {
|
||||
this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
|
||||
this.copySourceBtn.classList.add('disabled');
|
||||
}
|
||||
|
||||
$(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.$blobContentHolder.trigger('highlight:line');
|
||||
|
||||
this.toggleCopyButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
switchToViewer(name) {
|
||||
const newViewer = document.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}'])`);
|
||||
|
||||
if (oldButton) {
|
||||
oldButton.classList.remove('active');
|
||||
}
|
||||
|
||||
if (newButton) {
|
||||
newButton.classList.add('active');
|
||||
newButton.blur();
|
||||
}
|
||||
|
||||
if (oldViewer) {
|
||||
oldViewer.classList.add('hidden');
|
||||
}
|
||||
|
||||
newViewer.classList.remove('hidden');
|
||||
|
||||
this.activeViewer = newViewer;
|
||||
|
||||
this.toggleCopyButtonState();
|
||||
|
||||
this.loadViewer(newViewer);
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
import UserCallout from './user_callout';
|
||||
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
|
||||
import ShortcutsWiki from './shortcuts_wiki';
|
||||
import BlobViewer from './blob/viewer/index';
|
||||
|
||||
const ShortcutsBlob = require('./shortcuts_blob');
|
||||
|
||||
|
@ -299,6 +300,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
|
|||
gl.TargetBranchDropDown.bootstrap();
|
||||
break;
|
||||
case 'projects:blob:show':
|
||||
new BlobViewer();
|
||||
gl.TargetBranchDropDown.bootstrap();
|
||||
initBlob();
|
||||
break;
|
||||
|
|
|
@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo');
|
|||
LineHighlighter.prototype._hash = '';
|
||||
|
||||
function LineHighlighter(hash) {
|
||||
var range;
|
||||
if (hash == null) {
|
||||
// Initialize a LineHighlighter object
|
||||
//
|
||||
|
@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo');
|
|||
this.setHash = bind(this.setHash, this);
|
||||
this.highlightLine = bind(this.highlightLine, this);
|
||||
this.clickHandler = bind(this.clickHandler, this);
|
||||
this.highlightHash = this.highlightHash.bind(this);
|
||||
this._hash = hash;
|
||||
this.bindEvents();
|
||||
if (hash !== '') {
|
||||
range = this.hashToRange(hash);
|
||||
this.highlightHash();
|
||||
}
|
||||
|
||||
LineHighlighter.prototype.bindEvents = function() {
|
||||
const $blobContentHolder = $('#blob-content-holder');
|
||||
$blobContentHolder.on('click', 'a[data-line-number]', this.clickHandler);
|
||||
$blobContentHolder.on('highlight:line', this.highlightHash);
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.highlightHash = function() {
|
||||
var range;
|
||||
if (this._hash !== '') {
|
||||
range = this.hashToRange(this._hash);
|
||||
if (range[0]) {
|
||||
this.highlightRange(range);
|
||||
$.scrollTo("#L" + range[0], {
|
||||
|
@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo');
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LineHighlighter.prototype.bindEvents = function() {
|
||||
$('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.clickHandler = function(event) {
|
||||
|
|
|
@ -120,6 +120,10 @@
|
|||
// Ensure that image does not exceed viewport
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
table {
|
||||
@include markdown-table;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
|
|
|
@ -12,6 +12,13 @@
|
|||
max-width: $max_width;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mixin for markdown tables
|
||||
*/
|
||||
@mixin markdown-table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Base mixin for lists in GitLab
|
||||
*/
|
||||
|
|
|
@ -200,6 +200,7 @@
|
|||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
line-height: 1.8;
|
||||
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
|
|
|
@ -101,11 +101,16 @@ ul.related-merge-requests > li {
|
|||
}
|
||||
}
|
||||
|
||||
.merge-request-ci-status {
|
||||
.merge-request-ci-status,
|
||||
.related-merge-requests {
|
||||
.ci-status-link {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,10 @@ ul.notes {
|
|||
padding-left: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
@include markdown-table;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list {
|
|||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wiki {
|
||||
table {
|
||||
@include markdown-table;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,10 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def respond_422
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
def no_cache_headers
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
|
|
17
app/controllers/concerns/renders_blob.rb
Normal file
17
app/controllers/concerns/renders_blob.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
module RendersBlob
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def render_blob_json(blob)
|
||||
viewer =
|
||||
if params[:viewer] == 'rich'
|
||||
blob.rich_viewer
|
||||
else
|
||||
blob.simple_viewer
|
||||
end
|
||||
return render_404 unless viewer
|
||||
|
||||
render json: {
|
||||
html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
|
||||
}
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
class Projects::BlobController < Projects::ApplicationController
|
||||
include ExtractsPath
|
||||
include CreatesCommit
|
||||
include RendersBlob
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
# Raised when given an invalid file path
|
||||
|
@ -34,8 +35,20 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
|
||||
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
|
||||
@blob.override_max_size! if params[:override_max_size] == 'true'
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
|
||||
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
|
||||
|
||||
render 'show'
|
||||
end
|
||||
|
||||
format.json do
|
||||
render_blob_json(@blob)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
|
@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
private
|
||||
|
||||
def blob
|
||||
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
|
||||
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
|
||||
|
||||
if @blob
|
||||
@blob
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class Projects::BuildsController < Projects::ApplicationController
|
||||
before_action :build, except: [:index, :cancel_all]
|
||||
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
|
||||
before_action :authorize_read_build!, only: [:index, :show, :status, :raw, :trace]
|
||||
before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
|
||||
layout 'project'
|
||||
|
||||
|
@ -60,20 +60,22 @@ class Projects::BuildsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def retry
|
||||
return render_404 unless @build.retryable?
|
||||
return respond_422 unless @build.retryable?
|
||||
|
||||
build = Ci::Build.retry(@build, current_user)
|
||||
redirect_to build_path(build)
|
||||
end
|
||||
|
||||
def play
|
||||
return render_404 unless @build.playable?
|
||||
return respond_422 unless @build.playable?
|
||||
|
||||
build = @build.play(current_user)
|
||||
redirect_to build_path(build)
|
||||
end
|
||||
|
||||
def cancel
|
||||
return respond_422 unless @build.cancelable?
|
||||
|
||||
@build.cancel
|
||||
redirect_to build_path(@build)
|
||||
end
|
||||
|
@ -85,9 +87,12 @@ class Projects::BuildsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def erase
|
||||
@build.erase(erased_by: current_user)
|
||||
redirect_to namespace_project_build_path(project.namespace, project, @build),
|
||||
if @build.erase(erased_by: current_user)
|
||||
redirect_to namespace_project_build_path(project.namespace, project, @build),
|
||||
notice: "Build has been successfully erased!"
|
||||
else
|
||||
respond_422
|
||||
end
|
||||
end
|
||||
|
||||
def raw
|
||||
|
|
|
@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
|
|||
|
||||
return if cached_blob?
|
||||
|
||||
if @blob.lfs_pointer? && project.lfs_enabled?
|
||||
if @blob.valid_lfs_pointer?
|
||||
send_lfs_object
|
||||
else
|
||||
send_git_blob @repository, @blob
|
||||
|
|
|
@ -52,7 +52,7 @@ module BlobHelper
|
|||
|
||||
if !on_top_of_branch?(project, ref)
|
||||
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
|
||||
elsif blob.lfs_pointer?
|
||||
elsif blob.valid_lfs_pointer?
|
||||
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
|
||||
elsif can_modify_blob?(blob, project, ref)
|
||||
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
|
||||
|
@ -95,7 +95,7 @@ module BlobHelper
|
|||
end
|
||||
|
||||
def can_modify_blob?(blob, project = @project, ref = @ref)
|
||||
!blob.lfs_pointer? && can_edit_tree?(project, ref)
|
||||
!blob.valid_lfs_pointer? && can_edit_tree?(project, ref)
|
||||
end
|
||||
|
||||
def leave_edit_message
|
||||
|
@ -118,28 +118,15 @@ module BlobHelper
|
|||
icon("#{file_type_icon_class('file', mode, name)} fw")
|
||||
end
|
||||
|
||||
def blob_text_viewable?(blob)
|
||||
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
|
||||
end
|
||||
|
||||
def blob_rendered_as_text?(blob)
|
||||
blob_text_viewable?(blob) && blob.to_partial_path(@project) == 'text'
|
||||
end
|
||||
|
||||
def blob_size(blob)
|
||||
if blob.lfs_pointer?
|
||||
blob.lfs_size
|
||||
else
|
||||
blob.size
|
||||
end
|
||||
def blob_raw_url
|
||||
namespace_project_raw_path(@project.namespace, @project, @id)
|
||||
end
|
||||
|
||||
# SVGs can contain malicious JavaScript; only include whitelisted
|
||||
# elements and attributes. Note that this whitelist is by no means complete
|
||||
# and may omit some elements.
|
||||
def sanitize_svg(blob)
|
||||
blob.data = Gitlab::Sanitizers::SVG.clean(blob.data)
|
||||
blob
|
||||
def sanitize_svg_data(data)
|
||||
Gitlab::Sanitizers::SVG.clean(data)
|
||||
end
|
||||
|
||||
# If we blindly set the 'real' content type when serving a Git blob we
|
||||
|
@ -221,13 +208,42 @@ module BlobHelper
|
|||
clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
|
||||
end
|
||||
|
||||
def copy_blob_content_button(blob)
|
||||
return if markup?(blob.name)
|
||||
|
||||
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
|
||||
def copy_blob_source_button(blob)
|
||||
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
|
||||
end
|
||||
|
||||
def open_raw_file_button(path)
|
||||
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
|
||||
end
|
||||
|
||||
def blob_render_error_reason(viewer)
|
||||
case viewer.render_error
|
||||
when :too_large
|
||||
max_size =
|
||||
if viewer.absolutely_too_large?
|
||||
viewer.absolute_max_size
|
||||
elsif viewer.too_large?
|
||||
viewer.max_size
|
||||
end
|
||||
"it is larger than #{number_to_human_size(max_size)}"
|
||||
when :server_side_but_stored_in_lfs
|
||||
"it is stored in LFS"
|
||||
end
|
||||
end
|
||||
|
||||
def blob_render_error_options(viewer)
|
||||
options = []
|
||||
|
||||
if viewer.render_error == :too_large && viewer.can_override_max_size?
|
||||
options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
|
||||
end
|
||||
|
||||
if viewer.rich? && viewer.blob.rendered_as_text?
|
||||
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
|
||||
end
|
||||
|
||||
options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer')
|
||||
|
||||
options
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,41 @@ class Blob < SimpleDelegator
|
|||
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
|
||||
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
|
||||
|
||||
# The maximum size of an SVG that can be displayed.
|
||||
MAXIMUM_SVG_SIZE = 2.megabytes
|
||||
MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
|
||||
|
||||
# Finding a viewer for a blob happens based only on extension and whether the
|
||||
# blob is binary or text, which means 1 blob should only be matched by 1 viewer,
|
||||
# and the order of these viewers doesn't really matter.
|
||||
#
|
||||
# However, when the blob is an LFS pointer, we cannot know for sure whether the
|
||||
# file being pointed to is binary or text. In this case, we match only on
|
||||
# extension, preferring binary viewers over text ones if both exist, since the
|
||||
# large files referred to in "Large File Storage" are much more likely to be
|
||||
# binary than text.
|
||||
#
|
||||
# `.stl` files, for example, exist in both binary and text forms, and are
|
||||
# handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob
|
||||
# type. LFS pointers to `.stl` files are assumed to always be the binary kind,
|
||||
# and use the `BinarySTL` viewer.
|
||||
RICH_VIEWERS = [
|
||||
BlobViewer::Markup,
|
||||
BlobViewer::Notebook,
|
||||
BlobViewer::SVG,
|
||||
|
||||
BlobViewer::Image,
|
||||
BlobViewer::Sketch,
|
||||
BlobViewer::Balsamiq,
|
||||
|
||||
BlobViewer::PDF,
|
||||
|
||||
BlobViewer::BinarySTL,
|
||||
BlobViewer::TextSTL,
|
||||
].freeze
|
||||
|
||||
BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
|
||||
TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
|
||||
|
||||
attr_reader :project
|
||||
|
||||
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
|
||||
#
|
||||
|
@ -16,10 +49,16 @@ class Blob < SimpleDelegator
|
|||
#
|
||||
# blob = Blob.decorate(nil)
|
||||
# puts "truthy" if blob # No output
|
||||
def self.decorate(blob)
|
||||
def self.decorate(blob, project = nil)
|
||||
return if blob.nil?
|
||||
|
||||
new(blob)
|
||||
new(blob, project)
|
||||
end
|
||||
|
||||
def initialize(blob, project = nil)
|
||||
@project = project
|
||||
|
||||
super(blob)
|
||||
end
|
||||
|
||||
# Returns the data of the blob.
|
||||
|
@ -35,88 +74,107 @@ class Blob < SimpleDelegator
|
|||
end
|
||||
|
||||
def no_highlighting?
|
||||
size && size > 1.megabyte
|
||||
size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
|
||||
end
|
||||
|
||||
def only_display_raw?
|
||||
def too_large?
|
||||
size && truncated?
|
||||
end
|
||||
|
||||
# Returns the size of the file that this blob represents. If this blob is an
|
||||
# LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
|
||||
# the size of the blob itself.
|
||||
def raw_size
|
||||
if valid_lfs_pointer?
|
||||
lfs_size
|
||||
else
|
||||
size
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether the file that this blob represents is binary. If this blob is
|
||||
# an LFS pointer, we assume the file stored in LFS is binary, unless a
|
||||
# text-based rich blob viewer matched on the file's extension. Otherwise, this
|
||||
# depends on the type of the blob itself.
|
||||
def raw_binary?
|
||||
if valid_lfs_pointer?
|
||||
if rich_viewer
|
||||
rich_viewer.binary?
|
||||
else
|
||||
true
|
||||
end
|
||||
else
|
||||
binary?
|
||||
end
|
||||
end
|
||||
|
||||
def extension
|
||||
extname.downcase.delete('.')
|
||||
end
|
||||
|
||||
def svg?
|
||||
text? && language && language.name == 'SVG'
|
||||
end
|
||||
|
||||
def pdf?
|
||||
extension == 'pdf'
|
||||
end
|
||||
|
||||
def ipython_notebook?
|
||||
text? && language&.name == 'Jupyter Notebook'
|
||||
end
|
||||
|
||||
def sketch?
|
||||
binary? && extension == 'sketch'
|
||||
end
|
||||
|
||||
def balsamiq?
|
||||
binary? && extension == 'bmpr'
|
||||
end
|
||||
|
||||
def stl?
|
||||
extension == 'stl'
|
||||
end
|
||||
|
||||
def markup?
|
||||
text? && Gitlab::MarkupHelper.markup?(name)
|
||||
end
|
||||
|
||||
def size_within_svg_limits?
|
||||
size <= MAXIMUM_SVG_SIZE
|
||||
@extension ||= extname.downcase.delete('.')
|
||||
end
|
||||
|
||||
def video?
|
||||
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
|
||||
UploaderHelper::VIDEO_EXT.include?(extension)
|
||||
end
|
||||
|
||||
def to_partial_path(project)
|
||||
if lfs_pointer?
|
||||
if project.lfs_enabled?
|
||||
'download'
|
||||
else
|
||||
'text'
|
||||
end
|
||||
elsif image?
|
||||
'image'
|
||||
elsif svg?
|
||||
'svg'
|
||||
elsif pdf?
|
||||
'pdf'
|
||||
elsif ipython_notebook?
|
||||
'notebook'
|
||||
elsif sketch?
|
||||
'sketch'
|
||||
elsif stl?
|
||||
'stl'
|
||||
elsif balsamiq?
|
||||
'bmpr'
|
||||
elsif markup?
|
||||
if only_display_raw?
|
||||
'too_large'
|
||||
else
|
||||
'markup'
|
||||
end
|
||||
elsif text?
|
||||
if only_display_raw?
|
||||
'too_large'
|
||||
else
|
||||
'text'
|
||||
end
|
||||
else
|
||||
'download'
|
||||
def readable_text?
|
||||
text? && !valid_lfs_pointer? && !too_large?
|
||||
end
|
||||
|
||||
def valid_lfs_pointer?
|
||||
lfs_pointer? && project&.lfs_enabled?
|
||||
end
|
||||
|
||||
def invalid_lfs_pointer?
|
||||
lfs_pointer? && !project&.lfs_enabled?
|
||||
end
|
||||
|
||||
def simple_viewer
|
||||
@simple_viewer ||= simple_viewer_class.new(self)
|
||||
end
|
||||
|
||||
def rich_viewer
|
||||
return @rich_viewer if defined?(@rich_viewer)
|
||||
|
||||
@rich_viewer = rich_viewer_class&.new(self)
|
||||
end
|
||||
|
||||
def rendered_as_text?(ignore_errors: true)
|
||||
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
|
||||
end
|
||||
|
||||
def show_viewer_switcher?
|
||||
rendered_as_text? && rich_viewer
|
||||
end
|
||||
|
||||
def override_max_size!
|
||||
simple_viewer&.override_max_size = true
|
||||
rich_viewer&.override_max_size = true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def simple_viewer_class
|
||||
if empty?
|
||||
BlobViewer::Empty
|
||||
elsif raw_binary?
|
||||
BlobViewer::Download
|
||||
else # text
|
||||
BlobViewer::Text
|
||||
end
|
||||
end
|
||||
|
||||
def rich_viewer_class
|
||||
return if invalid_lfs_pointer? || empty?
|
||||
|
||||
classes =
|
||||
if valid_lfs_pointer?
|
||||
BINARY_VIEWERS + TEXT_VIEWERS
|
||||
elsif binary?
|
||||
BINARY_VIEWERS
|
||||
else # text
|
||||
TEXT_VIEWERS
|
||||
end
|
||||
|
||||
classes.find { |viewer_class| viewer_class.can_render?(self) }
|
||||
end
|
||||
end
|
||||
|
|
96
app/models/blob_viewer/base.rb
Normal file
96
app/models/blob_viewer/base.rb
Normal file
|
@ -0,0 +1,96 @@
|
|||
module BlobViewer
|
||||
class Base
|
||||
class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
|
||||
|
||||
delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
|
||||
|
||||
attr_reader :blob
|
||||
attr_accessor :override_max_size
|
||||
|
||||
def initialize(blob)
|
||||
@blob = blob
|
||||
end
|
||||
|
||||
def self.partial_path
|
||||
"projects/blob/viewers/#{partial_name}"
|
||||
end
|
||||
|
||||
def self.rich?
|
||||
type == :rich
|
||||
end
|
||||
|
||||
def self.simple?
|
||||
type == :simple
|
||||
end
|
||||
|
||||
def self.client_side?
|
||||
client_side
|
||||
end
|
||||
|
||||
def self.server_side?
|
||||
!client_side?
|
||||
end
|
||||
|
||||
def self.binary?
|
||||
binary
|
||||
end
|
||||
|
||||
def self.text?
|
||||
!binary?
|
||||
end
|
||||
|
||||
def self.can_render?(blob)
|
||||
!extensions || extensions.include?(blob.extension)
|
||||
end
|
||||
|
||||
def too_large?
|
||||
blob.raw_size > max_size
|
||||
end
|
||||
|
||||
def absolutely_too_large?
|
||||
blob.raw_size > absolute_max_size
|
||||
end
|
||||
|
||||
def can_override_max_size?
|
||||
too_large? && !absolutely_too_large?
|
||||
end
|
||||
|
||||
# This method is used on the server side to check whether we can attempt to
|
||||
# render the blob at all. Human-readable error messages are found in the
|
||||
# `BlobHelper#blob_render_error_reason` helper.
|
||||
#
|
||||
# This method does not and should not load the entire blob contents into
|
||||
# memory, and should not be overridden to do so in order to validate the
|
||||
# format of the blob.
|
||||
#
|
||||
# Prefer to implement a client-side viewer, where the JS component loads the
|
||||
# binary from `blob_raw_url` and does its own format validation and error
|
||||
# rendering, especially for potentially large binary formats.
|
||||
def render_error
|
||||
return @render_error if defined?(@render_error)
|
||||
|
||||
@render_error =
|
||||
if server_side_but_stored_in_lfs?
|
||||
# Files stored in LFS can only be rendered using a client-side viewer,
|
||||
# since we do not want to read large amounts of data into memory on the
|
||||
# server side. Client-side viewers use JS and can fetch the file from
|
||||
# `blob_raw_url` using AJAX.
|
||||
:server_side_but_stored_in_lfs
|
||||
elsif override_max_size ? absolutely_too_large? : too_large?
|
||||
:too_large
|
||||
end
|
||||
end
|
||||
|
||||
def prepare!
|
||||
if server_side? && blob.project
|
||||
blob.load_all_data!(blob.project.repository)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def server_side_but_stored_in_lfs?
|
||||
server_side? && blob.valid_lfs_pointer?
|
||||
end
|
||||
end
|
||||
end
|
10
app/models/blob_viewer/binary_stl.rb
Normal file
10
app/models/blob_viewer/binary_stl.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
module BlobViewer
|
||||
class BinarySTL < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'stl'
|
||||
self.extensions = %w(stl)
|
||||
self.binary = true
|
||||
end
|
||||
end
|
11
app/models/blob_viewer/client_side.rb
Normal file
11
app/models/blob_viewer/client_side.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module ClientSide
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.client_side = true
|
||||
self.max_size = 10.megabytes
|
||||
self.absolute_max_size = 50.megabytes
|
||||
end
|
||||
end
|
||||
end
|
17
app/models/blob_viewer/download.rb
Normal file
17
app/models/blob_viewer/download.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
module BlobViewer
|
||||
class Download < Base
|
||||
include Simple
|
||||
# We treat the Download 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
|
||||
|
||||
self.partial_name = 'download'
|
||||
self.binary = true
|
||||
|
||||
# We can always render the Download viewer, even if the blob is in LFS or too large.
|
||||
def render_error
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
9
app/models/blob_viewer/empty.rb
Normal file
9
app/models/blob_viewer/empty.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module BlobViewer
|
||||
class Empty < Base
|
||||
include Simple
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'empty'
|
||||
self.binary = true
|
||||
end
|
||||
end
|
12
app/models/blob_viewer/image.rb
Normal file
12
app/models/blob_viewer/image.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class Image < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'image'
|
||||
self.extensions = UploaderHelper::IMAGE_EXT
|
||||
self.binary = true
|
||||
self.switcher_icon = 'picture-o'
|
||||
self.switcher_title = 'image'
|
||||
end
|
||||
end
|
10
app/models/blob_viewer/markup.rb
Normal file
10
app/models/blob_viewer/markup.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
module BlobViewer
|
||||
class Markup < Base
|
||||
include Rich
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'markup'
|
||||
self.extensions = Gitlab::MarkupHelper::EXTENSIONS
|
||||
self.binary = false
|
||||
end
|
||||
end
|
12
app/models/blob_viewer/notebook.rb
Normal file
12
app/models/blob_viewer/notebook.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class Notebook < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'notebook'
|
||||
self.extensions = %w(ipynb)
|
||||
self.binary = false
|
||||
self.switcher_icon = 'file-text-o'
|
||||
self.switcher_title = 'notebook'
|
||||
end
|
||||
end
|
12
app/models/blob_viewer/pdf.rb
Normal file
12
app/models/blob_viewer/pdf.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class PDF < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'pdf'
|
||||
self.extensions = %w(pdf)
|
||||
self.binary = true
|
||||
self.switcher_icon = 'file-pdf-o'
|
||||
self.switcher_title = 'PDF'
|
||||
end
|
||||
end
|
11
app/models/blob_viewer/rich.rb
Normal file
11
app/models/blob_viewer/rich.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module Rich
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.type = :rich
|
||||
self.switcher_icon = 'file-text-o'
|
||||
self.switcher_title = 'rendered file'
|
||||
end
|
||||
end
|
||||
end
|
11
app/models/blob_viewer/server_side.rb
Normal file
11
app/models/blob_viewer/server_side.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module ServerSide
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.client_side = false
|
||||
self.max_size = 2.megabytes
|
||||
self.absolute_max_size = 5.megabytes
|
||||
end
|
||||
end
|
||||
end
|
11
app/models/blob_viewer/simple.rb
Normal file
11
app/models/blob_viewer/simple.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module Simple
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.type = :simple
|
||||
self.switcher_icon = 'code'
|
||||
self.switcher_title = 'source'
|
||||
end
|
||||
end
|
||||
end
|
12
app/models/blob_viewer/sketch.rb
Normal file
12
app/models/blob_viewer/sketch.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class Sketch < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'sketch'
|
||||
self.extensions = %w(sketch)
|
||||
self.binary = true
|
||||
self.switcher_icon = 'file-image-o'
|
||||
self.switcher_title = 'preview'
|
||||
end
|
||||
end
|
12
app/models/blob_viewer/svg.rb
Normal file
12
app/models/blob_viewer/svg.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class SVG < Base
|
||||
include Rich
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'svg'
|
||||
self.extensions = %w(svg)
|
||||
self.binary = false
|
||||
self.switcher_icon = 'picture-o'
|
||||
self.switcher_title = 'image'
|
||||
end
|
||||
end
|
11
app/models/blob_viewer/text.rb
Normal file
11
app/models/blob_viewer/text.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
class Text < Base
|
||||
include Simple
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'text'
|
||||
self.binary = false
|
||||
self.max_size = 1.megabyte
|
||||
self.absolute_max_size = 10.megabytes
|
||||
end
|
||||
end
|
5
app/models/blob_viewer/text_stl.rb
Normal file
5
app/models/blob_viewer/text_stl.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
module BlobViewer
|
||||
class TextSTL < BinarySTL
|
||||
self.binary = false
|
||||
end
|
||||
end
|
|
@ -316,7 +316,7 @@ class Commit
|
|||
def uri_type(path)
|
||||
entry = @raw.tree.path(path)
|
||||
if entry[:type] == :blob
|
||||
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]))
|
||||
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
|
||||
blob.image? || blob.video? ? :raw : :blob
|
||||
else
|
||||
entry[:type]
|
||||
|
|
|
@ -22,7 +22,7 @@ class ChatNotificationService < Service
|
|||
end
|
||||
|
||||
def can_test?
|
||||
super && valid?
|
||||
valid?
|
||||
end
|
||||
|
||||
def self.supported_events
|
||||
|
|
|
@ -450,7 +450,7 @@ class Repository
|
|||
|
||||
def blob_at(sha, path)
|
||||
unless Gitlab::Git.blank_ref?(sha)
|
||||
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
|
||||
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project)
|
||||
end
|
||||
rescue Gitlab::Git::Repository::NoRepository
|
||||
nil
|
||||
|
|
|
@ -26,6 +26,7 @@ class Service < ActiveRecord::Base
|
|||
has_one :service_hook
|
||||
|
||||
validates :project_id, presence: true, unless: proc { |service| service.template? }
|
||||
validates :type, presence: true
|
||||
|
||||
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
|
||||
scope :issue_trackers, -> { where(category: 'issue_tracker') }
|
||||
|
@ -131,7 +132,7 @@ class Service < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def can_test?
|
||||
!project.empty_repo?
|
||||
true
|
||||
end
|
||||
|
||||
# reason why service cannot be tested
|
||||
|
|
|
@ -1068,11 +1068,13 @@ class User < ActiveRecord::Base
|
|||
User.find_by_email(s)
|
||||
end
|
||||
|
||||
scope.create(
|
||||
user = scope.build(
|
||||
username: username,
|
||||
email: email,
|
||||
&creation_block
|
||||
)
|
||||
user.save(validate: false)
|
||||
user
|
||||
ensure
|
||||
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
|
||||
end
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
- else
|
||||
%hr
|
||||
- blob = diff_file.blob
|
||||
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
|
||||
- if blob && blob.readable_text?
|
||||
%table.code.white
|
||||
= render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, plain: true, email: true }
|
||||
- else
|
||||
|
|
|
@ -26,9 +26,4 @@
|
|||
%article.file-holder
|
||||
= render "projects/blob/header", blob: blob
|
||||
|
||||
- if blob.empty?
|
||||
.file-content.code
|
||||
.nothing-here-block
|
||||
Empty file
|
||||
- else
|
||||
= render blob.to_partial_path(@project), blob: blob
|
||||
= render 'projects/blob/content', blob: blob
|
||||
|
|
8
app/views/projects/blob/_content.html.haml
Normal file
8
app/views/projects/blob/_content.html.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- simple_viewer = blob.simple_viewer
|
||||
- rich_viewer = blob.rich_viewer
|
||||
- rich_viewer_active = rich_viewer && params[:viewer] != 'simple'
|
||||
|
||||
= render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active
|
||||
|
||||
- if rich_viewer
|
||||
= render 'projects/blob/viewer', viewer: rich_viewer, hidden: !rich_viewer_active
|
|
@ -1,7 +0,0 @@
|
|||
.file-content.blob_file.blob-no-preview
|
||||
.center
|
||||
= link_to namespace_project_raw_path(@project.namespace, @project, @id) do
|
||||
%h1.light
|
||||
%i.fa.fa-download
|
||||
%h4
|
||||
Download (#{number_to_human_size blob_size(blob)})
|
|
@ -9,17 +9,19 @@
|
|||
= copy_file_path_button(blob.path)
|
||||
|
||||
%small
|
||||
= number_to_human_size(blob_size(blob))
|
||||
= number_to_human_size(blob.raw_size)
|
||||
|
||||
.file-actions.hidden-xs
|
||||
= render 'projects/blob/viewer_switcher', blob: blob unless blame
|
||||
|
||||
.btn-group{ role: "group" }<
|
||||
= copy_blob_content_button(blob) if !blame && blob_rendered_as_text?(blob)
|
||||
= copy_blob_source_button(blob) if !blame && blob.rendered_as_text?(ignore_errors: false)
|
||||
= open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
|
||||
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
|
||||
|
||||
.btn-group{ role: "group" }<
|
||||
-# only show normal/blame view links for text files
|
||||
- if blob_text_viewable?(blob)
|
||||
- if blob.readable_text?
|
||||
- if blame
|
||||
= link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id),
|
||||
class: 'btn btn-sm'
|
||||
|
@ -34,7 +36,7 @@
|
|||
tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
|
||||
|
||||
.btn-group{ role: "group" }<
|
||||
= edit_blob_link if blob_text_viewable?(blob)
|
||||
= edit_blob_link if blob.readable_text?
|
||||
- if current_user
|
||||
= replace_blob_link
|
||||
= delete_blob_link
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
.file-content.image_file
|
||||
%img{ src: namespace_project_raw_path(@project.namespace, @project, @id), alt: blob.name }
|
7
app/views/projects/blob/_render_error.html.haml
Normal file
7
app/views/projects/blob/_render_error.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
.file-content.code
|
||||
.nothing-here-block
|
||||
The #{viewer.switcher_title} could not be displayed because #{blob_render_error_reason(viewer)}.
|
||||
|
||||
You can
|
||||
= blob_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
|
||||
instead.
|
|
@ -1,9 +0,0 @@
|
|||
- if blob.size_within_svg_limits?
|
||||
-# We need to scrub SVG but we cannot do so in the RawController: it would
|
||||
-# be wrong/strange if RawController modified the data.
|
||||
- blob.load_all_data!(@repository)
|
||||
- blob = sanitize_svg(blob)
|
||||
.file-content.image_file
|
||||
%img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: blob.name }
|
||||
- else
|
||||
= render 'too_large'
|
|
@ -1,2 +0,0 @@
|
|||
- blob.load_all_data!(@repository)
|
||||
= render 'shared/file_highlight', blob: blob, repository: @repository
|
|
@ -1,5 +0,0 @@
|
|||
.file-content.code
|
||||
.nothing-here-block
|
||||
The file could not be displayed as it is too large, you can
|
||||
#{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
|
||||
instead.
|
14
app/views/projects/blob/_viewer.html.haml
Normal file
14
app/views/projects/blob/_viewer.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
|||
- hidden = local_assigns.fetch(:hidden, false)
|
||||
- render_error = viewer.render_error
|
||||
- load_asynchronously = local_assigns.fetch(:load_asynchronously, viewer.server_side?) && render_error.nil?
|
||||
|
||||
- url = url_for(params.merge(viewer: viewer.type, format: :json)) if load_asynchronously
|
||||
.blob-viewer{ data: { type: viewer.type, url: 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')
|
||||
- elsif render_error
|
||||
= render 'projects/blob/render_error', viewer: viewer
|
||||
- else
|
||||
- viewer.prepare!
|
||||
= render viewer.partial_path, viewer: viewer
|
12
app/views/projects/blob/_viewer_switcher.html.haml
Normal file
12
app/views/projects/blob/_viewer_switcher.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
|||
- if blob.show_viewer_switcher?
|
||||
- simple_viewer = blob.simple_viewer
|
||||
- rich_viewer = blob.rich_viewer
|
||||
|
||||
.btn-group.js-blob-viewer-switcher{ role: "group" }
|
||||
- simple_label = "Display #{simple_viewer.switcher_title}"
|
||||
%button.btn.btn-default.btn-sm.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => simple_label, title: simple_label, data: { viewer: 'simple', container: 'body' } }>
|
||||
= icon(simple_viewer.switcher_icon)
|
||||
|
||||
- rich_label = "Display #{rich_viewer.switcher_title}"
|
||||
%button.btn.btn-default.btn-sm.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => rich_label, title: rich_label, data: { viewer: 'rich', container: 'body' } }>
|
||||
= icon(rich_viewer.switcher_icon)
|
|
@ -2,6 +2,9 @@
|
|||
- page_title @blob.path, @ref
|
||||
= render "projects/commits/head"
|
||||
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('blob')
|
||||
|
||||
%div{ class: container_class }
|
||||
= render 'projects/last_push'
|
||||
|
||||
|
|
7
app/views/projects/blob/viewers/_download.html.haml
Normal file
7
app/views/projects/blob/viewers/_download.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
.file-content.blob_file.blob-no-preview
|
||||
.center
|
||||
= link_to blob_raw_url do
|
||||
%h1.light
|
||||
= icon('download')
|
||||
%h4
|
||||
Download (#{number_to_human_size(viewer.blob.raw_size)})
|
3
app/views/projects/blob/viewers/_empty.html.haml
Normal file
3
app/views/projects/blob/viewers/_empty.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
.file-content.code
|
||||
.nothing-here-block
|
||||
Empty file
|
2
app/views/projects/blob/viewers/_image.html.haml
Normal file
2
app/views/projects/blob/viewers/_image.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
.file-content.image_file
|
||||
%img{ src: blob_raw_url, alt: viewer.blob.name }
|
3
app/views/projects/blob/viewers/_markup.html.haml
Normal file
3
app/views/projects/blob/viewers/_markup.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
- blob = viewer.blob
|
||||
.file-content.wiki
|
||||
= markup(blob.name, blob.data)
|
|
@ -2,4 +2,4 @@
|
|||
= page_specific_javascript_bundle_tag('common_vue')
|
||||
= page_specific_javascript_bundle_tag('notebook_viewer')
|
||||
|
||||
.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
|
||||
.file-content#js-notebook-viewer{ data: { endpoint: blob_raw_url } }
|
|
@ -2,4 +2,4 @@
|
|||
= page_specific_javascript_bundle_tag('common_vue')
|
||||
= page_specific_javascript_bundle_tag('pdf_viewer')
|
||||
|
||||
.file-content#js-pdf-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
|
||||
.file-content#js-pdf-viewer{ data: { endpoint: blob_raw_url } }
|
|
@ -2,6 +2,6 @@
|
|||
= page_specific_javascript_bundle_tag('common_vue')
|
||||
= page_specific_javascript_bundle_tag('sketch_viewer')
|
||||
|
||||
.file-content#js-sketch-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
|
||||
.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_url } }
|
||||
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
|
||||
= icon('spinner spin 2x', 'aria-hidden' => 'true');
|
|
@ -2,7 +2,7 @@
|
|||
= page_specific_javascript_bundle_tag('stl_viewer')
|
||||
|
||||
.file-content.is-stl-loading
|
||||
.text-center#js-stl-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
|
||||
.text-center#js-stl-viewer{ data: { endpoint: blob_raw_url } }
|
||||
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
|
||||
.text-center.prepend-top-default.append-bottom-default.stl-controls
|
||||
.btn-group
|
4
app/views/projects/blob/viewers/_svg.html.haml
Normal file
4
app/views/projects/blob/viewers/_svg.html.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
- blob = viewer.blob
|
||||
- data = sanitize_svg_data(blob.data)
|
||||
.file-content.image_file
|
||||
%img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(data)}", alt: blob.name }
|
1
app/views/projects/blob/viewers/_text.html.haml
Normal file
1
app/views/projects/blob/viewers/_text.html.haml
Normal file
|
@ -0,0 +1 @@
|
|||
= render 'shared/file_highlight', blob: viewer.blob, repository: @repository
|
|
@ -3,9 +3,9 @@
|
|||
- return unless blob.respond_to?(:text?)
|
||||
- if diff_file.too_large?
|
||||
.nothing-here-block This diff could not be displayed because it is too large.
|
||||
- elsif blob.only_display_raw?
|
||||
- elsif blob.too_large?
|
||||
.nothing-here-block The file could not be displayed because it is too large.
|
||||
- elsif blob_text_viewable?(blob)
|
||||
- elsif blob.readable_text?
|
||||
- if !project.repository.diffable?(blob)
|
||||
.nothing-here-block This diff was suppressed by a .gitattributes entry.
|
||||
- elsif diff_file.collapsed?
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
- diff_commit = commit_for_diff(diff_file)
|
||||
- blob = diff_file.blob(diff_commit)
|
||||
- next unless blob
|
||||
- blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
|
||||
- blob.load_all_data!(diffs.project.repository) unless blob.too_large?
|
||||
- file_hash = hexdigest(diff_file.file_path)
|
||||
|
||||
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
- unless diff_file.submodule?
|
||||
.file-actions.hidden-xs
|
||||
- if blob_text_viewable?(blob)
|
||||
- if blob.readable_text?
|
||||
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
|
||||
= icon('comment')
|
||||
\
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
.sidebar-collapsed-icon
|
||||
%strong
|
||||
= icon('exclamation', 'aria-hidden': 'true')
|
||||
%span= milestone.issues_visible_to_user(current_user).count
|
||||
%span= milestone.merge_requests.count
|
||||
.title.hide-collapsed
|
||||
Merge requests
|
||||
%span.badge= milestone.merge_requests.count
|
||||
|
|
|
@ -54,5 +54,5 @@
|
|||
= number_with_delimiter(project.star_count)
|
||||
%span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
|
||||
= visibility_level_icon(project.visibility_level, fw: true)
|
||||
.prepend-top-5
|
||||
.prepend-top-0
|
||||
updated #{updated_tooltip}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
.file-actions.hidden-xs
|
||||
.btn-group{ role: "group" }<
|
||||
= copy_blob_content_button(@snippet)
|
||||
= copy_blob_source_button(@snippet)
|
||||
= open_raw_file_button(raw_path)
|
||||
|
||||
- if defined?(download_path) && download_path
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Resolve "Add more tests for spec/controllers/projects/builds_controller_spec.rb"
|
||||
merge_request: 10244
|
||||
author: dosuken123
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Improves test settings for chat notification services for empty projects
|
||||
merge_request: 10886
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed milestone sidebar showing incorrect number of MRs when collapsed
|
||||
merge_request: 10933
|
||||
author:
|
5
changelogs/unreleased/dm-blob-viewers.yml
Normal file
5
changelogs/unreleased/dm-blob-viewers.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Source/Rendered switch to blobs for SVG, Markdown, Asciidoc and other text
|
||||
files that can be rendered
|
||||
merge_request:
|
||||
author:
|
4
changelogs/unreleased/dm-fix-ghost-user-validation.yml
Normal file
4
changelogs/unreleased/dm-fix-ghost-user-validation.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Skip validation when creating internal (ghost, service desk) users
|
||||
merge_request:
|
||||
author:
|
4
changelogs/unreleased/fix_build_header_line_height.yml
Normal file
4
changelogs/unreleased/fix_build_header_line_height.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Change line-height on build-header so elements don't overlap
|
||||
merge_request:
|
||||
author: Dino Maric
|
4
changelogs/unreleased/make_markdown_tables_thinner.yml
Normal file
4
changelogs/unreleased/make_markdown_tables_thinner.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Make markdown tables thinner
|
||||
merge_request: 10909
|
||||
author: blackst0ne
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed alignment of CI icon in issues related branches
|
||||
merge_request:
|
||||
author:
|
|
@ -173,7 +173,7 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
post :retry
|
||||
post :play
|
||||
post :erase
|
||||
get :trace
|
||||
get :trace, defaults: { format: 'json' }
|
||||
get :raw
|
||||
end
|
||||
|
||||
|
|
12
db/migrate/20170421102337_remove_nil_type_services.rb
Normal file
12
db/migrate/20170421102337_remove_nil_type_services.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class RemoveNilTypeServices < ActiveRecord::Migration
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
execute <<-SQL
|
||||
DELETE FROM services WHERE type IS NULL OR type = '';
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
end
|
||||
end
|
|
@ -1,14 +1,14 @@
|
|||
# Chat Commands
|
||||
|
||||
Chat commands allow user to perform common operations on GitLab right from there chat client.
|
||||
Right now both Mattermost and Slack are supported.
|
||||
Chat commands in Mattermost and Slack (also called Slack slash commands) allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it.
|
||||
|
||||
## Available commands
|
||||
Commands are scoped to a project, with a trigger term that is specified during configuration. (We suggest you use the project name as the trigger term for simplicty and clarity.) Taking the trigger term as `project-name`, the commands are:
|
||||
|
||||
The trigger is configurable, but for the sake of this example, we'll use `/trigger`
|
||||
|
||||
* `/trigger help` - Displays all available commands for this user
|
||||
* `/trigger issue new <title> <shift+return> <description>` - creates a new issue on the project
|
||||
* `/trigger issue show <id>` - Shows the issue with the given ID, if you've got access
|
||||
* `/trigger issue search <query>` - Shows a maximum of 5 items matching the query
|
||||
* `/trigger deploy <from> to <to>` - Deploy from an environment to another
|
||||
| Command | Effect |
|
||||
| ------- | ------ |
|
||||
| `/project-name help` | Shows all available chat commands |
|
||||
| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
|
||||
| `/project-name issue show <id>` | Shows the issue with id `<id>` |
|
||||
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
|
||||
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
|
|
@ -49,8 +49,8 @@ Click on the service links to see further configuration instructions and details
|
|||
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
|
||||
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
|
||||
| Pipelines emails | Email the pipeline status to a list of recipients |
|
||||
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
|
||||
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
|
||||
| [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications |
|
||||
| [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab |
|
||||
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
|
||||
| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
|
||||
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
|
||||
|
|
|
@ -1,51 +1,26 @@
|
|||
# Slack Notifications Service
|
||||
|
||||
## On Slack
|
||||
The Slack Notifications Service allows your GitLab project to send events (e.g. issue created) to your existing Slack team as notifications. This requires configurations in both Slack and GitLab.
|
||||
|
||||
To enable Slack integration you must create an incoming webhook integration on
|
||||
Slack:
|
||||
> Note: You can also use Slack slash commands to control GitLab inside Slack. This is the separately configured [Slack slash commands](slack_slash_commands.md).
|
||||
|
||||
1. [Sign in to Slack](https://slack.com/signin)
|
||||
1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
|
||||
1. Choose the channel name you want to send notifications to.
|
||||
1. Click **Add Incoming WebHooks Integration**
|
||||
1. Copy the **Webhook URL**, we'll need this later for GitLab.
|
||||
## Slack Configuration
|
||||
|
||||
## On GitLab
|
||||
1. Sign in to your Slack team and [start a new Incoming WebHooks configuration](https://my.slack.com/services/new/incoming-webhook/).
|
||||
1. Select the Slack channel where notifications will be sent to by default. Click the **Add Incoming WebHooks integration** button to add the configuration.
|
||||
1. Copy the **Webhook URL**, which we'll use later in the GitLab configuration.
|
||||
|
||||
After you set up Slack, it's time to set up GitLab.
|
||||
## GitLab Configuration
|
||||
|
||||
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
|
||||
and select the **Slack notifications** service to configure it.
|
||||
There, you will see a checkbox with the following events that can be triggered:
|
||||
1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings, i.e. **Project > Settings > Integrations**.
|
||||
1. Select the **Slack notifications** project service to configure it.
|
||||
1. Check the **Active** checkbox to turn on the service.
|
||||
1. Check the checkboxes corresponding to the GitLab events you want to send to Slack as a notification.
|
||||
1. For each event, optionally enter the Slack channel where you want to send the event. (Do _not_ include the `#` symbol.) If left empty, the event will be sent to the default channel that you configured in the Slack Configuration step.
|
||||
1. Paste the **Webhook URL** that you copied from the Slack Configuration step.
|
||||
1. Optionally customize the Slack bot username that will be sending the notifications.
|
||||
1. Configure the remaining options and click `Save changes`.
|
||||
|
||||
- Push
|
||||
- Issue
|
||||
- Confidential issue
|
||||
- Merge request
|
||||
- Note
|
||||
- Tag push
|
||||
- Pipeline
|
||||
- Wiki page
|
||||
Your Slack team will now start receiving GitLab event notifications as configured.
|
||||
|
||||
Below each of these event checkboxes, you have an input field to enter
|
||||
which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
|
||||
|
||||
At the end, fill in your Slack details:
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
|
||||
| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
|
||||
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
|
||||
|
||||
After you are all done, click **Save changes** for the changes to take effect.
|
||||
|
||||
>**Note:**
|
||||
You can set "branch,pushed,Compare changes" as highlight words on your Slack
|
||||
profile settings, so that you can be aware of new commits when somebody pushes
|
||||
them.
|
||||
|
||||
![Slack configuration](img/slack_configuration.png)
|
||||
|
||||
[slackhook]: https://my.slack.com/services/new/incoming-webhook
|
||||
![Slack configuration](img/slack_configuration.png)
|
|
@ -2,23 +2,22 @@
|
|||
|
||||
> Introduced in GitLab 8.15
|
||||
|
||||
Slack commands give users an extra interface to perform common operations
|
||||
from the chat environment. This allows one to, for example, create an issue as
|
||||
soon as the idea was discussed in chat.
|
||||
For all available commands try the help subcommand, for example: `/gitlab help`,
|
||||
all review the [full list of commands](../../../integration/chat_commands.md).
|
||||
Slack slash commands (also known as chat commmands) allow you to control GitLab and view content right inside Slack, without having to leave it. This requires configurations in both Slack and GitLab.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in
|
||||
Slack should be created beforehand, GitLab cannot create it for you.
|
||||
> Note: GitLab can also send events (e.g. issue created) to Slack as notifications. This is the separately configured [Slack Notifications Service](slack.md).
|
||||
|
||||
## Configuration
|
||||
|
||||
Go to your project's [Integrations page](project_services.md#accessing-the-project-services)
|
||||
and select the **Slack slash commands** service to configure it.
|
||||
1. Slack slash commands are scoped to a project. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings, i.e. **Project > Settings > Integrations**.
|
||||
1. Select the **Slack slash commands** project service to configure it. This page contains required information to complete the configuration in Slack. Leave this browser tab open.
|
||||
1. Open a new browser tab and sign in to your Slack team. [Start a new Slash Commands integration](https://my.slack.com/services/new/slash-commands).
|
||||
1. Enter a trigger term. We suggest you use the project name. Click **Add Slash Command Integration**.
|
||||
1. Complete the rest of the fields in the Slack configuration page using information from the GitLab browser tab. In particular, the URL needs to be copied and pasted. Click **Save Integration** to complete the configuration in Slack.
|
||||
1. While still on the Slack configuration page, copy the **token**. Go back to the GitLab browser tab and paste in the **token**.
|
||||
1. Check the **Active** checkbox and click **Save changes** to complete the configuration in GitLab.
|
||||
|
||||
![Slack setup instructions](img/slack_setup.png)
|
||||
|
||||
Once you've followed the instructions, mark the service as active and insert the token
|
||||
you've received from Slack. After saving the service you are good to go!
|
||||
## Usage
|
||||
|
||||
You can now use the [Slack slash commands](../../../integration/chat_commands.md).
|
|
@ -10,7 +10,8 @@ Feature: Project Source Browse Files
|
|||
Scenario: I browse files for specific ref
|
||||
Given I visit project source page for "6d39438"
|
||||
Then I should see files from repository for "6d39438"
|
||||
|
||||
|
||||
@javascript
|
||||
Scenario: I browse file content
|
||||
Given I click on ".gitignore" file in repo
|
||||
Then I should see its content
|
||||
|
|
|
@ -6,11 +6,13 @@ Feature: Project Source Markdown Render
|
|||
|
||||
# Tree README
|
||||
|
||||
@javascript
|
||||
Scenario: Tree view should have correct links in README
|
||||
Given I go directory which contains README file
|
||||
And I click on a relative link in README
|
||||
Then I should see the correct markdown
|
||||
|
||||
@javascript
|
||||
Scenario: I browse files from markdown branch
|
||||
Then I should see files from repository in markdown
|
||||
And I should see rendered README which contains correct links
|
||||
|
@ -29,36 +31,42 @@ Feature: Project Source Markdown Render
|
|||
And I click on GitLab API doc directory in README
|
||||
Then I should see correct doc/api directory rendered
|
||||
|
||||
@javascript
|
||||
Scenario: I view README in markdown branch to see reference links to file
|
||||
Then I should see files from repository in markdown
|
||||
And I should see rendered README which contains correct links
|
||||
And I click on Maintenance in README
|
||||
Then I should see correct maintenance file rendered
|
||||
|
||||
@javascript
|
||||
Scenario: README headers should have header links
|
||||
Then I should see rendered README which contains correct links
|
||||
And Header "Application details" should have correct id and link
|
||||
|
||||
# Blob
|
||||
|
||||
@javascript
|
||||
Scenario: I navigate to doc directory to view documentation in markdown
|
||||
And I navigate to the doc/api/README
|
||||
And I see correct file rendered
|
||||
And I click on users in doc/api/README
|
||||
Then I should see the correct document file
|
||||
|
||||
@javascript
|
||||
Scenario: I navigate to doc directory to view user doc in markdown
|
||||
And I navigate to the doc/api/README
|
||||
And I see correct file rendered
|
||||
And I click on raketasks in doc/api/README
|
||||
Then I should see correct directory rendered
|
||||
|
||||
@javascript
|
||||
Scenario: I navigate to doc directory to view user doc in markdown
|
||||
And I navigate to the doc/api/README
|
||||
And Header "GitLab API" should have correct id and link
|
||||
|
||||
# Markdown branch
|
||||
|
||||
@javascript
|
||||
Scenario: I browse files from markdown branch
|
||||
When I visit markdown branch
|
||||
Then I should see files from repository in markdown branch
|
||||
|
@ -73,6 +81,7 @@ Feature: Project Source Markdown Render
|
|||
And I click on Rake tasks in README
|
||||
Then I should see correct directory rendered for markdown branch
|
||||
|
||||
@javascript
|
||||
Scenario: I navigate to doc directory to view documentation in markdown branch
|
||||
When I visit markdown branch
|
||||
And I navigate to the doc/api/README
|
||||
|
@ -80,6 +89,7 @@ Feature: Project Source Markdown Render
|
|||
And I click on users in doc/api/README
|
||||
Then I should see the users document file in markdown branch
|
||||
|
||||
@javascript
|
||||
Scenario: I navigate to doc directory to view user doc in markdown branch
|
||||
When I visit markdown branch
|
||||
And I navigate to the doc/api/README
|
||||
|
@ -87,6 +97,7 @@ Feature: Project Source Markdown Render
|
|||
And I click on raketasks in doc/api/README
|
||||
Then I should see correct directory rendered for markdown branch
|
||||
|
||||
@javascript
|
||||
Scenario: Tree markdown links view empty urls should have correct urls
|
||||
When I visit markdown branch
|
||||
Then The link with text "empty" should have url "tree/markdown"
|
||||
|
@ -99,6 +110,7 @@ Feature: Project Source Markdown Render
|
|||
|
||||
# "ID" means "#id" on the tests below, because we are unable to escape the hash sign.
|
||||
# which Spinach interprets as the start of a comment.
|
||||
@javascript
|
||||
Scenario: All markdown links with ids should have correct urls
|
||||
When I visit markdown branch
|
||||
Then The link with text "ID" should have url "tree/markdownID"
|
||||
|
|
|
@ -4,6 +4,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
|
|||
include SharedProject
|
||||
include SharedPaths
|
||||
include RepoHelpers
|
||||
include WaitForAjax
|
||||
|
||||
step "I don't have write access" do
|
||||
@project = create(:project, :repository, name: "Other Project", path: "other-project")
|
||||
|
@ -36,10 +37,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I should see its content' do
|
||||
wait_for_ajax
|
||||
expect(page).to have_content old_gitignore_content
|
||||
end
|
||||
|
||||
step 'I should see its new content' do
|
||||
wait_for_ajax
|
||||
expect(page).to have_content new_gitignore_content
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
include SharedAuthentication
|
||||
include SharedPaths
|
||||
include SharedMarkdown
|
||||
include WaitForAjax
|
||||
|
||||
step 'I own project "Delta"' do
|
||||
@project = ::Project.find_by(name: "Delta")
|
||||
|
@ -34,6 +35,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
|
||||
step 'I should see correct document rendered' do
|
||||
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
|
||||
wait_for_ajax
|
||||
expect(page).to have_content "All API requests require authentication"
|
||||
end
|
||||
|
||||
|
@ -63,6 +65,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
|
||||
step 'I should see correct maintenance file rendered' do
|
||||
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md")
|
||||
wait_for_ajax
|
||||
expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production"
|
||||
end
|
||||
|
||||
|
@ -94,6 +97,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
|
||||
step 'I see correct file rendered' do
|
||||
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
|
||||
wait_for_ajax
|
||||
expect(page).to have_content "Contents"
|
||||
expect(page).to have_link "Users"
|
||||
expect(page).to have_link "Rake tasks"
|
||||
|
@ -138,6 +142,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
|
||||
step 'I see correct file rendered in markdown branch' do
|
||||
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
|
||||
wait_for_ajax
|
||||
expect(page).to have_content "Contents"
|
||||
expect(page).to have_link "Users"
|
||||
expect(page).to have_link "Rake tasks"
|
||||
|
@ -145,6 +150,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
|
||||
step 'I should see correct document rendered for markdown branch' do
|
||||
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
|
||||
wait_for_ajax
|
||||
expect(page).to have_content "All API requests require authentication"
|
||||
end
|
||||
|
||||
|
@ -162,6 +168,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
# Expected link contents
|
||||
|
||||
step 'The link with text "empty" should have url "tree/markdown"' do
|
||||
wait_for_ajax
|
||||
find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown")
|
||||
end
|
||||
|
||||
|
@ -197,6 +204,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do
|
||||
wait_for_ajax
|
||||
find('a', text: /^#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id'
|
||||
end
|
||||
|
||||
|
@ -291,10 +299,12 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
|
|||
|
||||
step 'I should see the correct markdown' do
|
||||
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md")
|
||||
wait_for_ajax
|
||||
expect(page).to have_content "List users"
|
||||
end
|
||||
|
||||
step 'Header "Application details" should have correct id and link' do
|
||||
wait_for_ajax
|
||||
header_should_have_correct_id_and_link(2, 'Application details', 'application-details')
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ module SharedMarkdown
|
|||
|
||||
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
|
||||
node = find("#{parent} h#{level} a#user-content-#{id}")
|
||||
expect(node[:href]).to eq "##{id}"
|
||||
expect(node[:href]).to end_with "##{id}"
|
||||
|
||||
# Work around a weird Capybara behavior where calling `parent` on a node
|
||||
# returns the whole document, not the node's actual parent element
|
||||
|
|
|
@ -41,7 +41,7 @@ module Gitlab
|
|||
type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
|
||||
|
||||
# Hash to be passed as post_receive_data
|
||||
data = {
|
||||
{
|
||||
object_kind: type,
|
||||
event_name: type,
|
||||
before: oldrev,
|
||||
|
@ -61,16 +61,15 @@ module Gitlab
|
|||
repository: project.hook_attrs.slice(:name, :url, :description, :homepage,
|
||||
:git_http_url, :git_ssh_url, :visibility_level)
|
||||
}
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
# This method provide a sample data generated with
|
||||
# existing project and commits to test webhooks
|
||||
def build_sample(project, user)
|
||||
commits = project.repository.commits(project.default_branch, limit: 3)
|
||||
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
|
||||
build(project, user, commits.last.id, commits.first.id, ref, commits)
|
||||
commits = project.repository.commits(project.default_branch.to_s, limit: 3) rescue []
|
||||
|
||||
build(project, user, commits.last&.id, commits.first&.id, ref, commits)
|
||||
end
|
||||
|
||||
def checkout_sha(repository, newrev, ref)
|
||||
|
|
|
@ -109,10 +109,6 @@ module Gitlab
|
|||
@binary.nil? ? super : @binary == true
|
||||
end
|
||||
|
||||
def empty?
|
||||
!data || data == ''
|
||||
end
|
||||
|
||||
def data
|
||||
encode! @data
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ sed -i 's/localhost/redis/g' config/resque.yml
|
|||
cp config/gitlab.yml.example config/gitlab.yml
|
||||
|
||||
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
|
||||
retry bundle install --clean $BUNDLE_INSTALL_FLAGS
|
||||
retry bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
|
||||
fi
|
||||
|
||||
# Only install knapsack after bundle install! Otherwise oddly some native
|
||||
|
|
|
@ -1,14 +1,69 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::BuildsController do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
include ApiHelpers
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe 'GET index' do
|
||||
context 'when scope is pending' do
|
||||
before do
|
||||
create(:ci_build, :pending, pipeline: pipeline)
|
||||
|
||||
get_index(scope: 'pending')
|
||||
end
|
||||
|
||||
it 'has only pending builds' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(assigns(:builds).first.status).to eq('pending')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when scope is running' do
|
||||
before do
|
||||
create(:ci_build, :running, pipeline: pipeline)
|
||||
|
||||
get_index(scope: 'running')
|
||||
end
|
||||
|
||||
it 'has only running builds' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(assigns(:builds).first.status).to eq('running')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when scope is finished' do
|
||||
before do
|
||||
create(:ci_build, :success, pipeline: pipeline)
|
||||
|
||||
get_index(scope: 'finished')
|
||||
end
|
||||
|
||||
it 'has only finished builds' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(assigns(:builds).first.status).to eq('success')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when page is specified' do
|
||||
let(:last_page) { project.builds.page.total_pages }
|
||||
|
||||
context 'when page number is eligible' do
|
||||
before do
|
||||
create_list(:ci_build, 2, pipeline: pipeline)
|
||||
|
||||
get_index(page: last_page.to_param)
|
||||
end
|
||||
|
||||
it 'redirects to the page' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(assigns(:builds).current_page).to eq(last_page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'number of queries' do
|
||||
before do
|
||||
Ci::Build::AVAILABLE_STATUSES.each do |status|
|
||||
|
@ -23,13 +78,8 @@ describe Projects::BuildsController do
|
|||
RequestStore.clear!
|
||||
end
|
||||
|
||||
def render
|
||||
get :index, namespace_id: project.namespace,
|
||||
project_id: project
|
||||
end
|
||||
|
||||
it "verifies number of queries" do
|
||||
recorded = ActiveRecord::QueryRecorder.new { render }
|
||||
recorded = ActiveRecord::QueryRecorder.new { get_index }
|
||||
expect(recorded.count).to be_within(5).of(8)
|
||||
end
|
||||
|
||||
|
@ -39,10 +89,83 @@ describe Projects::BuildsController do
|
|||
pipeline: pipeline, name: name, status: status)
|
||||
end
|
||||
end
|
||||
|
||||
def get_index(**extra_params)
|
||||
params = {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project
|
||||
}
|
||||
|
||||
get :index, params.merge(extra_params)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET show' do
|
||||
context 'when build exists' do
|
||||
let!(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
get_show(id: build.id)
|
||||
end
|
||||
|
||||
it 'has a build' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(assigns(:build).id).to eq(build.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build does not exist' do
|
||||
before do
|
||||
get_show(id: 1234)
|
||||
end
|
||||
|
||||
it 'renders not_found' do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
def get_show(**extra_params)
|
||||
params = {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project
|
||||
}
|
||||
|
||||
get :show, params.merge(extra_params)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET trace.json' do
|
||||
before do
|
||||
get_trace
|
||||
end
|
||||
|
||||
context 'when build has a trace' do
|
||||
let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
|
||||
|
||||
it 'returns a trace' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response['html']).to eq('BUILD TRACE')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build has no traces' do
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
it 'returns no traces' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response['html']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_trace
|
||||
get :trace, namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: build.id,
|
||||
format: :json
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET status.json' do
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
let(:status) { build.detailed_status(double('user')) }
|
||||
|
||||
|
@ -71,6 +194,7 @@ describe Projects::BuildsController do
|
|||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
|
||||
get_trace
|
||||
end
|
||||
|
||||
|
@ -84,6 +208,7 @@ describe Projects::BuildsController do
|
|||
context 'when user is logged in as non member' do
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
get_trace
|
||||
end
|
||||
|
||||
|
@ -101,4 +226,221 @@ describe Projects::BuildsController do
|
|||
format: :json
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST retry' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
|
||||
post_retry
|
||||
end
|
||||
|
||||
context 'when build is retryable' do
|
||||
let(:build) { create(:ci_build, :retryable, pipeline: pipeline) }
|
||||
|
||||
it 'redirects to the retried build page' do
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response).to redirect_to(namespace_project_build_path(id: Ci::Build.last.id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build is not retryable' do
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
it 'renders unprocessable_entity' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
def post_retry
|
||||
post :retry, namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: build.id
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST play' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
|
||||
post_play
|
||||
end
|
||||
|
||||
context 'when build is playable' do
|
||||
let(:build) { create(:ci_build, :playable, pipeline: pipeline) }
|
||||
|
||||
it 'redirects to the played build page' do
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response).to redirect_to(namespace_project_build_path(id: build.id))
|
||||
end
|
||||
|
||||
it 'transits to pending' do
|
||||
expect(build.reload).to be_pending
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build is not playable' do
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
it 'renders unprocessable_entity' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
def post_play
|
||||
post :play, namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: build.id
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST cancel' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
|
||||
post_cancel
|
||||
end
|
||||
|
||||
context 'when build is cancelable' do
|
||||
let(:build) { create(:ci_build, :cancelable, pipeline: pipeline) }
|
||||
|
||||
it 'redirects to the canceled build page' do
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response).to redirect_to(namespace_project_build_path(id: build.id))
|
||||
end
|
||||
|
||||
it 'transits to canceled' do
|
||||
expect(build.reload).to be_canceled
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build is not cancelable' do
|
||||
let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
|
||||
|
||||
it 'returns unprocessable_entity' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
def post_cancel
|
||||
post :cancel, namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: build.id
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST cancel_all' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when builds are cancelable' do
|
||||
before do
|
||||
create_list(:ci_build, 2, :cancelable, pipeline: pipeline)
|
||||
|
||||
post_cancel_all
|
||||
end
|
||||
|
||||
it 'redirects to a index page' do
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response).to redirect_to(namespace_project_builds_path)
|
||||
end
|
||||
|
||||
it 'transits to canceled' do
|
||||
expect(Ci::Build.all).to all(be_canceled)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when builds are not cancelable' do
|
||||
before do
|
||||
create_list(:ci_build, 2, :canceled, pipeline: pipeline)
|
||||
|
||||
post_cancel_all
|
||||
end
|
||||
|
||||
it 'redirects to a index page' do
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response).to redirect_to(namespace_project_builds_path)
|
||||
end
|
||||
end
|
||||
|
||||
def post_cancel_all
|
||||
post :cancel_all, namespace_id: project.namespace,
|
||||
project_id: project
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST erase' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
|
||||
post_erase
|
||||
end
|
||||
|
||||
context 'when build is erasable' do
|
||||
let(:build) { create(:ci_build, :erasable, :trace, pipeline: pipeline) }
|
||||
|
||||
it 'redirects to the erased build page' do
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response).to redirect_to(namespace_project_build_path(id: build.id))
|
||||
end
|
||||
|
||||
it 'erases artifacts' do
|
||||
expect(build.artifacts_file.exists?).to be_falsey
|
||||
expect(build.artifacts_metadata.exists?).to be_falsey
|
||||
end
|
||||
|
||||
it 'erases trace' do
|
||||
expect(build.trace.exist?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build is not erasable' do
|
||||
let(:build) { create(:ci_build, :erased, pipeline: pipeline) }
|
||||
|
||||
it 'returns unprocessable_entity' do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
def post_erase
|
||||
post :erase, namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: build.id
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET raw' do
|
||||
before do
|
||||
get_raw
|
||||
end
|
||||
|
||||
context 'when build has a trace file' do
|
||||
let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
|
||||
|
||||
it 'send a trace file' do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.content_type).to eq 'text/plain; charset=utf-8'
|
||||
expect(response.body).to eq 'BUILD TRACE'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build does not have a trace file' do
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
it 'returns not_found' do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
def get_raw
|
||||
post :raw, namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: build.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ describe Projects::ServicesController do
|
|||
before do
|
||||
sign_in(user)
|
||||
project.team << [user, :master]
|
||||
|
||||
controller.instance_variable_set(:@project, project)
|
||||
controller.instance_variable_set(:@service, service)
|
||||
end
|
||||
|
@ -18,20 +19,60 @@ describe Projects::ServicesController do
|
|||
end
|
||||
|
||||
describe "#test" do
|
||||
context 'success' do
|
||||
it "redirects and show success message" do
|
||||
expect(service).to receive(:test).and_return({ success: true, result: 'done' })
|
||||
context 'when can_test? returns false' do
|
||||
it 'renders 404' do
|
||||
allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
|
||||
|
||||
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
|
||||
expect(response.status).to redirect_to('/')
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'success' do
|
||||
context 'with empty project' do
|
||||
let(:project) { create(:empty_project) }
|
||||
|
||||
context 'with chat notification service' do
|
||||
let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
|
||||
|
||||
it 'redirects and show success message' do
|
||||
allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
|
||||
|
||||
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:notice]).to eq('We sent a request to the provided URL')
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects and show success message' do
|
||||
expect(service).to receive(:test).and_return(success: true, result: 'done')
|
||||
|
||||
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:notice]).to eq('We sent a request to the provided URL')
|
||||
end
|
||||
end
|
||||
|
||||
it "redirects and show success message" do
|
||||
expect(service).to receive(:test).and_return(success: true, result: 'done')
|
||||
|
||||
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:notice]).to eq('We sent a request to the provided URL')
|
||||
end
|
||||
end
|
||||
|
||||
context 'failure' do
|
||||
it "redirects and show failure message" do
|
||||
expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
|
||||
expect(service).to receive(:test).and_return(success: false, result: 'Bad test')
|
||||
|
||||
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
|
||||
expect(response.status).to redirect_to('/')
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -79,6 +79,19 @@ FactoryGirl.define do
|
|||
manual
|
||||
end
|
||||
|
||||
trait :retryable do
|
||||
success
|
||||
end
|
||||
|
||||
trait :cancelable do
|
||||
pending
|
||||
end
|
||||
|
||||
trait :erasable do
|
||||
success
|
||||
artifacts
|
||||
end
|
||||
|
||||
trait :tags do
|
||||
tag_list [:docker, :ruby]
|
||||
end
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
FactoryGirl.define do
|
||||
factory :service do
|
||||
project factory: :empty_project
|
||||
type 'Service'
|
||||
end
|
||||
|
||||
factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
|
||||
project factory: :empty_project
|
||||
type 'CustomIssueTrackerService'
|
||||
category 'issue_tracker'
|
||||
active true
|
||||
properties(
|
||||
project_url: 'https://project.url.com',
|
||||
issues_url: 'https://issues.url.com',
|
||||
new_issue_url: 'https://newissue.url.com'
|
||||
)
|
||||
end
|
||||
|
||||
factory :kubernetes_service do
|
||||
|
|
|
@ -479,6 +479,7 @@ describe 'Copy as GFM', feature: true, js: true do
|
|||
context 'from a blob' do
|
||||
before do
|
||||
visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
context 'selecting one word of text' do
|
||||
|
@ -520,6 +521,7 @@ describe 'Copy as GFM', feature: true, js: true do
|
|||
context 'from a GFM code block' do
|
||||
before do
|
||||
visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
context 'selecting one word of text' do
|
||||
|
|
|
@ -1,21 +1,313 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'File blob', feature: true do
|
||||
feature 'File blob', :js, feature: true do
|
||||
include TreeHelper
|
||||
include WaitForAjax
|
||||
|
||||
let(:project) { create(:project, :public, :test_repo) }
|
||||
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
|
||||
let(:branch) { 'master' }
|
||||
let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] }
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
context 'anonymous' do
|
||||
context 'from blob file path' do
|
||||
def visit_blob(path, fragment = nil)
|
||||
visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
|
||||
end
|
||||
|
||||
context 'Ruby file' do
|
||||
before do
|
||||
visit_blob('files/ruby/popen.rb')
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# shows highlighted Ruby code
|
||||
expect(page).to have_content("require 'fileutils'")
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Markdown file' do
|
||||
context 'visiting directly' do
|
||||
before do
|
||||
visit namespace_project_blob_path(project.namespace, project, tree_join(branch, file_path))
|
||||
visit_blob('files/markdown/ruby-style-guide.md')
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'updates content' do
|
||||
expect(page).to have_link 'Edit'
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows rendered Markdown
|
||||
expect(page).to have_link("PEP-8")
|
||||
|
||||
# shows a viewer switcher
|
||||
expect(page).to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows a disabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the simple viewer' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the rich viewer again' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# highlights the line in question
|
||||
expect(page).to have_selector('#LC1.hll')
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Markdown file (stored in LFS)' do
|
||||
before do
|
||||
project.add_master(project.creator)
|
||||
|
||||
Files::CreateService.new(
|
||||
project,
|
||||
project.creator,
|
||||
start_branch: 'master',
|
||||
branch_name: 'master',
|
||||
commit_message: "Add Markdown in LFS",
|
||||
file_path: 'files/lfs/file.md',
|
||||
file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data
|
||||
).execute
|
||||
end
|
||||
|
||||
context 'when LFS is enabled on the project' do
|
||||
before do
|
||||
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
|
||||
project.update_attribute(:lfs_enabled, true)
|
||||
|
||||
visit_blob('files/lfs/file.md')
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays an error' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows an error message
|
||||
expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can view the source or download it instead.')
|
||||
|
||||
# shows a viewer switcher
|
||||
expect(page).to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# does not show a copy button
|
||||
expect(page).not_to have_selector('.js-copy-blob-source-btn')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the simple viewer' do
|
||||
before do
|
||||
find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays an error' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# shows an error message
|
||||
expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.')
|
||||
|
||||
# does not show a copy button
|
||||
expect(page).not_to have_selector('.js-copy-blob-source-btn')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
aggregate_failures do
|
||||
# shows text
|
||||
expect(page).to have_content('size 1575078')
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'PDF file' do
|
||||
before do
|
||||
project.add_master(project.creator)
|
||||
|
||||
Files::CreateService.new(
|
||||
project,
|
||||
project.creator,
|
||||
start_branch: 'master',
|
||||
branch_name: 'master',
|
||||
commit_message: "Add PDF",
|
||||
file_path: 'files/test.pdf',
|
||||
file_content: File.read(Rails.root.join('spec/javascripts/blob/pdf/test.pdf'))
|
||||
).execute
|
||||
|
||||
visit_blob('files/test.pdf')
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# shows rendered PDF
|
||||
expect(page).to have_selector('.js-pdf-viewer')
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# does not show a copy button
|
||||
expect(page).not_to have_selector('.js-copy-blob-source-btn')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'ISO file (stored in LFS)' do
|
||||
context 'when LFS is enabled on the project' do
|
||||
before do
|
||||
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
|
||||
project.update_attribute(:lfs_enabled, true)
|
||||
|
||||
visit_blob('files/lfs/lfs_object.iso')
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# shows a download link
|
||||
expect(page).to have_link('Download (1.5 MB)')
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# does not show a copy button
|
||||
expect(page).not_to have_selector('.js-copy-blob-source-btn')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
aggregate_failures do
|
||||
# shows text
|
||||
expect(page).to have_content('size 1575078')
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'ZIP file' do
|
||||
before do
|
||||
visit_blob('Gemfile.zip')
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# shows a download link
|
||||
expect(page).to have_link('Download (2.11 KB)')
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# does not show a copy button
|
||||
expect(page).not_to have_selector('.js-copy-blob-source-btn')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'user browses project', feature: true do
|
||||
feature 'user browses project', feature: true, js: true do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
|
@ -13,7 +13,7 @@ feature 'user browses project', feature: true do
|
|||
scenario "can see blame of '.gitignore'" do
|
||||
click_link ".gitignore"
|
||||
click_link 'Blame'
|
||||
|
||||
|
||||
expect(page).to have_content "*.rb"
|
||||
expect(page).to have_content "Dmitriy Zaporozhets"
|
||||
expect(page).to have_content "Initial commit"
|
||||
|
@ -24,6 +24,7 @@ feature 'user browses project', feature: true do
|
|||
click_link 'files'
|
||||
click_link 'lfs'
|
||||
click_link 'lfs_object.iso'
|
||||
wait_for_ajax
|
||||
|
||||
expect(page).not_to have_content 'Download (1.5 MB)'
|
||||
expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
|
||||
|
|
|
@ -63,4 +63,28 @@ feature 'Project milestone', :feature do
|
|||
expect(page).not_to have_content('Assign some issues to this milestone.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has an issue' do
|
||||
before do
|
||||
create(:issue, project: project, milestone: milestone)
|
||||
|
||||
visit namespace_project_milestone_path(project.namespace, project, milestone)
|
||||
end
|
||||
|
||||
describe 'the collapsed sidebar' do
|
||||
before do
|
||||
find('.milestone-sidebar .gutter-toggle').click
|
||||
end
|
||||
|
||||
it 'shows the total MR and issue counts' do
|
||||
find('.milestone-sidebar .block', match: :first)
|
||||
blocks = all('.milestone-sidebar .block')
|
||||
|
||||
aggregate_failures 'MR and issue blocks' do
|
||||
expect(blocks[3]).to have_content 1
|
||||
expect(blocks[4]).to have_content 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -399,6 +399,44 @@ describe "Internal Project Access", feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /:project_path/builds/:id/trace' do
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
|
||||
|
||||
context 'when allowed for public and internal' do
|
||||
before do
|
||||
project.update(public_builds: true)
|
||||
end
|
||||
|
||||
it { is_expected.to be_allowed_for(:admin) }
|
||||
it { is_expected.to be_allowed_for(:owner).of(project) }
|
||||
it { is_expected.to be_allowed_for(:master).of(project) }
|
||||
it { is_expected.to be_allowed_for(:developer).of(project) }
|
||||
it { is_expected.to be_allowed_for(:reporter).of(project) }
|
||||
it { is_expected.to be_allowed_for(:guest).of(project) }
|
||||
it { is_expected.to be_allowed_for(:user) }
|
||||
it { is_expected.to be_denied_for(:external) }
|
||||
it { is_expected.to be_denied_for(:visitor) }
|
||||
end
|
||||
|
||||
context 'when disallowed for public and internal' do
|
||||
before do
|
||||
project.update(public_builds: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_allowed_for(:admin) }
|
||||
it { is_expected.to be_allowed_for(:owner).of(project) }
|
||||
it { is_expected.to be_allowed_for(:master).of(project) }
|
||||
it { is_expected.to be_allowed_for(:developer).of(project) }
|
||||
it { is_expected.to be_allowed_for(:reporter).of(project) }
|
||||
it { is_expected.to be_denied_for(:guest).of(project) }
|
||||
it { is_expected.to be_denied_for(:user) }
|
||||
it { is_expected.to be_denied_for(:external) }
|
||||
it { is_expected.to be_denied_for(:visitor) }
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /:project_path/environments" do
|
||||
subject { namespace_project_environments_path(project.namespace, project) }
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue