diff options
57 files changed, 908 insertions, 148 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 564edf82ddf..c5d4cee36a1 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.50.0 +0.51.0 @@ -398,7 +398,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.48.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 34f4e6af7e7..0a94a9ce497 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -273,7 +273,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.48.0) + gitaly-proto (0.51.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -1030,7 +1030,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.48.0) + gitaly-proto (~> 0.51.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) 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/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index c94384d2a1a..980bbf699b6 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -2,13 +2,13 @@ class Projects::MilestonesController < Projects::ApplicationController include MilestoneActions before_action :check_issuables_available! - before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels] + before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels, :promote] # Allow read any milestone before_action :authorize_read_milestone! # Allow admin milestone - before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels] + before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels, :promote] respond_to :html @@ -69,6 +69,14 @@ class Projects::MilestonesController < Projects::ApplicationController end end + def promote + promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) + flash[:notice] = "Milestone has been promoted to group milestone." + redirect_to group_milestone_path(project.group, promoted_milestone.iid) + rescue Milestones::PromoteService::PromoteMilestoneError => error + redirect_to milestone, alert: error.message + end + def destroy return access_denied! unless can?(current_user, :admin_milestone, @project) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 5a74511afa7..8ada746b244 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -19,11 +19,7 @@ module NavHelper end elsif current_path?('jobs#show') %w[page-gutter build-sidebar right-sidebar-expanded] - elsif current_path?('wikis#show') || - current_path?('wikis#edit') || - current_path?('wikis#update') || - current_path?('wikis#history') || - current_path?('wikis#git_access') + elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access') %w[page-gutter wiki-sidebar right-sidebar-expanded] else [] 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..413866b994a 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" @@ -1083,6 +1092,7 @@ class Project < ActiveRecord::Base def hook_attrs(backward: true) attrs = { + id: id, name: name, description: description, web_url: web_url, @@ -1394,6 +1404,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 +1427,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 +1616,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 +1658,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 +1683,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/milestones/promote_service.rb b/app/services/milestones/promote_service.rb new file mode 100644 index 00000000000..bd9cfd4e0ea --- /dev/null +++ b/app/services/milestones/promote_service.rb @@ -0,0 +1,80 @@ +module Milestones + class PromoteService < Milestones::BaseService + PromoteMilestoneError = Class.new(StandardError) + + def execute(milestone) + check_project_milestone!(milestone) + + Milestone.transaction do + # Destroy all milestones with same title across projects + destroy_old_milestones(milestone) + + group_milestone = clone_project_milestone(milestone) + + move_children_to_group_milestone(group_milestone) + + # Just to be safe + unless group_milestone.valid? + raise_error(group_milestone.errors.full_messages.to_sentence) + end + + group_milestone + end + end + + private + + def milestone_ids_for_merge(group_milestone) + # Pluck need to be used here instead of select so the array of ids + # is persistent after old milestones gets deleted. + @milestone_ids_for_merge ||= begin + search_params = { title: group_milestone.title, project_ids: group_project_ids, state: 'all' } + milestones = MilestonesFinder.new(search_params).execute + milestones.pluck(:id) + end + end + + def move_children_to_group_milestone(group_milestone) + milestone_ids_for_merge(group_milestone).in_groups_of(100) do |milestone_ids| + update_children(group_milestone, milestone_ids) + end + end + + def check_project_milestone!(milestone) + raise_error('Only project milestones can be promoted.') unless milestone.project_milestone? + end + + def clone_project_milestone(milestone) + params = milestone.slice(:title, :description, :start_date, :due_date, :state_event) + + create_service = CreateService.new(group, current_user, params) + + create_service.execute + end + + def update_children(group_milestone, milestone_ids) + issues = Issue.where(project_id: group_project_ids, milestone_id: milestone_ids) + merge_requests = MergeRequest.where(source_project_id: group_project_ids, milestone_id: milestone_ids) + + [issues, merge_requests].each do |issuable_collection| + issuable_collection.update_all(milestone_id: group_milestone.id) + end + end + + def group + @group ||= parent.group || raise_error('Project does not belong to a group.') + end + + def destroy_old_milestones(group_milestone) + Milestone.where(id: milestone_ids_for_merge(group_milestone)).destroy_all + end + + def group_project_ids + @group_project_ids ||= group.projects.map(&:id) + end + + def raise_error(message) + raise PromoteMilestoneError, "Promotion failed - #{message}" + end + end +end 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/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index a5153df1159..9fc297ab7f6 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -23,14 +23,18 @@ = milestone_date_range(@milestone) .milestone-buttons - if can?(current_user, :admin_milestone, @project) + = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do + Edit + + - if @project.group + = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do + Promote + - if @milestone.active? = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" - else = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do - Edit - = link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do Delete @@ -40,6 +44,7 @@ .detail-page-description.milestone-detail %h2.title = markdown_field(@milestone, :title) + %div - if @milestone.description.present? .description diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 305e2542281..7ba8f9d4313 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -49,6 +49,13 @@ = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do Edit \ + + - if @project.group + = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do + Promote + = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" + = link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do Delete + diff --git a/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml b/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml new file mode 100644 index 00000000000..daf7ac715bd --- /dev/null +++ b/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml @@ -0,0 +1,5 @@ +--- +title: Adds project_id to pipeline hook data +merge_request: 15044 +author: Jacopo Beschi @jacopo-beschi +type: added 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/39509-fix-wiki-create-sidebar-overlap.yml b/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml new file mode 100644 index 00000000000..aebf6363d97 --- /dev/null +++ b/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml @@ -0,0 +1,5 @@ +--- +title: Fix overlap of right-sidebar and main content when creating a Wiki page +merge_request: +author: +type: fixed 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/issue_38777.yml b/changelogs/unreleased/issue_38777.yml new file mode 100644 index 00000000000..5c49b2f7879 --- /dev/null +++ b/changelogs/unreleased/issue_38777.yml @@ -0,0 +1,5 @@ +--- +title: Allow promoting project milestones to group milestones +merge_request: +author: +type: added diff --git a/config/routes/project.rb b/config/routes/project.rb index 9f553085d50..746c0c46677 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -293,6 +293,7 @@ constraints(ProjectUrlConstrainer.new) do resources :milestones, constraints: { id: /\d+/ } do member do + post :promote put :sort_issues put :sort_merge_requests get :merge_requests 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/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 7abc600a680..df75e25e12b 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -76,6 +76,7 @@ X-Gitlab-Event: Push Hook "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", "project_id": 15, "project":{ + "id": 15, "name":"Diaspora", "description":"", "web_url":"http://example.com/mike/diaspora", @@ -156,6 +157,7 @@ X-Gitlab-Event: Tag Push Hook "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", "project_id": 1, "project":{ + "id": 1, "name":"Example", "description":"", "web_url":"http://example.com/jsmith/example", @@ -206,6 +208,7 @@ X-Gitlab-Event: Issue Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project": { + "id": 1, "name":"Gitlab Test", "description":"Aut reprehenderit ut est.", "web_url":"http://example.com/gitlabhq/gitlab-test", @@ -335,6 +338,7 @@ X-Gitlab-Event: Note Hook }, "project_id": 5, "project":{ + "id": 5, "name":"Gitlab Test", "description":"Aut reprehenderit ut est.", "web_url":"http://example.com/gitlabhq/gitlab-test", @@ -414,6 +418,7 @@ X-Gitlab-Event: Note Hook }, "project_id": 5, "project":{ + "id": 5, "name":"Gitlab Test", "description":"Aut reprehenderit ut est.", "web_url":"http://example.com/gitlab-org/gitlab-test", @@ -540,6 +545,7 @@ X-Gitlab-Event: Note Hook }, "project_id": 5, "project":{ + "id": 5, "name":"Gitlab Test", "description":"Aut reprehenderit ut est.", "web_url":"http://example.com/gitlab-org/gitlab-test", @@ -618,6 +624,7 @@ X-Gitlab-Event: Note Hook }, "project_id": 5, "project":{ + "id": 5, "name":"Gitlab Test", "description":"Aut reprehenderit ut est.", "web_url":"http://example.com/gitlab-org/gitlab-test", @@ -692,6 +699,7 @@ X-Gitlab-Event: Merge Request Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project": { + "id": 1, "name":"Gitlab Test", "description":"Aut reprehenderit ut est.", "web_url":"http://example.com/gitlabhq/gitlab-test", @@ -848,6 +856,7 @@ X-Gitlab-Event: Wiki Page Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" }, "project": { + "id": 1, "name": "awesome-project", "description": "This is awesome", "web_url": "http://example.com/root/awesome-project", @@ -919,6 +928,7 @@ X-Gitlab-Event: Pipeline Hook "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" }, "project":{ + "id": 1, "name": "Gitlab Test", "description": "Atque in sunt eos similique dolores voluptatem.", "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test", diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md index 876b98a4dc5..83adbd8cce2 100644 --- a/doc/user/project/milestones/index.md +++ b/doc/user/project/milestones/index.md @@ -29,7 +29,8 @@ In addition to that you will be able to filter issues or merge requests by group ## Milestone promotion -You will be able to promote a project milestone to a group milestone [in the future](https://gitlab.com/gitlab-org/gitlab-ce/issues/35833). +Project milestones can be promoted to group milestones if its project belongs to a group. When a milestone is promoted all other milestones across the group projects with the same title will be merged into it, which means all milestone's children like issues, merge requests and boards will be moved into the new promoted milestone. +The promote button can be found in the milestone view or milestones list. ## Special milestone filters 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/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index f01d5c96fc8..9b387a19388 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -35,10 +35,14 @@ module Gitlab end def delete_page(page_path, commit_details) - assert_type!(commit_details, CommitDetails) - - gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h) - nil + @repository.gitaly_migrate(:wiki_delete_page) do |is_enabled| + if is_enabled + gitaly_delete_page(page_path, commit_details) + gollum_wiki.clear_cache + else + gollum_delete_page(page_path, commit_details) + end + end end def update_page(page_path, title, format, content, commit_details) @@ -135,9 +139,20 @@ module Gitlab raise Gitlab::Git::Wiki::DuplicatePageError, e.message end + def gollum_delete_page(page_path, commit_details) + assert_type!(commit_details, CommitDetails) + + gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h) + nil + end + def gitaly_write_page(name, format, content, commit_details) gitaly_wiki_client.write_page(name, format, content, commit_details) end + + def gitaly_delete_page(page_path, commit_details) + gitaly_wiki_client.delete_page(page_path, commit_details) + end end end end diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index 03afcce81f0..b7407dc1cc1 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -15,11 +15,7 @@ module Gitlab repository: @gitaly_repo, name: GitalyClient.encode(name), format: format.to_s, - commit_details: Gitaly::WikiCommitDetails.new( - name: GitalyClient.encode(commit_details.name), - email: GitalyClient.encode(commit_details.email), - message: GitalyClient.encode(commit_details.message) - ) + commit_details: gitaly_commit_details(commit_details) ) strio = StringIO.new(content) @@ -40,6 +36,26 @@ module Gitlab raise Gitlab::Git::Wiki::DuplicatePageError, error end end + + def delete_page(page_path, commit_details) + request = Gitaly::WikiDeletePageRequest.new( + repository: @gitaly_repo, + page_path: GitalyClient.encode(page_path), + commit_details: gitaly_commit_details(commit_details) + ) + + GitalyClient.call(@repository.storage, :wiki_service, :wiki_delete_page, request) + end + + private + + def gitaly_commit_details(commit_details) + Gitaly::WikiCommitDetails.new( + name: GitalyClient.encode(commit_details.name), + email: GitalyClient.encode(commit_details.email), + message: GitalyClient.encode(commit_details.message) + ) + end end end end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 573530d0db0..209979e642d 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -86,4 +86,32 @@ describe Projects::MilestonesController do expect(last_note).to eq('removed milestone') end end + + describe '#promote' do + context 'promotion succeeds' do + before do + group = create(:group) + group.add_developer(user) + milestone.project.update(namespace: group) + end + + it 'shows group milestone' do + post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid + + group_milestone = assigns(:milestone) + + expect(response).to redirect_to(group_milestone_path(project.group, group_milestone.iid)) + expect(flash[:notice]).to eq('Milestone has been promoted to group milestone.') + end + end + + context 'promotion fails' do + it 'shows project milestone' do + post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid + + expect(response).to redirect_to(project_milestone_path(project, milestone)) + expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.') + end + end + end end 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/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/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 f5f48c47520..6c6b9154a0a 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/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index f10d9383ae2..95d58b96f33 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -265,23 +265,33 @@ describe ProjectWiki do end describe "#delete_page" do - before do - create_page("index", "some content") - @page = subject.wiki.page(title: "index") - end + shared_examples 'deleting a wiki page' do + before do + create_page("index", "some content") + @page = subject.wiki.page(title: "index") + end - it "deletes the page" do - subject.delete_page(@page) - expect(subject.pages.count).to eq(0) - end + it "deletes the page" do + subject.delete_page(@page) + expect(subject.pages.count).to eq(0) + end - it 'updates project activity' do - subject.delete_page(@page) + it 'updates project activity' do + subject.delete_page(@page) - project.reload + project.reload - expect(project.last_activity_at).to be_within(1.minute).of(Time.now) - expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) + expect(project.last_activity_at).to be_within(1.minute).of(Time.now) + expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) + end + end + + context 'when Gitaly wiki_delete_page is enabled' do + it_behaves_like 'deleting a wiki page' + end + + context 'when Gitaly wiki_delete_page is disabled', :skip_gitaly_mock do + it_behaves_like 'deleting a wiki page' end end @@ -343,6 +353,6 @@ describe ProjectWiki do end def destroy_page(page) - subject.delete_page(page, commit_details) + subject.delete_page(page, "test commit") end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 1f14d06997e..a7227b38850 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -402,7 +402,7 @@ describe WikiPage do def destroy_page(title) page = wiki.wiki.page(title: title) - wiki.delete_page(page, commit_details) + wiki.delete_page(page, "test commit") end def get_slugs(page_or_dir) 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/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 39d44245c3f..fb1281a6b42 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -426,18 +426,23 @@ describe 'project routing' do end end - # project_milestones GET /:project_id/milestones(.:format) milestones#index - # POST /:project_id/milestones(.:format) milestones#create - # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new - # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit - # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show - # PUT /:project_id/milestones/:id(.:format) milestones#update - # DELETE /:project_id/milestones/:id(.:format) milestones#destroy + # project_milestones GET /:project_id/milestones(.:format) milestones#index + # POST /:project_id/milestones(.:format) milestones#create + # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new + # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit + # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show + # PUT /:project_id/milestones/:id(.:format) milestones#update + # DELETE /:project_id/milestones/:id(.:format) milestones#destroy + # promote_project_milestone POST /:project_id/milestones/:id/promote milestones#promote describe Projects::MilestonesController, 'routing' do it_behaves_like 'RESTful project resources' do let(:controller) { 'milestones' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } end + + it 'to #promote' do + expect(post('/gitlab/gitlabhq/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1") + end end # project_labels GET /:project_id/labels(.:format) labels#index diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb new file mode 100644 index 00000000000..9f2df6d6d19 --- /dev/null +++ b/spec/services/milestones/promote_service_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Milestones::PromoteService do + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:user) { create(:user) } + let(:milestone_title) { 'project milestone' } + let(:milestone) { create(:milestone, project: project, title: milestone_title) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + before do + group.add_master(user) + end + + context 'validations' do + it 'raises error if milestone does not belong to a project' do + allow(milestone).to receive(:project_milestone?).and_return(false) + + expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError) + end + + it 'raises error if project does not belong to a group' do + project.update(namespace: user.namespace) + + expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError) + end + end + + context 'without duplicated milestone titles across projects' do + it 'promotes project milestone to group milestone' do + promoted_milestone = service.execute(milestone) + + expect(promoted_milestone).to be_group_milestone + end + + it 'sets issuables with new promoted milestone' do + issue = create(:issue, milestone: milestone, project: project) + merge_request = create(:merge_request, milestone: milestone, source_project: project) + + promoted_milestone = service.execute(milestone) + + expect(promoted_milestone).to be_group_milestone + expect(issue.reload.milestone).to eq(promoted_milestone) + expect(merge_request.reload.milestone).to eq(promoted_milestone) + end + end + + context 'with duplicated milestone titles across projects' do + let(:project_2) { create(:project, namespace: group) } + let!(:milestone_2) { create(:milestone, project: project_2, title: milestone_title) } + + it 'deletes project milestones with the same title' do + promoted_milestone = service.execute(milestone) + + expect(promoted_milestone).to be_group_milestone + expect(promoted_milestone).to be_valid + expect(Milestone.exists?(milestone.id)).to be_falsy + expect(Milestone.exists?(milestone_2.id)).to be_falsy + end + + it 'sets all issuables with new promoted milestone' do + issue = create(:issue, milestone: milestone, project: project) + issue_2 = create(:issue, milestone: milestone_2, project: project_2) + merge_request = create(:merge_request, milestone: milestone, source_project: project) + merge_request_2 = create(:merge_request, milestone: milestone_2, source_project: project_2) + + promoted_milestone = service.execute(milestone) + + expect(issue.reload.milestone).to eq(promoted_milestone) + expect(issue_2.reload.milestone).to eq(promoted_milestone) + expect(merge_request.reload.milestone).to eq(promoted_milestone) + expect(merge_request_2.reload.milestone).to eq(promoted_milestone) + end + end + end +end 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 |