summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--.gitlab/issue_templates/Bug.md4
-rw-r--r--Gemfile.lock2
-rw-r--r--README.md1
-rw-r--r--app/assets/javascripts/blob/pdf/index.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js120
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js9
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/environments/components/environment.vue (renamed from app/assets/javascripts/environments/components/environment.js)153
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js21
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js21
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue (renamed from app/assets/javascripts/environments/folder/environments_folder_view.js)104
-rw-r--r--app/assets/javascripts/line_highlighter.js21
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.js101
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js10
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js31
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js31
-rw-r--r--app/assets/stylesheets/pages/builds.scss1
-rw-r--r--app/assets/stylesheets/pages/issues.scss13
-rw-r--r--app/controllers/concerns/renders_blob.rb17
-rw-r--r--app/controllers/projects/blob_controller.rb19
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/helpers/blob_helper.rb62
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/blob.rb175
-rw-r--r--app/models/blob_viewer/base.rb96
-rw-r--r--app/models/blob_viewer/binary_stl.rb10
-rw-r--r--app/models/blob_viewer/client_side.rb11
-rw-r--r--app/models/blob_viewer/download.rb17
-rw-r--r--app/models/blob_viewer/empty.rb9
-rw-r--r--app/models/blob_viewer/image.rb12
-rw-r--r--app/models/blob_viewer/markup.rb10
-rw-r--r--app/models/blob_viewer/notebook.rb12
-rw-r--r--app/models/blob_viewer/pdf.rb12
-rw-r--r--app/models/blob_viewer/rich.rb11
-rw-r--r--app/models/blob_viewer/server_side.rb11
-rw-r--r--app/models/blob_viewer/simple.rb11
-rw-r--r--app/models/blob_viewer/sketch.rb12
-rw-r--r--app/models/blob_viewer/svg.rb12
-rw-r--r--app/models/blob_viewer/text.rb11
-rw-r--r--app/models/blob_viewer/text_stl.rb5
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/network/graph.rb3
-rw-r--r--app/models/project_services/chat_notification_service.rb2
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/service.rb3
-rw-r--r--app/models/user.rb4
-rw-r--r--app/views/notify/repository_push_email.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml7
-rw-r--r--app/views/projects/blob/_content.html.haml8
-rw-r--r--app/views/projects/blob/_download.html.haml7
-rw-r--r--app/views/projects/blob/_header.html.haml10
-rw-r--r--app/views/projects/blob/_image.html.haml2
-rw-r--r--app/views/projects/blob/_render_error.html.haml7
-rw-r--r--app/views/projects/blob/_svg.html.haml9
-rw-r--r--app/views/projects/blob/_text.html.haml2
-rw-r--r--app/views/projects/blob/_too_large.html.haml5
-rw-r--r--app/views/projects/blob/_viewer.html.haml14
-rw-r--r--app/views/projects/blob/_viewer_switcher.html.haml12
-rw-r--r--app/views/projects/blob/show.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml7
-rw-r--r--app/views/projects/blob/viewers/_empty.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_image.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_notebook.html.haml (renamed from app/views/projects/blob/_notebook.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_pdf.html.haml (renamed from app/views/projects/blob/_pdf.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_sketch.html.haml (renamed from app/views/projects/blob/_sketch.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_stl.html.haml (renamed from app/views/projects/blob/_stl.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_svg.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_text.html.haml1
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/snippets/_blob.html.haml2
-rw-r--r--changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml4
-rw-r--r--changelogs/unreleased/30973-fix-network-graph-ordering.yml4
-rw-r--r--changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml4
-rw-r--r--changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml4
-rw-r--r--changelogs/unreleased/dm-blob-viewers.yml5
-rw-r--r--changelogs/unreleased/dm-fix-ghost-user-validation.yml4
-rw-r--r--changelogs/unreleased/fix_build_header_line_height.yml4
-rw-r--r--changelogs/unreleased/related-branch-ci-status-icon-alignment.yml4
-rw-r--r--db/migrate/20170421102337_remove_nil_type_services.rb12
-rw-r--r--db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb10
-rw-r--r--db/schema.rb2
-rw-r--r--features/project/source/browse_files.feature3
-rw-r--r--features/project/source/markdown_render.feature12
-rw-r--r--features/steps/project/source/browse_files.rb3
-rw-r--r--features/steps/project/source/markdown_render.rb10
-rw-r--r--features/steps/shared/markdown.rb2
-rw-r--r--lib/gitlab/data_builder/push.rb9
-rw-r--r--lib/gitlab/git/blob.rb4
-rw-r--r--lib/gitlab/git/repository.rb23
-rwxr-xr-xscripts/prepare_build.sh2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb49
-rw-r--r--spec/factories/services.rb13
-rw-r--r--spec/features/admin/admin_cohorts_spec.rb15
-rw-r--r--spec/features/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb312
-rw-r--r--spec/features/projects/files/browse_files_spec.rb5
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb24
-rw-r--r--spec/helpers/blob_helper_spec.rb120
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js161
-rw-r--r--spec/javascripts/environments/environment_spec.js5
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js4
-rw-r--r--spec/javascripts/fixtures/blob.rb29
-rw-r--r--spec/javascripts/pipelines/time_ago_spec.js64
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb29
-rw-r--r--spec/lib/gitlab/import_export/project.json41
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb1
-rw-r--r--spec/models/blob_spec.rb258
-rw-r--r--spec/models/blob_viewer/base_spec.rb186
-rw-r--r--spec/models/network/graph_spec.rb21
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb4
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb2
-rw-r--r--spec/models/service_spec.rb55
-rw-r--r--spec/models/user_spec.rb10
-rw-r--r--spec/requests/api/v3/deployments_spec.rb12
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb50
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb96
123 files changed, 2423 insertions, 625 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 230ca698ad0..50f1f65e4e8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -403,13 +403,6 @@ docs:check:links:
# Check the internal links
- bundle exec nanoc check internal_links
-bundler:check:
- stage: test
- <<: *dedicated-runner
- <<: *ruby-static-analysis
- script:
- - bundle check
-
bundler:audit:
stage: test
<<: *ruby-static-analysis
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 6bb21e6a3af..241d90a204b 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -26,6 +26,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info
<details>
+<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`)
@@ -33,11 +34,13 @@ logs, and code as it's very hard to read otherwise.)
(For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+</pre>
</details>
#### Results of GitLab application Check
<details>
+<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`)
@@ -47,6 +50,7 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing)
+</pre>
</details>
### Possible fixes
diff --git a/Gemfile.lock b/Gemfile.lock
index 4c3b2480b54..52707628748 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -429,7 +429,7 @@ GEM
multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
- mail (2.6.4)
+ mail (2.6.5)
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
memoist (0.15.0)
diff --git a/README.md b/README.md
index 10d69efdc6b..59de828e1ac 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Test coverage
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index a74c2db9a61..9161be98853 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -31,7 +31,7 @@ export default () => {
},
},
template: `
- <div class="container-fluid md prepend-top-default append-bottom-default">
+ <div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
new file mode 100644
index 00000000000..7efa8537298
--- /dev/null
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -0,0 +1,120 @@
+/* global Flash */
+export default class BlobViewer {
+ constructor() {
+ this.switcher = document.querySelector('.js-blob-viewer-switcher');
+ this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
+ this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
+ this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
+ this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
+ this.$blobContentHolder = $('#blob-content-holder');
+
+ let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
+
+ this.initBindings();
+
+ if (this.switcher && location.hash.indexOf('#L') === 0) {
+ initialViewerName = 'simple';
+ }
+
+ this.switchToViewer(initialViewerName);
+ }
+
+ initBindings() {
+ if (this.switcherBtns.length) {
+ Array.from(this.switcherBtns)
+ .forEach((el) => {
+ el.addEventListener('click', this.switchViewHandler.bind(this));
+ });
+ }
+
+ if (this.copySourceBtn) {
+ this.copySourceBtn.addEventListener('click', () => {
+ if (this.copySourceBtn.classList.contains('disabled')) return;
+
+ this.switchToViewer('simple');
+ });
+ }
+ }
+
+ switchViewHandler(e) {
+ const target = e.currentTarget;
+
+ e.preventDefault();
+
+ this.switchToViewer(target.getAttribute('data-viewer'));
+ }
+
+ toggleCopyButtonState() {
+ if (!this.copySourceBtn) return;
+
+ if (this.simpleViewer.getAttribute('data-loaded')) {
+ this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
+ this.copySourceBtn.classList.remove('disabled');
+ } else if (this.activeViewer === this.simpleViewer) {
+ this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard');
+ this.copySourceBtn.classList.add('disabled');
+ } else {
+ this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
+ this.copySourceBtn.classList.add('disabled');
+ }
+
+ $(this.copySourceBtn).tooltip('fixTitle');
+ }
+
+ loadViewer(viewerParam) {
+ const viewer = viewerParam;
+ const url = viewer.getAttribute('data-url');
+
+ if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
+ return;
+ }
+
+ viewer.setAttribute('data-loading', 'true');
+
+ $.ajax({
+ url,
+ dataType: 'JSON',
+ })
+ .fail(() => new Flash('Error loading source view'))
+ .done((data) => {
+ viewer.innerHTML = data.html;
+ $(viewer).syntaxHighlight();
+
+ viewer.setAttribute('data-loaded', 'true');
+
+ this.$blobContentHolder.trigger('highlight:line');
+
+ this.toggleCopyButtonState();
+ });
+ }
+
+ switchToViewer(name) {
+ const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`);
+ if (this.activeViewer === newViewer) return;
+
+ const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
+ const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
+ const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`);
+
+ if (oldButton) {
+ oldButton.classList.remove('active');
+ }
+
+ if (newButton) {
+ newButton.classList.add('active');
+ newButton.blur();
+ }
+
+ if (oldViewer) {
+ oldViewer.classList.add('hidden');
+ }
+
+ newViewer.classList.remove('hidden');
+
+ this.activeViewer = newViewer;
+
+ this.toggleCopyButtonState();
+
+ this.loadViewer(newViewer);
+ }
+}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 68a1c1de1df..e704be8b53e 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
- beforeUpdate() {
- if (this.state.pipelines.length &&
- this.$children &&
- !this.isMakingRequest &&
- !this.isLoading) {
- this.store.startTimeAgoLoops.call(this, Vue);
- }
- },
-
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index b20673cd03c..d3d75c4bf4a 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -48,6 +48,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
+import BlobViewer from './blob/viewer/index';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -299,6 +300,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:show':
+ new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
initBlob();
break;
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.vue
index f7175e412da..f319d6ca0c8 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -1,6 +1,7 @@
+<script>
+
/* eslint-disable no-new */
/* global Flash */
-import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
@@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
-export default Vue.component('environment-component', {
+export default {
components: {
'environment-table': EnvironmentTable,
@@ -140,76 +141,90 @@ export default Vue.component('environment-component', {
});
},
},
-
- template: `
- <div :class="cssContainerClass">
- <div class="top-area">
- <ul v-if="!isLoading" class="nav-links">
- <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
- <a :href="projectEnvironmentsPath">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li v-bind:class="{ 'active' : scope === 'stopped' }">
- <a :href="projectStoppedEnvironmentsPath">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
- <a :href="newEnvironmentPath" class="btn btn-create">
- New environment
+};
+</script>
+<template>
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <ul
+ v-if="!isLoading"
+ class="nav-links">
+ <li :class="{ active: scope === null || scope === 'available' }">
+ <a :href="projectEnvironmentsPath">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li :class="{ active : scope === 'stopped' }">
+ <a :href="projectStoppedEnvironmentsPath">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
</a>
- </div>
+ </li>
+ </ul>
+ <div
+ v-if="canCreateEnvironmentParsed && !isLoading"
+ class="nav-controls">
+ <a
+ :href="newEnvironmentPath"
+ class="btn btn-create">
+ New environment
+ </a>
</div>
+ </div>
+
+ <div class="content-list environments-container">
+ <div
+ class="environments-list-loading text-center"
+ v-if="isLoading">
- <div class="content-list environments-container">
- <div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </h2>
- <p class="blank-state-text">
- Environments are places where code gets deployed, such as staging or production.
- <br />
- <a :href="helpPagePath">
- Read more about environments
- </a>
- </p>
-
- <a v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New Environment
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </div>
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.environments.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
</a>
- </div>
-
- <div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :service="service"
- :is-loading-folder-content="isLoadingFolderContent" />
- </div>
-
- <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation">
- </table-pagination>
+ </p>
+
+ <a
+ v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create js-new-environment-button">
+ New Environment
+ </a>
</div>
+
+ <div
+ class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :service="service"
+ :is-loading-folder-content="isLoadingFolderContent" />
+ </div>
+
+ <table-pagination
+ v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation" />
</div>
- `,
-});
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js
index 8d963b335cf..c0662125f28 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/environments_bundle.js
@@ -1,13 +1,10 @@
-import EnvironmentsComponent from './components/environment';
+import Vue from 'vue';
+import EnvironmentsComponent from './components/environment.vue';
-$(() => {
- window.gl = window.gl || {};
-
- if (gl.EnvironmentsListApp) {
- gl.EnvironmentsListApp.$destroy(true);
- }
-
- gl.EnvironmentsListApp = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
-});
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#environments-list-view',
+ components: {
+ 'environments-table-app': EnvironmentsComponent,
+ },
+ render: createElement => createElement('environments-table-app'),
+}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index f939eccf246..9add8c3d721 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,13 +1,10 @@
-import EnvironmentsFolderComponent from './environments_folder_view';
+import Vue from 'vue';
+import EnvironmentsFolderComponent from './environments_folder_view.vue';
-$(() => {
- window.gl = window.gl || {};
-
- if (gl.EnvironmentsListFolderApp) {
- gl.EnvironmentsListFolderApp.$destroy(true);
- }
-
- gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
- el: document.querySelector('#environments-folder-list-view'),
- });
-});
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#environments-folder-list-view',
+ components: {
+ 'environments-folder-app': EnvironmentsFolderComponent,
+ },
+ render: createElement => createElement('environments-folder-app'),
+}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 05d44f77d1d..d27b2acfcdf 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,6 +1,6 @@
+<script>
/* eslint-disable no-new */
/* global Flash */
-import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
@@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
-export default Vue.component('environment-folder-view', {
+export default {
components: {
'environment-table': EnvironmentTable,
'table-pagination': TablePaginationComponent,
@@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', {
return param;
},
},
+};
+</script>
+<template>
+ <div :class="cssContainerClass">
+ <div
+ class="top-area"
+ v-if="!isLoading">
+
+ <h4 class="js-folder-name environments-folder-name">
+ Environments / <b>{{folderName}}</b>
+ </h4>
+
+ <ul class="nav-links">
+ <li :class="{ active: scope === null || scope === 'available' }">
+ <a
+ :href="availablePath"
+ class="js-available-environments-folder-tab">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li :class="{ active : scope === 'stopped' }">
+ <a
+ :href="stoppedPath"
+ class="js-stopped-environments-folder-tab">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
- template: `
- <div :class="cssContainerClass">
- <div class="top-area" v-if="!isLoading">
-
- <h4 class="js-folder-name environments-folder-name">
- Environments / <b>{{folderName}}</b>
- </h4>
-
- <ul class="nav-links">
- <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
- <a :href="availablePath" class="js-available-environments-folder-tab">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li v-bind:class="{ 'active' : scope === 'stopped' }">
- <a :href="stoppedPath" class="js-stopped-environments-folder-tab">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
+ <div class="environments-container">
+ <div
+ class="environments-list-loading text-center"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"/>
</div>
- <div class="environments-container">
- <div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
+ <div
+ class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :service="service"/>
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :service="service"/>
- <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation"/>
- </div>
+ <table-pagination
+ v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation"/>
</div>
</div>
- `,
-});
+ </div>
+</template>
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 1821ca18053..a6f7bea99f5 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo');
LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) {
- var range;
if (hash == null) {
// Initialize a LineHighlighter object
//
@@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo');
this.setHash = bind(this.setHash, this);
this.highlightLine = bind(this.highlightLine, this);
this.clickHandler = bind(this.clickHandler, this);
+ this.highlightHash = this.highlightHash.bind(this);
this._hash = hash;
this.bindEvents();
- if (hash !== '') {
- range = this.hashToRange(hash);
+ this.highlightHash();
+ }
+
+ LineHighlighter.prototype.bindEvents = function() {
+ const $blobContentHolder = $('#blob-content-holder');
+ $blobContentHolder.on('click', 'a[data-line-number]', this.clickHandler);
+ $blobContentHolder.on('highlight:line', this.highlightHash);
+ };
+
+ LineHighlighter.prototype.highlightHash = function() {
+ var range;
+ if (this._hash !== '') {
+ range = this.hashToRange(this._hash);
if (range[0]) {
this.highlightRange(range);
$.scrollTo("#L" + range[0], {
@@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo');
});
}
}
- }
-
- LineHighlighter.prototype.bindEvents = function() {
- $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
};
LineHighlighter.prototype.clickHandler = function(event) {
diff --git a/app/assets/javascripts/pipelines/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js
index 498d0715f54..188f74cc705 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.js
+++ b/app/assets/javascripts/pipelines/components/time_ago.js
@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
export default {
+ props: {
+ finishedTime: {
+ type: String,
+ required: true,
+ },
+
+ duration: {
+ type: Number,
+ required: true,
+ },
+ },
+
data() {
return {
- currentTime: new Date(),
iconTimerSvg,
};
},
- props: ['pipeline'],
+
+ updated() {
+ $(this.$refs.tooltip).tooltip('fixTitle');
+ },
+
computed: {
- timeAgo() {
- return gl.utils.getTimeago();
+ hasDuration() {
+ return this.duration > 0;
},
- localTimeFinished() {
- return gl.utils.formatDate(this.pipeline.details.finished_at);
+
+ hasFinishedTime() {
+ return this.finishedTime !== '';
},
- timeStopped() {
- const changeTime = this.currentTime;
- const options = {
- weekday: 'long',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- options.timeZoneName = 'short';
- const finished = this.pipeline.details.finished_at;
- if (!finished && changeTime) return false;
- return ({ words: this.timeAgo.format(finished) });
+
+ localTimeFinished() {
+ return gl.utils.formatDate(this.finishedTime);
},
- duration() {
- const { duration } = this.pipeline.details;
- const date = new Date(duration * 1000);
+
+ durationFormated() {
+ const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
- if (hh < 10) hh = `0${hh}`;
- if (mm < 10) mm = `0${mm}`;
- if (ss < 10) ss = `0${ss}`;
+ // left pad
+ if (hh < 10) {
+ hh = `0${hh}`;
+ }
+ if (mm < 10) {
+ mm = `0${mm}`;
+ }
+ if (ss < 10) {
+ ss = `0${ss}`;
+ }
- if (duration !== null) return `${hh}:${mm}:${ss}`;
- return false;
+ return `${hh}:${mm}:${ss}`;
},
- },
- methods: {
- changeTime() {
- this.currentTime = new Date();
+
+ finishedTimeFormated() {
+ const timeAgo = gl.utils.getTimeago();
+
+ return timeAgo.format(this.finishedTime);
},
},
+
template: `
<td class="pipelines-time-ago">
- <p class="duration" v-if='duration'>
- <span v-html="iconTimerSvg"></span>
- {{duration}}
+ <p
+ class="duration"
+ v-if="hasDuration">
+ <span
+ v-html="iconTimerSvg">
+ </span>
+ {{durationFormated}}
</p>
- <p class="finished-at" v-if='timeStopped'>
- <i class="fa fa-calendar"></i>
+
+ <p
+ class="finished-at"
+ v-if="hasFinishedTime">
+
+ <i
+ class="fa fa-calendar"
+ aria-hidden="true" />
+
<time
+ ref="tooltip"
data-toggle="tooltip"
data-placement="top"
data-container="body"
- :data-original-title='localTimeFinished'>
- {{timeStopped.words}}
+ :title="localTimeFinished">
+ {{finishedTimeFormated}}
</time>
</p>
</td>
diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
index 6eea4812f33..93d4818231f 100644
--- a/app/assets/javascripts/pipelines/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
@@ -161,15 +160,6 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
- beforeUpdate() {
- if (this.state.pipelines.length &&
- this.$children &&
- !this.isMakingRequest &&
- !this.isLoading) {
- this.store.startTimeAgoLoops.call(this, Vue);
- }
- },
-
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
index 377ec8ba2cc..ffefe0192f2 100644
--- a/app/assets/javascripts/pipelines/stores/pipelines_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
@@ -1,6 +1,3 @@
-/* eslint-disable no-underscore-dangle*/
-import VueRealtimeListener from '../../vue_realtime_listener';
-
export default class PipelinesStore {
constructor() {
this.state = {};
@@ -30,32 +27,4 @@ export default class PipelinesStore {
this.state.pageInfo = paginationInfo;
}
-
- /**
- * FIXME: Move this inside the component.
- *
- * Once the data is received we will start the time ago loops.
- *
- * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
- * update the time to show how long as passed.
- *
- */
- startTimeAgoLoops() {
- const startTimeLoops = () => {
- this.timeLoopInterval = setInterval(() => {
- this.$children[0].$children.reduce((acc, component) => {
- const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
- acc.push(timeAgoComponent);
- return acc;
- }, []).forEach(e => e.changeTime());
- }, 10000);
- };
-
- startTimeLoops();
-
- const removeIntervals = () => clearInterval(this.timeLoopInterval);
- const startIntervals = () => startTimeLoops();
-
- VueRealtimeListener(removeIntervals, startIntervals);
- }
}
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js b/app/assets/javascripts/vue_realtime_listener/index.js
deleted file mode 100644
index 4ddb2f975b0..00000000000
--- a/app/assets/javascripts/vue_realtime_listener/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default (removeIntervals, startIntervals) => {
- window.removeEventListener('focus', startIntervals);
- window.removeEventListener('blur', removeIntervals);
- window.removeEventListener('onbeforeload', removeIntervals);
-
- window.addEventListener('focus', startIntervals);
- window.addEventListener('blur', removeIntervals);
- window.addEventListener('onbeforeload', removeIntervals);
-};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index 62b7131de51..79806bc7204 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,5 +1,4 @@
/* eslint-disable no-param-reassign */
-
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
@@ -166,6 +165,32 @@ export default {
}
return undefined;
},
+
+ /**
+ * Timeago components expects a number
+ *
+ * @return {type} description
+ */
+ pipelineDuration() {
+ if (this.pipeline.details && this.pipeline.details.duration) {
+ return this.pipeline.details.duration;
+ }
+
+ return 0;
+ },
+
+ /**
+ * Timeago component expects a String.
+ *
+ * @return {String}
+ */
+ pipelineFinishedAt() {
+ if (this.pipeline.details && this.pipeline.details.finished_at) {
+ return this.pipeline.details.finished_at;
+ }
+
+ return '';
+ },
},
template: `
@@ -192,7 +217,9 @@ export default {
</div>
</td>
- <time-ago :pipeline="pipeline"/>
+ <time-ago
+ :duration="pipelineDuration"
+ :finished-time="pipelineFinishedAt" />
<td class="pipeline-actions">
<div class="pull-right btn-group">
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 411f1c4442b..724b4080ee0 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -200,6 +200,7 @@
.header-content {
flex: 1;
+ line-height: 1.8;
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index b2f45625a2a..2aa52986e0a 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -101,11 +101,16 @@ ul.related-merge-requests > li {
}
}
-.merge-request-ci-status {
+.merge-request-ci-status,
+.related-merge-requests {
+ .ci-status-link {
+ display: block;
+ margin-top: 3px;
+ margin-right: 5px;
+ }
+
svg {
- margin-right: 4px;
- position: relative;
- top: 1px;
+ display: block;
}
}
diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb
new file mode 100644
index 00000000000..d478c3bb6ca
--- /dev/null
+++ b/app/controllers/concerns/renders_blob.rb
@@ -0,0 +1,17 @@
+module RendersBlob
+ extend ActiveSupport::Concern
+
+ def render_blob_json(blob)
+ viewer =
+ if params[:viewer] == 'rich'
+ blob.rich_viewer
+ else
+ blob.simple_viewer
+ end
+ return render_404 unless viewer
+
+ render json: {
+ html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
+ }
+ end
+end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 9fce1db6742..be5822b2cd4 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -2,6 +2,7 @@
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
+ include RendersBlob
include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
@@ -34,8 +35,20 @@ class Projects::BlobController < Projects::ApplicationController
end
def show
- environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
- @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+ @blob.override_max_size! if params[:override_max_size] == 'true'
+
+ respond_to do |format|
+ format.html do
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
+ render 'show'
+ end
+
+ format.json do
+ render_blob_json(@blob)
+ end
+ end
end
def edit
@@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
- @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
+ @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
if @blob
@blob
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index c55b37ae0dd..a0b08ad130f 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob?
- if @blob.lfs_pointer? && project.lfs_enabled?
+ if @blob.valid_lfs_pointer?
send_lfs_object
else
send_git_blob @repository, @blob
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 36b16421e8f..cc47654dc06 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -52,7 +52,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
- elsif blob.lfs_pointer?
+ elsif blob.valid_lfs_pointer?
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
@@ -95,7 +95,7 @@ module BlobHelper
end
def can_modify_blob?(blob, project = @project, ref = @ref)
- !blob.lfs_pointer? && can_edit_tree?(project, ref)
+ !blob.valid_lfs_pointer? && can_edit_tree?(project, ref)
end
def leave_edit_message
@@ -118,28 +118,15 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
- def blob_text_viewable?(blob)
- blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
- end
-
- def blob_rendered_as_text?(blob)
- blob_text_viewable?(blob) && blob.to_partial_path(@project) == 'text'
- end
-
- def blob_size(blob)
- if blob.lfs_pointer?
- blob.lfs_size
- else
- blob.size
- end
+ def blob_raw_url
+ namespace_project_raw_path(@project.namespace, @project, @id)
end
# SVGs can contain malicious JavaScript; only include whitelisted
# elements and attributes. Note that this whitelist is by no means complete
# and may omit some elements.
- def sanitize_svg(blob)
- blob.data = Gitlab::Sanitizers::SVG.clean(blob.data)
- blob
+ def sanitize_svg_data(data)
+ Gitlab::Sanitizers::SVG.clean(data)
end
# If we blindly set the 'real' content type when serving a Git blob we
@@ -221,13 +208,42 @@ module BlobHelper
clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
end
- def copy_blob_content_button(blob)
- return if markup?(blob.name)
-
- clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
+ def copy_blob_source_button(blob)
+ clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
end
def open_raw_file_button(path)
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
end
+
+ def blob_render_error_reason(viewer)
+ case viewer.render_error
+ when :too_large
+ max_size =
+ if viewer.absolutely_too_large?
+ viewer.absolute_max_size
+ elsif viewer.too_large?
+ viewer.max_size
+ end
+ "it is larger than #{number_to_human_size(max_size)}"
+ when :server_side_but_stored_in_lfs
+ "it is stored in LFS"
+ end
+ end
+
+ def blob_render_error_options(viewer)
+ options = []
+
+ if viewer.render_error == :too_large && viewer.can_override_max_size?
+ options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
+ end
+
+ if viewer.rich? && viewer.blob.rendered_as_text?
+ options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
+ end
+
+ options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer')
+
+ options
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index dd1a6922968..cf042717c95 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
+ validates :uuid, presence: true
+
validates :session_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -344,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base
private
+ def ensure_uuid!
+ return if uuid?
+
+ self.uuid = SecureRandom.uuid
+ end
+
def check_repository_storages
invalid = repository_storages - Gitlab.config.repositories.storages.keys
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 55872acef51..290df5d5520 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -3,8 +3,40 @@ class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
- # The maximum size of an SVG that can be displayed.
- MAXIMUM_SVG_SIZE = 2.megabytes
+ MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
+
+ # Finding a viewer for a blob happens based only on extension and whether the
+ # blob is binary or text, which means 1 blob should only be matched by 1 viewer,
+ # and the order of these viewers doesn't really matter.
+ #
+ # However, when the blob is an LFS pointer, we cannot know for sure whether the
+ # file being pointed to is binary or text. In this case, we match only on
+ # extension, preferring binary viewers over text ones if both exist, since the
+ # large files referred to in "Large File Storage" are much more likely to be
+ # binary than text.
+ #
+ # `.stl` files, for example, exist in both binary and text forms, and are
+ # handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob
+ # type. LFS pointers to `.stl` files are assumed to always be the binary kind,
+ # and use the `BinarySTL` viewer.
+ RICH_VIEWERS = [
+ BlobViewer::Markup,
+ BlobViewer::Notebook,
+ BlobViewer::SVG,
+
+ BlobViewer::Image,
+ BlobViewer::Sketch,
+
+ BlobViewer::PDF,
+
+ BlobViewer::BinarySTL,
+ BlobViewer::TextSTL,
+ ].freeze
+
+ BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
+ TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
+
+ attr_reader :project
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
@@ -16,10 +48,16 @@ class Blob < SimpleDelegator
#
# blob = Blob.decorate(nil)
# puts "truthy" if blob # No output
- def self.decorate(blob)
+ def self.decorate(blob, project = nil)
return if blob.nil?
- new(blob)
+ new(blob, project)
+ end
+
+ def initialize(blob, project = nil)
+ @project = project
+
+ super(blob)
end
# Returns the data of the blob.
@@ -35,82 +73,107 @@ class Blob < SimpleDelegator
end
def no_highlighting?
- size && size > 1.megabyte
+ size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
- def only_display_raw?
+ def too_large?
size && truncated?
end
+ # Returns the size of the file that this blob represents. If this blob is an
+ # LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
+ # the size of the blob itself.
+ def raw_size
+ if valid_lfs_pointer?
+ lfs_size
+ else
+ size
+ end
+ end
+
+ # Returns whether the file that this blob represents is binary. If this blob is
+ # an LFS pointer, we assume the file stored in LFS is binary, unless a
+ # text-based rich blob viewer matched on the file's extension. Otherwise, this
+ # depends on the type of the blob itself.
+ def raw_binary?
+ if valid_lfs_pointer?
+ if rich_viewer
+ rich_viewer.binary?
+ else
+ true
+ end
+ else
+ binary?
+ end
+ end
+
def extension
- extname.downcase.delete('.')
+ @extension ||= extname.downcase.delete('.')
end
- def svg?
- text? && language && language.name == 'SVG'
+ def video?
+ UploaderHelper::VIDEO_EXT.include?(extension)
end
- def pdf?
- extension == 'pdf'
+ def readable_text?
+ text? && !valid_lfs_pointer? && !too_large?
end
- def ipython_notebook?
- text? && language&.name == 'Jupyter Notebook'
+ def valid_lfs_pointer?
+ lfs_pointer? && project&.lfs_enabled?
end
- def sketch?
- binary? && extension == 'sketch'
+ def invalid_lfs_pointer?
+ lfs_pointer? && !project&.lfs_enabled?
end
- def stl?
- extension == 'stl'
+ def simple_viewer
+ @simple_viewer ||= simple_viewer_class.new(self)
end
- def markup?
- text? && Gitlab::MarkupHelper.markup?(name)
+ def rich_viewer
+ return @rich_viewer if defined?(@rich_viewer)
+
+ @rich_viewer = rich_viewer_class&.new(self)
end
- def size_within_svg_limits?
- size <= MAXIMUM_SVG_SIZE
+ def rendered_as_text?(ignore_errors: true)
+ simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end
- def video?
- UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
+ def show_viewer_switcher?
+ rendered_as_text? && rich_viewer
end
- def to_partial_path(project)
- if lfs_pointer?
- if project.lfs_enabled?
- 'download'
- else
- 'text'
- end
- elsif image?
- 'image'
- elsif svg?
- 'svg'
- elsif pdf?
- 'pdf'
- elsif ipython_notebook?
- 'notebook'
- elsif sketch?
- 'sketch'
- elsif stl?
- 'stl'
- elsif markup?
- if only_display_raw?
- 'too_large'
- else
- 'markup'
- end
- elsif text?
- if only_display_raw?
- 'too_large'
- else
- 'text'
- end
- else
- 'download'
+ def override_max_size!
+ simple_viewer&.override_max_size = true
+ rich_viewer&.override_max_size = true
+ end
+
+ private
+
+ def simple_viewer_class
+ if empty?
+ BlobViewer::Empty
+ elsif raw_binary?
+ BlobViewer::Download
+ else # text
+ BlobViewer::Text
end
end
+
+ def rich_viewer_class
+ return if invalid_lfs_pointer? || empty?
+
+ classes =
+ if valid_lfs_pointer?
+ BINARY_VIEWERS + TEXT_VIEWERS
+ elsif binary?
+ BINARY_VIEWERS
+ else # text
+ TEXT_VIEWERS
+ end
+
+ classes.find { |viewer_class| viewer_class.can_render?(self) }
+ end
end
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
new file mode 100644
index 00000000000..f944b00c9d3
--- /dev/null
+++ b/app/models/blob_viewer/base.rb
@@ -0,0 +1,96 @@
+module BlobViewer
+ class Base
+ class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
+
+ delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
+
+ attr_reader :blob
+ attr_accessor :override_max_size
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ def self.partial_path
+ "projects/blob/viewers/#{partial_name}"
+ end
+
+ def self.rich?
+ type == :rich
+ end
+
+ def self.simple?
+ type == :simple
+ end
+
+ def self.client_side?
+ client_side
+ end
+
+ def self.server_side?
+ !client_side?
+ end
+
+ def self.binary?
+ binary
+ end
+
+ def self.text?
+ !binary?
+ end
+
+ def self.can_render?(blob)
+ !extensions || extensions.include?(blob.extension)
+ end
+
+ def too_large?
+ blob.raw_size > max_size
+ end
+
+ def absolutely_too_large?
+ blob.raw_size > absolute_max_size
+ end
+
+ def can_override_max_size?
+ too_large? && !absolutely_too_large?
+ end
+
+ # This method is used on the server side to check whether we can attempt to
+ # render the blob at all. Human-readable error messages are found in the
+ # `BlobHelper#blob_render_error_reason` helper.
+ #
+ # This method does not and should not load the entire blob contents into
+ # memory, and should not be overridden to do so in order to validate the
+ # format of the blob.
+ #
+ # Prefer to implement a client-side viewer, where the JS component loads the
+ # binary from `blob_raw_url` and does its own format validation and error
+ # rendering, especially for potentially large binary formats.
+ def render_error
+ return @render_error if defined?(@render_error)
+
+ @render_error =
+ if server_side_but_stored_in_lfs?
+ # Files stored in LFS can only be rendered using a client-side viewer,
+ # since we do not want to read large amounts of data into memory on the
+ # server side. Client-side viewers use JS and can fetch the file from
+ # `blob_raw_url` using AJAX.
+ :server_side_but_stored_in_lfs
+ elsif override_max_size ? absolutely_too_large? : too_large?
+ :too_large
+ end
+ end
+
+ def prepare!
+ if server_side? && blob.project
+ blob.load_all_data!(blob.project.repository)
+ end
+ end
+
+ private
+
+ def server_side_but_stored_in_lfs?
+ server_side? && blob.valid_lfs_pointer?
+ end
+ end
+end
diff --git a/app/models/blob_viewer/binary_stl.rb b/app/models/blob_viewer/binary_stl.rb
new file mode 100644
index 00000000000..80393471ef2
--- /dev/null
+++ b/app/models/blob_viewer/binary_stl.rb
@@ -0,0 +1,10 @@
+module BlobViewer
+ class BinarySTL < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'stl'
+ self.extensions = %w(stl)
+ self.binary = true
+ end
+end
diff --git a/app/models/blob_viewer/client_side.rb b/app/models/blob_viewer/client_side.rb
new file mode 100644
index 00000000000..42ec68f864b
--- /dev/null
+++ b/app/models/blob_viewer/client_side.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module ClientSide
+ extend ActiveSupport::Concern
+
+ included do
+ self.client_side = true
+ self.max_size = 10.megabytes
+ self.absolute_max_size = 50.megabytes
+ end
+ end
+end
diff --git a/app/models/blob_viewer/download.rb b/app/models/blob_viewer/download.rb
new file mode 100644
index 00000000000..adc06587f69
--- /dev/null
+++ b/app/models/blob_viewer/download.rb
@@ -0,0 +1,17 @@
+module BlobViewer
+ class Download < Base
+ include Simple
+ # We treat the Download viewer as if it renders the content client-side,
+ # so that it doesn't attempt to load the entire blob contents and is
+ # rendered synchronously instead of loaded asynchronously.
+ include ClientSide
+
+ self.partial_name = 'download'
+ self.binary = true
+
+ # We can always render the Download viewer, even if the blob is in LFS or too large.
+ def render_error
+ nil
+ end
+ end
+end
diff --git a/app/models/blob_viewer/empty.rb b/app/models/blob_viewer/empty.rb
new file mode 100644
index 00000000000..d9d128eb273
--- /dev/null
+++ b/app/models/blob_viewer/empty.rb
@@ -0,0 +1,9 @@
+module BlobViewer
+ class Empty < Base
+ include Simple
+ include ServerSide
+
+ self.partial_name = 'empty'
+ self.binary = true
+ end
+end
diff --git a/app/models/blob_viewer/image.rb b/app/models/blob_viewer/image.rb
new file mode 100644
index 00000000000..c4eae5c79c2
--- /dev/null
+++ b/app/models/blob_viewer/image.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class Image < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'image'
+ self.extensions = UploaderHelper::IMAGE_EXT
+ self.binary = true
+ self.switcher_icon = 'picture-o'
+ self.switcher_title = 'image'
+ end
+end
diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb
new file mode 100644
index 00000000000..8fdbab30dd1
--- /dev/null
+++ b/app/models/blob_viewer/markup.rb
@@ -0,0 +1,10 @@
+module BlobViewer
+ class Markup < Base
+ include Rich
+ include ServerSide
+
+ self.partial_name = 'markup'
+ self.extensions = Gitlab::MarkupHelper::EXTENSIONS
+ self.binary = false
+ end
+end
diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb
new file mode 100644
index 00000000000..8632b8a9885
--- /dev/null
+++ b/app/models/blob_viewer/notebook.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class Notebook < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'notebook'
+ self.extensions = %w(ipynb)
+ self.binary = false
+ self.switcher_icon = 'file-text-o'
+ self.switcher_title = 'notebook'
+ end
+end
diff --git a/app/models/blob_viewer/pdf.rb b/app/models/blob_viewer/pdf.rb
new file mode 100644
index 00000000000..65805f5f388
--- /dev/null
+++ b/app/models/blob_viewer/pdf.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class PDF < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'pdf'
+ self.extensions = %w(pdf)
+ self.binary = true
+ self.switcher_icon = 'file-pdf-o'
+ self.switcher_title = 'PDF'
+ end
+end
diff --git a/app/models/blob_viewer/rich.rb b/app/models/blob_viewer/rich.rb
new file mode 100644
index 00000000000..be373dbc948
--- /dev/null
+++ b/app/models/blob_viewer/rich.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module Rich
+ extend ActiveSupport::Concern
+
+ included do
+ self.type = :rich
+ self.switcher_icon = 'file-text-o'
+ self.switcher_title = 'rendered file'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb
new file mode 100644
index 00000000000..899107d02ea
--- /dev/null
+++ b/app/models/blob_viewer/server_side.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module ServerSide
+ extend ActiveSupport::Concern
+
+ included do
+ self.client_side = false
+ self.max_size = 2.megabytes
+ self.absolute_max_size = 5.megabytes
+ end
+ end
+end
diff --git a/app/models/blob_viewer/simple.rb b/app/models/blob_viewer/simple.rb
new file mode 100644
index 00000000000..454a20495fc
--- /dev/null
+++ b/app/models/blob_viewer/simple.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module Simple
+ extend ActiveSupport::Concern
+
+ included do
+ self.type = :simple
+ self.switcher_icon = 'code'
+ self.switcher_title = 'source'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/sketch.rb b/app/models/blob_viewer/sketch.rb
new file mode 100644
index 00000000000..818456778e1
--- /dev/null
+++ b/app/models/blob_viewer/sketch.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class Sketch < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'sketch'
+ self.extensions = %w(sketch)
+ self.binary = true
+ self.switcher_icon = 'file-image-o'
+ self.switcher_title = 'preview'
+ end
+end
diff --git a/app/models/blob_viewer/svg.rb b/app/models/blob_viewer/svg.rb
new file mode 100644
index 00000000000..b7e5cd71e6b
--- /dev/null
+++ b/app/models/blob_viewer/svg.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class SVG < Base
+ include Rich
+ include ServerSide
+
+ self.partial_name = 'svg'
+ self.extensions = %w(svg)
+ self.binary = false
+ self.switcher_icon = 'picture-o'
+ self.switcher_title = 'image'
+ end
+end
diff --git a/app/models/blob_viewer/text.rb b/app/models/blob_viewer/text.rb
new file mode 100644
index 00000000000..e27b2c2b493
--- /dev/null
+++ b/app/models/blob_viewer/text.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ class Text < Base
+ include Simple
+ include ServerSide
+
+ self.partial_name = 'text'
+ self.binary = false
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 10.megabytes
+ end
+end
diff --git a/app/models/blob_viewer/text_stl.rb b/app/models/blob_viewer/text_stl.rb
new file mode 100644
index 00000000000..8184dc0104c
--- /dev/null
+++ b/app/models/blob_viewer/text_stl.rb
@@ -0,0 +1,5 @@
+module BlobViewer
+ class TextSTL < BinarySTL
+ self.binary = false
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 8b8b3f00202..bb4cb8efd15 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -316,7 +316,7 @@ class Commit
def uri_type(path)
entry = @raw.tree.path(path)
if entry[:type] == :blob
- blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]))
+ blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob
else
entry[:type]
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 0bbc9451ffd..59737bb6085 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -107,7 +107,8 @@ module Network
def find_commits(skip = 0)
opts = {
max_count: self.class.max_count,
- skip: skip
+ skip: skip,
+ order: :date
}
opts[:ref] = @commit.id if @filter_ref
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index f2dfb87dbda..fa782c6fbb7 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -22,7 +22,7 @@ class ChatNotificationService < Service
end
def can_test?
- super && valid?
+ valid?
end
def self.supported_events
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e74edb8e6f7..d02aea49689 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -450,7 +450,7 @@ class Repository
def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha)
- Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
+ Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project)
end
rescue Gitlab::Git::Repository::NoRepository
nil
diff --git a/app/models/service.rb b/app/models/service.rb
index dc76bf925d3..c71a7d169ec 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base
has_one :service_hook
validates :project_id, presence: true, unless: proc { |service| service.template? }
+ validates :type, presence: true
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') }
@@ -131,7 +132,7 @@ class Service < ActiveRecord::Base
end
def can_test?
- !project.empty_repo?
+ true
end
# reason why service cannot be tested
diff --git a/app/models/user.rb b/app/models/user.rb
index 774d4caa806..bd9c9f99663 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1068,11 +1068,13 @@ class User < ActiveRecord::Base
User.find_by_email(s)
end
- scope.create(
+ user = scope.build(
username: username,
email: email,
&creation_block
)
+ user.save(validate: false)
+ user
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c6b1db17f91..02eb7c8462c 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -74,7 +74,7 @@
- else
%hr
- blob = diff_file.blob
- - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
+ - if blob && blob.readable_text?
%table.code.white
= render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, plain: true, email: true }
- else
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 9aafff343f0..3f12d64d044 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -26,9 +26,4 @@
%article.file-holder
= render "projects/blob/header", blob: blob
- - if blob.empty?
- .file-content.code
- .nothing-here-block
- Empty file
- - else
- = render blob.to_partial_path(@project), blob: blob
+ = render 'projects/blob/content', blob: blob
diff --git a/app/views/projects/blob/_content.html.haml b/app/views/projects/blob/_content.html.haml
new file mode 100644
index 00000000000..7afbd85cd6d
--- /dev/null
+++ b/app/views/projects/blob/_content.html.haml
@@ -0,0 +1,8 @@
+- simple_viewer = blob.simple_viewer
+- rich_viewer = blob.rich_viewer
+- rich_viewer_active = rich_viewer && params[:viewer] != 'simple'
+
+= render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active
+
+- if rich_viewer
+ = render 'projects/blob/viewer', viewer: rich_viewer, hidden: !rich_viewer_active
diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml
deleted file mode 100644
index 7908fcae3de..00000000000
--- a/app/views/projects/blob/_download.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.file-content.blob_file.blob-no-preview
- .center
- = link_to namespace_project_raw_path(@project.namespace, @project, @id) do
- %h1.light
- %i.fa.fa-download
- %h4
- Download (#{number_to_human_size blob_size(blob)})
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index c553db84ee0..b89cd460455 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -9,17 +9,19 @@
= copy_file_path_button(blob.path)
%small
- = number_to_human_size(blob_size(blob))
+ = number_to_human_size(blob.raw_size)
.file-actions.hidden-xs
+ = render 'projects/blob/viewer_switcher', blob: blob unless blame
+
.btn-group{ role: "group" }<
- = copy_blob_content_button(blob) if !blame && blob_rendered_as_text?(blob)
+ = copy_blob_source_button(blob) if !blame && blob.rendered_as_text?(ignore_errors: false)
= open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
-# only show normal/blame view links for text files
- - if blob_text_viewable?(blob)
+ - if blob.readable_text?
- if blame
= link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
@@ -34,7 +36,7 @@
tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
.btn-group{ role: "group" }<
- = edit_blob_link if blob_text_viewable?(blob)
+ = edit_blob_link if blob.readable_text?
- if current_user
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
deleted file mode 100644
index 73877d730f5..00000000000
--- a/app/views/projects/blob/_image.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.file-content.image_file
- %img{ src: namespace_project_raw_path(@project.namespace, @project, @id), alt: blob.name }
diff --git a/app/views/projects/blob/_render_error.html.haml b/app/views/projects/blob/_render_error.html.haml
new file mode 100644
index 00000000000..9eef6cafd04
--- /dev/null
+++ b/app/views/projects/blob/_render_error.html.haml
@@ -0,0 +1,7 @@
+.file-content.code
+ .nothing-here-block
+ The #{viewer.switcher_title} could not be displayed because #{blob_render_error_reason(viewer)}.
+
+ You can
+ = blob_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
+ instead.
diff --git a/app/views/projects/blob/_svg.html.haml b/app/views/projects/blob/_svg.html.haml
deleted file mode 100644
index 93be58fc658..00000000000
--- a/app/views/projects/blob/_svg.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- if blob.size_within_svg_limits?
- -# We need to scrub SVG but we cannot do so in the RawController: it would
- -# be wrong/strange if RawController modified the data.
- - blob.load_all_data!(@repository)
- - blob = sanitize_svg(blob)
- .file-content.image_file
- %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: blob.name }
-- else
- = render 'too_large'
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
deleted file mode 100644
index 20638f6961d..00000000000
--- a/app/views/projects/blob/_text.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- blob.load_all_data!(@repository)
-= render 'shared/file_highlight', blob: blob, repository: @repository
diff --git a/app/views/projects/blob/_too_large.html.haml b/app/views/projects/blob/_too_large.html.haml
deleted file mode 100644
index a505f87df40..00000000000
--- a/app/views/projects/blob/_too_large.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.file-content.code
- .nothing-here-block
- The file could not be displayed as it is too large, you can
- #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
- instead.
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
new file mode 100644
index 00000000000..5326bb3e0cf
--- /dev/null
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -0,0 +1,14 @@
+- hidden = local_assigns.fetch(:hidden, false)
+- render_error = viewer.render_error
+- load_asynchronously = local_assigns.fetch(:load_asynchronously, viewer.server_side?) && render_error.nil?
+
+- url = url_for(params.merge(viewer: viewer.type, format: :json)) if load_asynchronously
+.blob-viewer{ data: { type: viewer.type, url: url }, class: ('hidden' if hidden) }
+ - if load_asynchronously
+ .text-center.prepend-top-default.append-bottom-default
+ = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
+ - elsif render_error
+ = render 'projects/blob/render_error', viewer: viewer
+ - else
+ - viewer.prepare!
+ = render viewer.partial_path, viewer: viewer
diff --git a/app/views/projects/blob/_viewer_switcher.html.haml b/app/views/projects/blob/_viewer_switcher.html.haml
new file mode 100644
index 00000000000..6a521069418
--- /dev/null
+++ b/app/views/projects/blob/_viewer_switcher.html.haml
@@ -0,0 +1,12 @@
+- if blob.show_viewer_switcher?
+ - simple_viewer = blob.simple_viewer
+ - rich_viewer = blob.rich_viewer
+
+ .btn-group.js-blob-viewer-switcher{ role: "group" }
+ - simple_label = "Display #{simple_viewer.switcher_title}"
+ %button.btn.btn-default.btn-sm.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => simple_label, title: simple_label, data: { viewer: 'simple', container: 'body' } }>
+ = icon(simple_viewer.switcher_icon)
+
+ - rich_label = "Display #{rich_viewer.switcher_title}"
+ %button.btn.btn-default.btn-sm.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => rich_label, title: rich_label, data: { viewer: 'rich', container: 'body' } }>
+ = icon(rich_viewer.switcher_icon)
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index b9b3f3ec7a3..67f57b5e4b9 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -2,6 +2,9 @@
- page_title @blob.path, @ref
= render "projects/commits/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('blob')
+
%div{ class: container_class }
= render 'projects/last_push'
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
new file mode 100644
index 00000000000..684240d02c7
--- /dev/null
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -0,0 +1,7 @@
+.file-content.blob_file.blob-no-preview
+ .center
+ = link_to blob_raw_url do
+ %h1.light
+ = icon('download')
+ %h4
+ Download (#{number_to_human_size(viewer.blob.raw_size)})
diff --git a/app/views/projects/blob/viewers/_empty.html.haml b/app/views/projects/blob/viewers/_empty.html.haml
new file mode 100644
index 00000000000..a293a8de231
--- /dev/null
+++ b/app/views/projects/blob/viewers/_empty.html.haml
@@ -0,0 +1,3 @@
+.file-content.code
+ .nothing-here-block
+ Empty file
diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml
new file mode 100644
index 00000000000..640d59b3174
--- /dev/null
+++ b/app/views/projects/blob/viewers/_image.html.haml
@@ -0,0 +1,2 @@
+.file-content.image_file
+ %img{ src: blob_raw_url, alt: viewer.blob.name }
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
new file mode 100644
index 00000000000..b9a998d96ff
--- /dev/null
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -0,0 +1,3 @@
+- blob = viewer.blob
+.file-content.wiki
+ = markup(blob.name, blob.data)
diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/viewers/_notebook.html.haml
index ab1cf933944..2399fb16265 100644
--- a/app/views/projects/blob/_notebook.html.haml
+++ b/app/views/projects/blob/viewers/_notebook.html.haml
@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('notebook_viewer')
-.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+.file-content#js-notebook-viewer{ data: { endpoint: blob_raw_url } }
diff --git a/app/views/projects/blob/_pdf.html.haml b/app/views/projects/blob/viewers/_pdf.html.haml
index 58dc88e3bf7..1dd179c4fdc 100644
--- a/app/views/projects/blob/_pdf.html.haml
+++ b/app/views/projects/blob/viewers/_pdf.html.haml
@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pdf_viewer')
-.file-content#js-pdf-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+.file-content#js-pdf-viewer{ data: { endpoint: blob_raw_url } }
diff --git a/app/views/projects/blob/_sketch.html.haml b/app/views/projects/blob/viewers/_sketch.html.haml
index dad9369cb2a..49f716c2c59 100644
--- a/app/views/projects/blob/_sketch.html.haml
+++ b/app/views/projects/blob/viewers/_sketch.html.haml
@@ -2,6 +2,6 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sketch_viewer')
-.file-content#js-sketch-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_url } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
diff --git a/app/views/projects/blob/_stl.html.haml b/app/views/projects/blob/viewers/_stl.html.haml
index a9332a0eeb6..e4e9d746176 100644
--- a/app/views/projects/blob/_stl.html.haml
+++ b/app/views/projects/blob/viewers/_stl.html.haml
@@ -2,7 +2,7 @@
= page_specific_javascript_bundle_tag('stl_viewer')
.file-content.is-stl-loading
- .text-center#js-stl-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+ .text-center#js-stl-viewer{ data: { endpoint: blob_raw_url } }
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
.text-center.prepend-top-default.append-bottom-default.stl-controls
.btn-group
diff --git a/app/views/projects/blob/viewers/_svg.html.haml b/app/views/projects/blob/viewers/_svg.html.haml
new file mode 100644
index 00000000000..62f647581b6
--- /dev/null
+++ b/app/views/projects/blob/viewers/_svg.html.haml
@@ -0,0 +1,4 @@
+- blob = viewer.blob
+- data = sanitize_svg_data(blob.data)
+.file-content.image_file
+ %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(data)}", alt: blob.name }
diff --git a/app/views/projects/blob/viewers/_text.html.haml b/app/views/projects/blob/viewers/_text.html.haml
new file mode 100644
index 00000000000..a91df321ca0
--- /dev/null
+++ b/app/views/projects/blob/viewers/_text.html.haml
@@ -0,0 +1 @@
+= render 'shared/file_highlight', blob: viewer.blob, repository: @repository
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 438a98c3e95..c781e423c4d 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -3,9 +3,9 @@
- return unless blob.respond_to?(:text?)
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- - elsif blob.only_display_raw?
+ - elsif blob.too_large?
.nothing-here-block The file could not be displayed because it is too large.
- - elsif blob_text_viewable?(blob)
+ - elsif blob.readable_text?
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 4b49bed835f..71a1b9e6c05 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -27,7 +27,7 @@
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
- next unless blob
- - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
+ - blob.load_all_data!(diffs.project.repository) unless blob.too_large?
- file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 4622b980754..f22b385fc0f 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -6,7 +6,7 @@
- unless diff_file.submodule?
.file-actions.hidden-xs
- - if blob_text_viewable?(blob)
+ - if blob.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index ccc808ff43e..774d20fb5ba 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -89,7 +89,7 @@
.sidebar-collapsed-icon
%strong
= icon('exclamation', 'aria-hidden': 'true')
- %span= milestone.issues_visible_to_user(current_user).count
+ %span= milestone.merge_requests.count
.title.hide-collapsed
Merge requests
%span.badge= milestone.merge_requests.count
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 895c3f1e99d..37c66ff2595 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -9,7 +9,7 @@
.file-actions.hidden-xs
.btn-group{ role: "group" }<
- = copy_blob_content_button(@snippet)
+ = copy_blob_source_button(@snippet)
= open_raw_file_button(raw_path)
- if defined?(download_path) && download_path
diff --git a/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml b/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml
new file mode 100644
index 00000000000..70d35f06af4
--- /dev/null
+++ b/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml
@@ -0,0 +1,4 @@
+---
+title: Lazily sets UUID in ApplicationSetting for new installations
+merge_request:
+author:
diff --git a/changelogs/unreleased/30973-fix-network-graph-ordering.yml b/changelogs/unreleased/30973-fix-network-graph-ordering.yml
new file mode 100644
index 00000000000..420ec107842
--- /dev/null
+++ b/changelogs/unreleased/30973-fix-network-graph-ordering.yml
@@ -0,0 +1,4 @@
+---
+title: Fix ordering of commits in the network graph
+merge_request: 10936
+author:
diff --git a/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml b/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml
new file mode 100644
index 00000000000..cb1de425d66
--- /dev/null
+++ b/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Improves test settings for chat notification services for empty projects
+merge_request: 10886
+author:
diff --git a/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml b/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml
new file mode 100644
index 00000000000..dee831c668b
--- /dev/null
+++ b/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed milestone sidebar showing incorrect number of MRs when collapsed
+merge_request: 10933
+author:
diff --git a/changelogs/unreleased/dm-blob-viewers.yml b/changelogs/unreleased/dm-blob-viewers.yml
new file mode 100644
index 00000000000..5e0d41f3f29
--- /dev/null
+++ b/changelogs/unreleased/dm-blob-viewers.yml
@@ -0,0 +1,5 @@
+---
+title: Add Source/Rendered switch to blobs for SVG, Markdown, Asciidoc and other text
+ files that can be rendered
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-fix-ghost-user-validation.yml b/changelogs/unreleased/dm-fix-ghost-user-validation.yml
new file mode 100644
index 00000000000..4214786cb5a
--- /dev/null
+++ b/changelogs/unreleased/dm-fix-ghost-user-validation.yml
@@ -0,0 +1,4 @@
+---
+title: Skip validation when creating internal (ghost, service desk) users
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix_build_header_line_height.yml b/changelogs/unreleased/fix_build_header_line_height.yml
new file mode 100644
index 00000000000..95b6221f8d2
--- /dev/null
+++ b/changelogs/unreleased/fix_build_header_line_height.yml
@@ -0,0 +1,4 @@
+---
+title: Change line-height on build-header so elements don't overlap
+merge_request:
+author: Dino Maric
diff --git a/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml b/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml
new file mode 100644
index 00000000000..198b6ce15ae
--- /dev/null
+++ b/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed alignment of CI icon in issues related branches
+merge_request:
+author:
diff --git a/db/migrate/20170421102337_remove_nil_type_services.rb b/db/migrate/20170421102337_remove_nil_type_services.rb
new file mode 100644
index 00000000000..b835b9c6ed9
--- /dev/null
+++ b/db/migrate/20170421102337_remove_nil_type_services.rb
@@ -0,0 +1,12 @@
+class RemoveNilTypeServices < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute <<-SQL
+ DELETE FROM services WHERE type IS NULL OR type = '';
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb b/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb
new file mode 100644
index 00000000000..58ad2c64075
--- /dev/null
+++ b/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb
@@ -0,0 +1,10 @@
+class FillMissingUuidOnApplicationSettings < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute("UPDATE application_settings SET uuid = #{quote(SecureRandom.uuid)} WHERE uuid is NULL")
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ff00951d5f6..49d7c996661 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170424142900) do
+ActiveRecord::Schema.define(version: 20170426175636) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index d81bc9802bc..472ec9544f3 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -10,7 +10,8 @@ Feature: Project Source Browse Files
Scenario: I browse files for specific ref
Given I visit project source page for "6d39438"
Then I should see files from repository for "6d39438"
-
+
+ @javascript
Scenario: I browse file content
Given I click on ".gitignore" file in repo
Then I should see its content
diff --git a/features/project/source/markdown_render.feature b/features/project/source/markdown_render.feature
index ecbd721c281..fd583618dcf 100644
--- a/features/project/source/markdown_render.feature
+++ b/features/project/source/markdown_render.feature
@@ -6,11 +6,13 @@ Feature: Project Source Markdown Render
# Tree README
+ @javascript
Scenario: Tree view should have correct links in README
Given I go directory which contains README file
And I click on a relative link in README
Then I should see the correct markdown
+ @javascript
Scenario: I browse files from markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
@@ -29,36 +31,42 @@ Feature: Project Source Markdown Render
And I click on GitLab API doc directory in README
Then I should see correct doc/api directory rendered
+ @javascript
Scenario: I view README in markdown branch to see reference links to file
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Maintenance in README
Then I should see correct maintenance file rendered
+ @javascript
Scenario: README headers should have header links
Then I should see rendered README which contains correct links
And Header "Application details" should have correct id and link
# Blob
+ @javascript
Scenario: I navigate to doc directory to view documentation in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on users in doc/api/README
Then I should see the correct document file
+ @javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on raketasks in doc/api/README
Then I should see correct directory rendered
+ @javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And Header "GitLab API" should have correct id and link
# Markdown branch
+ @javascript
Scenario: I browse files from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
@@ -73,6 +81,7 @@ Feature: Project Source Markdown Render
And I click on Rake tasks in README
Then I should see correct directory rendered for markdown branch
+ @javascript
Scenario: I navigate to doc directory to view documentation in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
@@ -80,6 +89,7 @@ Feature: Project Source Markdown Render
And I click on users in doc/api/README
Then I should see the users document file in markdown branch
+ @javascript
Scenario: I navigate to doc directory to view user doc in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
@@ -87,6 +97,7 @@ Feature: Project Source Markdown Render
And I click on raketasks in doc/api/README
Then I should see correct directory rendered for markdown branch
+ @javascript
Scenario: Tree markdown links view empty urls should have correct urls
When I visit markdown branch
Then The link with text "empty" should have url "tree/markdown"
@@ -99,6 +110,7 @@ Feature: Project Source Markdown Render
# "ID" means "#id" on the tests below, because we are unable to escape the hash sign.
# which Spinach interprets as the start of a comment.
+ @javascript
Scenario: All markdown links with ids should have correct urls
When I visit markdown branch
Then The link with text "ID" should have url "tree/markdownID"
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index b4741f06d1b..36fe21a047c 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -4,6 +4,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include SharedProject
include SharedPaths
include RepoHelpers
+ include WaitForAjax
step "I don't have write access" do
@project = create(:project, :repository, name: "Other Project", path: "other-project")
@@ -36,10 +37,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see its content' do
+ wait_for_ajax
expect(page).to have_content old_gitignore_content
end
step 'I should see its new content' do
+ wait_for_ajax
expect(page).to have_content new_gitignore_content
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 0f0827f0477..abdbd795cd5 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -5,6 +5,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedMarkdown
+ include WaitForAjax
step 'I own project "Delta"' do
@project = ::Project.find_by(name: "Delta")
@@ -34,6 +35,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct document rendered' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "All API requests require authentication"
end
@@ -63,6 +65,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct maintenance file rendered' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md")
+ wait_for_ajax
expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production"
end
@@ -94,6 +97,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see correct file rendered' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "Contents"
expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks"
@@ -138,6 +142,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see correct file rendered in markdown branch' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "Contents"
expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks"
@@ -145,6 +150,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct document rendered for markdown branch' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "All API requests require authentication"
end
@@ -162,6 +168,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
# Expected link contents
step 'The link with text "empty" should have url "tree/markdown"' do
+ wait_for_ajax
find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown")
end
@@ -197,6 +204,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
end
step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do
+ wait_for_ajax
find('a', text: /^#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id'
end
@@ -291,10 +299,12 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see the correct markdown' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md")
+ wait_for_ajax
expect(page).to have_content "List users"
end
step 'Header "Application details" should have correct id and link' do
+ wait_for_ajax
header_should_have_correct_id_and_link(2, 'Application details', 'application-details')
end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index 875d27d9383..6610b97ecb2 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -3,7 +3,7 @@ module SharedMarkdown
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
node = find("#{parent} h#{level} a#user-content-#{id}")
- expect(node[:href]).to eq "##{id}"
+ expect(node[:href]).to end_with "##{id}"
# Work around a weird Capybara behavior where calling `parent` on a node
# returns the whole document, not the node's actual parent element
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index d76aa38f741..1ff34553f0a 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -41,7 +41,7 @@ module Gitlab
type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
# Hash to be passed as post_receive_data
- data = {
+ {
object_kind: type,
event_name: type,
before: oldrev,
@@ -61,16 +61,15 @@ module Gitlab
repository: project.hook_attrs.slice(:name, :url, :description, :homepage,
:git_http_url, :git_ssh_url, :visibility_level)
}
-
- data
end
# This method provide a sample data generated with
# existing project and commits to test webhooks
def build_sample(project, user)
- commits = project.repository.commits(project.default_branch, limit: 3)
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
- build(project, user, commits.last.id, commits.first.id, ref, commits)
+ commits = project.repository.commits(project.default_branch.to_s, limit: 3) rescue []
+
+ build(project, user, commits.last&.id, commits.first&.id, ref, commits)
end
def checkout_sha(repository, newrev, ref)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 98fd4e78126..e8bb9e1f805 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -109,10 +109,6 @@ module Gitlab
@binary.nil? ? super : @binary == true
end
- def empty?
- !data || data == ''
- end
-
def data
encode! @data
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index d7dac9f6149..452dba7971d 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -494,7 +494,9 @@ module Gitlab
# :contains is the commit contained by the refs from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip
- # :order is the commits order and allowed value is :date(default) or :topo
+ # :order is the commits order and allowed value is :none (default), :date, or :topo
+ # commit ordering types are documented here:
+ # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
#
def find_commits(options = {})
actual_options = options.dup
@@ -522,11 +524,8 @@ module Gitlab
end
end
- if actual_options[:order] == :topo
- walker.sorting(Rugged::SORT_TOPO)
- else
- walker.sorting(Rugged::SORT_NONE)
- end
+ sort_type = rugged_sort_type(actual_options[:order])
+ walker.sorting(sort_type)
commits = []
offset = actual_options[:skip]
@@ -1273,6 +1272,18 @@ module Gitlab
def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self)
end
+
+ # Returns the `Rugged` sorting type constant for a given
+ # sort type key. Valid keys are `:none`, `:topo`, and `:date`
+ def rugged_sort_type(key)
+ @rugged_sort_types ||= {
+ none: Rugged::SORT_NONE,
+ topo: Rugged::SORT_TOPO,
+ date: Rugged::SORT_DATE
+ }
+
+ @rugged_sort_types.fetch(key, Rugged::SORT_NONE)
+ end
end
end
end
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index de7379425cf..fd173c0ba88 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -32,7 +32,7 @@ sed -i 's/localhost/redis/g' config/resque.yml
cp config/gitlab.yml.example config/gitlab.yml
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
- retry bundle install --clean $BUNDLE_INSTALL_FLAGS
+ retry bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
fi
# Only install knapsack after bundle install! Otherwise oddly some native
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 16365642a34..2d892f4a2b7 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -8,6 +8,7 @@ describe Projects::ServicesController do
before do
sign_in(user)
project.team << [user, :master]
+
controller.instance_variable_set(:@project, project)
controller.instance_variable_set(:@service, service)
end
@@ -18,20 +19,60 @@ describe Projects::ServicesController do
end
describe "#test" do
+ context 'when can_test? returns false' do
+ it 'renders 404' do
+ allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
context 'success' do
+ context 'with empty project' do
+ let(:project) { create(:empty_project) }
+
+ context 'with chat notification service' do
+ let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
+
+ it 'redirects and show success message' do
+ allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ end
+ end
+
+ it 'redirects and show success message' do
+ expect(service).to receive(:test).and_return(success: true, result: 'done')
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ end
+ end
+
it "redirects and show success message" do
- expect(service).to receive(:test).and_return({ success: true, result: 'done' })
+ expect(service).to receive(:test).and_return(success: true, result: 'done')
+
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
- expect(response.status).to redirect_to('/')
+
+ expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('We sent a request to the provided URL')
end
end
context 'failure' do
it "redirects and show failure message" do
- expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
+ expect(service).to receive(:test).and_return(success: false, result: 'Bad test')
+
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
- expect(response.status).to redirect_to('/')
+
+ expect(response).to redirect_to(root_path)
expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
end
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 88f6c265505..62aa71ae8d8 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -1,6 +1,19 @@
FactoryGirl.define do
factory :service do
project factory: :empty_project
+ type 'Service'
+ end
+
+ factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
+ project factory: :empty_project
+ type 'CustomIssueTrackerService'
+ category 'issue_tracker'
+ active true
+ properties(
+ project_url: 'https://project.url.com',
+ issues_url: 'https://issues.url.com',
+ new_issue_url: 'https://newissue.url.com'
+ )
end
factory :kubernetes_service do
diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb
new file mode 100644
index 00000000000..dd14ffdb2ce
--- /dev/null
+++ b/spec/features/admin/admin_cohorts_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+feature 'Admin cohorts page', feature: true do
+ before do
+ login_as :admin
+ end
+
+ scenario 'See users count per month' do
+ 2.times { create(:user) }
+
+ visit admin_cohorts_path
+
+ expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
+ end
+end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 344e31e5ef5..f197fb44608 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -479,6 +479,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a blob' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
+ wait_for_ajax
end
context 'selecting one word of text' do
@@ -520,6 +521,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a GFM code block' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
+ wait_for_ajax
end
context 'selecting one word of text' do
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 7cfa5b9716f..cc11cb7a55f 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -1,21 +1,313 @@
require 'spec_helper'
-feature 'File blob', feature: true do
+feature 'File blob', :js, feature: true do
include TreeHelper
+ include WaitForAjax
- let(:project) { create(:project, :public, :test_repo) }
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
- let(:branch) { 'master' }
- let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] }
+ let(:project) { create(:project, :public) }
- context 'anonymous' do
- context 'from blob file path' do
+ def visit_blob(path, fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
+ end
+
+ context 'Ruby file' do
+ before do
+ visit_blob('files/ruby/popen.rb')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ context 'visiting directly' do
before do
- visit namespace_project_blob_path(project.namespace, project, tree_join(branch, file_path))
+ visit_blob('files/markdown/ruby-style-guide.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit_blob('files/markdown/ruby-style-guide.md', 'L1')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'Markdown file (stored in LFS)' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add Markdown in LFS",
+ file_path: 'files/lfs/file.md',
+ file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data
+ ).execute
+ end
+
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/file.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an error message
+ expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can view the source or download it instead.')
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows an error message
+ expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+ end
+
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/file.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'PDF file' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add PDF",
+ file_path: 'files/test.pdf',
+ file_content: File.read(Rails.root.join('spec/javascripts/blob/pdf/test.pdf'))
+ ).execute
+
+ visit_blob('files/test.pdf')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows rendered PDF
+ expect(page).to have_selector('.js-pdf-viewer')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+
+ context 'ISO file (stored in LFS)' do
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (1.5 MB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'ZIP file' do
+ before do
+ visit_blob('Gemfile.zip')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (2.11 KB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
- it 'updates content' do
- expect(page).to have_link 'Edit'
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
end
end
end
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index d281043caa3..70e96efd557 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user browses project', feature: true do
+feature 'user browses project', feature: true, js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,7 +13,7 @@ feature 'user browses project', feature: true do
scenario "can see blame of '.gitignore'" do
click_link ".gitignore"
click_link 'Blame'
-
+
expect(page).to have_content "*.rb"
expect(page).to have_content "Dmitriy Zaporozhets"
expect(page).to have_content "Initial commit"
@@ -24,6 +24,7 @@ feature 'user browses project', feature: true do
click_link 'files'
click_link 'lfs'
click_link 'lfs_object.iso'
+ wait_for_ajax
expect(page).not_to have_content 'Download (1.5 MB)'
expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index dab78fd3571..5e19907eef9 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -63,4 +63,28 @@ feature 'Project milestone', :feature do
expect(page).not_to have_content('Assign some issues to this milestone.')
end
end
+
+ context 'when project has an issue' do
+ before do
+ create(:issue, project: project, milestone: milestone)
+
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ describe 'the collapsed sidebar' do
+ before do
+ find('.milestone-sidebar .gutter-toggle').click
+ end
+
+ it 'shows the total MR and issue counts' do
+ find('.milestone-sidebar .block', match: :first)
+ blocks = all('.milestone-sidebar .block')
+
+ aggregate_failures 'MR and issue blocks' do
+ expect(blocks[3]).to have_content 1
+ expect(blocks[4]).to have_content 0
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 508aeb7cf67..379f62f73e1 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -56,15 +56,14 @@ describe BlobHelper do
end
end
- describe "#sanitize_svg" do
+ describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { open(input_svg_path).read }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
let(:expected) { open(expected_svg_path).read }
it 'retains essential elements' do
- blob = OpenStruct.new(data: data)
- expect(sanitize_svg(blob).data).to eq(expected)
+ expect(sanitize_svg_data(data)).to eq(expected)
end
end
@@ -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')
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
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
new file mode 100644
index 00000000000..13f122b68b2
--- /dev/null
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -0,0 +1,161 @@
+/* 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);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
index 9762688af1a..1c54cc3054c 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -1,15 +1,18 @@
import Vue from 'vue';
import '~/flash';
-import EnvironmentsComponent from '~/environments/components/environment';
+import environmentsComponent from '~/environments/components/environment.vue';
import { environment, folder } from './mock_data';
describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw');
+ let EnvironmentsComponent;
let component;
beforeEach(() => {
loadFixtures('static/environments/environments.html.raw');
+
+ EnvironmentsComponent = Vue.extend(environmentsComponent);
});
describe('successfull request', () => {
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 72f3db29a66..350078ad5f5 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import '~/flash';
-import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view';
+import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw');
+ let EnvironmentsFolderViewComponent;
beforeEach(() => {
loadFixtures('static/environments/environments_folder_view.html.raw');
+ EnvironmentsFolderViewComponent = Vue.extend(environmentsFolderViewComponent);
window.history.pushState({}, null, 'environments/folders/build');
});
diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb
new file mode 100644
index 00000000000..16490ad5039
--- /dev/null
+++ b/spec/javascripts/fixtures/blob.rb
@@ -0,0 +1,29 @@
+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
diff --git a/spec/javascripts/pipelines/time_ago_spec.js b/spec/javascripts/pipelines/time_ago_spec.js
new file mode 100644
index 00000000000..24581e8c672
--- /dev/null
+++ b/spec/javascripts/pipelines/time_ago_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import timeAgo from '~/pipelines/components/time_ago';
+
+describe('Timeago component', () => {
+ let TimeAgo;
+ beforeEach(() => {
+ TimeAgo = Vue.extend(timeAgo);
+ });
+
+ describe('with duration', () => {
+ it('should render duration and timer svg', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 10,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.duration')).toBeDefined();
+ expect(component.$el.querySelector('.duration svg')).toBeDefined();
+ });
+ });
+
+ describe('without duration', () => {
+ it('should not render duration and timer svg', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.duration')).toBe(null);
+ });
+ });
+
+ describe('with finishedTime', () => {
+ it('should render time and calendar icon', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '2017-04-26T12:40:23.277Z',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.finished-at')).toBeDefined();
+ expect(component.$el.querySelector('.finished-at i.fa-calendar')).toBeDefined();
+ expect(component.$el.querySelector('.finished-at time')).toBeDefined();
+ });
+ });
+
+ describe('without finishedTime', () => {
+ it('should not render time and calendar icon', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.finished-at')).toBe(null);
+ });
+ });
+});
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 3d6d7292b42..f88653cb1fe 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1031,6 +1031,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#find_commits' do
+ it 'should return a return a collection of commits' do
+ commits = repository.find_commits
+
+ expect(commits).not_to be_empty
+ expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) )
+ end
+
+ context 'while applying a sort order based on the `order` option' do
+ it "allows ordering topologically (no parents shown before their children)" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO)
+
+ repository.find_commits(order: :topo)
+ end
+
+ it "allows ordering by date" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE)
+
+ repository.find_commits(order: :date)
+ end
+
+ it "applies no sorting by default" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE)
+
+ repository.find_commits
+ end
+ end
+ end
+
describe '#branches with deleted branch' do
before(:each) do
ref = double()
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 7a0b0b06d4b..bfecfa28ed1 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6981,28 +6981,6 @@
],
"services": [
{
- "id": 164,
- "title": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:07.372Z",
- "updated_at": "2016-06-14T15:02:07.372Z",
- "active": false,
- "properties": {
-
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "build_events": true,
- "category": "issue_tracker",
- "type": "CustomIssueTrackerService",
- "default": true,
- "wiki_page_events": true
- },
- {
"id": 100,
"title": "JetBrains TeamCity CI",
"project_id": 5,
@@ -7019,6 +6997,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "TeamcityService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7040,6 +7019,7 @@
"tag_push_events": true,
"note_events": true,
"pipeline_events": true,
+ "type": "SlackService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7061,6 +7041,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "RedmineService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7082,6 +7063,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "PushoverService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7103,6 +7085,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "PivotalTrackerService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7125,6 +7108,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "JiraService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7146,6 +7130,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "IrkerService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7167,6 +7152,7 @@
"tag_push_events": true,
"note_events": true,
"pipeline_events": true,
+ "type": "HipchatService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7188,6 +7174,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "GemnasiumService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7209,6 +7196,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "FlowdockService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7230,6 +7218,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "ExternalWikiService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7251,6 +7240,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "EmailsOnPushService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7272,6 +7262,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "DroneCiService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7293,6 +7284,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "CustomIssueTrackerService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7314,6 +7306,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "CampfireService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7335,6 +7328,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "BuildkiteService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7356,6 +7350,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "BambooService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7377,6 +7372,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "AssemblaService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7398,6 +7394,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "AssemblaService",
"category": "common",
"default": false,
"wiki_page_events": true
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index fcc23a75ca1..06cd8ab87ed 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -60,7 +60,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
end
context 'original service exists' do
- let(:service_id) { Service.create(project: project).id }
+ let(:service_id) { create(:service, project: project).id }
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 01ca1584ed2..c2c19c62048 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -4,6 +4,7 @@ describe ApplicationSetting, models: true do
let(:setting) { ApplicationSetting.create_from_defaults }
it { expect(setting).to be_valid }
+ it { expect(setting.uuid).to be_present }
describe 'validations' do
let(:http) { 'http://example.com' }
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index e5dd57fc4bb..7e8a1c8add7 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -2,6 +2,14 @@
require 'rails_helper'
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
it 'returns NilClass when given nil' do
expect(described_class.decorate(nil)).to be_nil
@@ -12,7 +20,7 @@ describe Blob do
context 'using a binary blob' do
it 'returns the data as-is' do
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)
end
@@ -20,202 +28,176 @@ describe Blob do
context 'using a text blob' 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���")
end
end
end
- describe '#svg?' do
- it 'is falsey when not text' do
- git_blob = double(text?: false)
+ describe '#raw_binary?' do
+ context 'if the blob is a valid LFS pointer' do
+ 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
- end
-
- it 'is falsey when no language is detected' do
- git_blob = double(text?: true, language: nil)
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
+ context 'if the viewer is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md', lfs: true)
- it' is falsey when language is not SVG' do
- git_blob = double(text?: true, language: double(name: 'XML'))
-
- expect(described_class.decorate(git_blob)).not_to be_svg
- 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
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
- it 'is truthy when file extension is .pdf' do
- git_blob = Gitlab::Git::Blob.new(name: 'git_blob.pdf')
+ context "if the extension doesn't have a rich viewer" do
+ 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
- describe '#ipython_notebook?' do
- it 'is falsey when language is not Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'JSON'))
+ context 'if the blob is not an LFS pointer' do
+ context 'if the blob is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
- end
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
- it 'is truthy when language is Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
+ context 'if the blob is text-based' do
+ 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
- describe '#sketch?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: "design.png")
-
- 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")
+ describe '#extension' do
+ it 'returns the extension' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).to be_sketch
+ expect(blob.extension).to eq('md')
end
end
- describe '#video?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+ describe '#simple_viewer' do
+ context 'when the blob is empty' do
+ it 'returns an empty viewer' do
+ blob = fake_blob(data: '')
- expect(described_class.decorate(git_blob)).not_to be_video
- 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
+ expect(blob.simple_viewer).to be_a(BlobViewer::Empty)
end
end
- end
- describe '#stl?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.png')
+ context 'when the file represented by the blob is binary' do
+ it 'returns a download viewer' do
+ 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
- it 'is truthy with STL extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
+ context 'when the file represented by the blob is text-based' do
+ 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
- describe '#to_partial_path' do
- let(:project) { double(lfs_enabled?: true) }
+ describe '#rich_viewer' do
+ context 'when the blob is an invalid LFS pointer' do
+ before do
+ project.lfs_enabled = false
+ end
- def stubbed_blob(overrides = {})
- overrides.reverse_merge!(
- name: nil,
- image?: false,
- language: nil,
- lfs_pointer?: false,
- svg?: false,
- text?: false,
- binary?: false,
- stl?: false
- )
+ it 'returns nil' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- described_class.decorate(Gitlab::Git::Blob.new({})).tap do |blob|
- allow(blob).to receive_messages(overrides)
+ expect(blob.rich_viewer).to be_nil
end
end
- it 'handles LFS pointers with LFS enabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- expect(blob.to_partial_path(project)).to eq 'download'
- 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
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
- it 'handles SVGs' do
- blob = stubbed_blob(text?: true, svg?: true)
- expect(blob.to_partial_path(project)).to eq 'svg'
+ expect(blob.rich_viewer).to be_nil
+ end
end
- it 'handles images' do
- blob = stubbed_blob(image?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
- end
+ context 'when the blob is a valid LFS pointer' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- it 'handles text' do
- blob = stubbed_blob(text?: true, name: 'test.txt')
- 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'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles PDFs' do
- blob = stubbed_blob(name: 'blob.pdf', pdf?: true)
- expect(blob.to_partial_path(project)).to eq 'pdf'
- end
+ context 'when the blob is binary' do
+ it 'returns a matching binary viewer' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- it 'handles iPython notebooks' do
- blob = stubbed_blob(text?: true, ipython_notebook?: true)
- expect(blob.to_partial_path(project)).to eq 'notebook'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles Sketch files' do
- blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
- expect(blob.to_partial_path(project)).to eq 'sketch'
- end
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'file.md')
- it 'handles STLs' do
- blob = stubbed_blob(text?: true, stl?: true)
- expect(blob.to_partial_path(project)).to eq 'stl'
+ expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
+ end
end
end
- describe '#size_within_svg_limits?' do
- let(:blob) { described_class.decorate(double(:blob)) }
+ describe '#rendered_as_text?' do
+ 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).to receive(:size).and_return(42)
+ expect(blob.rendered_as_text?).to be_truthy
+ 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
- it 'returns true when the blob size is equal to the SVG limit' do
- expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE)
+ context 'when not ignoring errors' do
+ 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)
- end
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_falsey
+ end
+ end
- it 'returns false when the blob size is larger than the SVG limit' do
- expect(blob).to receive(:size).and_return(1.terabyte)
+ context "when the viewer doesn't have render errors" do
+ 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
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
new file mode 100644
index 00000000000..a3e598de56d
--- /dev/null
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -0,0 +1,186 @@
+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
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index 492c4e01bd8..46b36e11c23 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -9,4 +9,25 @@ describe Network::Graph, models: true do
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end
+
+ describe "#commits" do
+ let(:graph) { described_class.new(project, 'refs/heads/master', project.repository.commit, nil) }
+
+ it "returns a list of commits" do
+ commits = graph.commits
+
+ expect(commits).not_to be_empty
+ expect(commits).to all( be_kind_of(Network::Commit) )
+ end
+
+ it "sorts the commits by commit date (descending)" do
+ # Remove duplicate timestamps because they make it harder to
+ # assert that the commits are sorted as expected.
+ commits = graph.commits.uniq(&:date)
+ sorted_commits = commits.sort_by(&:date).reverse
+
+ expect(commits).not_to be_empty
+ expect(commits.map(&:id)).to eq(sorted_commits.map(&:id))
+ end
+ end
end
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index 592c90cda36..8fbe42248ae 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -11,10 +11,10 @@ describe ChatNotificationService, models: true do
describe '#can_test?' do
context 'with empty repository' do
- it 'returns false' do
+ it 'returns true' do
subject.project = create(:empty_project, :empty_repo)
- expect(subject.can_test?).to be false
+ expect(subject.can_test?).to be true
end
end
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
index fbe6f344a98..869b25b933b 100644
--- a/spec/models/project_services/issue_tracker_service_spec.rb
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -8,7 +8,7 @@ describe IssueTrackerService, models: true do
let(:service) { RedmineService.new(project: project, active: true) }
before do
- create(:service, project: project, active: true, category: 'issue_tracker')
+ create(:custom_issue_tracker_service, project: project)
end
context 'when service is changed manually by user' do
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 0e2f07e945f..134882648b9 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -6,44 +6,53 @@ describe Service, models: true do
it { is_expected.to have_one :service_hook }
end
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:type) }
+ end
+
describe "Test Button" do
- before do
- @service = Service.new
- end
+ describe '#can_test?' do
+ let(:service) { create(:service, project: project) }
- describe "Testable" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
+
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
+ end
+
+ describe '#test' do
+ let(:data) { 'test' }
+ let(:service) { create(:service, project: project) }
- describe '#test' do
- let(:data) { 'test' }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
it 'test runs execute' do
- expect(@service).to receive(:execute).with(data)
+ expect(service).to receive(:execute).with(data)
- @service.test(data)
+ service.test(data)
end
end
- end
- describe "With commits" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
- end
+ it 'test runs execute' do
+ expect(service).to receive(:execute).with(data)
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ service.test(data)
+ end
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 0a2860f2505..0bcebc27598 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1556,6 +1556,16 @@ describe User, models: true do
expect(ghost.email).to eq('ghost1@example.com')
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
describe '#update_two_factor_requirement' do
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
index 694786c3046..0389a264781 100644
--- a/spec/requests/api/v3/deployments_spec.rb
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::Deployments do
+describe API::V3::Deployments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
@@ -24,11 +24,11 @@ describe API::Deployments do
describe 'GET /projects/:id/deployments' do
context 'as member of the project' do
it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/deployments", user) }
+ let(:request) { get v3_api("/projects/#{project.id}/deployments", user) }
end
it 'returns projects deployments' do
- get api("/projects/#{project.id}/deployments", user)
+ get v3_api("/projects/#{project.id}/deployments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -40,7 +40,7 @@ describe API::Deployments do
context 'as non member' do
it 'returns a 404 status code' do
- get api("/projects/#{project.id}/deployments", non_member)
+ get v3_api("/projects/#{project.id}/deployments", non_member)
expect(response).to have_http_status(404)
end
@@ -50,7 +50,7 @@ describe API::Deployments do
describe 'GET /projects/:id/deployments/:deployment_id' do
context 'as a member of the project' do
it 'returns the projects deployment' do
- get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+ get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user)
expect(response).to have_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
@@ -60,7 +60,7 @@ describe API::Deployments do
context 'as non member' do
it 'returns a 404 status code' do
- get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+ get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
expect(response).to have_http_status(404)
end
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
new file mode 100644
index 00000000000..b29af732ad3
--- /dev/null
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -0,0 +1,50 @@
+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
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
new file mode 100644
index 00000000000..a4915264abe
--- /dev/null
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -0,0 +1,96 @@
+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