summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/clusters.js22
-rw-r--r--app/assets/javascripts/header.js14
-rw-r--r--app/assets/javascripts/init_legacy_filters.js4
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js5
-rw-r--r--app/assets/javascripts/issue.js4
-rw-r--r--app/assets/javascripts/issue_status_select.js57
-rw-r--r--app/assets/javascripts/main.js6
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue15
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/modal.vue6
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/upload.vue67
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue1
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue7
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js31
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--app/models/merge_request_diff_commit.rb4
-rw-r--r--app/models/project.rb44
-rw-r--r--app/models/project_services/packagist_service.rb65
-rw-r--r--app/models/service.rb1
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb2
-rw-r--r--app/uploaders/file_uploader.rb2
-rw-r--r--changelogs/unreleased/3674-hashed-storage-attachments.yml5
-rw-r--r--changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml5
-rw-r--r--changelogs/unreleased/39639-clusters-poll.yml5
-rw-r--r--changelogs/unreleased/add-packagist-project-service.yml5
-rw-r--r--changelogs/unreleased/ph-multi-file-upload-file.yml5
-rw-r--r--doc/administration/repository_storage_types.md25
-rw-r--r--doc/api/pipelines.md2
-rw-r--r--doc/api/services.md34
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/services.rb21
-rw-r--r--lib/api/v3/services.rb20
-rw-r--r--lib/gitlab/database.rb8
-rw-r--r--spec/features/projects/services/user_activates_packagist_spec.rb24
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb1
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb48
-rw-r--r--spec/javascripts/header_spec.js3
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js56
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js6
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js100
-rw-r--r--spec/lib/gitlab/database_spec.rb22
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb83
-rw-r--r--spec/models/project_services/packagist_service_spec.rb46
-rw-r--r--spec/models/project_spec.rb68
-rw-r--r--spec/requests/api/merge_requests_spec.rb24
-rw-r--r--spec/services/projects/hashed_storage_migration_service_spec.rb2
-rw-r--r--spec/uploaders/file_uploader_spec.rb52
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