diff options
49 files changed, 912 insertions, 124 deletions
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js index 180aa30e98c..661870c226c 100644 --- a/app/assets/javascripts/clusters.js +++ b/app/assets/javascripts/clusters.js @@ -64,19 +64,16 @@ export default class Clusters { this.poll = new Poll({ resource: this.service, method: 'fetchData', - successCallback: (data) => { - const { status, status_reason } = data.data; - this.updateContainer(status, status_reason); - }, - errorCallback: () => { - Flash(s__('ClusterIntegration|Something went wrong on our end.')); - }, + successCallback: data => this.handleSuccess(data), + errorCallback: () => Clusters.handleError(), }); if (!Visibility.hidden()) { this.poll.makeRequest(); } else { - this.service.fetchData(); + this.service.fetchData() + .then(data => this.handleSuccess(data)) + .catch(() => Clusters.handleError()); } Visibility.change(() => { @@ -88,6 +85,15 @@ export default class Clusters { }); } + static handleError() { + Flash(s__('ClusterIntegration|Something went wrong on our end.')); + } + + handleSuccess(data) { + const { status, status_reason } = data.data; + this.updateContainer(status, status_reason); + } + hideAll() { this.errorContainer.classList.add('hidden'); this.successContainer.classList.add('hidden'); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index ea2e2205077..33a352e158a 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility'; * @param {jQuery.Event} e * @param {String} count */ -$(document).on('todo:toggle', (e, count) => { - const parsedCount = parseInt(count, 10); - const $todoPendingCount = $('.todos-count'); +export default function initTodoToggle() { + $(document).on('todo:toggle', (e, count) => { + const parsedCount = parseInt(count, 10); + const $todoPendingCount = $('.todos-count'); - $todoPendingCount.text(highCountTrim(parsedCount)); - $todoPendingCount.toggleClass('hidden', parsedCount === 0); -}); + $todoPendingCount.text(highCountTrim(parsedCount)); + $todoPendingCount.toggleClass('hidden', parsedCount === 0); + }); +} diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js index 1211c2c802c..fcf424408f2 100644 --- a/app/assets/javascripts/init_legacy_filters.js +++ b/app/assets/javascripts/init_legacy_filters.js @@ -1,15 +1,15 @@ /* eslint-disable no-new */ /* global LabelsSelect */ /* global MilestoneSelect */ -/* global IssueStatusSelect */ /* global SubscriptionSelect */ import UsersSelect from './users_select'; +import issueStatusSelect from './issue_status_select'; export default () => { new UsersSelect(); new LabelsSelect(); new MilestoneSelect(); - new IssueStatusSelect(); + issueStatusSelect(); new SubscriptionSelect(); }; diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index bb509089b1d..4a15ec8b147 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,12 +1,11 @@ /* eslint-disable class-methods-use-this, no-new */ /* global LabelsSelect */ /* global MilestoneSelect */ -/* global IssueStatusSelect */ /* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import './milestone_select'; -import './issue_status_select'; +import issueStatusSelect from './issue_status_select'; import './subscription_select'; import './labels_select'; @@ -49,7 +48,7 @@ export default class IssuableBulkUpdateSidebar { initDropdowns() { new LabelsSelect(); new MilestoneSelect(); - new IssueStatusSelect(); + issueStatusSelect(); new SubscriptionSelect(); } diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 3fc29f9a661..acd5730cf3c 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -6,7 +6,7 @@ import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import IssuablesHelper from './helpers/issuables_helper'; -class Issue { +export default class Issue { constructor() { if ($('a.btn-close').length) { this.taskList = new TaskList({ @@ -147,5 +147,3 @@ class Issue { }); } } - -export default Issue; diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 56cb536dcde..03546f61d1f 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,34 +1,23 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ -(function() { - this.IssueStatusSelect = (function() { - function IssueStatusSelect() { - $('.js-issue-status').each(function(i, el) { - var fieldName; - fieldName = $(el).data("field-name"); - return $(el).glDropdown({ - selectable: true, - fieldName: fieldName, - toggleLabel: (function(_this) { - return function(selected, el, instance) { - var $item, label; - label = 'Author'; - $item = instance.dropdown.find('.is-active'); - if ($item.length) { - label = $item.text(); - } - return label; - }; - })(this), - clicked: function(options) { - return options.e.preventDefault(); - }, - id: function(obj, el) { - return $(el).data("id"); - } - }); - }); - } - - return IssueStatusSelect; - })(); -}).call(window); +export default function issueStatusSelect() { + $('.js-issue-status').each((i, el) => { + const fieldName = $(el).data('field-name'); + return $(el).glDropdown({ + selectable: true, + fieldName, + toggleLabel(selected, element, instance) { + let label = 'Author'; + const $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); + } + return label; + }, + clicked(options) { + return options.e.preventDefault(); + }, + id(obj, element) { + return $(element).data('id'); + }, + }); + }); +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index fd9d0c335a5..d743f20c615 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -54,11 +54,8 @@ import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; import './gl_form'; -import './header'; +import initTodoToggle from './header'; import initImporterStatus from './importer_status'; -import './issuable_form'; -import './issue'; -import './issue_status_select'; import './labels_select'; import './layout_nav'; import LazyLoader from './lazy_loader'; @@ -137,6 +134,7 @@ $(function () { initBreadcrumbs(); initImporterStatus(); + initTodoToggle(); // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue index 3ccb50213ab..3a331ed805f 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/index.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue @@ -3,10 +3,12 @@ import RepoHelper from '../../helpers/repo_helper'; import eventHub from '../../event_hub'; import newModal from './modal.vue'; + import upload from './upload.vue'; export default { components: { newModal, + upload, }, data() { return { @@ -23,10 +25,12 @@ toggleModalOpen() { this.openModal = !this.openModal; }, - createNewEntryInStore(name, type) { - RepoHelper.createNewEntry(name, type); + createNewEntryInStore(options, openEditMode = true) { + RepoHelper.createNewEntry(options, openEditMode); - this.toggleModalOpen(); + if (options.toggleModal) { + this.toggleModalOpen(); + } }, }, created() { @@ -65,6 +69,11 @@ </a> </li> <li> + <upload + :current-path="currentPath" + /> + </li> + <li> <a href="#" role="button" diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue index 5ef629e0dde..b49586de6bb 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue @@ -24,7 +24,11 @@ }, methods: { createEntryInStore() { - eventHub.$emit('createNewEntry', this.entryName, this.type); + eventHub.$emit('createNewEntry', { + name: this.entryName, + type: this.type, + toggleModal: true, + }); }, toggleModalOpen() { this.$emit('toggle'); diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/repo/components/new_dropdown/upload.vue new file mode 100644 index 00000000000..cbea9c08249 --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/upload.vue @@ -0,0 +1,67 @@ +<script> + import eventHub from '../../event_hub'; + + export default { + props: { + currentPath: { + type: String, + required: true, + }, + }, + methods: { + createFile(target, file, isText) { + const { name } = file; + const nameWithPath = `${this.currentPath !== '' ? `${this.currentPath}/` : ''}${name}`; + let { result } = target; + + if (!isText) { + result = result.split('base64,')[1]; + } + + eventHub.$emit('createNewEntry', { + name: nameWithPath, + type: 'blob', + content: result, + toggleModal: false, + base64: !isText, + }, isText); + }, + readFile(file) { + const reader = new FileReader(); + const isText = file.type.match(/text.*/) !== null; + + reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true }); + + if (isText) { + reader.readAsText(file); + } else { + reader.readAsDataURL(file); + } + }, + openFile() { + Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); + }, + }, + mounted() { + this.$refs.fileUpload.addEventListener('change', this.openFile); + }, + beforeDestroy() { + this.$refs.fileUpload.removeEventListener('change', this.openFile); + }, + }; +</script> + +<template> + <label + role="button" + class="menu-item" + > + {{ __('Upload file') }} + <input + id="file-upload" + type="file" + class="hidden" + ref="fileUpload" + /> + </label> +</template> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 0d6259a37a8..649c69c43fd 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -52,6 +52,7 @@ export default { action: f.tempFile ? 'create' : 'update', file_path: f.path, content: f.newContent, + encoding: f.base64 ? 'base64' : 'text', })); const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch; const payload = { diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index b5be771d539..264694f01a2 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -50,6 +50,13 @@ export default { v-html="activeFile.html"> </div> <div + v-else-if="activeFile.tempFile" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed for this temporary file. + </p> + </div> + <div v-else-if="activeFile.tooLarge" class="vertical-center render-error"> <p class="text-center"> diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index fb26f3b7380..1c677049b31 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -155,7 +155,7 @@ const RepoHelper = { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { newFile.tooLarge = true; } - newFile.newContent = ''; + newFile.newContent = file.newContent ? file.newContent : ''; Store.addToOpenedFiles(newFile); Store.setActiveFiles(newFile); @@ -276,7 +276,13 @@ const RepoHelper = { removeAllTmpFiles(storeFilesKey) { Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile); }, - createNewEntry(name, type) { + createNewEntry(options, openEditMode = true) { + const { + name, + type, + content = '', + base64 = false, + } = options; const originalPath = Store.path; let entryName = name; @@ -304,9 +310,24 @@ const RepoHelper = { if ((type === 'tree' && tree.tempFile) || type === 'blob') { const file = this.findOrCreateEntry('blob', tree, fileName); - if (!file.exists) { - this.setFile(file.entry, file.entry); - this.openEditMode(); + if (file.exists) { + Flash(`The name "${file.entry.name}" is already taken in this directory.`); + } else { + const { entry } = file; + entry.newContent = content; + entry.base64 = base64; + + if (entry.base64) { + entry.render_error = true; + } + + this.setFile(entry, entry); + + if (openEditMode) { + this.openEditMode(); + } else { + file.entry.render_error = 'asdsad'; + } } } diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index c9fa5cc8bf8..d003e2b0a5e 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -19,7 +19,7 @@ const RepoService = { getRaw(file) { if (file.tempFile) { return Promise.resolve({ - data: '', + data: file.newContent ? file.newContent : '', }); } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a9d804e735d..1aa53b8f8cf 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -776,12 +776,15 @@ a, button, .menu-item { + margin-bottom: 0; border-radius: 0; box-shadow: none; padding: 8px 16px; text-align: left; white-space: normal; width: 100%; + font-weight: $gl-font-weight-normal; + line-height: normal; &.dropdown-menu-user-link { white-space: nowrap; diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 670b26d4ca3..b75387e236e 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -17,7 +17,9 @@ class MergeRequestDiffCommit < ActiveRecord::Base commit_hash.merge( merge_request_diff_id: merge_request_diff_id, relative_order: index, - sha: sha_attribute.type_cast_for_database(sha) + sha: sha_attribute.type_cast_for_database(sha), + authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]), + committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]) ) end diff --git a/app/models/project.rb b/app/models/project.rb index 7185b4d44fc..a494193c2d7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -26,7 +26,15 @@ class Project < ActiveRecord::Base NUMBER_OF_PERMITTED_BOARDS = 1 UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze - LATEST_STORAGE_VERSION = 1 + # Hashed Storage versions handle rolling out new storage to project and dependents models: + # nil: legacy + # 1: repository + # 2: attachments + LATEST_STORAGE_VERSION = 2 + HASHED_STORAGE_FEATURES = { + repository: 1, + attachments: 2 + }.freeze cache_markdown_field :description, pipeline: :description @@ -120,6 +128,7 @@ class Project < ActiveRecord::Base has_one :mock_deployment_service has_one :mock_monitoring_service has_one :microsoft_teams_service + has_one :packagist_service # TODO: replace these relations with the fork network versions has_one :forked_project_link, foreign_key: "forked_to_project_id" @@ -1394,6 +1403,19 @@ class Project < ActiveRecord::Base end end + def after_rename_repo + path_before_change = previous_changes['path'].first + + # We need to check if project had been rolled out to move resource to hashed storage or not and decide + # if we need execute any take action or no-op. + + unless hashed_storage?(:attachments) + Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) + end + + Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) + end + def rename_repo_notify! send_move_instructions(full_path_was) expires_full_path_cache @@ -1404,13 +1426,6 @@ class Project < ActiveRecord::Base reload_repository! end - def after_rename_repo - path_before_change = previous_changes['path'].first - - Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) - Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path) - end - def running_or_pending_build_count(force: false) Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do builds.running_or_pending.count(:all) @@ -1600,8 +1615,13 @@ class Project < ActiveRecord::Base [nil, 0].include?(self.storage_version) end - def hashed_storage? - self.storage_version && self.storage_version >= 1 + # Check if Hashed Storage is enabled for the project with at least informed feature rolled out + # + # @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments) + def hashed_storage?(feature) + raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature) + + self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature] end def renamed? @@ -1637,7 +1657,7 @@ class Project < ActiveRecord::Base end def migrate_to_hashed_storage! - return if hashed_storage? + return if hashed_storage?(:repository) update!(repository_read_only: true) @@ -1662,7 +1682,7 @@ class Project < ActiveRecord::Base def storage @storage ||= - if hashed_storage? + if hashed_storage?(:repository) Storage::HashedProject.new(self) else Storage::LegacyProject.new(self) diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb new file mode 100644 index 00000000000..f68a0c1a3c3 --- /dev/null +++ b/app/models/project_services/packagist_service.rb @@ -0,0 +1,65 @@ +class PackagistService < Service + include HTTParty + + prop_accessor :username, :token, :server + + validates :username, presence: true, if: :activated? + validates :token, presence: true, if: :activated? + + default_value_for :push_events, true + default_value_for :tag_push_events, true + + after_save :compose_service_hook, if: :activated? + + def title + 'Packagist' + end + + def description + 'Update your project on Packagist, the main Composer repository' + end + + def self.to_param + 'packagist' + end + + def fields + [ + { type: 'text', name: 'username', placeholder: '', required: true }, + { type: 'text', name: 'token', placeholder: '', required: true }, + { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false } + ] + end + + def self.supported_events + %w(push merge_request tag_push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + service_hook.execute(data) + end + + def test(data) + begin + result = execute(data) + return { success: false, result: result[:message] } if result[:http_status] != 202 + rescue StandardError => error + return { success: false, result: error } + end + + { success: true, result: result[:message] } + end + + def compose_service_hook + hook = service_hook || build_service_hook + hook.url = hook_url + hook.save + end + + def hook_url + base_url = server.present? ? server : 'https://packagist.org' + "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}" + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 6b64079215f..fdd2605e3e3 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -238,6 +238,7 @@ class Service < ActiveRecord::Base kubernetes mattermost_slash_commands mattermost + packagist pipelines_email pivotaltracker prometheus diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb index 41259de3a16..f5945f3b87f 100644 --- a/app/services/projects/hashed_storage_migration_service.rb +++ b/app/services/projects/hashed_storage_migration_service.rb @@ -10,7 +10,7 @@ module Projects end def execute - return if project.hashed_storage? + return if project.hashed_storage?(:repository) @old_disk_path = project.disk_path has_wiki = project.wiki.repository_exists? diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 7027ac4b5db..d4ba3a028be 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -30,7 +30,7 @@ class FileUploader < GitlabUploader # # Returns a String without a trailing slash def self.dynamic_path_segment(model) - File.join(CarrierWave.root, base_dir, model.full_path) + File.join(CarrierWave.root, base_dir, model.disk_path) end attr_accessor :model diff --git a/changelogs/unreleased/3674-hashed-storage-attachments.yml b/changelogs/unreleased/3674-hashed-storage-attachments.yml new file mode 100644 index 00000000000..41bdb5fa568 --- /dev/null +++ b/changelogs/unreleased/3674-hashed-storage-attachments.yml @@ -0,0 +1,5 @@ +--- +title: Hashed Storage support for Attachments +merge_request: 15068 +author: +type: added diff --git a/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml b/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml new file mode 100644 index 00000000000..95251b46ecc --- /dev/null +++ b/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml @@ -0,0 +1,5 @@ +--- +title: Fix namespacing for MergeWhenPipelineSucceedsService in MR API +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39639-clusters-poll.yml b/changelogs/unreleased/39639-clusters-poll.yml new file mode 100644 index 00000000000..f0a82f58b19 --- /dev/null +++ b/changelogs/unreleased/39639-clusters-poll.yml @@ -0,0 +1,5 @@ +--- +title: Adds callback functions for initial request in clusters page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/add-packagist-project-service.yml b/changelogs/unreleased/add-packagist-project-service.yml new file mode 100644 index 00000000000..a13d00e91f7 --- /dev/null +++ b/changelogs/unreleased/add-packagist-project-service.yml @@ -0,0 +1,5 @@ +--- +title: Add Packagist project service +merge_request: 14493 +author: Matt Coleman +type: added diff --git a/changelogs/unreleased/ph-multi-file-upload-file.yml b/changelogs/unreleased/ph-multi-file-upload-file.yml new file mode 100644 index 00000000000..a2bd3cfe459 --- /dev/null +++ b/changelogs/unreleased/ph-multi-file-upload-file.yml @@ -0,0 +1,5 @@ +--- +title: Allow files to uploaded in the multi-file editor +merge_request: +author: +type: added diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index fa882bbe28a..0cb2648cc1e 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -25,7 +25,10 @@ Any change in the URL will need to be reflected on disk (when groups / users or of load in big installations, and can be even worst if they are using any type of network based filesystem. Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct -order or we may end-up with wrong repository or missing data temporarily. +order or we may end-up with wrong repository or missing data temporarily. + +This pattern also exists in other objects stored in GitLab, like issue Attachments, GitLab Pages artifacts, +Docker Containers for the integrated Registry, etc. ## Hashed Storage @@ -67,3 +70,23 @@ To migrate your existing projects to the new storage type, check the specific [r [ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283 [rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage [storage-paths]: repository_storage_types.md + +### Hashed Storage coverage + +We are incrementally moving every storable object in GitLab to the Hashed Storage pattern. You can check the current +coverage status below. + +Not that things stored in S3 compatible endpoint, will not have the downsides mentioned earlier, if they are not +prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects. + +| Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version | +| ----------------| -------------- | -------------- | ------------- | -------------- | +| Repository | Yes | Yes | - | 10.0 | +| Attachments | Yes | Yes | - | 10.2 | +| Avatars | Yes | No | - | - | +| Pages | Yes | No | - | - | +| Docker Registry | Yes | No | - | - | +| CI Build Logs | No | No | - | - | +| CI Artifacts | No | No | - | - | +| CI Cache | No | No | Yes | - | +| LFS Objects | Yes | No | Yes (EEP) | - | diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 890945cfc7e..a6631cab8c3 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -57,7 +57,7 @@ GET /projects/:id/pipelines/:pipeline_id | `pipeline_id` | integer | yes | The ID of a pipeline | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline/46" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46" ``` Example of response diff --git a/doc/api/services.md b/doc/api/services.md index 6c8f196fd5c..e642ec964de 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -582,6 +582,40 @@ Delete Mattermost slash command service for a project. DELETE /projects/:id/services/mattermost-slash-commands ``` +## Packagist + +Update your project on Packagist, the main Composer repository, when commits or tags are pushed to GitLab. + +### Create/Edit Packagist service + +Set Packagist service for a project. + +``` +PUT /projects/:id/services/packagist +``` + +Parameters: + +- `username` (**required**) +- `token` (**required**) +- `server` (optional) + +### Delete Packagist service + +Delete Packagist service for a project. + +``` +DELETE /projects/:id/services/packagist +``` + +### Get Packagist service settings + +Get Packagist service settings for a project. + +``` +GET /projects/:id/services/packagist +``` + ## Pipeline-Emails Get emails for GitLab CI pipelines. diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index 51989ccaaea..a0405161495 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -43,6 +43,7 @@ Click on the service links to see further configuration instructions and details | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | | [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors | +| Packagist | Update your project on Packagist, the main Composer repository | | Pipelines emails | Email the pipeline status to a list of recipients | | [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications | | [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab | diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index be843ec8251..726f09e3669 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -295,7 +295,7 @@ module API unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) - ::MergeRequest::MergeWhenPipelineSucceedsService + ::MergeRequests::MergeWhenPipelineSucceedsService .new(merge_request.target_project, current_user) .cancel(merge_request) end diff --git a/lib/api/services.rb b/lib/api/services.rb index 1e4f7c29633..6454e475036 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -374,6 +374,26 @@ module API desc: 'The Slack token' } ], + 'packagist' => [ + { + required: true, + name: :username, + type: String, + desc: 'The username' + }, + { + required: true, + name: :token, + type: String, + desc: 'The Packagist API token' + }, + { + required: false, + name: :server, + type: String, + desc: 'The server' + } + ], 'pipelines-email' => [ { required: true, @@ -551,6 +571,7 @@ module API KubernetesService, MattermostSlashCommandsService, SlackSlashCommandsService, + PackagistService, PipelinesEmailService, PivotaltrackerService, PrometheusService, diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index 2d13d6fabfd..44ed94d2869 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -395,6 +395,26 @@ module API desc: 'The Slack token' } ], + 'packagist' => [ + { + required: true, + name: :username, + type: String, + desc: 'The username' + }, + { + required: true, + name: :token, + type: String, + desc: 'The Packagist API token' + }, + { + required: false, + name: :server, + type: String, + desc: 'The server' + } + ], 'pipelines-email' => [ { required: true, diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 357f16936c6..43a00d6cedb 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -4,6 +4,10 @@ module Gitlab # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html # http://dev.mysql.com/doc/refman/5.7/en/integer-types.html MAX_INT_VALUE = 2147483647 + # The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz: + # https://www.postgresql.org/docs/9.1/static/datatype-datetime.html + # https://dev.mysql.com/doc/refman/5.7/en/datetime.html + MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze def self.config ActiveRecord::Base.configurations[Rails.env] @@ -120,6 +124,10 @@ module Gitlab EOF end + def self.sanitize_timestamp(timestamp) + MAX_TIMESTAMP_VALUE > timestamp ? timestamp : MAX_TIMESTAMP_VALUE.dup + end + # pool_size - The size of the DB pool. # host - An optional host name to use instead of the default one. def self.create_connection_pool(pool_size, host = nil) diff --git a/spec/features/projects/services/user_activates_packagist_spec.rb b/spec/features/projects/services/user_activates_packagist_spec.rb new file mode 100644 index 00000000000..b0cc818f093 --- /dev/null +++ b/spec/features/projects/services/user_activates_packagist_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe 'User activates Packagist' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Packagist') + end + + it 'activates service' do + check('Active') + fill_in('Username', with: 'theUser') + fill_in('Token', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('Packagist activated.') + end +end diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb index f86591c2633..5c5e8b66642 100644 --- a/spec/features/projects/services/user_views_services_spec.rb +++ b/spec/features/projects/services/user_views_services_spec.rb @@ -21,5 +21,6 @@ describe 'User views services' do expect(page).to have_content('JetBrains TeamCity') expect(page).to have_content('Asana') expect(page).to have_content('Irker (IRC gateway)') + expect(page).to have_content('Packagist') end end diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb new file mode 100644 index 00000000000..7dbe4fd0aa5 --- /dev/null +++ b/spec/features/projects/tree/upload_file_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +feature 'Multi-file editor upload file', :js do + include WaitForRequests + + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') } + let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') } + + before do + project.add_master(user) + sign_in(user) + + page.driver.set_cookie('new_repo', 'true') + + visit project_tree_path(project, :master) + + wait_for_requests + end + + it 'uploads text file' do + find('.add-to-tree').click + + # make the field visible so capybara can use it + execute_script('document.querySelector("#file-upload").classList.remove("hidden")') + attach_file('file-upload', txt_file) + + find('.add-to-tree').click + + expect(page).to have_selector('.repo-tab', text: 'doc_sample.txt') + expect(page).to have_content(File.open(txt_file, &:readline)) + end + + it 'uploads image file' do + find('.add-to-tree').click + + # make the field visible so capybara can use it + execute_script('document.querySelector("#file-upload").classList.remove("hidden")') + attach_file('file-upload', img_file) + + find('.add-to-tree').click + + expect(page).to have_selector('.repo-tab', text: 'dk.png') + expect(page).not_to have_selector('.monaco-editor') + expect(page).to have_content('The source could not be displayed for this temporary file.') + end +end diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 4751eb868a4..2443ffd48f3 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,4 +1,4 @@ -import '~/header'; +import initTodoToggle from '~/header'; describe('Header', function () { const todosPendingCount = '.todos-count'; @@ -14,6 +14,7 @@ describe('Header', function () { preloadFixtures(fixtureTemplate); beforeEach(() => { + initTodoToggle(); loadFixtures(fixtureTemplate); }); diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index ddbfdab582d..ddffef53300 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -74,25 +74,38 @@ describe('new dropdown component', () => { it('closes modal after creating file', () => { vm.openModal = true; - eventHub.$emit('createNewEntry', 'testing', type); + eventHub.$emit('createNewEntry', { + name: 'testing', + type, + toggleModal: true, + }); expect(vm.openModal).toBeFalsy(); }); it('sets editMode to true', () => { - eventHub.$emit('createNewEntry', 'testing', type); + eventHub.$emit('createNewEntry', { + name: 'testing', + type, + }); expect(RepoStore.editMode).toBeTruthy(); }); it('toggles blob view', () => { - eventHub.$emit('createNewEntry', 'testing', type); + eventHub.$emit('createNewEntry', { + name: 'testing', + type, + }); expect(RepoStore.isPreviewView()).toBeFalsy(); }); it('adds file into activeFiles', () => { - eventHub.$emit('createNewEntry', 'testing', type); + eventHub.$emit('createNewEntry', { + name: 'testing', + type, + }); expect(RepoStore.openedFiles.length).toBe(1); }); @@ -100,7 +113,10 @@ describe('new dropdown component', () => { it(`creates ${type} in the current stores path`, () => { RepoStore.path = 'testing'; - eventHub.$emit('createNewEntry', 'testing/app', type); + eventHub.$emit('createNewEntry', { + name: 'testing/app', + type, + }); expect(RepoStore.files[0].path).toBe('testing/app'); expect(RepoStore.files[0].name).toBe('app'); @@ -116,7 +132,10 @@ describe('new dropdown component', () => { describe('file', () => { it('creates new file', () => { - eventHub.$emit('createNewEntry', 'testing', 'blob'); + eventHub.$emit('createNewEntry', { + name: 'testing', + type: 'blob', + }); expect(RepoStore.files.length).toBe(1); expect(RepoStore.files[0].name).toBe('testing'); @@ -129,7 +148,10 @@ describe('new dropdown component', () => { name: 'testing', })); - eventHub.$emit('createNewEntry', 'testing', 'blob'); + eventHub.$emit('createNewEntry', { + name: 'testing', + type: 'blob', + }); expect(RepoStore.files.length).toBe(1); expect(RepoStore.files[0].name).toBe('testing'); @@ -140,7 +162,10 @@ describe('new dropdown component', () => { describe('tree', () => { it('creates new tree', () => { - eventHub.$emit('createNewEntry', 'testing', 'tree'); + eventHub.$emit('createNewEntry', { + name: 'testing', + type: 'tree', + }); expect(RepoStore.files.length).toBe(1); expect(RepoStore.files[0].name).toBe('testing'); @@ -151,7 +176,10 @@ describe('new dropdown component', () => { }); it('creates multiple trees when entryName has slashes', () => { - eventHub.$emit('createNewEntry', 'app/test', 'tree'); + eventHub.$emit('createNewEntry', { + name: 'app/test', + type: 'tree', + }); expect(RepoStore.files.length).toBe(1); expect(RepoStore.files[0].name).toBe('app'); @@ -164,7 +192,10 @@ describe('new dropdown component', () => { name: 'app', })); - eventHub.$emit('createNewEntry', 'app/test', 'tree'); + eventHub.$emit('createNewEntry', { + name: 'app/test', + type: 'tree', + }); expect(RepoStore.files.length).toBe(1); expect(RepoStore.files[0].name).toBe('app'); @@ -179,7 +210,10 @@ describe('new dropdown component', () => { name: 'app', })); - eventHub.$emit('createNewEntry', 'app', 'tree'); + eventHub.$emit('createNewEntry', { + name: 'app', + type: 'tree', + }); expect(RepoStore.files.length).toBe(1); expect(RepoStore.files[0].name).toBe('app'); diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js index 4c5cdc47c6e..d9fd9b9a595 100644 --- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js @@ -70,7 +70,11 @@ describe('new file modal component', () => { vm.createEntryInStore(); - expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', 'testing', 'tree'); + expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { + name: 'testing', + type: 'tree', + toggleModal: true, + }); }); }); }); diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js new file mode 100644 index 00000000000..31878e9d327 --- /dev/null +++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; +import upload from '~/repo/components/new_dropdown/upload.vue'; +import eventHub from '~/repo/event_hub'; +import createComponent from '../../../helpers/vue_mount_component_helper'; + +describe('new dropdown upload', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(upload); + + vm = createComponent(Component, { + currentPath: '', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('readFile', () => { + beforeEach(() => { + spyOn(FileReader.prototype, 'readAsText'); + spyOn(FileReader.prototype, 'readAsDataURL'); + }); + + it('calls readAsText for text files', () => { + const file = { + type: 'text/html', + }; + + vm.readFile(file); + + expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file); + }); + + it('calls readAsDataURL for non-text files', () => { + const file = { + type: 'images/png', + }; + + vm.readFile(file); + + expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file); + }); + }); + + describe('createFile', () => { + const target = { + result: 'content', + }; + const binaryTarget = { + result: 'base64,base64content', + }; + const file = { + name: 'file', + }; + + beforeEach(() => { + spyOn(eventHub, '$emit'); + }); + + it('emits createNewEntry event', () => { + vm.createFile(target, file, true); + + expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { + name: 'file', + type: 'blob', + content: 'content', + toggleModal: false, + base64: false, + }, true); + }); + + it('createNewEntry event name contains current path', () => { + vm.currentPath = 'testing'; + vm.createFile(target, file, true); + + expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { + name: 'testing/file', + type: 'blob', + content: 'content', + toggleModal: false, + base64: false, + }, true); + }); + + it('splits content on base64 if binary', () => { + vm.createFile(binaryTarget, file, false); + + expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', { + name: 'file', + type: 'blob', + content: 'base64content', + toggleModal: false, + base64: true, + }, false); + }); + }); +}); diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 5fa94999d25..7aeb85b8f5a 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -256,4 +256,26 @@ describe Gitlab::Database do expect(described_class.false_value).to eq 0 end end + + describe '#sanitize_timestamp' do + let(:max_timestamp) { Time.at((1 << 31) - 1) } + + subject { described_class.sanitize_timestamp(timestamp) } + + context 'with a timestamp smaller than MAX_TIMESTAMP_VALUE' do + let(:timestamp) { max_timestamp - 10.years } + + it 'returns the given timestamp' do + expect(subject).to eq(timestamp) + end + end + + context 'with a timestamp larger than MAX_TIMESTAMP_VALUE' do + let(:timestamp) { max_timestamp + 1.second } + + it 'returns MAX_TIMESTAMP_VALUE' do + expect(subject).to eq(max_timestamp) + end + end + end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 29baa70d5ae..3824bb0757c 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -195,6 +195,7 @@ project: - mattermost_slash_commands_service - slack_slash_commands_service - irker_service +- packagist_service - pivotaltracker_service - prometheus_service - hipchat_service diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb index 9d4a0ecf8c0..7709cf43200 100644 --- a/spec/models/merge_request_diff_commit_spec.rb +++ b/spec/models/merge_request_diff_commit_spec.rb @@ -2,14 +2,93 @@ require 'rails_helper' describe MergeRequestDiffCommit do let(:merge_request) { create(:merge_request) } - subject { merge_request.commits.first } + let(:project) { merge_request.project } describe '#to_hash' do + subject { merge_request.commits.first } + it 'returns the same results as Commit#to_hash, except for parent_ids' do - commit_from_repo = merge_request.project.repository.commit(subject.sha) + commit_from_repo = project.repository.commit(subject.sha) commit_from_repo_hash = commit_from_repo.to_hash.merge(parent_ids: []) expect(subject.to_hash).to eq(commit_from_repo_hash) end end + + describe '.create_bulk' do + let(:sha_attribute) { Gitlab::Database::ShaAttribute.new } + let(:merge_request_diff_id) { merge_request.merge_request_diff.id } + let(:commits) do + [ + project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e'), + project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + ] + end + let(:rows) do + [ + { + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "authored_date": "2014-02-27T10:01:38.000+01:00".to_time, + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T10:01:38.000+01:00".to_time, + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com", + "merge_request_diff_id": merge_request_diff_id, + "relative_order": 0, + "sha": sha_attribute.type_cast_for_database('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + }, + { + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "authored_date": "2014-02-27T09:57:31.000+01:00".to_time, + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:57:31.000+01:00".to_time, + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com", + "merge_request_diff_id": merge_request_diff_id, + "relative_order": 1, + "sha": sha_attribute.type_cast_for_database('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + } + ] + end + + subject { described_class.create_bulk(merge_request_diff_id, commits) } + + it 'inserts the commits into the database en masse' do + expect(Gitlab::Database).to receive(:bulk_insert) + .with(described_class.table_name, rows) + + subject + end + + context 'with dates larger than the DB limit' do + let(:commits) do + # This commit's date is "Sun Aug 17 07:12:55 292278994 +0000" + [project.commit('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')] + end + let(:timestamp) { Time.at((1 << 31) - 1) } + let(:rows) do + [{ + "message": "Weird commit date\n", + "authored_date": timestamp, + "author_name": "Alejandro RodrÃguez", + "author_email": "alejorro70@gmail.com", + "committed_date": timestamp, + "committer_name": "Alejandro RodrÃguez", + "committer_email": "alejorro70@gmail.com", + "merge_request_diff_id": merge_request_diff_id, + "relative_order": 0, + "sha": sha_attribute.type_cast_for_database('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69') + }] + end + + it 'uses a sanitized date' do + expect(Gitlab::Database).to receive(:bulk_insert) + .with(described_class.table_name, rows) + + subject + end + end + end end diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb new file mode 100644 index 00000000000..6acee311700 --- /dev/null +++ b/spec/models/project_services/packagist_service_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe PackagistService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + let(:project) { create(:project) } + + let(:packagist_server) { 'https://packagist.example.com' } + let(:packagist_username) { 'theUser' } + let(:packagist_token) { 'verySecret' } + let(:packagist_hook_url) do + "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}" + end + + let(:packagist_params) do + { + active: true, + project: project, + properties: { + username: packagist_username, + token: packagist_token, + server: packagist_server + } + } + end + + describe '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } + let(:packagist_service) { described_class.create(packagist_params) } + + before do + stub_request(:post, packagist_hook_url) + end + + it 'calls Packagist API' do + packagist_service.execute(push_sample_data) + + expect(a_request(:post, packagist_hook_url)).to have_been_made.once + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 74eba7e33f6..ed6e42d476e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -24,6 +24,7 @@ describe Project do it { is_expected.to have_one(:slack_service) } it { is_expected.to have_one(:microsoft_teams_service) } it { is_expected.to have_one(:mattermost_service) } + it { is_expected.to have_one(:packagist_service) } it { is_expected.to have_one(:pushover_service) } it { is_expected.to have_one(:asana_service) } it { is_expected.to have_many(:boards) } @@ -2452,6 +2453,7 @@ describe Project do context 'legacy storage' do let(:project) { create(:project, :repository) } let(:gitlab_shell) { Gitlab::Shell.new } + let(:project_storage) { project.send(:storage) } before do allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) @@ -2493,7 +2495,7 @@ describe Project do describe '#hashed_storage?' do it 'returns false' do - expect(project.hashed_storage?).to be_falsey + expect(project.hashed_storage?(:repository)).to be_falsey end end @@ -2546,6 +2548,30 @@ describe Project do it { expect { subject }.to raise_error(StandardError) } end + + context 'gitlab pages' do + before do + expect(project_storage).to receive(:rename_repo) { true } + end + + it 'moves pages folder to new location' do + expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) + + project.rename_repo + end + end + + context 'attachments' do + before do + expect(project_storage).to receive(:rename_repo) { true } + end + + it 'moves uploads folder to new location' do + expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project) + + project.rename_repo + end + end end describe '#pages_path' do @@ -2605,8 +2631,14 @@ describe Project do end describe '#hashed_storage?' do - it 'returns true' do - expect(project.hashed_storage?).to be_truthy + it 'returns true if rolled out' do + expect(project.hashed_storage?(:attachments)).to be_truthy + end + + it 'returns false when not rolled out yet' do + project.storage_version = 1 + + expect(project.hashed_storage?(:attachments)).to be_falsey end end @@ -2649,10 +2681,6 @@ describe Project do .to receive(:execute_hooks_for) .with(project, :rename) - expect_any_instance_of(Gitlab::UploadsTransfer) - .to receive(:rename_project) - .with('foo', project.path, project.namespace.full_path) - expect(project).to receive(:expire_caches_before_rename) expect(project).to receive(:expires_full_path_cache) @@ -2673,6 +2701,32 @@ describe Project do it { expect { subject }.to raise_error(StandardError) } end + + context 'gitlab pages' do + it 'moves pages folder to new location' do + expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) + + project.rename_repo + end + end + + context 'attachments' do + it 'keeps uploads folder location unchanged' do + expect_any_instance_of(Gitlab::UploadsTransfer).not_to receive(:rename_project) + + project.rename_repo + end + + context 'when not rolled out' do + let(:project) { create(:project, :repository, storage_version: 1) } + + it 'moves pages folder to new location' do + expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project) + + project.rename_repo + end + end + end end describe '#pages_path' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 28b1404a4f7..024cfe8b372 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1061,6 +1061,30 @@ describe API::MergeRequests do end end + describe 'POST :id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do + before do + ::MergeRequests::MergeWhenPipelineSucceedsService.new(merge_request.target_project, user).execute(merge_request) + end + + it 'removes the merge_when_pipeline_succeeds status' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/cancel_merge_when_pipeline_succeeds", user) + + expect(response).to have_gitlab_http_status(201) + end + + it 'returns 404 if the merge request is not found' do + post api("/projects/#{project.id}/merge_requests/123/merge_when_pipeline_succeeds", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 if the merge request id is used instead of iid' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge_when_pipeline_succeeds", user) + + expect(response).to have_gitlab_http_status(404) + end + end + describe 'Time tracking' do let(:issuable) { merge_request } diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb index aa1988d29d6..b71b47c59b6 100644 --- a/spec/services/projects/hashed_storage_migration_service_spec.rb +++ b/spec/services/projects/hashed_storage_migration_service_spec.rb @@ -23,7 +23,7 @@ describe Projects::HashedStorageMigrationService do it 'updates project to be hashed and not read-only' do service.execute - expect(project.hashed_storage?).to be_truthy + expect(project.hashed_storage?(:repository)).to be_truthy expect(project.repository_read_only).to be_falsey end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 2492d56a5cf..f52b2bab05b 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -3,25 +3,51 @@ require 'spec_helper' describe FileUploader do let(:uploader) { described_class.new(build_stubbed(:project)) } - describe '.absolute_path' do - it 'returns the correct absolute path by building it dynamically' do - project = build_stubbed(:project) - upload = double(model: project, path: 'secret/foo.jpg') + context 'legacy storage' do + let(:project) { build_stubbed(:project) } - dynamic_segment = project.path_with_namespace + describe '.absolute_path' do + it 'returns the correct absolute path by building it dynamically' do + upload = double(model: project, path: 'secret/foo.jpg') - expect(described_class.absolute_path(upload)) - .to end_with("#{dynamic_segment}/secret/foo.jpg") + dynamic_segment = project.full_path + + expect(described_class.absolute_path(upload)) + .to end_with("#{dynamic_segment}/secret/foo.jpg") + end + end + + describe "#store_dir" do + it "stores in the namespace path" do + uploader = described_class.new(project) + + expect(uploader.store_dir).to include(project.full_path) + expect(uploader.store_dir).not_to include("system") + end end end - describe "#store_dir" do - it "stores in the namespace path" do - project = build_stubbed(:project) - uploader = described_class.new(project) + context 'hashed storage' do + let(:project) { build_stubbed(:project, :hashed) } + + describe '.absolute_path' do + it 'returns the correct absolute path by building it dynamically' do + upload = double(model: project, path: 'secret/foo.jpg') + + dynamic_segment = project.disk_path + + expect(described_class.absolute_path(upload)) + .to end_with("#{dynamic_segment}/secret/foo.jpg") + end + end + + describe "#store_dir" do + it "stores in the namespace path" do + uploader = described_class.new(project) - expect(uploader.store_dir).to include(project.path_with_namespace) - expect(uploader.store_dir).not_to include("system") + expect(uploader.store_dir).to include(project.disk_path) + expect(uploader.store_dir).not_to include("system") + end end end |