Commit a8de96bf authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent afe2b984
...@@ -254,4 +254,4 @@ danger-review: ...@@ -254,4 +254,4 @@ danger-review:
- git version - git version
- node --version - node --version
- yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- danger --fail-on-errors=true --new-comment --remove-previous-comments --verbose - danger --fail-on-errors=true --verbose
...@@ -12,7 +12,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla ...@@ -12,7 +12,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
## Developer checklist ## Developer checklist
- [ ] **Make sure this merge request mentions the [GitLab Security] issue it belongs to (i.e. `Related to <issue_id>`).** - [ ] **On "Related issues" section, write down the [GitLab Security] issue it belongs to (i.e. `Related to <issue_id>`).**
- [ ] Merge request targets `master`, or `X-Y-stable` for backports. - [ ] Merge request targets `master`, or `X-Y-stable` for backports.
- [ ] Milestone is set for the version this merge request applies to. A closed milestone can be assigned via [quick actions]. - [ ] Milestone is set for the version this merge request applies to. A closed milestone can be assigned via [quick actions].
- [ ] Title of this merge request is the same as for all backports. - [ ] Title of this merge request is the same as for all backports.
......
<script>
import { GlFormInput } from '@gitlab/ui';
export default {
components: {
GlFormInput,
},
props: {
value: {
type: String,
required: true,
},
},
data() {
return {
name: this.value,
};
},
};
</script>
<template>
<div class="js-file-title file-title-flex-parent">
<gl-form-input
id="snippet_file_name"
v-model="name"
:placeholder="
s__('Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby')
"
name="snippet_file_name"
class="form-control js-snippet-file-name qa-snippet-file-name"
type="text"
@change="$emit('input', name)"
/>
</div>
</template>
...@@ -63,7 +63,9 @@ export default { ...@@ -63,7 +63,9 @@ export default {
methods: { methods: {
toggleForm() { toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; if (this.isEditable) {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
}
}, },
updateLockedAttribute(locked) { updateLockedAttribute(locked) {
this.mediator.service this.mediator.service
......
query ($fullPath: ID!, $iid: String!) {
project (fullPath: $fullPath) {
issue (iid: $iid) {
iid
}
}
}
query ($fullPath: ID!, $iid: String!) {
project (fullPath: $fullPath) {
issue (iid: $iid) {
iid
}
}
}
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
import sidebarDetailsForHealthStatusFeatureFlagQuery from 'ee_else_ce/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql';
export const gqClient = createGqClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);
export default class SidebarService { export default class SidebarService {
constructor(endpointMap) { constructor(endpointMap) {
...@@ -7,6 +17,8 @@ export default class SidebarService { ...@@ -7,6 +17,8 @@ export default class SidebarService {
this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint; this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint; this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint; this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
this.fullPath = endpointMap.fullPath;
this.id = endpointMap.id;
SidebarService.singleton = this; SidebarService.singleton = this;
} }
...@@ -15,7 +27,20 @@ export default class SidebarService { ...@@ -15,7 +27,20 @@ export default class SidebarService {
} }
get() { get() {
return axios.get(this.endpoint); const hasHealthStatusFeatureFlag = gon.features && gon.features.saveIssuableHealthStatus;
return Promise.all([
axios.get(this.endpoint),
gqClient.query({
query: hasHealthStatusFeatureFlag
? sidebarDetailsForHealthStatusFeatureFlagQuery
: sidebarDetailsQuery,
variables: {
fullPath: this.fullPath,
iid: this.id.toString(),
},
}),
]);
} }
update(key, data) { update(key, data) {
......
...@@ -19,6 +19,8 @@ export default class SidebarMediator { ...@@ -19,6 +19,8 @@ export default class SidebarMediator {
toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint, toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint, moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
fullPath: options.fullPath,
id: options.id,
}); });
SidebarMediator.singleton = this; SidebarMediator.singleton = this;
} }
...@@ -45,8 +47,8 @@ export default class SidebarMediator { ...@@ -45,8 +47,8 @@ export default class SidebarMediator {
fetch() { fetch() {
return this.service return this.service
.get() .get()
.then(({ data }) => { .then(([restResponse, graphQlResponse]) => {
this.processFetchedData(data); this.processFetchedData(restResponse.data, graphQlResponse.data);
}) })
.catch(() => new Flash(__('Error occurred when fetching sidebar data'))); .catch(() => new Flash(__('Error occurred when fetching sidebar data')));
} }
......
...@@ -17,7 +17,7 @@ const initAce = () => { ...@@ -17,7 +17,7 @@ const initAce = () => {
const initMonaco = () => { const initMonaco = () => {
const editorEl = document.getElementById('editor'); const editorEl = document.getElementById('editor');
const contentEl = document.querySelector('.snippet-file-content'); const contentEl = document.querySelector('.snippet-file-content');
const fileNameEl = document.querySelector('.snippet-file-name'); const fileNameEl = document.querySelector('.js-snippet-file-name');
const form = document.querySelector('.snippet-form-holder form'); const form = document.querySelector('.snippet-form-holder form');
editor = new Editor(); editor = new Editor();
......
...@@ -77,3 +77,5 @@ ...@@ -77,3 +77,5 @@
.gl-text-red-700 { @include gl-text-red-700; } .gl-text-red-700 { @include gl-text-red-700; }
.gl-text-orange-700 { @include gl-text-orange-700; } .gl-text-orange-700 { @include gl-text-orange-700; }
.gl-text-green-700 { @include gl-text-green-700; } .gl-text-green-700 { @include gl-text-green-700; }
.gl-align-items-center { @include gl-align-items-center; }
...@@ -44,6 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -44,6 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group) push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:save_issuable_health_status, project.group)
end end
around_action :allow_gitaly_ref_name_caching, only: [:discussions] around_action :allow_gitaly_ref_name_caching, only: [:discussions]
......
...@@ -35,7 +35,7 @@ module AnalyticsNavbarHelper ...@@ -35,7 +35,7 @@ module AnalyticsNavbarHelper
return unless project_nav_tab?(:cycle_analytics) return unless project_nav_tab?(:cycle_analytics)
navbar_sub_item( navbar_sub_item(
title: _('Value Stream Analytics'), title: _('Value Stream'),
path: 'cycle_analytics#show', path: 'cycle_analytics#show',
link: project_cycle_analytics_path(project), link: project_cycle_analytics_path(project),
link_to_options: { class: 'shortcuts-project-cycle-analytics' } link_to_options: { class: 'shortcuts-project-cycle-analytics' }
...@@ -47,7 +47,7 @@ module AnalyticsNavbarHelper ...@@ -47,7 +47,7 @@ module AnalyticsNavbarHelper
return if project.empty_repo? return if project.empty_repo?
navbar_sub_item( navbar_sub_item(
title: _('Repository Analytics'), title: _('Repository'),
path: 'graphs#charts', path: 'graphs#charts',
link: charts_project_graph_path(project, current_ref), link: charts_project_graph_path(project, current_ref),
link_to_options: { class: 'shortcuts-repository-charts' } link_to_options: { class: 'shortcuts-repository-charts' }
...@@ -60,7 +60,7 @@ module AnalyticsNavbarHelper ...@@ -60,7 +60,7 @@ module AnalyticsNavbarHelper
return unless project.feature_available?(:builds, current_user) || !project.empty_repo? return unless project.feature_available?(:builds, current_user) || !project.empty_repo?
navbar_sub_item( navbar_sub_item(
title: _('CI / CD Analytics'), title: _('CI / CD'),
path: 'pipelines#charts', path: 'pipelines#charts',
link: charts_project_pipelines_path(project) link: charts_project_pipelines_path(project)
) )
......
...@@ -463,6 +463,7 @@ module IssuablesHelper ...@@ -463,6 +463,7 @@ module IssuablesHelper
currentUser: issuable[:current_user], currentUser: issuable[:current_user],
rootPath: root_path, rootPath: root_path,
fullPath: issuable[:project_full_path], fullPath: issuable[:project_full_path],
id: issuable[:id],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours
} }
end end
......
...@@ -52,7 +52,9 @@ class BroadcastMessage < ApplicationRecord ...@@ -52,7 +52,9 @@ class BroadcastMessage < ApplicationRecord
end end
def cache def cache
Gitlab::JsonCache.new(cache_key_with_version: false) ::Gitlab::SafeRequestStore.fetch(:broadcast_message_json_cache) do
Gitlab::JsonCache.new(cache_key_with_version: false)
end
end end
def cache_expires_in def cache_expires_in
...@@ -68,9 +70,9 @@ class BroadcastMessage < ApplicationRecord ...@@ -68,9 +70,9 @@ class BroadcastMessage < ApplicationRecord
now_or_future = messages.select(&:now_or_future?) now_or_future = messages.select(&:now_or_future?)
# If there are cached entries but none are to be displayed we'll purge the # If there are cached entries but they don't match the ones we are
# cache so we don't keep running this code all the time. # displaying we'll refresh the cache so we don't need to keep filtering.
cache.expire(cache_key) if now_or_future.empty? cache.expire(cache_key) if now_or_future != messages
now_or_future.select(&:now?).select { |message| message.matches_current_path(current_path) } now_or_future.select(&:now?).select { |message| message.matches_current_path(current_path) }
end end
......
...@@ -21,7 +21,7 @@ class InternalId < ApplicationRecord ...@@ -21,7 +21,7 @@ class InternalId < ApplicationRecord
belongs_to :project belongs_to :project
belongs_to :namespace belongs_to :namespace
enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 } enum usage: ::InternalIdEnums.usage_resources
validates :usage, presence: true validates :usage, presence: true
......
# frozen_string_literal: true
module InternalIdEnums
def self.usage_resources
# when adding new resource, make sure it doesn't conflict with EE usage_resources
{ issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 }
end
end
InternalIdEnums.prepend_if_ee('EE::InternalIdEnums')
...@@ -11,11 +11,7 @@ module Groups ...@@ -11,11 +11,7 @@ module Groups
end end
def execute def execute
unless @current_user.can?(:admin_group, @group) validate_user_permissions
raise ::Gitlab::ImportExport::Error.new(
"User with ID: %s does not have permission to Group %s with ID: %s." %
[@current_user.id, @group.name, @group.id])
end
save! save!
ensure ensure
...@@ -26,6 +22,14 @@ module Groups ...@@ -26,6 +22,14 @@ module Groups
attr_accessor :shared attr_accessor :shared
def validate_user_permissions
unless @current_user.can?(:admin_group, @group)
@shared.error(::Gitlab::ImportExport::Error.permission_error(@current_user, @group))
notify_error!
end
end
def save! def save!
if savers.all?(&:save) if savers.all?(&:save)
notify_success notify_success
......
...@@ -12,15 +12,14 @@ module Groups ...@@ -12,15 +12,14 @@ module Groups
end end
def execute def execute
validate_user_permissions if valid_user_permissions? && import_file && restorer.restore
notify_success
if import_file && restorer.restore
@group @group
else else
raise StandardError.new(@shared.errors.to_sentence) notify_error!
end end
rescue => e
raise StandardError.new(e.message)
ensure ensure
remove_import_file remove_import_file
end end
...@@ -49,13 +48,37 @@ module Groups ...@@ -49,13 +48,37 @@ module Groups
upload.save! upload.save!
end end
def validate_user_permissions def valid_user_permissions?
unless current_user.can?(:admin_group, group) if current_user.can?(:admin_group, group)
raise ::Gitlab::ImportExport::Error.new( true
"User with ID: %s does not have permission to Group %s with ID: %s." % else
[current_user.id, group.name, group.id]) @shared.error(::Gitlab::ImportExport::Error.permission_error(current_user, group))
false
end end
end end
def notify_success
@shared.logger.info(
group_id: @group.id,
group_name: @group.name,
message: 'Group Import/Export: Import succeeded'
)
end
def notify_error
@shared.logger.error(
group_id: @group.id,
group_name: @group.name,
message: "Group Import/Export: Errors occurred, see '#{Gitlab::ErrorTracking::Logger.file_name}' for details"
)
end
def notify_error!
notify_error
raise Gitlab::ImportExport::Error.new(@shared.errors.to_sentence)
end
end end
end end
end end
...@@ -60,7 +60,7 @@ module MergeRequests ...@@ -60,7 +60,7 @@ module MergeRequests
def commit def commit
repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message, first_parent_ref) repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message, first_parent_ref)
rescue Gitlab::Git::PreReceiveError => error rescue Gitlab::Git::PreReceiveError, Gitlab::Git::CommandError => error
raise MergeError, error.message raise MergeError, error.message
end end
end end
......
...@@ -24,7 +24,7 @@ module Metrics ...@@ -24,7 +24,7 @@ module Metrics
def execute def execute
catch(:error) do catch(:error) do
throw(:error, error(_(%q(You can't commit to this project)), :forbidden)) unless push_authorized? throw(:error, error(_(%q(You are not allowed to push into this branch. Create another branch or open a merge request.)), :forbidden)) unless push_authorized?
result = ::Files::CreateService.new(project, current_user, dashboard_attrs).execute result = ::Files::CreateService.new(project, current_user, dashboard_attrs).execute
throw(:error, wrap_error(result)) unless result[:status] == :success throw(:error, wrap_error(result)) unless result[:status] == :success
......
...@@ -9,7 +9,7 @@ module Metrics ...@@ -9,7 +9,7 @@ module Metrics
def execute def execute
catch(:error) do catch(:error) do
throw(:error, error(_(%q(You can't commit to this project)), :forbidden)) unless push_authorized? throw(:error, error(_(%q(You are not allowed to push into this branch. Create another branch or open a merge request.)), :forbidden)) unless push_authorized?
result = ::Files::UpdateService.new(project, current_user, dashboard_attrs).execute result = ::Files::UpdateService.new(project, current_user, dashboard_attrs).execute
throw(:error, result.merge(http_status: :bad_request)) unless result[:status] == :success throw(:error, result.merge(http_status: :bad_request)) unless result[:status] == :success
......
...@@ -5,9 +5,7 @@ module Projects ...@@ -5,9 +5,7 @@ module Projects
class ExportService < BaseService class ExportService < BaseService
def execute(after_export_strategy = nil, options = {}) def execute(after_export_strategy = nil, options = {})
unless project.template_source? || can?(current_user, :admin_project, project) unless project.template_source? || can?(current_user, :admin_project, project)
raise ::Gitlab::ImportExport::Error.new( raise ::Gitlab::ImportExport::Error.permission_error(current_user, project)
"User with ID: %s does not have permission to Project %s with ID: %s." %
[current_user.id, project.name, project.id])
end end
@shared = project.import_export_shared @shared = project.import_export_shared
......
...@@ -48,9 +48,9 @@ ...@@ -48,9 +48,9 @@
- unless should_display_analytics_pages_in_sidebar - unless should_display_analytics_pages_in_sidebar
- if group_sidebar_link?(:contribution_analytics) - if group_sidebar_link?(:contribution_analytics)
= nav_link(path: 'contribution_analytics#show') do = nav_link(path: 'contribution_analytics#show') do
= link_to group_contribution_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do = link_to group_contribution_analytics_path(@group), title: _('Contribution'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do
%span %span
= _('Contribution Analytics') = _('Contribution')
= render_if_exists 'layouts/nav/group_insights_link' = render_if_exists 'layouts/nav/group_insights_link'
......
...@@ -42,8 +42,8 @@ ...@@ -42,8 +42,8 @@
- unless should_display_analytics_pages_in_sidebar - unless should_display_analytics_pages_in_sidebar
- if can?(current_user, :read_cycle_analytics, @project) - if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do = nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Value Stream Analytics'), class: 'shortcuts-project-cycle-analytics' do = link_to project_cycle_analytics_path(@project), title: _('Value Stream'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Value Stream Analytics') %span= _('Value Stream')
= render_if_exists 'layouts/nav/project_insights_link' = render_if_exists 'layouts/nav/project_insights_link'
......
...@@ -129,6 +129,9 @@ ...@@ -129,6 +129,9 @@
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
- if Feature.enabled?(:save_issuable_health_status, @project.group) && issuable_sidebar[:type] == "issue"
.js-sidebar-status-entry-point
- if issuable_sidebar.has_key?(:confidential) - if issuable_sidebar.has_key?(:confidential)
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= f.label :file_name, s_('Snippets|File') = f.label :file_name, s_('Snippets|File')
.file-holder.snippet .file-holder.snippet
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
= f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control snippet-file-name qa-snippet-file-name' = f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control js-snippet-file-name qa-snippet-file-name'
.file-content.code .file-content.code
%pre#editor{ data: { 'editor-loading': true } }= @snippet.content %pre#editor{ data: { 'editor-loading': true } }= @snippet.content
= f.hidden_field :content, class: 'snippet-file-content' = f.hidden_field :content, class: 'snippet-file-content'
......
...@@ -865,7 +865,7 @@ ...@@ -865,7 +865,7 @@
:weight: 2 :weight: 2
:idempotent: :idempotent:
- :name: create_evidence - :name: create_evidence
:feature_category: :release_governance :feature_category: :release_evidence
:has_external_dependencies: :has_external_dependencies:
:urgency: :default :urgency: :default
:resource_boundary: :unknown :resource_boundary: :unknown
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker include ApplicationWorker
feature_category :release_governance feature_category :release_evidence
weight 2 weight 2
def perform(release_id) def perform(release_id)
......
---
title: Prevent unauthorized users to lock an issue from the collapsed sidebar.
merge_request: 26324
author: Gilang Gumilar
type: fixed
---
title: Remove "Analytics" suffix from the sidebar menu items
merge_request: 26415
author:
type: removed
---
title: Fix avg_cycle_analytics uncaught error and optimize query
merge_request: 26381
author:
type: fixed
---
title: Backfill LfsObjectsProject records of forks
merge_request: 25343
author:
type: other
---
title: Create approval todos on update
merge_request: 26077
author:
type: fixed
---
title: Fix MergeToRefService raises Gitlab::Git::CommandError
merge_request: 26465
author:
type: fixed
---
title: Ensure all errors are logged in Group Import
merge_request: 25619
author:
type: changed
---
title: Fix error messages for dashboard clonning process.
merge_request: 26290
author:
type: fixed
---
title: Add migration for Requirement model
merge_request: 26097
author:
type: added
---
title: Remove unnecessary Redis deletes for broadcast messages
merge_request: 26541
author:
type: performance
...@@ -25,8 +25,7 @@ ...@@ -25,8 +25,7 @@
- code_quality - code_quality
- code_review - code_review
- collection - collection
- compliance_controls - compliance_management
- compliance_frameworks
- container_network_security - container_network_security
- container_registry - container_registry
- container_scanning - container_scanning
...@@ -37,7 +36,7 @@ ...@@ -37,7 +36,7 @@
- dependency_proxy - dependency_proxy
- dependency_scanning - dependency_scanning
- design_management - design_management
- devops_score - devops_reports
- digital_experience_management - digital_experience_management
- disaster_recovery - disaster_recovery
- dynamic_application_security_testing - dynamic_application_security_testing
...@@ -52,6 +51,7 @@ ...@@ -52,6 +51,7 @@
- gitaly - gitaly
- gitlab_handbook - gitlab_handbook
- gitter - gitter
- global_search
- helm_chart_registry - helm_chart_registry
- importers - importers
- incident_management - incident_management
...@@ -61,6 +61,8 @@ ...@@ -61,6 +61,8 @@
- interactive_application_security_testing - interactive_application_security_testing
- internationalization - internationalization
- issue_tracking - issue_tracking
- jenkins_importer
- jira_importer
- jupyter_notebooks - jupyter_notebooks
- kanban_boards - kanban_boards
- kubernetes_management - kubernetes_management
...@@ -70,13 +72,14 @@ ...@@ -70,13 +72,14 @@
- load_testing - load_testing
- logging - logging
- malware_scanning - malware_scanning
- merge_trains
- metrics - metrics
- omnibus_package - omnibus_package
- package_registry - package_registry
- pages - pages
- pki_management
- planning_analytics
- quality_management - quality_management
- release_governance - release_evidence
- release_orchestration - release_orchestration
- requirements_management - requirements_management
- responsible_disclosure - responsible_disclosure
...@@ -86,7 +89,6 @@ ...@@ -86,7 +89,6 @@
- runner - runner
- runtime_application_self_protection - runtime_application_self_protection
- sdk - sdk
- search
- secret_detection - secret_detection
- secrets_management - secrets_management
- serverless - serverless
...@@ -97,8 +99,6 @@ ...@@ -97,8 +99,6 @@
- static_site_editor - static_site_editor
- status_page - status_page
- subgroups - subgroups
- system_testing
- teams
- templates - templates
- threat_detection - threat_detection
- time_tracking - time_tracking
...@@ -113,4 +113,3 @@ ...@@ -113,4 +113,3 @@
- web_ide - web_ide
- web_performance - web_performance
- wiki - wiki
- workspaces
# frozen_string_literal: true
class CreateRequirements < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :requirements do |t|
t.timestamps_with_timezone null: false
t.integer :project_id, null: false
t.integer :author_id
t.integer :iid, null: false
t.integer :cached_markdown_version
t.integer :state, limit: 2, default: 1, null: false
t.string :title, limit: 255, null: false
t.text :title_html
t.index :project_id
t.index :author_id
t.index :title, name: "index_requirements_on_title_trigram", using: :gin, opclass: :gin_trgm_ops
t.index :state
t.index :created_at
t.index :updated_at
t.index %w(project_id iid), name: 'index_requirements_on_project_id_and_iid', where: 'project_id IS NOT NULL', unique: true, using: :btree
end
end
end
# frozen_string_literal: true
class RequirementsAddProjectFk < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key(:requirements, :projects, column: :project_id, on_delete: :cascade) # rubocop: disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key(:requirements, column: :project_id)
end
end
end
# frozen_string_literal: true
class RequirementsAddAuthorFk < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key(:requirements, :users, column: :author_id, on_delete: :nullify) # rubocop: disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key(:requirements, column: :author_id)
end
end
end
# frozen_string_literal: true
class RescheduleLinkLfsObjects < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'LinkLfsObjects'
BATCH_SIZE = 1_000
disable_ddl_transaction!
def up
forks = Gitlab::BackgroundMigration::LinkLfsObjects::Project.with_non_existing_lfs_objects
queue_background_migration_jobs_by_range_at_intervals(
forks,
MIGRATION,
BackgroundMigrationWorker.minimum_interval,
batch_size: BATCH_SIZE
)
end
def down
# No-op. No need to make this reversible. In case the jobs enqueued runs and
# fails at some point, some records will be created. When rescheduled, those
# records won't be re-created. It's also hard to track which records to clean
# up if ever.
end
end
...@@ -3723,6 +3723,25 @@ ActiveRecord::Schema.define(version: 2020_03_04_160823) do ...@@ -3723,6 +3723,25 @@ ActiveRecord::Schema.define(version: 2020_03_04_160823) do
t.index ["project_id", "programming_language_id"], name: "index_repository_languages_on_project_and_languages_id", unique: true t.index ["project_id", "programming_language_id"], name: "index_repository_languages_on_project_and_languages_id", unique: true
end end
create_table "requirements", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id", null: false
t.integer "author_id"
t.integer "iid", null: false
t.integer "cached_markdown_version"
t.integer "state", limit: 2, default: 1, null: false
t.string "title", limit: 255, null: false
t.text "title_html"
t.index ["author_id"], name: "index_requirements_on_author_id"
t.index ["created_at"], name: "index_requirements_on_created_at"
t.index ["project_id", "iid"], name: "index_requirements_on_project_id_and_iid", unique: true, where: "(project_id IS NOT NULL)"
t.index ["project_id"], name: "index_requirements_on_project_id"
t.index ["state"], name: "index_requirements_on_state"
t.index ["title"], name: "index_requirements_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["updated_at"], name: "index_requirements_on_updated_at"
end
create_table "resource_label_events", force: :cascade do |t| create_table "resource_label_events", force: :cascade do |t|
t.integer "action", null: false t.integer "action", null: false
t.integer "issue_id" t.integer "issue_id"
...@@ -5001,6 +5020,8 @@ ActiveRecord::Schema.define(version: 2020_03_04_160823) do ...@@ -5001,6 +5020,8 @@ ActiveRecord::Schema.define(version: 2020_03_04_160823) do
add_foreign_key "releases", "users", column: "author_id", name: "fk_8e4456f90f", on_delete: :nullify add_foreign_key "releases", "users", column: "author_id", name: "fk_8e4456f90f", on_delete: :nullify
add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade
add_foreign_key "repository_languages", "projects", on_delete: :cascade add_foreign_key "repository_languages", "projects", on_delete: :cascade
add_foreign_key "requirements", "projects", on_delete: :cascade
add_foreign_key "requirements", "users", column: "author_id", on_delete: :nullify
add_foreign_key "resource_label_events", "epics", on_delete: :cascade add_foreign_key "resource_label_events", "epics", on_delete: :cascade
add_foreign_key "resource_label_events", "issues", on_delete: :cascade add_foreign_key "resource_label_events", "issues", on_delete: :cascade
add_foreign_key "resource_label_events", "labels", on_delete: :nullify add_foreign_key "resource_label_events", "labels", on_delete: :nullify
......
...@@ -54,8 +54,10 @@ Add the following to your `sshd_config` file. This is usually located at ...@@ -54,8 +54,10 @@ Add the following to your `sshd_config` file. This is usually located at
Omnibus Docker: Omnibus Docker:
```plaintext ```plaintext
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k Match User git # Apply the AuthorizedKeysCommands to the git user only
AuthorizedKeysCommandUser git AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k
AuthorizedKeysCommandUser git
Match all # End match, settings apply to all users again
``` ```
Reload OpenSSH: Reload OpenSSH:
......
...@@ -104,7 +104,8 @@ Review Apps are automatically stopped 2 days after the last deployment thanks to ...@@ -104,7 +104,8 @@ Review Apps are automatically stopped 2 days after the last deployment thanks to
the [Environment auto-stop](../../ci/environments.html#environments-auto-stop) feature. the [Environment auto-stop](../../ci/environments.html#environments-auto-stop) feature.
If you need your Review App to stay up for a longer time, you can If you need your Review App to stay up for a longer time, you can
[pin its environment](../../ci/environments.html#auto-stop-example). [pin its environment](../../ci/environments.html#auto-stop-example) or retry the
`review-deploy` job to update the "latest deployed at" time.
The `review-cleanup` job that automatically runs in scheduled The `review-cleanup` job that automatically runs in scheduled
pipelines (and is manual in merge request) stops stale Review Apps after 5 days, pipelines (and is manual in merge request) stops stale Review Apps after 5 days,
......
...@@ -6,8 +6,6 @@ module Gitlab ...@@ -6,8 +6,6 @@ module Gitlab
class LinkLfsObjects class LinkLfsObjects
# Model definition used for migration # Model definition used for migration
class ForkNetworkMember < ActiveRecord::Base class ForkNetworkMember < ActiveRecord::Base
include EachBatch
self.table_name = 'fork_network_members' self.table_name = 'fork_network_members'
def self.with_non_existing_lfs_objects def self.with_non_existing_lfs_objects
...@@ -25,62 +23,8 @@ module Gitlab ...@@ -25,62 +23,8 @@ module Gitlab
end end
end end
# Model definition used for migration
class Project < ActiveRecord::Base
include EachBatch
self.table_name = 'projects'
has_one :fork_network_member, class_name: 'LinkLfsObjects::ForkNetworkMember'
def self.with_non_existing_lfs_objects
fork_network_members =
ForkNetworkMember.with_non_existing_lfs_objects
.select(1)
.where('fork_network_members.project_id = projects.id')
where('EXISTS (?)', fork_network_members)
end
end
# Model definition used for migration
class LfsObjectsProject < ActiveRecord::Base
include EachBatch
self.table_name = 'lfs_objects_projects'
end
BATCH_SIZE = 1000
def perform(start_id, end_id) def perform(start_id, end_id)
forks = # no-op as some queries times out
Project
.with_non_existing_lfs_objects
.where(id: start_id..end_id)
forks.includes(:fork_network_member).find_each do |project|
LfsObjectsProject
.select("lfs_objects_projects.lfs_object_id, #{project.id}, NOW(), NOW()")
.where(project_id: project.fork_network_member.forked_from_project_id)
.each_batch(of: BATCH_SIZE) do |batch|
execute <<~SQL
INSERT INTO lfs_objects_projects (lfs_object_id, project_id, created_at, updated_at)
#{batch.to_sql}
SQL
end
end
logger.info(message: "LinkLfsObjects: created missing LfsObjectsProject for Projects #{forks.map(&:id).join(', ')}")
end
private
def execute(sql)
::ActiveRecord::Base.connection.execute(sql)
end
def logger
@logger ||= Gitlab::BackgroundMigration::Logger.build
end end
end end
end end
......
...@@ -3,15 +3,32 @@ ...@@ -3,15 +3,32 @@
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class UsageData class UsageData
include Gitlab::Utils::StrongMemoize
PROJECTS_LIMIT = 10 PROJECTS_LIMIT = 10
attr_reader :projects, :options attr_reader :options
def initialize def initialize
@projects = Project.sorted_by_activity.limit(PROJECTS_LIMIT)
@options = { from: 7.days.ago } @options = { from: 7.days.ago }
end end
def projects
strong_memoize(:projects) do
projects = Project.where.not(last_activity_at: nil).order(last_activity_at: :desc).limit(10) +
Project.where.not(last_repository_updated_at: nil).order(last_repository_updated_at: :desc).limit(10)
projects = projects.uniq.sort_by do |project|
[project.last_activity_at, project.last_repository_updated_at].min
end
if projects.size < 10
projects.concat(Project.where(last_activity_at: nil, last_repository_updated_at: nil).limit(10))
end
projects.uniq.first(10)
end
end
def to_json(*) def to_json(*)
total = 0 total = 0
......
...@@ -2,6 +2,13 @@ ...@@ -2,6 +2,13 @@
module Gitlab module Gitlab
module ImportExport module ImportExport
Error = Class.new(StandardError) class Error < StandardError
def self.permission_error(user, importable)
self.new(
"User with ID: %s does not have required permissions for %s: %s with ID: %s" %
[user.id, importable.class.name, importable.name, importable.id]
)
end
end
end end
end end
...@@ -49,11 +49,7 @@ module Gitlab ...@@ -49,11 +49,7 @@ module Gitlab
json = IO.read(@path) json = IO.read(@path)
ActiveSupport::JSON.decode(json) ActiveSupport::JSON.decode(json)
rescue => e rescue => e
@shared.logger.error( @shared.error(e)
group_id: @group.id,
group_name: @group.name,
message: "Import/Export error: #{e.message}"
)
raise Gitlab::ImportExport::Error.new('Incorrect JSON format') raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
end end
......
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class BaseTask
include Gitlab::WithRequestStore
def initialize(opts, logger: Logger.new($stdout))
@project_path = opts.fetch(:project_path)
@file_path = opts.fetch(:file_path)
@namespace = Namespace.find_by_full_path(opts.fetch(:namespace_path))
@current_user = User.find_by_username(opts.fetch(:username))
@measurement_enabled = opts.fetch(:measurement_enabled)
@measurement = Gitlab::Utils::Measuring.new(logger: logger) if @measurement_enabled
@logger = logger
end
private
attr_reader :measurement, :project, :namespace, :current_user, :file_path, :project_path, :logger
def measurement_enabled?
@measurement_enabled
end
def success(message)
logger.info(message)
true
end
def error(message)
logger.error(message)
false
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class ExportTask < BaseTask
def initialize(*)
super
@project = namespace.projects.find_by_path(@project_path)
end
def export
return error("Project with path: #{project_path} was not found. Please provide correct project path") unless project
return error("Invalid file path: #{file_path}. Please provide correct file path") unless file_path_exists?
with_export do
::Projects::ImportExport::ExportService.new(project, current_user)
.execute(Gitlab::ImportExport::AfterExportStrategies::MoveFileStrategy.new(archive_path: file_path))
end
success('Done!')
end
private
def file_path_exists?
directory = File.dirname(file_path)
Dir.exist?(directory)
end
def with_export
with_request_store do
::Gitlab::GitalyClient.allow_n_plus_1_calls do
measurement_enabled? ? measurement.with_measuring { yield } : yield
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class ImportTask < BaseTask
def import
show_import_start_message
run_isolated_sidekiq_job
show_import_failures_count
return error(project.import_state.last_error) if project.import_state&.last_error
return error(project.errors.full_messages.to_sentence) if project.errors.any?
success('Done!')
end
private
# We want to ensure that all Sidekiq jobs are executed
# synchronously as part of that process.
# This ensures that all expensive operations do not escape
# to general Sidekiq clusters/nodes.
def with_isolated_sidekiq_job
Sidekiq::Testing.fake! do
with_request_store do
# If you are attempting to import a large project into a development environment,
# you may see Gitaly throw an error about too many calls or invocations.
# This is due to a n+1 calls limit being set for development setups (not enforced in production)
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24475#note_283090635
# For development setups, this code-path will be excluded from n+1 detection.
::Gitlab::GitalyClient.allow_n_plus_1_calls do
measurement_enabled? ? measurement.with_measuring { yield } : yield
end
end
true
end
end
def run_isolated_sidekiq_job
with_isolated_sidekiq_job do
@project = create_project
execute_sidekiq_job
end
end
def create_project
# We are disabling ObjectStorage for `import`
# as it is too slow to handle big archives:
# 1. DB transaction timeouts on upload
# 2. Download of archive before unpacking
disable_upload_object_storage do
service = Projects::GitlabProjectsImportService.new(
current_user,
{
namespace_id: namespace.id,
path: project_path,
file: File.open(file_path)
}
)
service.execute
end
end
def execute_sidekiq_job
Sidekiq::Worker.drain_all
end
def disable_upload_object_storage
overwrite_uploads_setting('background_upload', false) do
overwrite_uploads_setting('direct_upload', false) do
yield
end
end
end
def overwrite_uploads_setting(key, value)
old_value = Settings.uploads.object_store[key]
Settings.uploads.object_store[key] = value
yield
ensure
Settings.uploads.object_store[key] = old_value
end
def full_path
"#{namespace.full_path}/#{project_path}"
end
def show_import_start_message
logger.info "Importing GitLab export: #{file_path} into GitLab" \
" #{full_path}" \
" as #{current_user.name}"
end
def show_import_failures_count
return unless project.import_failures.exists?
logger.info "Total number of not imported relations: #{project.import_failures.count}"
end
end
end
end
end
...@@ -94,14 +94,6 @@ module Gitlab ...@@ -94,14 +94,6 @@ module Gitlab
end end
end end
def log_error(details)
@logger.error(log_base_data.merge(details))
end
def log_debug(details)
@logger.debug(log_base_data.merge(details))
end
def log_base_data def log_base_data
log = { log = {
importer: 'Import/Export', importer: 'Import/Export',
......
...@@ -122,6 +122,8 @@ module Gitlab ...@@ -122,6 +122,8 @@ module Gitlab
def cycle_analytics_usage_data def cycle_analytics_usage_data
Gitlab::CycleAnalytics::UsageData.new.to_json Gitlab::CycleAnalytics::UsageData.new.to_json
rescue ActiveRecord::StatementInvalid
{ avg_cycle_analytics: {} }
end end
def features_usage_data def features_usage_data
...@@ -232,7 +234,7 @@ module Gitlab ...@@ -232,7 +234,7 @@ module Gitlab
end end
def count(relation, column = nil, fallback: -1, batch: true) def count(relation, column = nil, fallback: -1, batch: true)
if batch && Feature.enabled?(:usage_ping_batch_counter) if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_count(relation, column) Gitlab::Database::BatchCount.batch_count(relation, column)
else else
relation.count relation.count
...@@ -242,7 +244,7 @@ module Gitlab ...@@ -242,7 +244,7 @@ module Gitlab
end end
def distinct_count(relation, column = nil, fallback: -1, batch: true) def distinct_count(relation, column = nil, fallback: -1, batch: true)
if batch && Feature.enabled?(:usage_ping_batch_counter) if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_distinct_count(relation, column) Gitlab::Database::BatchCount.batch_distinct_count(relation, column)
else else
relation.distinct_count_by(column) relation.distinct_count_by(column)
......
...@@ -59,14 +59,15 @@ module Gitlab ...@@ -59,14 +59,15 @@ module Gitlab
end end
def duration_in_numbers(duration_in_seconds) def duration_in_numbers(duration_in_seconds)
milliseconds = duration_in_seconds.in_milliseconds % 1.second.in_milliseconds
seconds = duration_in_seconds % 1.minute seconds = duration_in_seconds % 1.minute
minutes = (duration_in_seconds / 1.minute) % (1.hour / 1.minute) minutes = (duration_in_seconds / 1.minute) % (1.hour / 1.minute)
hours = duration_in_seconds / 1.hour hours = duration_in_seconds / 1.hour
if hours == 0 if hours == 0
"%02d:%02d" % [minutes, seconds] "%02d:%02d:%03d" % [minutes, seconds, milliseconds]
else else
"%02d:%02d:%02d" % [hours, minutes, seconds] "%02d:%02d:%02d:%03d" % [hours, minutes, seconds, milliseconds]
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
require 'gitlab/with_request_store'
# Export project to archive # Export project to archive
# #
# @example # @example
...@@ -14,81 +12,36 @@ namespace :gitlab do ...@@ -14,81 +12,36 @@ namespace :gitlab do
# Load it here to avoid polluting Rake tasks with Sidekiq test warnings # Load it here to avoid polluting Rake tasks with Sidekiq test warnings
require 'sidekiq/testing' require 'sidekiq/testing'
warn_user_is_not_gitlab logger = Logger.new($stdout)
if ENV['IMPORT_DEBUG'].present? begin
ActiveRecord::Base.logger = Logger.new(STDOUT) warn_user_is_not_gitlab
Gitlab::Metrics::Exporter::SidekiqExporter.instance.start
end if ENV['EXPORT_DEBUG'].present?
ActiveRecord::Base.logger = logger
GitlabProjectExport.new( Gitlab::Metrics::Exporter::SidekiqExporter.instance.start
namespace_path: args.namespace_path, logger.level = Logger::DEBUG
project_path: args.project_path, else
username: args.username, logger.level = Logger::INFO
file_path: args.archive_path, end
measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled)
).export task = Gitlab::ImportExport::Project::ExportTask.new(
end namespace_path: args.namespace_path,
end project_path: args.project_path,
end username: args.username,
file_path: args.archive_path,
class GitlabProjectExport measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled),
include Gitlab::WithRequestStore logger: logger
)
def initialize(opts)
@project_path = opts.fetch(:project_path) success = task.export
@file_path = opts.fetch(:file_path)
@current_user = User.find_by_username(opts.fetch(:username)) exit(success)
namespace = Namespace.find_by_full_path(opts.fetch(:namespace_path)) rescue StandardError => e
@project = namespace.projects.find_by_path(@project_path) logger.error "Exception: #{e.message}"
@measurement_enabled = opts.fetch(:measurement_enabled) logger.debug e.backtrace
@measurable = Gitlab::Utils::Measuring.new if @measurement_enabled exit 1
end
def export
validate_project
validate_file_path
with_export do
::Projects::ImportExport::ExportService.new(project, current_user)
.execute(Gitlab::ImportExport::AfterExportStrategies::MoveFileStrategy.new(archive_path: file_path))
end
puts 'Done!'
rescue StandardError => e
puts "Exception: #{e.message}"
puts e.backtrace
exit 1
end
private
attr_reader :measurable, :project, :current_user, :file_path, :project_path
def validate_project
unless project
puts "Error: Project with path: #{project_path} was not found. Please provide correct project path"
exit 1
end
end
def validate_file_path
directory = File.dirname(file_path)
unless Dir.exist?(directory)
puts "Error: Invalid file path: #{file_path}. Please provide correct file path"
exit 1
end
end
def with_export
with_request_store do
::Gitlab::GitalyClient.allow_n_plus_1_calls do
measurement_enabled? ? measurable.with_measuring { yield } : yield
end end
end end
end end
def measurement_enabled?
@measurement_enabled
end
end end
# frozen_string_literal: true # frozen_string_literal: true
require 'gitlab/with_request_store'
# Import large project archives # Import large project archives
# #
# This task: # This task:
...@@ -18,148 +16,36 @@ namespace :gitlab do ...@@ -18,148 +16,36 @@ namespace :gitlab do
# Load it here to avoid polluting Rake tasks with Sidekiq test warnings # Load it here to avoid polluting Rake tasks with Sidekiq test warnings
require 'sidekiq/testing' require 'sidekiq/testing'
warn_user_is_not_gitlab logger = Logger.new($stdout)
if ENV['IMPORT_DEBUG'].present?
ActiveRecord::Base.logger = Logger.new(STDOUT)
end
GitlabProjectImport.new(
namespace_path: args.namespace_path,
project_path: args.project_path,
username: args.username,
file_path: args.archive_path,
measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled)
).import
end
end
end
class GitlabProjectImport
include Gitlab::WithRequestStore
def initialize(opts)
@project_path = opts.fetch(:project_path)
@file_path = opts.fetch(:file_path)
@namespace = Namespace.find_by_full_path(opts.fetch(:namespace_path))
@current_user = User.find_by_username(opts.fetch(:username))
@measurement_enabled = opts.fetch(:measurement_enabled)
@measurement = Gitlab::Utils::Measuring.new if @measurement_enabled
end
def import
show_import_start_message
run_isolated_sidekiq_job begin
warn_user_is_not_gitlab
show_import_failures_count if ENV['IMPORT_DEBUG'].present?
ActiveRecord::Base.logger = logger
if project&.import_state&.last_error Gitlab::Metrics::Exporter::SidekiqExporter.instance.start
puts "ERROR: #{project.import_state.last_error}" logger.level = Logger::DEBUG
exit 1 else
elsif project.errors.any? logger.level = Logger::INFO
puts "ERROR: #{project.errors.full_messages.join(', ')}"
exit 1
else
puts 'Done!'
end
rescue StandardError => e
puts "Exception: #{e.message}"
puts e.backtrace
exit 1
end
private
attr_reader :measurement, :project, :namespace, :current_user, :file_path, :project_path
def measurement_enabled?
@measurement_enabled
end
# We want to ensure that all Sidekiq jobs are executed
# synchronously as part of that process.
# This ensures that all expensive operations do not escape
# to general Sidekiq clusters/nodes.
def with_isolated_sidekiq_job
Sidekiq::Testing.fake! do
with_request_store do
# If you are attempting to import a large project into a development environment,
# you may see Gitaly throw an error about too many calls or invocations.
# This is due to a n+1 calls limit being set for development setups (not enforced in production)
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24475#note_283090635
# For development setups, this code-path will be excluded from n+1 detection.
::Gitlab::GitalyClient.allow_n_plus_1_calls do
measurement_enabled? ? measurement.with_measuring { yield } : yield
end end
end
true
end
end
def run_isolated_sidekiq_job
with_isolated_sidekiq_job do
@project = create_project
execute_sidekiq_job
end
end
def create_project
# We are disabling ObjectStorage for `import`
# as it is too slow to handle big archives:
# 1. DB transaction timeouts on upload
# 2. Download of archive before unpacking
disable_upload_object_storage do
service = Projects::GitlabProjectsImportService.new(
current_user,
{
namespace_id: namespace.id,
path: project_path,
file: File.open(file_path)
}
)
service.execute
end
end
def execute_sidekiq_job
Sidekiq::Worker.drain_all
end
def disable_upload_object_storage task = Gitlab::ImportExport::Project::ImportTask.new(
overwrite_uploads_setting('background_upload', false) do namespace_path: args.namespace_path,
overwrite_uploads_setting('direct_upload', false) do project_path: args.project_path,
yield username: args.username,
file_path: args.archive_path,
measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled),
logger: logger
)
success = task.import
exit(success)
rescue StandardError => e
logger.error "Exception: #{e.message}"
logger.debug e.backtrace
exit 1
end end
end end
end end
def overwrite_uploads_setting(key, value)
old_value = Settings.uploads.object_store[key]
Settings.uploads.object_store[key] = value
yield
ensure
Settings.uploads.object_store[key] = old_value
end
def full_path
"#{namespace.full_path}/#{project_path}"
end
def show_import_start_message
puts "Importing GitLab export: #{file_path} into GitLab" \
" #{full_path}" \
" as #{current_user.name}"
end
def show_import_failures_count
return unless project.import_failures.exists?
puts "Total number of not imported relations: #{project.import_failures.count}"
end
end end
...@@ -1142,6 +1142,9 @@ msgstr "" ...@@ -1142,6 +1142,9 @@ msgstr ""
msgid "Add email address" msgid "Add email address"
msgstr "" msgstr ""
msgid "Add environment"
msgstr ""
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface" msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr "" msgstr ""
...@@ -2453,6 +2456,9 @@ msgstr "" ...@@ -2453,6 +2456,9 @@ msgstr ""
msgid "At least one of group_id or project_id must be specified" msgid "At least one of group_id or project_id must be specified"
msgstr "" msgstr ""
msgid "At risk"
msgstr ""
msgid "Attach a file" msgid "Attach a file"
msgstr "" msgstr ""
...@@ -3133,9 +3139,6 @@ msgstr "" ...@@ -3133,9 +3139,6 @@ msgstr ""
msgid "CI / CD" msgid "CI / CD"
msgstr "" msgstr ""
msgid "CI / CD Analytics"
msgstr ""
msgid "CI / CD Charts" msgid "CI / CD Charts"
msgstr "" msgstr ""
...@@ -5612,6 +5615,9 @@ msgstr "" ...@@ -5612,6 +5615,9 @@ msgstr ""
msgid "Create" msgid "Create"
msgstr "" msgstr ""
msgid "Create %{environment}"
msgstr ""
msgid "Create %{type} token" msgid "Create %{type} token"
msgstr "" msgstr ""
...@@ -7432,6 +7438,9 @@ msgstr "" ...@@ -7432,6 +7438,9 @@ msgstr ""
msgid "Enter a number" msgid "Enter a number"
msgstr "" msgstr ""
msgid "Enter a whole number between 0 and 100"
msgstr ""
msgid "Enter at least three characters to search" msgid "Enter at least three characters to search"
msgstr "" msgstr ""
...@@ -7456,6 +7465,9 @@ msgstr "" ...@@ -7456,6 +7465,9 @@ msgstr ""
msgid "Enter number of issues" msgid "Enter number of issues"
msgstr "" msgstr ""
msgid "Enter one or more user ID separated by commas"
msgstr ""
msgid "Enter the issue description" msgid "Enter the issue description"
msgstr "" msgstr ""
...@@ -8554,6 +8566,18 @@ msgstr "" ...@@ -8554,6 +8566,18 @@ msgstr ""
msgid "FeatureFlags|User IDs" msgid "FeatureFlags|User IDs"
msgstr "" msgstr ""
msgid "FeatureFlag|Delete strategy"
msgstr ""
msgid "FeatureFlag|Percentage"
msgstr ""
msgid "FeatureFlag|Type"
msgstr ""
msgid "FeatureFlag|User IDs"
msgstr ""
msgid "Feb" msgid "Feb"
msgstr "" msgstr ""
...@@ -12790,6 +12814,9 @@ msgstr "" ...@@ -12790,6 +12814,9 @@ msgstr ""
msgid "Need help?" msgid "Need help?"
msgstr "" msgstr ""
msgid "Needs attention"
msgstr ""
msgid "Network" msgid "Network"
msgstr "" msgstr ""
...@@ -13398,6 +13425,9 @@ msgstr "" ...@@ -13398,6 +13425,9 @@ msgstr ""
msgid "Omnibus Protected Paths throttle is active. From 12.4, Omnibus throttle is deprecated and will be removed in a future release. Please read the %{relative_url_link_start}Migrating Protected Paths documentation%{relative_url_link_end}." msgid "Omnibus Protected Paths throttle is active. From 12.4, Omnibus throttle is deprecated and will be removed in a future release. Please read the %{relative_url_link_start}Migrating Protected Paths documentation%{relative_url_link_end}."
msgstr "" msgstr ""
msgid "On track"
msgstr ""
msgid "Onboarding" msgid "Onboarding"
msgstr "" msgstr ""
...@@ -13901,6 +13931,9 @@ msgstr "" ...@@ -13901,6 +13931,9 @@ msgstr ""
msgid "People without permission will never get a notification." msgid "People without permission will never get a notification."
msgstr "" msgstr ""
msgid "Percent rollout (logged in users)"
msgstr ""
msgid "Percentage" msgid "Percentage"
msgstr "" msgstr ""
...@@ -14552,6 +14585,9 @@ msgstr "" ...@@ -14552,6 +14585,9 @@ msgstr ""
msgid "Proceed" msgid "Proceed"
msgstr "" msgstr ""
msgid "Productivity"
msgstr ""
msgid "Productivity Analytics" msgid "Productivity Analytics"
msgstr "" msgstr ""
...@@ -16431,9 +16467,6 @@ msgstr "" ...@@ -16431,9 +16467,6 @@ msgstr ""
msgid "Repository" msgid "Repository"
msgstr "" msgstr ""
msgid "Repository Analytics"
msgstr ""
msgid "Repository Graph" msgid "Repository Graph"
msgstr "" msgstr ""
...@@ -17425,6 +17458,9 @@ msgstr "" ...@@ -17425,6 +17458,9 @@ msgstr ""
msgid "Select source branch" msgid "Select source branch"
msgstr "" msgstr ""
msgid "Select strategy activation method"
msgstr ""
msgid "Select target branch" msgid "Select target branch"
msgstr "" msgstr ""
...@@ -17913,6 +17949,9 @@ msgstr "" ...@@ -17913,6 +17949,9 @@ msgstr ""
msgid "Sidebar|Only numeral characters allowed" msgid "Sidebar|Only numeral characters allowed"
msgstr "" msgstr ""
msgid "Sidebar|Status"
msgstr ""
msgid "Sidebar|Weight" msgid "Sidebar|Weight"
msgstr "" msgstr ""
...@@ -21383,6 +21422,9 @@ msgstr "" ...@@ -21383,6 +21422,9 @@ msgstr ""
msgid "User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled." msgid "User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled."
msgstr "" msgstr ""
msgid "User IDs"
msgstr ""
msgid "User OAuth applications" msgid "User OAuth applications"
msgstr "" msgstr ""
...@@ -21740,6 +21782,9 @@ msgstr "" ...@@ -21740,6 +21782,9 @@ msgstr ""
msgid "Value" msgid "Value"
msgstr "" msgstr ""
msgid "Value Stream"
msgstr ""
msgid "Value Stream Analytics" msgid "Value Stream Analytics"
msgstr "" msgstr ""
...@@ -22449,6 +22494,9 @@ msgstr "" ...@@ -22449,6 +22494,9 @@ msgstr ""
msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?" msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
msgid "You are not allowed to push into this branch. Create another branch or open a merge request."
msgstr ""
msgid "You are not allowed to unlink your primary login account" msgid "You are not allowed to unlink your primary login account"
msgstr "" msgstr ""
...@@ -22569,9 +22617,6 @@ msgstr "" ...@@ -22569,9 +22617,6 @@ msgstr ""
msgid "You can try again using %{begin_link}basic search%{end_link}" msgid "You can try again using %{begin_link}basic search%{end_link}"
msgstr "" msgstr ""
msgid "You can't commit to this project"
msgstr ""
msgid "You cannot access the raw file. Please wait a minute." msgid "You cannot access the raw file. Please wait a minute."
msgstr "" msgstr ""
...@@ -23512,6 +23557,9 @@ msgstr "" ...@@ -23512,6 +23557,9 @@ msgstr ""
msgid "is not an email you own" msgid "is not an email you own"
msgstr "" msgstr ""
msgid "is not in the group enforcing Group Managed Account"
msgstr ""
msgid "is too long (%{current_value}). The maximum size is %{max_size}." msgid "is too long (%{current_value}). The maximum size is %{max_size}."
msgstr "" msgstr ""
......
...@@ -47,7 +47,7 @@ describe 'The group page' do ...@@ -47,7 +47,7 @@ describe 'The group page' do
expect(page).to have_link('Group overview') expect(page).to have_link('Group overview')
expect(page).to have_link('Details') expect(page).to have_link('Details')
expect(page).not_to have_link('Activity') expect(page).not_to have_link('Activity')
expect(page).not_to have_link('Contribution Analytics') expect(page).not_to have_link('Contribution')
expect(page).not_to have_link('Issues') expect(page).not_to have_link('Issues')
expect(page).not_to have_link('Merge Requests') expect(page).not_to have_link('Merge Requests')
......
...@@ -10,7 +10,7 @@ describe 'Group navbar' do ...@@ -10,7 +10,7 @@ describe 'Group navbar' do
{ {
nav_item: _('Analytics'), nav_item: _('Analytics'),
nav_sub_items: [ nav_sub_items: [
_('Contribution Analytics') _('Contribution')
] ]
} }
end end
...@@ -63,7 +63,7 @@ describe 'Group navbar' do ...@@ -63,7 +63,7 @@ describe 'Group navbar' do
before do before do
stub_licensed_features(productivity_analytics: true) stub_licensed_features(productivity_analytics: true)
analytics_nav_item[:nav_sub_items] << _('Productivity Analytics') analytics_nav_item[:nav_sub_items] << _('Productivity')
group.add_maintainer(user) group.add_maintainer(user)
sign_in(user) sign_in(user)
...@@ -78,7 +78,7 @@ describe 'Group navbar' do ...@@ -78,7 +78,7 @@ describe 'Group navbar' do
before do before do
stub_licensed_features(cycle_analytics_for_groups: true) stub_licensed_features(cycle_analytics_for_groups: true)
analytics_nav_item[:nav_sub_items] << _('Value Stream Analytics') analytics_nav_item[:nav_sub_items] << _('Value Stream')
group.add_maintainer(user) group.add_maintainer(user)
sign_in(user) sign_in(user)
......
...@@ -225,6 +225,29 @@ describe 'Issue Sidebar' do ...@@ -225,6 +225,29 @@ describe 'Issue Sidebar' do
it 'does not have a option to edit labels' do it 'does not have a option to edit labels' do
expect(page).not_to have_selector('.block.labels .edit-link') expect(page).not_to have_selector('.block.labels .edit-link')
end end
context 'interacting with collapsed sidebar', :js do
collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
lock_sidebar_block = '.block.lock'
lock_button = '.block.lock .btn-close'
collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
before do
resize_screen_sm
end
it 'expands then does not show the lock dialog form' do
expect(page).to have_css(collapsed_sidebar_selector)
page.within(lock_sidebar_block) do
find(collapsed_sidebar_block_icon).click
end
expect(page).to have_css(expanded_sidebar_selector)
expect(page).not_to have_selector(lock_button)
end
end
end end
def visit_issue(project, issue) def visit_issue(project, issue)
......
...@@ -136,16 +136,16 @@ describe 'Project active tab' do ...@@ -136,16 +136,16 @@ describe 'Project active tab' do
context 'on project Analytics/Repository Analytics' do context 'on project Analytics/Repository Analytics' do
it_behaves_like 'page has active tab', _('Analytics') it_behaves_like 'page has active tab', _('Analytics')
it_behaves_like 'page has active sub tab', _('Repository Analytics') it_behaves_like 'page has active sub tab', _('Repository')
end end
context 'on project Analytics/Cycle Analytics' do context 'on project Analytics/Cycle Analytics' do
before do before do
click_tab(_('CI / CD Analytics')) click_tab(_('CI / CD'))
end end
it_behaves_like 'page has active tab', _('Analytics') it_behaves_like 'page has active tab', _('Analytics')
it_behaves_like 'page has active sub tab', _('CI / CD Analytics') it_behaves_like 'page has active sub tab', _('CI / CD')
end end
end end
end end
......
...@@ -10,10 +10,10 @@ describe 'Project navbar' do ...@@ -10,10 +10,10 @@ describe 'Project navbar' do
{ {
nav_item: _('Analytics'), nav_item: _('Analytics'),
nav_sub_items: [ nav_sub_items: [
_('CI / CD Analytics'), _('CI / CD'),
(_('Code Review') if Gitlab.ee?), (_('Code Review') if Gitlab.ee?),
_('Repository Analytics'), _('Repository'),
_('Value Stream Analytics') _('Value Stream')
] ]
} }
end end
...@@ -114,7 +114,7 @@ describe 'Project navbar' do ...@@ -114,7 +114,7 @@ describe 'Project navbar' do
before do before do
stub_licensed_features(issues_analytics: true) stub_licensed_features(issues_analytics: true)
analytics_nav_item[:nav_sub_items] << _('Issues Analytics') analytics_nav_item[:nav_sub_items] << _('Issues')
analytics_nav_item[:nav_sub_items].sort! analytics_nav_item[:nav_sub_items].sort!
visit project_path(project) visit project_path(project)
......
...@@ -222,7 +222,7 @@ describe 'User uses shortcuts', :js do ...@@ -222,7 +222,7 @@ describe 'User uses shortcuts', :js do
find('body').native.send_key('d') find('body').native.send_key('d')
expect(page).to have_active_navigation(_('Analytics')) expect(page).to have_active_navigation(_('Analytics'))
expect(page).to have_active_sub_navigation(_('Repository Analytics')) expect(page).to have_active_sub_navigation(_('Repository'))
end end
end end
end end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Blob Header Editing rendering matches the snapshot 1`] = `
<div
class="js-file-title file-title-flex-parent"
>
<gl-form-input-stub
class="form-control js-snippet-file-name qa-snippet-file-name"
id="snippet_file_name"
name="snippet_file_name"
placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby"
type="text"
value="foo.md"
/>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
import { GlFormInput } from '@gitlab/ui';
describe('Blob Header Editing', () => {
let wrapper;
const value = 'foo.md';
function createComponent() {
wrapper = shallowMount(BlobEditHeader, {
propsData: {
value,
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('rendering', () => {
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('contains a form input field', () => {
expect(wrapper.contains(GlFormInput)).toBe(true);
});
});
describe('functionality', () => {
it('emits input event when the blob name is changed', () => {
const inputComponent = wrapper.find(GlFormInput);
const newValue = 'bar.txt';
wrapper.setData({
name: newValue,
});
inputComponent.vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().input[0]).toEqual([newValue]);
});
});
});
});
...@@ -178,8 +178,17 @@ const RESPONSE_MAP = { ...@@ -178,8 +178,17 @@ const RESPONSE_MAP = {
}, },
}; };
const graphQlResponseData = {
project: {
issue: {
healthStatus: 'onTrack',
},
},
};
const mockData = { const mockData = {
responseMap: RESPONSE_MAP, responseMap: RESPONSE_MAP,
graphQlResponseData,
mediator: { mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras', endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras',
toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription', toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
...@@ -195,6 +204,7 @@ const mockData = { ...@@ -195,6 +204,7 @@ const mockData = {
}, },
rootPath: '/', rootPath: '/',
fullPath: '/gitlab-org/gitlab-shell', fullPath: '/gitlab-org/gitlab-shell',
id: 1,
}, },
time: { time: {
time_estimate: 3600, time_estimate: 3600,
......
...@@ -27,7 +27,7 @@ describe('Snippet editor', () => { ...@@ -27,7 +27,7 @@ describe('Snippet editor', () => {
setHTMLFixture(` setHTMLFixture(`
<div class="snippet-form-holder"> <div class="snippet-form-holder">
<form> <form>
<input class="snippet-file-name" type="text" value="${name}"> <input class="js-snippet-file-name" type="text" value="${name}">
<input class="snippet-file-content" type="hidden" value="${content}"> <input class="snippet-file-content" type="hidden" value="${content}">
<pre id="editor"></pre> <pre id="editor"></pre>
</form> </form>
...@@ -39,7 +39,7 @@ describe('Snippet editor', () => { ...@@ -39,7 +39,7 @@ describe('Snippet editor', () => {
setUpFixture(name, content); setUpFixture(name, content);
editorEl = document.getElementById('editor'); editorEl = document.getElementById('editor');
contentEl = document.querySelector('.snippet-file-content'); contentEl = document.querySelector('.snippet-file-content');
fileNameEl = document.querySelector('.snippet-file-name'); fileNameEl = document.querySelector('.js-snippet-file-name');
form = document.querySelector('.snippet-form-holder form'); form = document.querySelector('.snippet-form-holder form');
initEditor(); initEditor();
......
...@@ -83,4 +83,17 @@ describe('LockIssueSidebar', () => { ...@@ -83,4 +83,17 @@ describe('LockIssueSidebar', () => {
done(); done();
}); });
}); });
it('does not display the edit form when opened from collapsed state if not editable', done => {
expect(vm2.isLockDialogOpen).toBe(false);
vm2.$el.querySelector('.sidebar-collapsed-icon').click();
Vue.nextTick()
.then(() => {
expect(vm2.isLockDialogOpen).toBe(false);
})
.then(done)
.catch(done.fail);
});
}); });
...@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
import Mock from './mock_data'; import Mock from './mock_data';
const { mediator: mediatorMockData } = Mock; const { mediator: mediatorMockData } = Mock;
...@@ -44,12 +44,18 @@ describe('Sidebar mediator', function() { ...@@ -44,12 +44,18 @@ describe('Sidebar mediator', function() {
it('fetches the data', done => { it('fetches the data', done => {
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint]; const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
mock.onGet(mediatorMockData.endpoint).reply(200, mockData); mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
const mockGraphQlData = Mock.graphQlResponseData;
spyOn(gqClient, 'query').and.returnValue({
data: mockGraphQlData,
});
spyOn(this.mediator, 'processFetchedData').and.callThrough(); spyOn(this.mediator, 'processFetchedData').and.callThrough();
this.mediator this.mediator
.fetch() .fetch()
.then(() => { .then(() => {
expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData); expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData, mockGraphQlData);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::LinkLfsObjects, :migration, schema: 2020_02_10_062432 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:fork_networks) { table(:fork_networks) }
let(:fork_network_members) { table(:fork_network_members) }
let(:lfs_objects) { table(:lfs_objects) }
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') }
let!(:source_project) { projects.create(namespace_id: namespace.id) }
let!(:another_source_project) { projects.create(namespace_id: namespace.id) }
let!(:project) { projects.create(namespace_id: namespace.id) }
let!(:another_project) { projects.create(namespace_id: namespace.id) }
let!(:other_project) { projects.create(namespace_id: namespace.id) }
let!(:linked_project) { projects.create(namespace_id: namespace.id) }
let(:fork_network) { fork_networks.create(root_project_id: source_project.id) }
let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) }
let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) }
let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) }
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
# Create links between projects
fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil)
[project, another_project, linked_project].each do |p|
fork_network_members.create(
fork_network_id: fork_network.id,
project_id: p.id,
forked_from_project_id: fork_network.root_project_id
)
end
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil)
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: other_project.id, forked_from_project_id: another_fork_network.root_project_id)
# Links LFS objects to some projects
[source_project, another_source_project, linked_project].each do |p|
lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id)
lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id)
end
end
it 'creates LfsObjectsProject records for forks within the specified range of project IDs' do
expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger|
expect(logger).to receive(:info).twice
end
expect { subject.perform(project.id, other_project.id) }.to change { lfs_objects_projects.count }.by(6)
expect(lfs_object_ids_for(project)).to match_array(lfs_object_ids_for(source_project))
expect(lfs_object_ids_for(another_project)).to match_array(lfs_object_ids_for(source_project))
expect(lfs_object_ids_for(other_project)).to match_array(lfs_object_ids_for(another_source_project))
expect { subject.perform(project.id, other_project.id) }.not_to change { lfs_objects_projects.count }
end
context 'when it is not necessary to create LfsObjectProject records' do
it 'does not create LfsObjectProject records' do
expect { subject.perform(linked_project.id, linked_project.id) }
.not_to change { lfs_objects_projects.count }
end
end
def lfs_object_ids_for(project)
lfs_objects_projects.where(project_id: project.id).pluck(:lfs_object_id)
end
end
...@@ -467,6 +467,7 @@ project: ...@@ -467,6 +467,7 @@ project:
- resource_groups - resource_groups
- autoclose_referenced_issues - autoclose_referenced_issues
- status_page_setting - status_page_setting
- requirements
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ImportExport::Error do
describe '.permission_error' do
subject(:error) do
described_class.permission_error(user, importable)
end
let(:user) { build(:user, id: 1) }
context 'when supplied a project' do
let(:importable) { build(:project, id: 1, name: 'project1') }
it 'returns an error with the correct message' do
expect(error.message)
.to eq 'User with ID: 1 does not have required permissions for Project: project1 with ID: 1'
end
end
context 'when supplied a group' do
let(:importable) { build(:group, id: 1, name: 'group1') }
it 'returns an error with the correct message' do
expect(error.message)
.to eq 'User with ID: 1 does not have required permissions for Group: group1 with ID: 1'
end
end
end
end
# frozen_string_literal: true
require 'rake_helper'
describe Gitlab::ImportExport::Project::ExportTask do
let(:username) { 'root' }
let(:namespace_path) { username }
let!(:user) { create(:user, username: username) }
let(:measurement_enabled) { false }
let(:file_path) { 'spec/fixtures/gitlab/import_export/test_project_export.tar.gz' }
let(:project) { create(:project, creator: user, namespace: user.namespace) }
let(:project_name) { project.name }
let(:task_params) do
{
username: username,
namespace_path: namespace_path,
project_path: project_name,
file_path: file_path,
measurement_enabled: measurement_enabled
}
end
subject { described_class.new(task_params).export }
context 'when project is found' do
let(:project) { create(:project, creator: user, namespace: user.namespace) }
around do |example|
example.run
ensure
File.delete(file_path)
end
it 'performs project export successfully' do
expect { subject }.to output(/Done!/).to_stdout
expect(subject).to eq(true)
expect(File).to exist(file_path)
end
it_behaves_like 'measurable'
end
context 'when project is not found' do
let(:project_name) { 'invalid project name' }
it 'logs an error' do
expect { subject }.to output(/Project with path: #{project_name} was not found. Please provide correct project path/).to_stdout
end
it 'returns false' do
expect(subject).to eq(false)
end
end
context 'when file path is invalid' do
let(:file_path) { '/invalid_file_path/test_project_export.tar.gz' }
it 'logs an error' do
expect { subject }.to output(/Invalid file path: #{file_path}. Please provide correct file path/ ).to_stdout
end
it 'returns false' do
expect(subject).to eq(false)
end
end
end
...@@ -2,19 +2,25 @@ ...@@ -2,19 +2,25 @@
require 'rake_helper' require 'rake_helper'
describe 'gitlab:import_export:import rake task' do describe Gitlab::ImportExport::Project::ImportTask do
let(:username) { 'root' } let(:username) { 'root' }
let(:namespace_path) { username } let(:namespace_path) { username }
let!(:user) { create(:user, username: username) } let!(:user) { create(:user, username: username) }
let(:measurement_enabled) { false } let(:measurement_enabled) { false }
let(:task_params) { [username, namespace_path, project_name, archive_path, measurement_enabled] }
let(:project) { Project.find_by_full_path("#{namespace_path}/#{project_name}") } let(:project) { Project.find_by_full_path("#{namespace_path}/#{project_name}") }
let(:import_task) { described_class.new(task_params) }
let(:task_params) do
{
username: username,
namespace_path: namespace_path,
project_path: project_name,
file_path: file_path,
measurement_enabled: measurement_enabled
}
end
before do before do
Rake.application.rake_require('tasks/gitlab/import_export/import')
allow(Settings.uploads.object_store).to receive(:[]=).and_call_original allow(Settings.uploads.object_store).to receive(:[]=).and_call_original
allow_any_instance_of(GitlabProjectImport).to receive(:exit)
.and_raise(RuntimeError, 'exit not handled')
end end
around do |example| around do |example|
...@@ -30,15 +36,16 @@ describe 'gitlab:import_export:import rake task' do ...@@ -30,15 +36,16 @@ describe 'gitlab:import_export:import rake task' do
Settings.uploads.object_store['background_upload'] = old_background_upload_setting Settings.uploads.object_store['background_upload'] = old_background_upload_setting
end end
subject { run_rake_task('gitlab:import_export:import', task_params) } subject { import_task.import }
context 'when project import is valid' do context 'when project import is valid' do
let(:project_name) { 'import_rake_test_project' } let(:project_name) { 'import_rake_test_project' }
let(:archive_path) { 'spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz' } let(:file_path) { 'spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz' }
it 'performs project import successfully' do it 'performs project import successfully' do
expect { subject }.to output(/Done!/).to_stdout expect { subject }.to output(/Done!/).to_stdout
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
expect(subject).to eq(true)
expect(project.merge_requests.count).to be > 0 expect(project.merge_requests.count).to be > 0
expect(project.issues.count).to be > 0 expect(project.issues.count).to be > 0
...@@ -56,15 +63,13 @@ describe 'gitlab:import_export:import rake task' do ...@@ -56,15 +63,13 @@ describe 'gitlab:import_export:import rake task' do
end end
end end
expect_next_instance_of(GitlabProjectImport) do |importer| expect(import_task).to receive(:execute_sidekiq_job).and_wrap_original do |m|
expect(importer).to receive(:execute_sidekiq_job).and_wrap_original do |m| expect(Settings.uploads.object_store['background_upload']).to eq(true)
expect(Settings.uploads.object_store['background_upload']).to eq(true) expect(Settings.uploads.object_store['direct_upload']).to eq(true)
expect(Settings.uploads.object_store['direct_upload']).to eq(true) expect(Settings.uploads.object_store).not_to receive(:[]=).with('backgroud_upload', false)
expect(Settings.uploads.object_store).not_to receive(:[]=).with('backgroud_upload', false) expect(Settings.uploads.object_store).not_to receive(:[]=).with('direct_upload', false)
expect(Settings.uploads.object_store).not_to receive(:[]=).with('direct_upload', false)
m.call m.call
end
end end
subject subject
...@@ -75,13 +80,13 @@ describe 'gitlab:import_export:import rake task' do ...@@ -75,13 +80,13 @@ describe 'gitlab:import_export:import rake task' do
context 'when project import is invalid' do context 'when project import is invalid' do
let(:project_name) { 'import_rake_invalid_test_project' } let(:project_name) { 'import_rake_invalid_test_project' }
let(:archive_path) { 'spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz' } let(:file_path) { 'spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz' }
let(:not_imported_message) { /Total number of not imported relations: 1/ } let(:not_imported_message) { /Total number of not imported relations: 1/ }
let(:error) { /Validation failed: Notes is invalid/ }
it 'performs project import successfully' do it 'performs project import successfully' do
expect { subject }.to output(not_imported_message).to_stdout expect { subject }.to output(not_imported_message).to_stdout
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
expect(subject).to eq(true)
expect(project.merge_requests).to be_empty expect(project.merge_requests).to be_empty
expect(project.import_state.last_error).to be_nil expect(project.import_state.last_error).to be_nil
......
...@@ -324,6 +324,24 @@ describe Gitlab::UsageData do ...@@ -324,6 +324,24 @@ describe Gitlab::UsageData do
end end
end end
describe '#cycle_analytics_usage_data' do
subject { described_class.cycle_analytics_usage_data }
it 'works when queries time out in new' do
allow(Gitlab::CycleAnalytics::UsageData)
.to receive(:new).and_raise(ActiveRecord::StatementInvalid.new(''))
expect { subject }.not_to raise_error
end
it 'works when queries time out in to_json' do
allow_any_instance_of(Gitlab::CycleAnalytics::UsageData)
.to receive(:to_json).and_raise(ActiveRecord::StatementInvalid.new(''))
expect { subject }.not_to raise_error
end
end
describe '#ingress_modsecurity_usage' do describe '#ingress_modsecurity_usage' do
subject { described_class.ingress_modsecurity_usage } subject { described_class.ingress_modsecurity_usage }
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200217091401_reschedule_link_lfs_objects.rb')
describe RescheduleLinkLfsObjects, :migration, :sidekiq do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:fork_networks) { table(:fork_networks) }
let(:fork_network_members) { table(:fork_network_members) }
let(:lfs_objects) { table(:lfs_objects) }
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') }
let(:fork_network) { fork_networks.create(root_project_id: source_project.id) }
let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) }
let!(:source_project) { projects.create(namespace_id: namespace.id) }
let!(:another_source_project) { projects.create(namespace_id: namespace.id) }
let!(:project) { projects.create(namespace_id: namespace.id) }
let!(:another_project) { projects.create(namespace_id: namespace.id) }
let!(:other_project) { projects.create(namespace_id: namespace.id) }
let!(:linked_project) { projects.create(namespace_id: namespace.id) }
let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) }
let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) }
before do
# Create links between projects
fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil)
[project, another_project, linked_project].each do |p|
fork_network_members.create(
fork_network_id: fork_network.id,
project_id: p.id,
forked_from_project_id: fork_network.root_project_id
)
end
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil)
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: other_project.id, forked_from_project_id: another_fork_network.root_project_id)
end
context 'when there are forks to be backfilled' do
before do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
# Links LFS objects to some projects
[source_project, another_source_project, linked_project].each do |p|
lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id)
lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id)
end
end
it 'schedules background migration to link LFS objects' do
Sidekiq::Testing.fake! do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(2.minutes, project.id, another_project.id)
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(4.minutes, other_project.id, other_project.id)
end
end
end
context 'when there are no forks to be backfilled' do
before do
# Links LFS objects to all projects
projects.all.each do |p|
lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id)
lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id)
end
end
it 'does not schedule any job' do
Sidekiq::Testing.fake! do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(0)
end
end
end
end
...@@ -65,6 +65,17 @@ describe BroadcastMessage do ...@@ -65,6 +65,17 @@ describe BroadcastMessage do
end end
end end
it 'expires the value if a broadcast message has ended', :request_store do
message = create(:broadcast_message, broadcast_type: broadcast_type, ends_at: Time.now.utc + 1.day)
expect(subject.call).to match_array([message])
expect(described_class.cache).to receive(:expire).and_call_original
Timecop.travel(1.week) do
2.times { expect(subject.call).to be_empty }
end
end
it 'does not create new records' do it 'does not create new records' do
create(:broadcast_message, broadcast_type: broadcast_type) create(:broadcast_message, broadcast_type: broadcast_type)
......
...@@ -38,12 +38,31 @@ describe Groups::ImportExport::ExportService do ...@@ -38,12 +38,31 @@ describe Groups::ImportExport::ExportService do
let!(:another_user) { create(:user) } let!(:another_user) { create(:user) }
let(:service) { described_class.new(group: group, user: another_user, params: { shared: shared }) } let(:service) { described_class.new(group: group, user: another_user, params: { shared: shared }) }
let(:expected_message) do
"User with ID: %s does not have required permissions for Group: %s with ID: %s" %
[another_user.id, group.name, group.id]
end
it 'fails' do it 'fails' do
expected_message =
"User with ID: %s does not have permission to Group %s with ID: %s." %
[another_user.id, group.name, group.id]
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message) expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end end
it 'logs the error' do
expect(shared.logger).to receive(:error).with(
group_id: group.id,
group_name: group.name,
error: expected_message,
message: 'Group Import/Export: Export failed'
)
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
end
it 'tracks the error' do
expect(shared).to receive(:error) { |param| expect(param.message).to eq expected_message }
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
end
end end
context 'when export fails' do context 'when export fails' do
......
...@@ -9,6 +9,8 @@ describe Groups::ImportExport::ImportService do ...@@ -9,6 +9,8 @@ describe Groups::ImportExport::ImportService do
let(:service) { described_class.new(group: group, user: user) } let(:service) { described_class.new(group: group, user: user) }
let(:import_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') } let(:import_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
let(:import_logger) { instance_double(Gitlab::Import::Logger) }
subject { service.execute } subject { service.execute }
before do before do
...@@ -25,13 +27,82 @@ describe Groups::ImportExport::ImportService do ...@@ -25,13 +27,82 @@ describe Groups::ImportExport::ImportService do
expect(group.import_export_upload.import_file.file).to be_nil expect(group.import_export_upload.import_file.file).to be_nil
end end
it 'logs the import success' do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
expect(import_logger).to receive(:info).with(
group_id: group.id,
group_name: group.name,
message: 'Group Import/Export: Import succeeded'
)
subject
end
end end
context 'when user does not have correct permissions' do context 'when user does not have correct permissions' do
let(:user) { create(:user) } let(:user) { create(:user) }
it 'raises exception' do it 'logs the error and raises an exception' do
expect { subject }.to raise_error(StandardError) allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
expect(import_logger).to receive(:error).with(
group_id: group.id,
group_name: group.name,
message: a_string_including('Errors occurred')
)
expect { subject }.to raise_error(Gitlab::ImportExport::Error)
end
it 'tracks the error' do
shared = Gitlab::ImportExport::Shared.new(group)
allow(Gitlab::ImportExport::Shared).to receive(:new).and_return(shared)
expect(shared).to receive(:error) do |param|
expect(param.message).to include 'does not have required permissions for'
end
expect { subject }.to raise_error(Gitlab::ImportExport::Error)
end
end
context 'when there are errors with the import file' do
let(:import_file) { fixture_file_upload('spec/fixtures/symlink_export.tar.gz') }
before do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
end
it 'logs the error and raises an exception' do
expect(import_logger).to receive(:error).with(
group_id: group.id,
group_name: group.name,
message: a_string_including('Errors occurred')
)
expect { subject }.to raise_error(Gitlab::ImportExport::Error)
end
end
context 'when there are errors with the sub-relations' do
let(:import_file) { fixture_file_upload('spec/fixtures/group_export_invalid_subrelations.tar.gz') }
it 'successfully imports the group' do
expect(subject).to be_truthy
end
it 'logs the import success' do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
expect(import_logger).to receive(:info).with(
group_id: group.id,
group_name: group.name,
message: 'Group Import/Export: Import succeeded'
)
subject
end end
end end
end end
......
...@@ -91,6 +91,17 @@ describe MergeRequests::MergeToRefService do ...@@ -91,6 +91,17 @@ describe MergeRequests::MergeToRefService do
it_behaves_like 'successfully evaluates pre-condition checks' it_behaves_like 'successfully evaluates pre-condition checks'
it 'returns an error when Gitlab::Git::CommandError is raised during merge' do
allow(project.repository).to receive(:merge_to_ref) do
raise Gitlab::Git::CommandError.new('Failed to create merge commit')
end
result = service.execute(merge_request)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Failed to create merge commit')
end
context 'commit history comparison with regular MergeService' do context 'commit history comparison with regular MergeService' do
before do before do
# The merge service needs an authorized user while merge-to-ref # The merge service needs an authorized user while merge-to-ref
......
...@@ -29,7 +29,7 @@ describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memory_stor ...@@ -29,7 +29,7 @@ describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memory_stor
end end
context 'user does not have push right to repository' do context 'user does not have push right to repository' do
it_behaves_like 'misconfigured dashboard service response', :forbidden, %q(You can't commit to this project) it_behaves_like 'misconfigured dashboard service response', :forbidden, %q(You are not allowed to push into this branch. Create another branch or open a merge request.)
end end
context 'with rights to push to the repository' do context 'with rights to push to the repository' do
......
...@@ -27,7 +27,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto ...@@ -27,7 +27,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
end end
context 'user does not have push right to repository' do context 'user does not have push right to repository' do
it_behaves_like 'misconfigured dashboard service response', :forbidden, "You can't commit to this project" it_behaves_like 'misconfigured dashboard service response', :forbidden, "You are not allowed to push into this branch. Create another branch or open a merge request."
end end
context 'with rights to push to the repository' do context 'with rights to push to the repository' do
......
...@@ -164,7 +164,7 @@ describe Projects::ImportExport::ExportService do ...@@ -164,7 +164,7 @@ describe Projects::ImportExport::ExportService do
it 'fails' do it 'fails' do
expected_message = expected_message =
"User with ID: %s does not have permission to Project %s with ID: %s." % "User with ID: %s does not have required permissions for Project: %s with ID: %s" %
[another_user.id, project.name, project.id] [another_user.id, project.name, project.id]
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message) expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end end
......
...@@ -18,7 +18,7 @@ RSpec.shared_examples 'measurable' do ...@@ -18,7 +18,7 @@ RSpec.shared_examples 'measurable' do
end end
context 'when measurement is not provided' do context 'when measurement is not provided' do
let(:task_params) { [username, namespace_path, project_name, archive_path] } let(:measurement_enabled) { nil }
it 'does not output measurement results' do it 'does not output measurement results' do
expect { subject }.not_to output(/Measuring enabled.../).to_stdout expect { subject }.not_to output(/Measuring enabled.../).to_stdout
......
# frozen_string_literal: true
require 'rake_helper'
describe 'gitlab:import_export:export rake task' do
let(:username) { 'root' }
let(:namespace_path) { username }
let!(:user) { create(:user, username: username) }
let(:measurement_enabled) { false }
let(:task_params) { [username, namespace_path, project_name, archive_path, measurement_enabled] }
before do
Rake.application.rake_require('tasks/gitlab/import_export/export')
end
subject { run_rake_task('gitlab:import_export:export', task_params) }
context 'when project is found' do
let(:project) { create(:project, creator: user, namespace: user.namespace) }
let(:project_name) { project.name }
let(:archive_path) { 'spec/fixtures/gitlab/import_export/test_project_export.tar.gz' }
around do |example|
example.run
ensure
File.delete(archive_path)
end
it 'performs project export successfully' do
expect { subject }.to output(/Done!/).to_stdout
expect(File).to exist(archive_path)
end
it_behaves_like 'measurable'
end
end
...@@ -173,7 +173,7 @@ describe 'layouts/nav/sidebar/_project' do ...@@ -173,7 +173,7 @@ describe 'layouts/nav/sidebar/_project' do
it 'shows the value stream analytics entry' do it 'shows the value stream analytics entry' do
render render
expect(rendered).to have_link('Value Stream Analytics', href: project_cycle_analytics_path(project)) expect(rendered).to have_link('Value Stream', href: project_cycle_analytics_path(project))
end end
end end
...@@ -183,7 +183,7 @@ describe 'layouts/nav/sidebar/_project' do ...@@ -183,7 +183,7 @@ describe 'layouts/nav/sidebar/_project' do
it 'does not show the value stream analytics entry' do it 'does not show the value stream analytics entry' do
render render
expect(rendered).not_to have_link('Value Stream Analytics', href: project_cycle_analytics_path(project)) expect(rendered).not_to have_link('Value Stream', href: project_cycle_analytics_path(project))
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment