Commit 79e2a524 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into deploy-keys-load-async

parents b5b33200 31b87b8c
...@@ -68,6 +68,13 @@ stages: ...@@ -68,6 +68,13 @@ stages:
- //@gitlab-org/gitlab-ee - //@gitlab-org/gitlab-ee
- //@gitlab/gitlab-ee - //@gitlab/gitlab-ee
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
.except-docs: &except-docs
except:
- /^docs\/.*/
.rspec-knapsack: &rspec-knapsack .rspec-knapsack: &rspec-knapsack
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
...@@ -91,11 +98,13 @@ stages: ...@@ -91,11 +98,13 @@ stages:
.rspec-knapsack-pg: &rspec-knapsack-pg .rspec-knapsack-pg: &rspec-knapsack-pg
<<: *rspec-knapsack <<: *rspec-knapsack
<<: *use-pg <<: *use-pg
<<: *except-docs
.rspec-knapsack-mysql: &rspec-knapsack-mysql .rspec-knapsack-mysql: &rspec-knapsack-mysql
<<: *rspec-knapsack <<: *rspec-knapsack
<<: *use-mysql <<: *use-mysql
<<: *only-master-and-ee-or-mysql <<: *only-master-and-ee-or-mysql
<<: *except-docs
.spinach-knapsack: &spinach-knapsack .spinach-knapsack: &spinach-knapsack
stage: test stage: test
...@@ -120,16 +129,19 @@ stages: ...@@ -120,16 +129,19 @@ stages:
.spinach-knapsack-pg: &spinach-knapsack-pg .spinach-knapsack-pg: &spinach-knapsack-pg
<<: *spinach-knapsack <<: *spinach-knapsack
<<: *use-pg <<: *use-pg
<<: *except-docs
.spinach-knapsack-mysql: &spinach-knapsack-mysql .spinach-knapsack-mysql: &spinach-knapsack-mysql
<<: *spinach-knapsack <<: *spinach-knapsack
<<: *use-mysql <<: *use-mysql
<<: *only-master-and-ee-or-mysql <<: *only-master-and-ee-or-mysql
<<: *except-docs
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
knapsack: knapsack:
<<: *knapsack-state <<: *knapsack-state
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: prepare stage: prepare
script: script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/ - mkdir -p knapsack/${CI_PROJECT_NAME}/
...@@ -156,6 +168,7 @@ update-knapsack: ...@@ -156,6 +168,7 @@ update-knapsack:
setup-test-env: setup-test-env:
<<: *use-pg <<: *use-pg
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: prepare stage: prepare
script: script:
- node --version - node --version
...@@ -243,6 +256,7 @@ spinach mysql 9 10: *spinach-knapsack-mysql ...@@ -243,6 +256,7 @@ spinach mysql 9 10: *spinach-knapsack-mysql
.exec: &exec .exec: &exec
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- bundle exec $CI_JOB_NAME - bundle exec $CI_JOB_NAME
...@@ -250,6 +264,7 @@ spinach mysql 9 10: *spinach-knapsack-mysql ...@@ -250,6 +264,7 @@ spinach mysql 9 10: *spinach-knapsack-mysql
rubocop: rubocop:
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- bundle exec "rubocop --require rubocop-rspec" - bundle exec "rubocop --require rubocop-rspec"
...@@ -266,6 +281,7 @@ rake downtime_check: ...@@ -266,6 +281,7 @@ rake downtime_check:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?$/ - /^[\d-]+-stable(-ee)?$/
- /^docs\/*/
rake ee_compat_check: rake ee_compat_check:
<<: *exec <<: *exec
...@@ -296,10 +312,12 @@ rake ee_compat_check: ...@@ -296,10 +312,12 @@ rake ee_compat_check:
rake pg db:migrate:reset: rake pg db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:migrate:reset: rake mysql db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-rollback: &db-rollback .db-rollback: &db-rollback
stage: test stage: test
...@@ -311,10 +329,12 @@ rake mysql db:migrate:reset: ...@@ -311,10 +329,12 @@ rake mysql db:migrate:reset:
rake pg db:rollback: rake pg db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:rollback: rake mysql db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-seed_fu: &db-seed_fu .db-seed_fu: &db-seed_fu
stage: test stage: test
...@@ -336,14 +356,17 @@ rake mysql db:rollback: ...@@ -336,14 +356,17 @@ rake mysql db:rollback:
rake pg db:seed_fu: rake pg db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:seed_fu: rake mysql db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-mysql <<: *use-mysql
<<: *except-docs
rake gitlab:assets:compile: rake gitlab:assets:compile:
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
dependencies: [] dependencies: []
variables: variables:
NODE_ENV: "production" NODE_ENV: "production"
...@@ -367,6 +390,7 @@ rake karma: ...@@ -367,6 +390,7 @@ rake karma:
stage: test stage: test
<<: *use-pg <<: *use-pg
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
variables: variables:
BABEL_ENV: "coverage" BABEL_ENV: "coverage"
script: script:
...@@ -403,13 +427,6 @@ docs:check:links: ...@@ -403,13 +427,6 @@ docs:check:links:
# Check the internal links # Check the internal links
- bundle exec nanoc check internal_links - bundle exec nanoc check internal_links
bundler:check:
stage: test
<<: *dedicated-runner
<<: *ruby-static-analysis
script:
- bundle check
bundler:audit: bundler:audit:
stage: test stage: test
<<: *ruby-static-analysis <<: *ruby-static-analysis
...@@ -454,6 +471,7 @@ coverage: ...@@ -454,6 +471,7 @@ coverage:
stage: post-test stage: post-test
services: [] services: []
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
...@@ -469,6 +487,7 @@ coverage: ...@@ -469,6 +487,7 @@ coverage:
lint:javascript: lint:javascript:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
before_script: [] before_script: []
script: script:
...@@ -476,6 +495,7 @@ lint:javascript: ...@@ -476,6 +495,7 @@ lint:javascript:
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: post-test stage: post-test
before_script: [] before_script: []
script: script:
......
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
filtered by the "regression" or "bug" label:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate.
------
### Summary ### Summary
(Summarize the bug encountered concisely) (Summarize the bug encountered concisely)
...@@ -56,3 +70,5 @@ logs, and code as it's very hard to read otherwise.) ...@@ -56,3 +70,5 @@ logs, and code as it's very hard to read otherwise.)
### Possible fixes ### Possible fixes
(If you can, link to the line of code that might be responsible for the problem) (If you can, link to the line of code that might be responsible for the problem)
/label ~bug
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
filtered by the "feature proposal" label:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate.
------
### Description ### Description
(Include problem, use cases, benefits, and/or goals) (Include problem, use cases, benefits, and/or goals)
...@@ -15,3 +28,5 @@ ...@@ -15,3 +28,5 @@
3. How does someone use this 3. How does someone use this
During implementation, this can then be copied and used as a starter for the documentation.) During implementation, this can then be copied and used as a starter for the documentation.)
/label ~"feature proposal"
...@@ -31,7 +31,7 @@ export default () => { ...@@ -31,7 +31,7 @@ export default () => {
}, },
}, },
template: ` 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 <div
class="text-center loading" class="text-center loading"
v-if="loading && !error"> v-if="loading && !error">
......
/* 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);
}
}
...@@ -49,6 +49,7 @@ import UserCallout from './user_callout'; ...@@ -49,6 +49,7 @@ import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki'; import ShortcutsWiki from './shortcuts_wiki';
import SettingsDeployKeys from './settings/settings_repository'; import SettingsDeployKeys from './settings/settings_repository';
import BlobViewer from './blob/viewer/index';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -300,6 +301,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -300,6 +301,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
gl.TargetBranchDropDown.bootstrap(); gl.TargetBranchDropDown.bootstrap();
break; break;
case 'projects:blob:show': case 'projects:blob:show':
new BlobViewer();
gl.TargetBranchDropDown.bootstrap(); gl.TargetBranchDropDown.bootstrap();
initBlob(); initBlob();
break; break;
......
...@@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo'); ...@@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo');
LineHighlighter.prototype._hash = ''; LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) { function LineHighlighter(hash) {
var range;
if (hash == null) { if (hash == null) {
// Initialize a LineHighlighter object // Initialize a LineHighlighter object
// //
...@@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo'); ...@@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo');
this.setHash = bind(this.setHash, this); this.setHash = bind(this.setHash, this);
this.highlightLine = bind(this.highlightLine, this); this.highlightLine = bind(this.highlightLine, this);
this.clickHandler = bind(this.clickHandler, this); this.clickHandler = bind(this.clickHandler, this);
this.highlightHash = this.highlightHash.bind(this);
this._hash = hash; this._hash = hash;
this.bindEvents(); this.bindEvents();
if (hash !== '') { this.highlightHash();
range = this.hashToRange(hash); }
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]) { if (range[0]) {
this.highlightRange(range); this.highlightRange(range);
$.scrollTo("#L" + range[0], { $.scrollTo("#L" + range[0], {
...@@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo'); ...@@ -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) { LineHighlighter.prototype.clickHandler = function(event) {
......
...@@ -120,6 +120,10 @@ ...@@ -120,6 +120,10 @@
// Ensure that image does not exceed viewport // Ensure that image does not exceed viewport
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
} }
table {
@include markdown-table;
}
} }
.toolbar-group { .toolbar-group {
......
...@@ -12,6 +12,13 @@ ...@@ -12,6 +12,13 @@
max-width: $max_width; max-width: $max_width;
} }
/*
* Mixin for markdown tables
*/
@mixin markdown-table {
width: auto;
}
/* /*
* Base mixin for lists in GitLab * Base mixin for lists in GitLab
*/ */
......
...@@ -200,6 +200,7 @@ ...@@ -200,6 +200,7 @@
.header-content { .header-content {
flex: 1; flex: 1;
line-height: 1.8;
a { a {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -101,11 +101,16 @@ ul.related-merge-requests > li { ...@@ -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 { svg {
margin-right: 4px; display: block;
position: relative;
top: 1px;
} }
} }
......
...@@ -97,6 +97,10 @@ ul.notes { ...@@ -97,6 +97,10 @@ ul.notes {
padding-left: 1.3em; padding-left: 1.3em;
} }
} }
table {
@include markdown-table;
}
} }
} }
......
...@@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list { ...@@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list {
padding: 5px 0; padding: 5px 0;
} }
} }
.wiki {
table {
@include markdown-table;
}
}
...@@ -118,6 +118,10 @@ class ApplicationController < ActionController::Base ...@@ -118,6 +118,10 @@ class ApplicationController < ActionController::Base
end end
end end
def respond_422
head :unprocessable_entity
end
def no_cache_headers def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache" response.headers["Pragma"] = "no-cache"
......
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 @@ ...@@ -2,6 +2,7 @@
class Projects::BlobController < Projects::ApplicationController class Projects::BlobController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include CreatesCommit include CreatesCommit
include RendersBlob
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path # Raised when given an invalid file path
...@@ -34,8 +35,20 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -34,8 +35,20 @@ class Projects::BlobController < Projects::ApplicationController
end end
def show def show
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } @blob.override_max_size! if params[:override_max_size] == 'true'
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
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 end
def edit def edit
...@@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController
private private
def blob def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path)) @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
if @blob if @blob
@blob @blob
......
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] 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] before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
layout 'project' layout 'project'
...@@ -60,20 +60,22 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -60,20 +60,22 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def retry def retry
return render_404 unless @build.retryable? return respond_422 unless @build.retryable?
build = Ci::Build.retry(@build, current_user) build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build) redirect_to build_path(build)
end end
def play def play
return render_404 unless @build.playable? return respond_422 unless @build.playable?
build = @build.play(current_user) build = @build.play(current_user)
redirect_to build_path(build) redirect_to build_path(build)
end end
def cancel def cancel
return respond_422 unless @build.cancelable?
@build.cancel @build.cancel
redirect_to build_path(@build) redirect_to build_path(@build)
end end
...@@ -85,9 +87,12 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -85,9 +87,12 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def erase def erase
@build.erase(erased_by: current_user) if @build.erase(erased_by: current_user)
redirect_to namespace_project_build_path(project.namespace, project, @build), redirect_to namespace_project_build_path(project.namespace, project, @build),
notice: "Build has been successfully erased!" notice: "Build has been successfully erased!"
else
respond_422
end
end end
def raw def raw
......
...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob? return if cached_blob?
if @blob.lfs_pointer? && project.lfs_enabled? if @blob.valid_lfs_pointer?
send_lfs_object send_lfs_object
else else
send_git_blob @repository, @blob send_git_blob @repository, @blob
......
if Rails.env.test?
class UnicornTestController < ActionController::Base
def pid
render plain: Process.pid.to_s
end
def kill
Process.kill(params[:signal], Process.pid)
render plain: 'Bye!'
end
end
end
...@@ -52,7 +52,7 @@ module BlobHelper ...@@ -52,7 +52,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref) 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' } 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' } 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) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
...@@ -95,7 +95,7 @@ module BlobHelper ...@@ -95,7 +95,7 @@ module BlobHelper
end end
def can_modify_blob?(blob, project = @project, ref = @ref) 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 end
def leave_edit_message def leave_edit_message
...@@ -118,28 +118,15 @@ module BlobHelper ...@@ -118,28 +118,15 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw") icon("#{file_type_icon_class('file', mode, name)} fw")
end end
def blob_text_viewable?(blob) def blob_raw_url
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw? namespace_project_raw_path(@project.namespace, @project, @id)
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
end end
# SVGs can contain malicious JavaScript; only include whitelisted # SVGs can contain malicious JavaScript; only include whitelisted
# elements and attributes. Note that this whitelist is by no means complete # elements and attributes. Note that this whitelist is by no means complete
# and may omit some elements. # and may omit some elements.
def sanitize_svg(blob) def sanitize_svg_data(data)
blob.data = Gitlab::Sanitizers::SVG.clean(blob.data) Gitlab::Sanitizers::SVG.clean(data)
blob
end end
# If we blindly set the 'real' content type when serving a Git blob we # If we blindly set the 'real' content type when serving a Git blob we
...@@ -221,13 +208,42 @@ module BlobHelper ...@@ -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') clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
end end
def copy_blob_content_button(blob) def copy_blob_source_button(blob)
return if markup?(blob.name) clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
end end
def open_raw_file_button(path) 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' } 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 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 end
...@@ -3,8 +3,40 @@ class Blob < SimpleDelegator ...@@ -3,8 +3,40 @@ class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute 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 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_TEXT_HIGHLIGHT_SIZE = 1.megabyte
MAXIMUM_SVG_SIZE = 2.megabytes
# 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::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 # Wrap a Gitlab::Git::Blob object, or return nil when given nil
# #
...@@ -16,10 +48,16 @@ class Blob < SimpleDelegator ...@@ -16,10 +48,16 @@ class Blob < SimpleDelegator
# #
# blob = Blob.decorate(nil) # blob = Blob.decorate(nil)
# puts "truthy" if blob # No output # puts "truthy" if blob # No output
def self.decorate(blob) def self.decorate(blob, project = nil)
return if blob.nil? return if blob.nil?
new(blob) new(blob, project)
end
def initialize(blob, project = nil)
@project = project
super(blob)
end end
# Returns the data of the blob. # Returns the data of the blob.
...@@ -35,82 +73,107 @@ class Blob < SimpleDelegator ...@@ -35,82 +73,107 @@ class Blob < SimpleDelegator
end end
def no_highlighting? def no_highlighting?
size && size > 1.megabyte size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end end
def only_display_raw? def too_large?
size && truncated? size && truncated?
end 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 def extension
extname.downcase.delete('.') @extension ||= extname.downcase.delete('.')
end end
def svg? def video?
text? && language && language.name == 'SVG' UploaderHelper::VIDEO_EXT.include?(extension)
end end
def pdf? def readable_text?
extension == 'pdf' text? && !valid_lfs_pointer? && !too_large?
end end
def ipython_notebook? def valid_lfs_pointer?
text? && language&.name == 'Jupyter Notebook' lfs_pointer? && project&.lfs_enabled?
end end
def sketch? def invalid_lfs_pointer?
binary? && extension == 'sketch' lfs_pointer? && !project&.lfs_enabled?
end end
def stl? def simple_viewer
extension == 'stl' @simple_viewer ||= simple_viewer_class.new(self)
end end
def markup? def rich_viewer
text? && Gitlab::MarkupHelper.markup?(name) return @rich_viewer if defined?(@rich_viewer)
@rich_viewer = rich_viewer_class&.new(self)
end end
def size_within_svg_limits? def rendered_as_text?(ignore_errors: true)
size <= MAXIMUM_SVG_SIZE simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end end
def video? def show_viewer_switcher?
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) rendered_as_text? && rich_viewer
end end
def to_partial_path(project) def override_max_size!
if lfs_pointer? simple_viewer&.override_max_size = true
if project.lfs_enabled? rich_viewer&.override_max_size = true
'download' end
else
'text' private
end
elsif image? def simple_viewer_class
'image' if empty?
elsif svg? BlobViewer::Empty
'svg' elsif raw_binary?
elsif pdf? BlobViewer::Download
'pdf' else # text
elsif ipython_notebook? BlobViewer::Text
'notebook'
elsif sketch?
'sketch'
elsif stl?
'stl'
elsif markup?
if only_display_raw?
'too_large'
else
'markup'
end
elsif text?
if only_display_raw?
'too_large'
else
'text'
end
else
'download'
end end
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 end
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
module BlobViewer
class BinarySTL < Base
include Rich
include ClientSide
self.partial_name = 'stl'
self.extensions = %w(stl)
self.binary = true
end
end
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
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
module BlobViewer
class Empty < Base
include Simple
include ServerSide
self.partial_name = 'empty'
self.binary = true
end
end
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
module BlobViewer
class Markup < Base
include Rich
include ServerSide
self.partial_name = 'markup'
self.extensions = Gitlab::MarkupHelper::EXTENSIONS
self.binary = false
end
end
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
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
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
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
module BlobViewer
module Simple
extend ActiveSupport::Concern
included do
self.type = :simple
self.switcher_icon = 'code'
self.switcher_title = 'source'
end
end
end
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
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
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
module BlobViewer
class TextSTL < BinarySTL
self.binary = false
end
end
...@@ -316,7 +316,7 @@ class Commit ...@@ -316,7 +316,7 @@ class Commit
def uri_type(path) def uri_type(path)
entry = @raw.tree.path(path) entry = @raw.tree.path(path)
if entry[:type] == :blob 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 blob.image? || blob.video? ? :raw : :blob
else else
entry[:type] entry[:type]
......
...@@ -34,6 +34,7 @@ class Label < ActiveRecord::Base ...@@ -34,6 +34,7 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) } scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) } scope :with_title, ->(title) { where(title: title) }
scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) }
def self.prioritized(project) def self.prioritized(project)
joins(:priorities) joins(:priorities)
......
...@@ -450,7 +450,7 @@ class Repository ...@@ -450,7 +450,7 @@ class Repository
def blob_at(sha, path) def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha) 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 end
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
nil nil
......
...@@ -1068,11 +1068,13 @@ class User < ActiveRecord::Base ...@@ -1068,11 +1068,13 @@ class User < ActiveRecord::Base
User.find_by_email(s) User.find_by_email(s)
end end
scope.create( user = scope.build(
username: username, username: username,
email: email, email: email,
&creation_block &creation_block
) )
user.save(validate: false)
user
ensure ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid) Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end end
......
...@@ -61,7 +61,7 @@ module Boards ...@@ -61,7 +61,7 @@ module Boards
if moving_to_list.movable? if moving_to_list.movable?
moving_from_list.label_id moving_from_list.label_id
else else
project.boards.joins(:lists).merge(List.movable).pluck(:label_id) Label.on_project_boards(project.id).pluck(:label_id)
end end
Array(label_ids).compact Array(label_ids).compact
......
...@@ -330,6 +330,28 @@ module SlashCommands ...@@ -330,6 +330,28 @@ module SlashCommands
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
end end
desc 'Move issue from one column of the board to another'
params '~"Target column"'
condition do
issuable.is_a?(Issue) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable) &&
issuable.project.boards.count == 1
end
command :board_move do |target_list_name|
label_ids = find_label_ids(target_list_name)
if label_ids.size == 1
label_id = label_ids.first
# Ensure this label corresponds to a list on the board
next unless Label.on_project_boards(issuable.project_id).where(id: label_id).exists?
@updates[:remove_label_ids] =
issuable.labels.on_project_boards(issuable.project_id).where.not(id: label_id).pluck(:id)
@updates[:add_label_ids] = [label_id]
end
end
def find_label_ids(labels_param) def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id) label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
......
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
- else - else
%hr %hr
- blob = diff_file.blob - blob = diff_file.blob
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) - if blob && blob.readable_text?
%table.code.white %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 } = render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, plain: true, email: true }
- else - else
......
...@@ -26,9 +26,4 @@ ...@@ -26,9 +26,4 @@
%article.file-holder %article.file-holder
= render "projects/blob/header", blob: blob = render "projects/blob/header", blob: blob
- if blob.empty? = render 'projects/blob/content', blob: blob
.file-content.code
.nothing-here-block
Empty file
- else
= render blob.to_partial_path(@project), blob: blob
- 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
...@@ -9,17 +9,19 @@ ...@@ -9,17 +9,19 @@
= copy_file_path_button(blob.path) = copy_file_path_button(blob.path)
%small %small
= number_to_human_size(blob_size(blob)) = number_to_human_size(blob.raw_size)
.file-actions.hidden-xs .file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }< .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)) = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
= view_on_environment_button(@commit.sha, @path, @environment) if @environment = view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
-# only show normal/blame view links for text files -# only show normal/blame view links for text files
- if blob_text_viewable?(blob) - if blob.readable_text?
- if blame - if blame
= link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id), = link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn btn-sm' class: 'btn btn-sm'
...@@ -34,7 +36,7 @@ ...@@ -34,7 +36,7 @@
tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
= edit_blob_link if blob_text_viewable?(blob) = edit_blob_link if blob.readable_text?
- if current_user - if current_user
= replace_blob_link = replace_blob_link
= delete_blob_link = delete_blob_link
......
.file-content.image_file
%img{ src: namespace_project_raw_path(@project.namespace, @project, @id), alt: blob.name }
.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.
- 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'
- blob.load_all_data!(@repository)
= render 'shared/file_highlight', blob: blob, repository: @repository
.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.
- 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
- 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 @@ ...@@ -2,6 +2,9 @@
- page_title @blob.path, @ref - page_title @blob.path, @ref
= render "projects/commits/head" = render "projects/commits/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('blob')
%div{ class: container_class } %div{ class: container_class }
= render 'projects/last_push' = render 'projects/last_push'
......
.file-content.blob_file.blob-no-preview .file-content.blob_file.blob-no-preview
.center .center
= link_to namespace_project_raw_path(@project.namespace, @project, @id) do = link_to blob_raw_url do
%h1.light %h1.light
%i.fa.fa-download = icon('download')
%h4 %h4
Download (#{number_to_human_size blob_size(blob)}) Download (#{number_to_human_size(viewer.blob.raw_size)})
.file-content.code
.nothing-here-block
Empty file
.file-content.image_file
%img{ src: blob_raw_url, alt: viewer.blob.name }
- blob = viewer.blob
.file-content.wiki
= markup(blob.name, blob.data)
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('notebook_viewer') = 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 @@ ...@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pdf_viewer') = 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 @@ ...@@ -2,6 +2,6 @@
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sketch_viewer') = 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' } .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'); = icon('spinner spin 2x', 'aria-hidden' => 'true');
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= page_specific_javascript_bundle_tag('stl_viewer') = page_specific_javascript_bundle_tag('stl_viewer')
.file-content.is-stl-loading .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') = 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 .text-center.prepend-top-default.append-bottom-default.stl-controls
.btn-group .btn-group
......
- 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 }
= render 'shared/file_highlight', blob: viewer.blob, repository: @repository
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
- return unless blob.respond_to?(:text?) - return unless blob.respond_to?(:text?)
- if diff_file.too_large? - if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is 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. .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) - if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry. .nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed? - elsif diff_file.collapsed?
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
- diff_commit = commit_for_diff(diff_file) - diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit) - blob = diff_file.blob(diff_commit)
- next unless blob - 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) - file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project, = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- unless diff_file.submodule? - unless diff_file.submodule?
.file-actions.hidden-xs .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 = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment') = icon('comment')
\ \
......
...@@ -54,5 +54,5 @@ ...@@ -54,5 +54,5 @@
= number_with_delimiter(project.star_count) = number_with_delimiter(project.star_count)
%span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } %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) = visibility_level_icon(project.visibility_level, fw: true)
.prepend-top-5 .prepend-top-0
updated #{updated_tooltip} updated #{updated_tooltip}
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.file-actions.hidden-xs .file-actions.hidden-xs
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
= copy_blob_content_button(@snippet) = copy_blob_source_button(@snippet)
= open_raw_file_button(raw_path) = open_raw_file_button(raw_path)
- if defined?(download_path) && download_path - if defined?(download_path) && download_path
......
---
title: Add board_move slash command
merge_request: 10433
author: Alex Sanford
---
title: Resolve "Add more tests for spec/controllers/projects/builds_controller_spec.rb"
merge_request: 10244
author: dosuken123
---
title: Add index on ci_runners.contacted_at
merge_request: 10876
author: blackst0ne
---
title: Add Source/Rendered switch to blobs for SVG, Markdown, Asciidoc and other text
files that can be rendered
merge_request:
author:
---
title: Skip validation when creating internal (ghost, service desk) users
merge_request:
author:
---
title: Change line-height on build-header so elements don't overlap
merge_request:
author: Dino Maric
---
title: Make markdown tables thinner
merge_request: 10909
author: blackst0ne
---
title: Fixed alignment of CI icon in issues related branches
merge_request:
author:
...@@ -99,5 +99,7 @@ Rails.application.routes.draw do ...@@ -99,5 +99,7 @@ Rails.application.routes.draw do
end end
end end
draw :test if Rails.env.test?
get '*unmatched_route', to: 'application#route_not_found' get '*unmatched_route', to: 'application#route_not_found'
end end
...@@ -173,7 +173,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -173,7 +173,7 @@ constraints(ProjectUrlConstrainer.new) do
post :retry post :retry
post :play post :play
post :erase post :erase
get :trace get :trace, defaults: { format: 'json' }
get :raw get :raw
end end
......
get '/unicorn_test/pid' => 'unicorn_test#pid'
post '/unicorn_test/kill' => 'unicorn_test#kill'
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexOnCiRunnersContactedAt < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_runners, :contacted_at
end
def down
remove_concurrent_index :ci_runners, :contacted_at if index_exists?(:ci_runners, :contacted_at)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170426175636) do ActiveRecord::Schema.define(version: 20170426181740) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -296,6 +296,7 @@ ActiveRecord::Schema.define(version: 20170426175636) do ...@@ -296,6 +296,7 @@ ActiveRecord::Schema.define(version: 20170426175636) do
t.boolean "locked", default: false, null: false t.boolean "locked", default: false, null: false
end end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
......
...@@ -70,3 +70,27 @@ All the docs follow the same [styleguide](doc_styleguide.md). ...@@ -70,3 +70,27 @@ All the docs follow the same [styleguide](doc_styleguide.md).
### Markdown ### Markdown
Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
## Testing
We try to treat documentation as code, thus have implemented some testing.
Currently, the following tests are in place:
1. `docs:check:links`: Check that all internal (relative) links work correctly
1. `docs:check:apilint`: Check that the API docs follow some conventions
If your contribution contains **only** documentation changes, you can speed up
the CI process by prepending to the name of your branch: `docs/`. For example,
a valid name would be `docs/update-api-issues` and it will run only the docs
tests. If the name is `docs-update-api-issues`, the whole test suite will run
(including docs).
---
When you submit a merge request to GitLab Community Edition (CE), there is an
additional job called `rake ee_compat_check` that runs against Enterprise
Edition (EE) and checks if your changes can apply cleanly to the EE codebase.
If that job fails, read the instructions in the job log for what to do next.
Contributors do not need to submit their changes to EE, GitLab Inc. employees
on the other hand need to make sure that their changes apply cleanly to both
CE and EE.
# Chat Commands # Chat Commands
Chat commands allow user to perform common operations on GitLab right from there chat client. 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.
Right now both Mattermost and Slack are supported.
## 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 | Command | Effect |
* `/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 | `/project-name help` | Shows all available chat commands |
* `/trigger issue search <query>` - Shows a maximum of 5 items matching the query | `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
* `/trigger deploy <from> to <to>` - Deploy from an environment to another | `/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 |
\ No newline at end of file
...@@ -49,8 +49,8 @@ Click on the service links to see further configuration instructions and details ...@@ -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 | | [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 | | [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 | | Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Receive event notifications in Slack | | [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications |
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands | | [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab |
| PivotalTracker | Project Management Software (Source Commits Endpoint) | | PivotalTracker | Project Management Software (Source Commits Endpoint) |
| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps | | [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 | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
......
# Slack Notifications Service # 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 > 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).
Slack:
1. [Sign in to Slack](https://slack.com/signin) ## Slack Configuration
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.
## 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) 1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings, i.e. **Project > Settings > Integrations**.
and select the **Slack notifications** service to configure it. 1. Select the **Slack notifications** project service to configure it.
There, you will see a checkbox with the following events that can be triggered: 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 Your Slack team will now start receiving GitLab event notifications as configured.
- Issue
- Confidential issue
- Merge request
- Note
- Tag push
- Pipeline
- Wiki page
Below each of these event checkboxes, you have an input field to enter ![Slack configuration](img/slack_configuration.png)
which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`). \ No newline at end of file
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
...@@ -2,23 +2,22 @@ ...@@ -2,23 +2,22 @@
> Introduced in GitLab 8.15 > Introduced in GitLab 8.15
Slack commands give users an extra interface to perform common operations 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.
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).
## Prerequisites > Note: GitLab can also send events (e.g. issue created) to Slack as notifications. This is the separately configured [Slack Notifications Service](slack.md).
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.
## Configuration ## Configuration
Go to your project's [Integrations page](project_services.md#accessing-the-project-services) 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**.
and select the **Slack slash commands** service to configure it. 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) ![Slack setup instructions](img/slack_setup.png)
Once you've followed the instructions, mark the service as active and insert the token ## Usage
you've received from Slack. After saving the service you are good to go!
You can now use the [Slack slash commands](../../../integration/chat_commands.md).
\ No newline at end of file
...@@ -36,3 +36,4 @@ do. ...@@ -36,3 +36,4 @@ do.
| `/remove_time_spent` | Remove time spent | | `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request | | `/target_branch <Branch Name>` | Set target branch for current merge request |
| `/award :emoji:` | Toggle award for :emoji: | | `/award :emoji:` | Toggle award for :emoji: |
| `/board_move ~column` | Move issue to column on the board |
...@@ -10,7 +10,8 @@ Feature: Project Source Browse Files ...@@ -10,7 +10,8 @@ Feature: Project Source Browse Files
Scenario: I browse files for specific ref Scenario: I browse files for specific ref
Given I visit project source page for "6d39438" Given I visit project source page for "6d39438"
Then I should see files from repository for "6d39438" Then I should see files from repository for "6d39438"
@javascript
Scenario: I browse file content Scenario: I browse file content
Given I click on ".gitignore" file in repo Given I click on ".gitignore" file in repo
Then I should see its content Then I should see its content
......
...@@ -6,11 +6,13 @@ Feature: Project Source Markdown Render ...@@ -6,11 +6,13 @@ Feature: Project Source Markdown Render
# Tree README # Tree README
@javascript
Scenario: Tree view should have correct links in README Scenario: Tree view should have correct links in README
Given I go directory which contains README file Given I go directory which contains README file
And I click on a relative link in README And I click on a relative link in README
Then I should see the correct markdown Then I should see the correct markdown
@javascript
Scenario: I browse files from markdown branch Scenario: I browse files from markdown branch
Then I should see files from repository in markdown Then I should see files from repository in markdown
And I should see rendered README which contains correct links And I should see rendered README which contains correct links
...@@ -29,36 +31,42 @@ Feature: Project Source Markdown Render ...@@ -29,36 +31,42 @@ Feature: Project Source Markdown Render
And I click on GitLab API doc directory in README And I click on GitLab API doc directory in README
Then I should see correct doc/api directory rendered Then I should see correct doc/api directory rendered
@javascript
Scenario: I view README in markdown branch to see reference links to file Scenario: I view README in markdown branch to see reference links to file
Then I should see files from repository in markdown Then I should see files from repository in markdown
And I should see rendered README which contains correct links And I should see rendered README which contains correct links
And I click on Maintenance in README And I click on Maintenance in README
Then I should see correct maintenance file rendered Then I should see correct maintenance file rendered
@javascript
Scenario: README headers should have header links Scenario: README headers should have header links
Then I should see rendered README which contains correct links Then I should see rendered README which contains correct links
And Header "Application details" should have correct id and link And Header "Application details" should have correct id and link
# Blob # Blob
@javascript
Scenario: I navigate to doc directory to view documentation in markdown Scenario: I navigate to doc directory to view documentation in markdown
And I navigate to the doc/api/README And I navigate to the doc/api/README
And I see correct file rendered And I see correct file rendered
And I click on users in doc/api/README And I click on users in doc/api/README
Then I should see the correct document file Then I should see the correct document file
@javascript
Scenario: I navigate to doc directory to view user doc in markdown Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README And I navigate to the doc/api/README
And I see correct file rendered And I see correct file rendered
And I click on raketasks in doc/api/README And I click on raketasks in doc/api/README
Then I should see correct directory rendered Then I should see correct directory rendered
@javascript
Scenario: I navigate to doc directory to view user doc in markdown Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README And I navigate to the doc/api/README
And Header "GitLab API" should have correct id and link And Header "GitLab API" should have correct id and link
# Markdown branch # Markdown branch
@javascript
Scenario: I browse files from markdown branch Scenario: I browse files from markdown branch
When I visit markdown branch When I visit markdown branch
Then I should see files from repository in markdown branch Then I should see files from repository in markdown branch
...@@ -73,6 +81,7 @@ Feature: Project Source Markdown Render ...@@ -73,6 +81,7 @@ Feature: Project Source Markdown Render
And I click on Rake tasks in README And I click on Rake tasks in README
Then I should see correct directory rendered for markdown branch Then I should see correct directory rendered for markdown branch
@javascript
Scenario: I navigate to doc directory to view documentation in markdown branch Scenario: I navigate to doc directory to view documentation in markdown branch
When I visit markdown branch When I visit markdown branch
And I navigate to the doc/api/README And I navigate to the doc/api/README
...@@ -80,6 +89,7 @@ Feature: Project Source Markdown Render ...@@ -80,6 +89,7 @@ Feature: Project Source Markdown Render
And I click on users in doc/api/README And I click on users in doc/api/README
Then I should see the users document file in markdown branch 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 Scenario: I navigate to doc directory to view user doc in markdown branch
When I visit markdown branch When I visit markdown branch
And I navigate to the doc/api/README And I navigate to the doc/api/README
...@@ -87,6 +97,7 @@ Feature: Project Source Markdown Render ...@@ -87,6 +97,7 @@ Feature: Project Source Markdown Render
And I click on raketasks in doc/api/README And I click on raketasks in doc/api/README
Then I should see correct directory rendered for markdown branch Then I should see correct directory rendered for markdown branch
@javascript
Scenario: Tree markdown links view empty urls should have correct urls Scenario: Tree markdown links view empty urls should have correct urls
When I visit markdown branch When I visit markdown branch
Then The link with text "empty" should have url "tree/markdown" Then The link with text "empty" should have url "tree/markdown"
...@@ -99,6 +110,7 @@ Feature: Project Source Markdown Render ...@@ -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. # "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. # which Spinach interprets as the start of a comment.
@javascript
Scenario: All markdown links with ids should have correct urls Scenario: All markdown links with ids should have correct urls
When I visit markdown branch When I visit markdown branch
Then The link with text "ID" should have url "tree/markdownID" Then The link with text "ID" should have url "tree/markdownID"
......
...@@ -4,6 +4,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -4,6 +4,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include SharedProject include SharedProject
include SharedPaths include SharedPaths
include RepoHelpers include RepoHelpers
include WaitForAjax
step "I don't have write access" do step "I don't have write access" do
@project = create(:project, :repository, name: "Other Project", path: "other-project") @project = create(:project, :repository, name: "Other Project", path: "other-project")
...@@ -36,10 +37,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -36,10 +37,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end end
step 'I should see its content' do step 'I should see its content' do
wait_for_ajax
expect(page).to have_content old_gitignore_content expect(page).to have_content old_gitignore_content
end end
step 'I should see its new content' do step 'I should see its new content' do
wait_for_ajax
expect(page).to have_content new_gitignore_content expect(page).to have_content new_gitignore_content
end end
......
...@@ -5,6 +5,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -5,6 +5,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
include SharedAuthentication include SharedAuthentication
include SharedPaths include SharedPaths
include SharedMarkdown include SharedMarkdown
include WaitForAjax
step 'I own project "Delta"' do step 'I own project "Delta"' do
@project = ::Project.find_by(name: "Delta") @project = ::Project.find_by(name: "Delta")
...@@ -34,6 +35,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -34,6 +35,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct document rendered' do 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") 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" expect(page).to have_content "All API requests require authentication"
end end
...@@ -63,6 +65,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -63,6 +65,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct maintenance file rendered' do 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") 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" expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production"
end end
...@@ -94,6 +97,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -94,6 +97,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see correct file rendered' do step 'I see correct file rendered' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") 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_content "Contents"
expect(page).to have_link "Users" expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks" expect(page).to have_link "Rake tasks"
...@@ -138,6 +142,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -138,6 +142,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see correct file rendered in markdown branch' do 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") 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_content "Contents"
expect(page).to have_link "Users" expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks" expect(page).to have_link "Rake tasks"
...@@ -145,6 +150,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -145,6 +150,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct document rendered for markdown branch' do 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") 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" expect(page).to have_content "All API requests require authentication"
end end
...@@ -162,6 +168,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -162,6 +168,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
# Expected link contents # Expected link contents
step 'The link with text "empty" should have url "tree/markdown"' do 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") find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown")
end end
...@@ -197,6 +204,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -197,6 +204,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
end end
step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do 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' find('a', text: /^#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id'
end end
...@@ -291,10 +299,12 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -291,10 +299,12 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see the correct markdown' do 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") 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" expect(page).to have_content "List users"
end end
step 'Header "Application details" should have correct id and link' do 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') header_should_have_correct_id_and_link(2, 'Application details', 'application-details')
end end
......
...@@ -3,7 +3,7 @@ module SharedMarkdown ...@@ -3,7 +3,7 @@ module SharedMarkdown
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki") def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
node = find("#{parent} h#{level} a#user-content-#{id}") 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 # Work around a weird Capybara behavior where calling `parent` on a node
# returns the whole document, not the node's actual parent element # returns the whole document, not the node's actual parent element
......
...@@ -109,10 +109,6 @@ module Gitlab ...@@ -109,10 +109,6 @@ module Gitlab
@binary.nil? ? super : @binary == true @binary.nil? ? super : @binary == true
end end
def empty?
!data || data == ''
end
def data def data
encode! @data encode! @data
end end
......
...@@ -2,7 +2,7 @@ desc 'Security check via brakeman' ...@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
task :brakeman do task :brakeman do
# We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge # We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
# requests are welcome! # requests are welcome!
if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z)) if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb,app/controllers/unicorn_test_controller.rb -w3 -z))
puts 'Security check succeed' puts 'Security check succeed'
else else
puts 'Security check failed' puts 'Security check failed'
......
...@@ -32,7 +32,7 @@ sed -i 's/localhost/redis/g' config/resque.yml ...@@ -32,7 +32,7 @@ sed -i 's/localhost/redis/g' config/resque.yml
cp config/gitlab.yml.example config/gitlab.yml cp config/gitlab.yml.example config/gitlab.yml
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
retry bundle install --clean $BUNDLE_INSTALL_FLAGS retry bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
fi fi
# Only install knapsack after bundle install! Otherwise oddly some native # Only install knapsack after bundle install! Otherwise oddly some native
......
require 'spec_helper' require 'spec_helper'
describe Projects::BuildsController do describe Projects::BuildsController do
let(:user) { create(:user) } include ApiHelpers
let(:project) { create(:empty_project, :public) }
before do let(:project) { create(:empty_project, :public) }
sign_in(user) let(:pipeline) { create(:ci_pipeline, project: project) }
end let(:user) { create(:user) }
describe 'GET index' do 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 context 'number of queries' do
before do before do
Ci::Build::AVAILABLE_STATUSES.each do |status| Ci::Build::AVAILABLE_STATUSES.each do |status|
...@@ -23,13 +78,8 @@ describe Projects::BuildsController do ...@@ -23,13 +78,8 @@ describe Projects::BuildsController do
RequestStore.clear! RequestStore.clear!
end end
def render
get :index, namespace_id: project.namespace,
project_id: project
end
it "verifies number of queries" do 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) expect(recorded.count).to be_within(5).of(8)
end end
...@@ -39,10 +89,83 @@ describe Projects::BuildsController do ...@@ -39,10 +89,83 @@ describe Projects::BuildsController do
pipeline: pipeline, name: name, status: status) pipeline: pipeline, name: name, status: status)
end end
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 end
describe 'GET status.json' do describe 'GET status.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
let(:status) { build.detailed_status(double('user')) } let(:status) { build.detailed_status(double('user')) }
...@@ -71,6 +194,7 @@ describe Projects::BuildsController do ...@@ -71,6 +194,7 @@ describe Projects::BuildsController do
before do before do
project.add_developer(user) project.add_developer(user)
sign_in(user) sign_in(user)
get_trace get_trace
end end
...@@ -84,6 +208,7 @@ describe Projects::BuildsController do ...@@ -84,6 +208,7 @@ describe Projects::BuildsController do
context 'when user is logged in as non member' do context 'when user is logged in as non member' do
before do before do
sign_in(user) sign_in(user)
get_trace get_trace
end end
...@@ -101,4 +226,221 @@ describe Projects::BuildsController do ...@@ -101,4 +226,221 @@ describe Projects::BuildsController do
format: :json format: :json
end end
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 end
...@@ -79,6 +79,19 @@ FactoryGirl.define do ...@@ -79,6 +79,19 @@ FactoryGirl.define do
manual manual
end end
trait :retryable do
success
end
trait :cancelable do
pending
end
trait :erasable do
success
artifacts
end
trait :tags do trait :tags do
tag_list [:docker, :ruby] tag_list [:docker, :ruby]
end end
......
...@@ -479,6 +479,7 @@ describe 'Copy as GFM', feature: true, js: true do ...@@ -479,6 +479,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a blob' do context 'from a blob' do
before do before do
visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb')) visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
wait_for_ajax
end end
context 'selecting one word of text' do context 'selecting one word of text' do
...@@ -520,6 +521,7 @@ describe 'Copy as GFM', feature: true, js: true do ...@@ -520,6 +521,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a GFM code block' do context 'from a GFM code block' do
before do before do
visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md')) visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
wait_for_ajax
end end
context 'selecting one word of text' do context 'selecting one word of text' do
......
require 'spec_helper' require 'spec_helper'
feature 'File blob', feature: true do feature 'File blob', :js, feature: true do
include TreeHelper include TreeHelper
include WaitForAjax
let(:project) { create(:project, :public, :test_repo) } let(:project) { create(:project, :public) }
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] }
context 'anonymous' do def visit_blob(path, fragment = nil)
context 'from blob file path' do 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 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 '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
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')
it 'updates content' do # does not show a copy button
expect(page).to have_link 'Edit' expect(page).not_to have_selector('.js-copy-blob-source-btn')
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
feature 'user browses project', feature: true do feature 'user browses project', feature: true, js: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -13,7 +13,7 @@ feature 'user browses project', feature: true do ...@@ -13,7 +13,7 @@ feature 'user browses project', feature: true do
scenario "can see blame of '.gitignore'" do scenario "can see blame of '.gitignore'" do
click_link ".gitignore" click_link ".gitignore"
click_link 'Blame' click_link 'Blame'
expect(page).to have_content "*.rb" expect(page).to have_content "*.rb"
expect(page).to have_content "Dmitriy Zaporozhets" expect(page).to have_content "Dmitriy Zaporozhets"
expect(page).to have_content "Initial commit" expect(page).to have_content "Initial commit"
...@@ -24,6 +24,7 @@ feature 'user browses project', feature: true do ...@@ -24,6 +24,7 @@ feature 'user browses project', feature: true do
click_link 'files' click_link 'files'
click_link 'lfs' click_link 'lfs'
click_link 'lfs_object.iso' click_link 'lfs_object.iso'
wait_for_ajax
expect(page).not_to have_content 'Download (1.5 MB)' expect(page).not_to have_content 'Download (1.5 MB)'
expect(page).to have_content 'version https://git-lfs.github.com/spec/v1' expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
......
...@@ -399,6 +399,44 @@ describe "Internal Project Access", feature: true do ...@@ -399,6 +399,44 @@ describe "Internal Project Access", feature: true do
end end
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 describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) } subject { namespace_project_environments_path(project.namespace, project) }
......
...@@ -388,6 +388,38 @@ describe "Private Project Access", feature: true do ...@@ -388,6 +388,38 @@ describe "Private Project Access", feature: true do
end end
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) }
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) }
context 'when public builds is enabled' do
before do
project.update(public_builds: true)
end
it { is_expected.to be_allowed_for(:guest).of(project) }
end
context 'when public builds is disabled' do
before do
project.update(public_builds: false)
end
it { is_expected.to be_denied_for(:guest).of(project) }
end
end
describe "GET /:project_path/environments" do describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) } subject { namespace_project_environments_path(project.namespace, project) }
......
...@@ -219,6 +219,44 @@ describe "Public Project Access", feature: true do ...@@ -219,6 +219,44 @@ describe "Public Project Access", feature: true do
end end
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' 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_allowed_for(:external) }
it { is_expected.to be_allowed_for(:visitor) }
end
context 'when disallowed for public' 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 describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) } subject { namespace_project_environments_path(project.namespace, project) }
......
...@@ -56,15 +56,14 @@ describe BlobHelper do ...@@ -56,15 +56,14 @@ describe BlobHelper do
end end
end end
describe "#sanitize_svg" do describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') } let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { open(input_svg_path).read } let(:data) { open(input_svg_path).read }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') } let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
let(:expected) { open(expected_svg_path).read } let(:expected) { open(expected_svg_path).read }
it 'retains essential elements' do it 'retains essential elements' do
blob = OpenStruct.new(data: data) expect(sanitize_svg_data(data)).to eq(expected)
expect(sanitize_svg(blob).data).to eq(expected)
end end
end end
...@@ -105,4 +104,119 @@ describe BlobHelper do ...@@ -105,4 +104,119 @@ describe BlobHelper do
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10') expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
end end
end end
context 'viewer related' do
include FakeBlobHelpers
let(:project) { build(:empty_project, lfs_enabled: true) }
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
let(:viewer_class) do
Class.new(BlobViewer::Base) do
self.max_size = 1.megabyte
self.absolute_max_size = 5.megabytes
self.type = :rich
self.client_side = false
end
end
let(:viewer) { viewer_class.new(blob) }
let(:blob) { fake_blob }
describe '#blob_render_error_reason' do
context 'for error :too_large' do
context 'when the blob size is larger than the absolute max size' do
let(:blob) { fake_blob(size: 10.megabytes) }
it 'returns an error message' do
expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 5 MB')
end
end
context 'when the blob size is larger than the max size' do
let(:blob) { fake_blob(size: 2.megabytes) }
it 'returns an error message' do
expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 1 MB')
end
end
end
context 'for error :server_side_but_stored_in_lfs' do
let(:blob) { fake_blob(lfs: true) }
it 'returns an error message' do
expect(helper.blob_render_error_reason(viewer)).to eq('it is stored in LFS')
end
end
end
describe '#blob_render_error_options' do
before do
assign(:project, project)
assign(:id, File.join('master', blob.path))
controller.params[:controller] = 'projects/blob'
controller.params[:action] = 'show'
controller.params[:namespace_id] = project.namespace.to_param
controller.params[:project_id] = project.to_param
controller.params[:id] = File.join('master', blob.path)
end
context 'for error :too_large' do
context 'when the max size can be overridden' do
let(:blob) { fake_blob(size: 2.megabytes) }
it 'includes a "load it anyway" link' do
expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/)
end
end
context 'when the max size cannot be overridden' do
let(:blob) { fake_blob(size: 10.megabytes) }
it 'does not include a "load it anyway" link' do
expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
end
end
end
context 'when the viewer is rich' do
context 'the blob is rendered as text' do
let(:blob) { fake_blob(path: 'file.md', lfs: true) }
it 'includes a "view the source" link' do
expect(helper.blob_render_error_options(viewer)).to include(/view the source/)
end
end
context 'the blob is not rendered as text' do
let(:blob) { fake_blob(path: 'file.pdf', binary: true, lfs: true) }
it 'does not include a "view the source" link' do
expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
end
end
end
context 'when the viewer is not rich' do
before do
viewer_class.type = :simple
end
let(:blob) { fake_blob(path: 'file.md', lfs: true) }
it 'does not include a "view the source" link' do
expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
end
end
it 'includes a "download it" link' do
expect(helper.blob_render_error_options(viewer)).to include(/download it/)
end
end
end
end end
/* eslint-disable no-new */
import BlobViewer from '~/blob/viewer/index';
describe('Blob viewer', () => {
let blob;
preloadFixtures('blob/show.html.raw');
beforeEach(() => {
loadFixtures('blob/show.html.raw');
$('#modal-upload-blob').remove();
blob = new BlobViewer();
spyOn($, 'ajax').and.callFake(() => {
const d = $.Deferred();
d.resolve({
html: '<div>testing</div>',
});
return d.promise();
});
});
afterEach(() => {
location.hash = '';
});
it('loads source file after switching views', (done) => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
expect($.ajax).toHaveBeenCalled();
expect(
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
.classList.contains('hidden'),
).toBeFalsy();
done();
});
});
it('loads source file when line number is in hash', (done) => {
location.hash = '#L1';
new BlobViewer();
setTimeout(() => {
expect($.ajax).toHaveBeenCalled();
expect(
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
.classList.contains('hidden'),
).toBeFalsy();
done();
});
});
it('doesnt reload file if already loaded', (done) => {
const asyncClick = () => new Promise((resolve) => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(resolve);
});
asyncClick()
.then(() => {
expect($.ajax).toHaveBeenCalled();
return asyncClick();
})
.then(() => {
expect($.ajax.calls.count()).toBe(1);
expect(
document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
).toBe('true');
done();
})
.catch(() => {
fail();
done();
});
});
describe('copy blob button', () => {
it('disabled on load', () => {
expect(
document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
).toBeTruthy();
});
it('has tooltip when disabled', () => {
expect(
document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
).toBe('Switch to the source to copy it to the clipboard');
});
it('enables after switching to simple view', (done) => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
expect($.ajax).toHaveBeenCalled();
expect(
document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
).toBeFalsy();
done();
});
});
it('updates tooltip after switching to simple view', (done) => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
expect($.ajax).toHaveBeenCalled();
expect(
document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
).toBe('Copy source to clipboard');
done();
});
});
});
describe('switchToViewer', () => {
it('removes active class from old viewer button', () => {
blob.switchToViewer('simple');
expect(
document.querySelector('.js-blob-viewer-switch-btn.active[data-viewer="rich"]'),
).toBeNull();
});
it('adds active class to new viewer button', () => {
const simpleBtn = document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]');
spyOn(simpleBtn, 'blur');
blob.switchToViewer('simple');
expect(
simpleBtn.classList.contains('active'),
).toBeTruthy();
expect(simpleBtn.blur).toHaveBeenCalled();
});
it('sends AJAX request when switching to simple view', () => {
blob.switchToViewer('simple');
expect($.ajax).toHaveBeenCalled();
});
it('does not send AJAX request when switching to rich view', () => {
blob.switchToViewer('simple');
blob.switchToViewer('rich');
expect($.ajax.calls.count()).toBe(1);
});
});
});
require 'spec_helper'
describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
render_views
before(:all) do
clean_frontend_fixtures('blob/')
end
before(:each) do
sign_in(admin)
end
it 'blob/show.html.raw' do |example|
get(:show,
namespace_id: project.namespace,
project_id: project,
id: 'add-ipython-files/files/ipython/basic.ipynb')
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
...@@ -2,6 +2,14 @@ ...@@ -2,6 +2,14 @@
require 'rails_helper' require 'rails_helper'
describe Blob do describe Blob do
include FakeBlobHelpers
let(:project) { build(:empty_project, lfs_enabled: true) }
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
describe '.decorate' do describe '.decorate' do
it 'returns NilClass when given nil' do it 'returns NilClass when given nil' do
expect(described_class.decorate(nil)).to be_nil expect(described_class.decorate(nil)).to be_nil
...@@ -12,7 +20,7 @@ describe Blob do ...@@ -12,7 +20,7 @@ describe Blob do
context 'using a binary blob' do context 'using a binary blob' do
it 'returns the data as-is' do it 'returns the data as-is' do
data = "\n\xFF\xB9\xC3" data = "\n\xFF\xB9\xC3"
blob = described_class.new(double(binary?: true, data: data)) blob = fake_blob(binary: true, data: data)
expect(blob.data).to eq(data) expect(blob.data).to eq(data)
end end
...@@ -20,202 +28,176 @@ describe Blob do ...@@ -20,202 +28,176 @@ describe Blob do
context 'using a text blob' do context 'using a text blob' do
it 'converts the data to UTF-8' do it 'converts the data to UTF-8' do
blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3")) blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3")
expect(blob.data).to eq("\n���") expect(blob.data).to eq("\n���")
end end
end end
end end
describe '#svg?' do describe '#raw_binary?' do
it 'is falsey when not text' do context 'if the blob is a valid LFS pointer' do
git_blob = double(text?: false) context 'if the extension has a rich viewer' do
context 'if the viewer is binary' do
it 'returns true' do
blob = fake_blob(path: 'file.pdf', lfs: true)
expect(described_class.decorate(git_blob)).not_to be_svg expect(blob.raw_binary?).to be_truthy
end end
end
it 'is falsey when no language is detected' do
git_blob = double(text?: true, language: nil)
expect(described_class.decorate(git_blob)).not_to be_svg context 'if the viewer is text-based' do
end it 'return false' do
blob = fake_blob(path: 'file.md', lfs: true)
it' is falsey when language is not SVG' do expect(blob.raw_binary?).to be_falsey
git_blob = double(text?: true, language: double(name: 'XML')) end
end
expect(described_class.decorate(git_blob)).not_to be_svg end
end
it 'is truthy when language is SVG' do
git_blob = double(text?: true, language: double(name: 'SVG'))
expect(described_class.decorate(git_blob)).to be_svg
end
end
describe '#pdf?' do
it 'is falsey when file extension is not .pdf' do
git_blob = Gitlab::Git::Blob.new(name: 'git_blob.txt')
expect(described_class.decorate(git_blob)).not_to be_pdf
end
it 'is truthy when file extension is .pdf' do context "if the extension doesn't have a rich viewer" do
git_blob = Gitlab::Git::Blob.new(name: 'git_blob.pdf') it 'returns true' do
blob = fake_blob(path: 'file.exe', lfs: true)
expect(described_class.decorate(git_blob)).to be_pdf expect(blob.raw_binary?).to be_truthy
end
end
end end
end
describe '#ipython_notebook?' do context 'if the blob is not an LFS pointer' do
it 'is falsey when language is not Jupyter Notebook' do context 'if the blob is binary' do
git_blob = double(text?: true, language: double(name: 'JSON')) it 'returns true' do
blob = fake_blob(path: 'file.pdf', binary: true)
expect(described_class.decorate(git_blob)).not_to be_ipython_notebook expect(blob.raw_binary?).to be_truthy
end end
end
it 'is truthy when language is Jupyter Notebook' do context 'if the blob is text-based' do
git_blob = double(text?: true, language: double(name: 'Jupyter Notebook')) it 'return false' do
blob = fake_blob(path: 'file.md')
expect(described_class.decorate(git_blob)).to be_ipython_notebook expect(blob.raw_binary?).to be_falsey
end
end
end end
end end
describe '#sketch?' do describe '#extension' do
it 'is falsey with image extension' do it 'returns the extension' do
git_blob = Gitlab::Git::Blob.new(name: "design.png") blob = fake_blob(path: 'file.md')
expect(described_class.decorate(git_blob)).not_to be_sketch
end
it 'is truthy with sketch extension' do
git_blob = Gitlab::Git::Blob.new(name: "design.sketch")
expect(described_class.decorate(git_blob)).to be_sketch expect(blob.extension).to eq('md')
end end
end end
describe '#video?' do describe '#simple_viewer' do
it 'is falsey with image extension' do context 'when the blob is empty' do
git_blob = Gitlab::Git::Blob.new(name: 'image.png') it 'returns an empty viewer' do
blob = fake_blob(data: '')
expect(described_class.decorate(git_blob)).not_to be_video expect(blob.simple_viewer).to be_a(BlobViewer::Empty)
end
UploaderHelper::VIDEO_EXT.each do |ext|
it "is truthy when extension is .#{ext}" do
git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
expect(described_class.decorate(git_blob)).to be_video
end end
end end
end
describe '#stl?' do context 'when the file represented by the blob is binary' do
it 'is falsey with image extension' do it 'returns a download viewer' do
git_blob = Gitlab::Git::Blob.new(name: 'file.png') blob = fake_blob(binary: true)
expect(described_class.decorate(git_blob)).not_to be_stl expect(blob.simple_viewer).to be_a(BlobViewer::Download)
end
end end
it 'is truthy with STL extension' do context 'when the file represented by the blob is text-based' do
git_blob = Gitlab::Git::Blob.new(name: 'file.stl') it 'returns a text viewer' do
blob = fake_blob
expect(described_class.decorate(git_blob)).to be_stl expect(blob.simple_viewer).to be_a(BlobViewer::Text)
end
end end
end end
describe '#to_partial_path' do describe '#rich_viewer' do
let(:project) { double(lfs_enabled?: true) } context 'when the blob is an invalid LFS pointer' do
before do
project.lfs_enabled = false
end
def stubbed_blob(overrides = {}) it 'returns nil' do
overrides.reverse_merge!( blob = fake_blob(path: 'file.pdf', lfs: true)
name: nil,
image?: false,
language: nil,
lfs_pointer?: false,
svg?: false,
text?: false,
binary?: false,
stl?: false
)
described_class.decorate(Gitlab::Git::Blob.new({})).tap do |blob| expect(blob.rich_viewer).to be_nil
allow(blob).to receive_messages(overrides)
end end
end end
it 'handles LFS pointers with LFS enabled' do context 'when the blob is empty' do
blob = stubbed_blob(lfs_pointer?: true, text?: true) it 'returns nil' do
expect(blob.to_partial_path(project)).to eq 'download' blob = fake_blob(data: '')
end
it 'handles LFS pointers with LFS disabled' do
blob = stubbed_blob(lfs_pointer?: true, text?: true)
project = double(lfs_enabled?: false)
expect(blob.to_partial_path(project)).to eq 'text'
end
it 'handles SVGs' do expect(blob.rich_viewer).to be_nil
blob = stubbed_blob(text?: true, svg?: true) end
expect(blob.to_partial_path(project)).to eq 'svg'
end end
it 'handles images' do context 'when the blob is a valid LFS pointer' do
blob = stubbed_blob(image?: true) it 'returns a matching viewer' do
expect(blob.to_partial_path(project)).to eq 'image' blob = fake_blob(path: 'file.pdf', lfs: true)
end
it 'handles text' do expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
blob = stubbed_blob(text?: true, name: 'test.txt') end
expect(blob.to_partial_path(project)).to eq 'text'
end
it 'defaults to download' do
blob = stubbed_blob
expect(blob.to_partial_path(project)).to eq 'download'
end end
it 'handles PDFs' do context 'when the blob is binary' do
blob = stubbed_blob(name: 'blob.pdf', pdf?: true) it 'returns a matching binary viewer' do
expect(blob.to_partial_path(project)).to eq 'pdf' blob = fake_blob(path: 'file.pdf', binary: true)
end
it 'handles iPython notebooks' do expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
blob = stubbed_blob(text?: true, ipython_notebook?: true) end
expect(blob.to_partial_path(project)).to eq 'notebook'
end end
it 'handles Sketch files' do context 'when the blob is text-based' do
blob = stubbed_blob(text?: true, sketch?: true, binary?: true) it 'returns a matching text-based viewer' do
expect(blob.to_partial_path(project)).to eq 'sketch' blob = fake_blob(path: 'file.md')
end
it 'handles STLs' do expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
blob = stubbed_blob(text?: true, stl?: true) end
expect(blob.to_partial_path(project)).to eq 'stl'
end end
end end
describe '#size_within_svg_limits?' do describe '#rendered_as_text?' do
let(:blob) { described_class.decorate(double(:blob)) } context 'when ignoring errors' do
context 'when the simple viewer is text-based' do
it 'returns true' do
blob = fake_blob(path: 'file.md', size: 100.megabytes)
it 'returns true when the blob size is smaller than the SVG limit' do expect(blob.rendered_as_text?).to be_truthy
expect(blob).to receive(:size).and_return(42) end
end
context 'when the simple viewer is binary' do
it 'returns false' do
blob = fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes)
expect(blob.size_within_svg_limits?).to eq(true) expect(blob.rendered_as_text?).to be_falsey
end
end
end end
it 'returns true when the blob size is equal to the SVG limit' do context 'when not ignoring errors' do
expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE) context 'when the viewer has render errors' do
it 'returns false' do
blob = fake_blob(path: 'file.md', size: 100.megabytes)
expect(blob.size_within_svg_limits?).to eq(true) expect(blob.rendered_as_text?(ignore_errors: false)).to be_falsey
end end
end
it 'returns false when the blob size is larger than the SVG limit' do context "when the viewer doesn't have render errors" do
expect(blob).to receive(:size).and_return(1.terabyte) it 'returns true' do
blob = fake_blob(path: 'file.md')
expect(blob.size_within_svg_limits?).to eq(false) expect(blob.rendered_as_text?(ignore_errors: false)).to be_truthy
end
end
end end
end end
end end
require 'spec_helper'
describe BlobViewer::Base, model: true do
include FakeBlobHelpers
let(:project) { build(:empty_project) }
let(:viewer_class) do
Class.new(described_class) do
self.extensions = %w(pdf)
self.max_size = 1.megabyte
self.absolute_max_size = 5.megabytes
self.client_side = false
end
end
let(:viewer) { viewer_class.new(blob) }
describe '.can_render?' do
context 'when the extension is supported' do
let(:blob) { fake_blob(path: 'file.pdf') }
it 'returns true' do
expect(viewer_class.can_render?(blob)).to be_truthy
end
end
context 'when the extension is not supported' do
let(:blob) { fake_blob(path: 'file.txt') }
it 'returns false' do
expect(viewer_class.can_render?(blob)).to be_falsey
end
end
end
describe '#too_large?' do
context 'when the blob size is larger than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns true' do
expect(viewer.too_large?).to be_truthy
end
end
context 'when the blob size is smaller than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns false' do
expect(viewer.too_large?).to be_falsey
end
end
end
describe '#absolutely_too_large?' do
context 'when the blob size is larger than the absolute max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns true' do
expect(viewer.absolutely_too_large?).to be_truthy
end
end
context 'when the blob size is smaller than the absolute max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns false' do
expect(viewer.absolutely_too_large?).to be_falsey
end
end
end
describe '#can_override_max_size?' do
context 'when the blob size is larger than the max size' do
context 'when the blob size is larger than the absolute max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns false' do
expect(viewer.can_override_max_size?).to be_falsey
end
end
context 'when the blob size is smaller than the absolute max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns true' do
expect(viewer.can_override_max_size?).to be_truthy
end
end
end
context 'when the blob size is smaller than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns false' do
expect(viewer.can_override_max_size?).to be_falsey
end
end
end
describe '#render_error' do
context 'when the max size is overridden' do
before do
viewer.override_max_size = true
end
context 'when the blob size is larger than the absolute max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns :too_large' do
expect(viewer.render_error).to eq(:too_large)
end
end
context 'when the blob size is smaller than the absolute max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns nil' do
expect(viewer.render_error).to be_nil
end
end
end
context 'when the max size is not overridden' do
context 'when the blob size is larger than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns :too_large' do
expect(viewer.render_error).to eq(:too_large)
end
end
context 'when the blob size is smaller than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns nil' do
expect(viewer.render_error).to be_nil
end
end
end
context 'when the viewer is server side but the blob is stored in LFS' do
let(:project) { build(:empty_project, lfs_enabled: true) }
let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
it 'return :server_side_but_stored_in_lfs' do
expect(viewer.render_error).to eq(:server_side_but_stored_in_lfs)
end
end
end
describe '#prepare!' do
context 'when the viewer is server side' do
let(:blob) { fake_blob(path: 'file.md') }
before do
viewer_class.client_side = false
end
it 'loads all blob data' do
expect(blob).to receive(:load_all_data!)
viewer.prepare!
end
end
context 'when the viewer is client side' do
let(:blob) { fake_blob(path: 'file.md') }
before do
viewer_class.client_side = true
end
it "doesn't load all blob data" do
expect(blob).not_to receive(:load_all_data!)
viewer.prepare!
end
end
end
end
...@@ -1556,6 +1556,16 @@ describe User, models: true do ...@@ -1556,6 +1556,16 @@ describe User, models: true do
expect(ghost.email).to eq('ghost1@example.com') expect(ghost.email).to eq('ghost1@example.com')
end end
end end
context 'when a domain whitelist is in place' do
before do
stub_application_setting(domain_whitelist: ['gitlab.com'])
end
it 'creates a ghost user' do
expect(User.ghost).to be_persisted
end
end
end end
describe '#update_two_factor_requirement' do describe '#update_two_factor_requirement' do
......
...@@ -52,7 +52,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -52,7 +52,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unassign command' do shared_examples 'unassign command' do
it 'populates assignee_id: nil if content contains /unassign' do it 'populates assignee_id: nil if content contains /unassign' do
issuable.update(assignee_id: developer.id) issuable.update!(assignee_id: developer.id)
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(assignee_id: nil) expect(updates).to eq(assignee_id: nil)
...@@ -70,7 +70,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -70,7 +70,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'remove_milestone command' do shared_examples 'remove_milestone command' do
it 'populates milestone_id: nil if content contains /remove_milestone' do it 'populates milestone_id: nil if content contains /remove_milestone' do
issuable.update(milestone_id: milestone.id) issuable.update!(milestone_id: milestone.id)
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(milestone_id: nil) expect(updates).to eq(milestone_id: nil)
...@@ -108,7 +108,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -108,7 +108,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unlabel command' do shared_examples 'unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
issuable.update(label_ids: [inprogress.id]) # populate the label issuable.update!(label_ids: [inprogress.id]) # populate the label
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(remove_label_ids: [inprogress.id]) expect(updates).to eq(remove_label_ids: [inprogress.id])
...@@ -117,7 +117,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -117,7 +117,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'multiple unlabel command' do shared_examples 'multiple unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains mutiple /unlabel' do it 'fetches label ids and populates remove_label_ids if content contains mutiple /unlabel' do
issuable.update(label_ids: [inprogress.id, bug.id]) # populate the label issuable.update!(label_ids: [inprogress.id, bug.id]) # populate the label
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id]) expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
...@@ -126,7 +126,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -126,7 +126,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unlabel command with no argument' do shared_examples 'unlabel command with no argument' do
it 'populates label_ids: [] if content contains /unlabel with no arguments' do it 'populates label_ids: [] if content contains /unlabel with no arguments' do
issuable.update(label_ids: [inprogress.id]) # populate the label issuable.update!(label_ids: [inprogress.id]) # populate the label
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(label_ids: []) expect(updates).to eq(label_ids: [])
...@@ -135,7 +135,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -135,7 +135,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'relabel command' do shared_examples 'relabel command' do
it 'populates label_ids: [] if content contains /relabel' do it 'populates label_ids: [] if content contains /relabel' do
issuable.update(label_ids: [bug.id]) # populate the label issuable.update!(label_ids: [bug.id]) # populate the label
inprogress # populate the label inprogress # populate the label
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
...@@ -187,7 +187,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -187,7 +187,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'remove_due_date command' do shared_examples 'remove_due_date command' do
it 'populates due_date: nil if content contains /remove_due_date' do it 'populates due_date: nil if content contains /remove_due_date' do
issuable.update(due_date: Date.today) issuable.update!(due_date: Date.today)
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(due_date: nil) expect(updates).to eq(due_date: nil)
...@@ -204,7 +204,7 @@ describe SlashCommands::InterpretService, services: true do ...@@ -204,7 +204,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unwip command' do shared_examples 'unwip command' do
it 'returns wip_event: "unwip" if content contains /wip' do it 'returns wip_event: "unwip" if content contains /wip' do
issuable.update(title: issuable.wip_title) issuable.update!(title: issuable.wip_title)
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
expect(updates).to eq(wip_event: 'unwip') expect(updates).to eq(wip_event: 'unwip')
...@@ -727,5 +727,75 @@ describe SlashCommands::InterpretService, services: true do ...@@ -727,5 +727,75 @@ describe SlashCommands::InterpretService, services: true do
end end
end end
end end
context '/board_move command' do
let(:todo) { create(:label, project: project, title: 'To Do') }
let(:inreview) { create(:label, project: project, title: 'In Review') }
let(:content) { %{/board_move ~"#{inreview.title}"} }
let!(:board) { create(:board, project: project) }
let!(:todo_list) { create(:list, board: board, label: todo) }
let!(:inreview_list) { create(:list, board: board, label: inreview) }
let!(:inprogress_list) { create(:list, board: board, label: inprogress) }
it 'populates remove_label_ids for all current board columns' do
issue.update!(label_ids: [todo.id, inprogress.id])
_, updates = service.execute(content, issue)
expect(updates[:remove_label_ids]).to match_array([todo.id, inprogress.id])
end
it 'populates add_label_ids with the id of the given label' do
_, updates = service.execute(content, issue)
expect(updates[:add_label_ids]).to eq([inreview.id])
end
it 'does not include the given label id in remove_label_ids' do
issue.update!(label_ids: [todo.id, inreview.id])
_, updates = service.execute(content, issue)
expect(updates[:remove_label_ids]).to match_array([todo.id])
end
it 'does not remove label ids that are not lists on the board' do
issue.update!(label_ids: [todo.id, bug.id])
_, updates = service.execute(content, issue)
expect(updates[:remove_label_ids]).to match_array([todo.id])
end
context 'if the project has multiple boards' do
let(:issuable) { issue }
before { create(:board, project: project) }
it_behaves_like 'empty command'
end
context 'if the given label does not exist' do
let(:issuable) { issue }
let(:content) { '/board_move ~"Fake Label"' }
it_behaves_like 'empty command'
end
context 'if multiple labels are given' do
let(:issuable) { issue }
let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} }
it_behaves_like 'empty command'
end
context 'if the given label is not a list on the board' do
let(:issuable) { issue }
let(:content) { %{/board_move ~"#{bug.title}"} }
it_behaves_like 'empty command'
end
context 'if issuable is not an Issue' do
let(:issuable) { merge_request }
it_behaves_like 'empty command'
end
end
end end
end end
module FakeBlobHelpers
class FakeBlob
include Linguist::BlobHelper
attr_reader :path, :size, :data, :lfs_oid, :lfs_size
def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil)
@path = path
@size = size
@data = data
@binary = binary
@lfs_pointer = lfs.present?
if @lfs_pointer
@lfs_oid = SecureRandom.hex(20)
@lfs_size = 1.megabyte
end
end
alias_method :name, :path
def mode
nil
end
def id
0
end
def binary?
@binary
end
def load_all_data!(repository)
# No-op
end
def lfs_pointer?
@lfs_pointer
end
def truncated?
false
end
end
def fake_blob(**kwargs)
Blob.decorate(FakeBlob.new(**kwargs), project)
end
end
...@@ -51,7 +51,7 @@ module AccessMatchers ...@@ -51,7 +51,7 @@ module AccessMatchers
emulate_user(user, @membership) emulate_user(user, @membership)
visit(url) visit(url)
status_code != 404 && current_path != new_user_session_path status_code == 200 && current_path != new_user_session_path
end end
chain :of do |membership| chain :of do |membership|
...@@ -66,7 +66,7 @@ module AccessMatchers ...@@ -66,7 +66,7 @@ module AccessMatchers
emulate_user(user, @membership) emulate_user(user, @membership)
visit(url) visit(url)
status_code == 404 || current_path == new_user_session_path [401, 404].include?(status_code) || current_path == new_user_session_path
end end
chain :of do |membership| chain :of do |membership|
......
require 'fileutils'
require 'excon'
require 'spec_helper'
describe 'Unicorn' do
before(:all) do
config_lines = File.read('config/unicorn.rb.example').split("\n")
# Remove these because they make setup harder.
config_lines = config_lines.reject do |line|
%w[
working_directory
worker_processes
listen
pid
stderr_path
stdout_path
].any? { |prefix| line.start_with?(prefix) }
end
config_lines << "working_directory '#{Rails.root}'"
# We want to have exactly 1 worker process because that makes it
# predictable which process will handle our requests.
config_lines << 'worker_processes 1'
@socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket')
config_lines << "listen '#{@socket_path}'"
ready_file = 'tmp/tests/unicorn-worker-ready'
FileUtils.rm_f(ready_file)
after_fork_index = config_lines.index { |l| l.start_with?('after_fork') }
config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)")
config_path = 'tmp/tests/unicorn.rb'
File.write(config_path, config_lines.join("\n") + "\n")
cmd = %W[unicorn -E test -c #{config_path} #{Rails.root.join('config.ru')}]
@unicorn_master_pid = spawn(*cmd)
wait_unicorn_boot!(@unicorn_master_pid, ready_file)
WebMock.allow_net_connect!
end
%w[SIGQUIT SIGTERM SIGKILL].each do |signal|
it "has a worker that self-terminates on signal #{signal}" do
response = Excon.get('unix:///unicorn_test/pid', socket: @socket_path)
expect(response.status).to eq(200)
worker_pid = response.body.to_i
expect(worker_pid).to be > 0
begin
Excon.post('unix:///unicorn_test/kill', socket: @socket_path, body: "signal=#{signal}")
rescue Excon::Error::Socket
# The connection may be closed abruptly
end
expect(pid_gone?(worker_pid)).to eq(true)
end
end
after(:all) do
WebMock.disable_net_connect!(allow_localhost: true)
Process.kill('TERM', @unicorn_master_pid)
end
def wait_unicorn_boot!(master_pid, ready_file)
# Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout.
timeout = 120
timeout.times do
return if File.exist?(ready_file)
pid = Process.waitpid(master_pid, Process::WNOHANG)
raise "unicorn failed to boot: #{$?}" unless pid.nil?
sleep 1
end
raise "unicorn boot timed out after #{timeout} seconds"
end
def pid_gone?(pid)
# Worker termination should take less than a second. That makes 10
# seconds a generous timeout.
10.times do
begin
Process.kill(0, pid)
rescue Errno::ESRCH
return true
end
sleep 1
end
false
end
end
require 'spec_helper'
describe 'projects/blob/_viewer.html.haml', :view do
include FakeBlobHelpers
let(:project) { build(:empty_project) }
let(:viewer_class) do
Class.new(BlobViewer::Base) do
include BlobViewer::Rich
self.partial_name = 'text'
self.max_size = 1.megabyte
self.absolute_max_size = 5.megabytes
self.client_side = false
end
end
let(:viewer) { viewer_class.new(blob) }
let(:blob) { fake_blob }
before do
assign(:project, project)
assign(:id, File.join('master', blob.path))
controller.params[:controller] = 'projects/blob'
controller.params[:action] = 'show'
controller.params[:namespace_id] = project.namespace.to_param
controller.params[:project_id] = project.to_param
controller.params[:id] = File.join('master', blob.path)
end
def render_view
render partial: 'projects/blob/viewer', locals: { viewer: viewer }
end
context 'when the viewer is server side' do
before do
viewer_class.client_side = false
end
context 'when there is no render error' do
it 'adds a URL to the blob viewer element' do
render_view
expect(rendered).to have_css('.blob-viewer[data-url]')
end
it 'displays a spinner' do
render_view
expect(rendered).to have_css('i[aria-label="Loading content"]')
end
end
context 'when there is a render error' do
let(:blob) { fake_blob(size: 10.megabytes) }
it 'renders the error' do
render_view
expect(view).to render_template('projects/blob/_render_error')
end
end
end
context 'when the viewer is client side' do
before do
viewer_class.client_side = true
end
context 'when there is no render error' do
it 'prepares the viewer' do
expect(viewer).to receive(:prepare!)
render_view
end
it 'renders the viewer' do
render_view
expect(view).to render_template('projects/blob/viewers/_text')
end
end
context 'when there is a render error' do
let(:blob) { fake_blob(size: 10.megabytes) }
it 'renders the error' do
render_view
expect(view).to render_template('projects/blob/_render_error')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment