Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-02 12:10:23 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-02 12:10:23 +0300
commit4b9ace6c1fead1b44f173eaee0cfaa58f46a258a (patch)
treea411c934419690755623a57ff7ea5f47050050e2 /app
parent03a521732276f8abc4ba069dd985b22cd9bc5929 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue137
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue22
-rw-r--r--app/assets/javascripts/issue_show/components/incident_tabs.vue26
-rw-r--r--app/assets/javascripts/issue_show/components/pinned_links.vue2
-rw-r--r--app/assets/javascripts/issue_show/incident.js21
-rw-r--r--app/assets/javascripts/issue_show/issue.js (renamed from app/assets/javascripts/issue_show/index.js)5
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js13
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/finders/concerns/merged_at_filter.rb12
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb4
-rw-r--r--app/graphql/types/merge_request_sort_enum.rb11
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/container_registry_helper.rb8
-rw-r--r--app/helpers/issuables_helper.rb5
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/application_setting_implementation.rb3
-rw-r--r--app/models/atlassian/identity.rb26
-rw-r--r--app/models/merge_request.rb26
-rw-r--r--app/models/service.rb4
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/user.rb1
-rw-r--r--app/services/ci/parse_dotenv_artifact_service.rb2
-rw-r--r--app/services/projects/container_repository/gitlab/delete_tags_service.rb35
-rw-r--r--app/services/projects/container_repository/third_party/delete_tags_service.rb2
-rw-r--r--app/views/admin/application_settings/_registry.html.haml6
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/shared/wikis/_form.html.haml2
-rw-r--r--app/views/users/show.html.haml8
28 files changed, 299 insertions, 100 deletions
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
index 80b44f7bb13..78647065c8e 100644
--- a/app/assets/javascripts/diffs/components/settings_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -1,16 +1,24 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlDeprecatedButton, GlIcon } from '@gitlab/ui';
+import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
components: {
- GlDeprecatedButton,
- GlIcon,
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
},
computed: {
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
},
+ mounted() {
+ this.patchAriaLabel();
+ },
+ updated() {
+ this.patchAriaLabel();
+ },
methods: {
...mapActions('diffs', [
'setInlineDiffViewType',
@@ -18,74 +26,69 @@ export default {
'setRenderTreeList',
'setShowWhitespace',
]),
+ patchAriaLabel() {
+ this.$el
+ .querySelector('.js-show-diff-settings')
+ .setAttribute('aria-label', __('Diff view settings'));
+ },
},
};
</script>
<template>
- <div class="dropdown">
- <button
- type="button"
- class="btn btn-default js-show-diff-settings"
- data-toggle="dropdown"
- data-display="static"
- >
- <gl-icon name="settings" /> <gl-icon name="chevron-down" />
- </button>
- <div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3">
- <div>
- <span class="bold d-block mb-1">{{ __('File browser') }}</span>
- <div class="btn-group d-flex">
- <gl-deprecated-button
- :class="{ active: !renderTreeList }"
- class="w-100 js-list-view"
- @click="setRenderTreeList(false)"
- >
- {{ __('List view') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- :class="{ active: renderTreeList }"
- class="w-100 js-tree-view"
- @click="setRenderTreeList(true)"
- >
- {{ __('Tree view') }}
- </gl-deprecated-button>
- </div>
- </div>
- <div class="mt-2">
- <span class="bold d-block mb-1">{{ __('Compare changes') }}</span>
- <div class="btn-group d-flex js-diff-view-buttons">
- <gl-deprecated-button
- id="inline-diff-btn"
- :class="{ active: isInlineView }"
- class="w-100 js-inline-diff-button"
- data-view-type="inline"
- @click="setInlineDiffViewType"
- >
- {{ __('Inline') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- id="parallel-diff-btn"
- :class="{ active: isParallelView }"
- class="w-100 js-parallel-diff-button"
- data-view-type="parallel"
- @click="setParallelDiffViewType"
- >
- {{ __('Side-by-side') }}
- </gl-deprecated-button>
- </div>
- </div>
- <div class="mt-2">
- <label class="mb-0">
- <input
- id="show-whitespace"
- type="checkbox"
- :checked="showWhitespace"
- @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
- />
- {{ __('Show whitespace changes') }}
- </label>
- </div>
+ <gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right>
+ <div class="gl-px-3">
+ <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span>
+ <gl-button-group class="gl-display-flex">
+ <gl-button
+ :class="{ selected: !renderTreeList }"
+ class="gl-w-half js-list-view"
+ @click="setRenderTreeList(false)"
+ >
+ {{ __('List view') }}
+ </gl-button>
+ <gl-button
+ :class="{ selected: renderTreeList }"
+ class="gl-w-half js-tree-view"
+ @click="setRenderTreeList(true)"
+ >
+ {{ __('Tree view') }}
+ </gl-button>
+ </gl-button-group>
+ </div>
+ <div class="gl-mt-3 gl-px-3">
+ <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('Compare changes') }}</span>
+ <gl-button-group class="gl-display-flex js-diff-view-buttons">
+ <gl-button
+ id="inline-diff-btn"
+ :class="{ selected: isInlineView }"
+ class="gl-w-half js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </gl-button>
+ <gl-button
+ id="parallel-diff-btn"
+ :class="{ selected: isParallelView }"
+ class="gl-w-half js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </gl-button>
+ </gl-button-group>
+ </div>
+ <div class="gl-mt-3 gl-px-3">
+ <label class="gl-mb-0">
+ <input
+ id="show-whitespace"
+ type="checkbox"
+ :checked="showWhitespace"
+ @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
+ />
+ {{ __('Show whitespace changes') }}
+ </label>
</div>
- </div>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 992d87a969f..1cc04003aa6 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -20,7 +20,6 @@ export default {
components: {
GlIcon,
GlIntersectionObserver,
- descriptionComponent,
titleComponent,
editedComponent,
formComponent,
@@ -152,6 +151,18 @@ export default {
required: false,
default: 0,
},
+ descriptionComponent: {
+ type: Object,
+ required: false,
+ default: () => {
+ return descriptionComponent;
+ },
+ },
+ showTitleBorder: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
const store = new Store({
@@ -209,6 +220,11 @@ export default {
isOpenStatus() {
return this.issuableStatus === IssuableStatus.Open;
},
+ pinnedLinkClasses() {
+ return this.showTitleBorder
+ ? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
+ : '';
+ },
statusIcon() {
return this.isOpenStatus ? 'issue-open-m' : 'mobile-issue-close';
},
@@ -447,9 +463,11 @@ export default {
<pinned-links
:zoom-meeting-url="zoomMeetingUrl"
:published-incident-url="publishedIncidentUrl"
+ :class="pinnedLinkClasses"
/>
- <description-component
+ <component
+ :is="descriptionComponent"
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
diff --git a/app/assets/javascripts/issue_show/components/incident_tabs.vue b/app/assets/javascripts/issue_show/components/incident_tabs.vue
new file mode 100644
index 00000000000..f6e82cfaa74
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/incident_tabs.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import DescriptionComponent from './description.vue';
+
+export default {
+ components: {
+ GlTab,
+ GlTabs,
+ DescriptionComponent,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-tabs
+ content-class="gl-reset-line-height gl-mt-3"
+ class="gl-mt-n3"
+ data-testid="incident-tabs"
+ >
+ <gl-tab :title="__('Summary')">
+ <description-component v-bind="$attrs" />
+ </gl-tab>
+ </gl-tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue
index a877aa2ac96..36375ca743b 100644
--- a/app/assets/javascripts/issue_show/components/pinned_links.vue
+++ b/app/assets/javascripts/issue_show/components/pinned_links.vue
@@ -45,7 +45,7 @@ export default {
</script>
<template>
- <div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start">
+ <div class="gl-display-flex gl-justify-content-start">
<template v-for="(link, i) in pinnedLinks">
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
<gl-button
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js
new file mode 100644
index 00000000000..82b862a2195
--- /dev/null
+++ b/app/assets/javascripts/issue_show/incident.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import issuableApp from './components/app.vue';
+import incidentTabs from './components/incident_tabs.vue';
+
+export default function initIssuableApp(issuableData = {}) {
+ return new Vue({
+ el: document.getElementById('js-issuable-app'),
+ components: {
+ issuableApp,
+ },
+ render(createElement) {
+ return createElement('issuable-app', {
+ props: {
+ ...issuableData,
+ descriptionComponent: incidentTabs,
+ showTitleBorder: false,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/issue.js
index e170d338408..f9f61d5aa64 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -1,8 +1,7 @@
import Vue from 'vue';
import issuableApp from './components/app.vue';
-import { parseIssuableData } from './utils/parse_data';
-export default function initIssueableApp() {
+export default function initIssuableApp(issuableData) {
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
@@ -10,7 +9,7 @@ export default function initIssueableApp() {
},
render(createElement) {
return createElement('issuable-app', {
- props: parseIssuableData(),
+ props: issuableData,
});
},
});
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 5ac6c17e09d..a577d2e1ecd 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -4,14 +4,23 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import { store } from '~/notes/stores';
-import initIssueableApp from '~/issue_show';
+import initIssueApp from '~/issue_show/issue';
+import initIncidentApp from '~/issue_show/incident';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
+import { parseIssuableData } from '~/issue_show/utils/parse_data';
export default function() {
- initIssueableApp();
+ const { issueType, ...issuableData } = parseIssuableData();
+
+ if (issueType === 'incident') {
+ initIncidentApp(issuableData);
+ } else {
+ initIssueApp(issuableData);
+ }
+
initIssuableHeaderWarning(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 77170f7de5e..8aaeb92eb7a 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -1033,3 +1033,9 @@ $mr-widget-min-height: 69px;
.diff-file-row.is-active {
background-color: $gray-50;
}
+
+.merge-request-container {
+ .flash-container {
+ @include gl-mb-4;
+ }
+}
diff --git a/app/finders/concerns/merged_at_filter.rb b/app/finders/concerns/merged_at_filter.rb
index e92bee3934c..581bcca3c25 100644
--- a/app/finders/concerns/merged_at_filter.rb
+++ b/app/finders/concerns/merged_at_filter.rb
@@ -3,7 +3,6 @@
module MergedAtFilter
private
- # rubocop: disable CodeReuse/ActiveRecord
def by_merged_at(items)
return items unless merged_after || merged_before
@@ -11,11 +10,8 @@ module MergedAtFilter
mr_metrics_scope = mr_metrics_scope.merged_after(merged_after) if merged_after.present?
mr_metrics_scope = mr_metrics_scope.merged_before(merged_before) if merged_before.present?
- scope = items.joins(:metrics).merge(mr_metrics_scope)
- scope = target_project_id_filter_on_metrics(scope) if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
- scope
+ items.join_metrics.merge(mr_metrics_scope)
end
- # rubocop: enable CodeReuse/ActiveRecord
def merged_after
params[:merged_after]
@@ -24,10 +20,4 @@ module MergedAtFilter
def merged_before
params[:merged_before]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def target_project_id_filter_on_metrics(scope)
- scope.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index e428e9f115f..677f84e5795 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -37,6 +37,10 @@ module Resolvers
argument :milestone_title, GraphQL::STRING_TYPE,
required: false,
description: 'Title of the milestone'
+ argument :sort, Types::MergeRequestSortEnum,
+ description: 'Sort merge requests by this criteria',
+ required: false,
+ default_value: 'created_desc'
def self.single
::Resolvers::MergeRequestResolver
diff --git a/app/graphql/types/merge_request_sort_enum.rb b/app/graphql/types/merge_request_sort_enum.rb
new file mode 100644
index 00000000000..c64ae367a76
--- /dev/null
+++ b/app/graphql/types/merge_request_sort_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class MergeRequestSortEnum < IssuableSortEnum
+ graphql_name 'MergeRequestSort'
+ description 'Values for sorting merge requests'
+
+ value 'MERGED_AT_ASC', 'Merge time by ascending order', value: :merged_at_asc
+ value 'MERGED_AT_DESC', 'Merge time by descending order', value: :merged_at_desc
+ end
+end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index d68ae1a56ca..7b73af4bd09 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -327,7 +327,8 @@ module ApplicationSettingsHelper
:group_import_limit,
:group_export_limit,
:group_download_export_limit,
- :wiki_page_max_content_bytes
+ :wiki_page_max_content_bytes,
+ :container_registry_delete_tags_service_timeout
]
end
diff --git a/app/helpers/container_registry_helper.rb b/app/helpers/container_registry_helper.rb
new file mode 100644
index 00000000000..9a5d84a90dd
--- /dev/null
+++ b/app/helpers/container_registry_helper.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module ContainerRegistryHelper
+ def limit_delete_tags_service?
+ Feature.enabled?(:container_registry_expiration_policies_throttling) &&
+ ContainerRegistry::Client.supports_tag_delete?
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 0b859a39c4f..398e76b6697 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -292,6 +292,7 @@ module IssuablesHelper
{
hasClosingMergeRequest: issuable.merge_requests_count(current_user) != 0,
+ issueType: issuable.issue_type,
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier # rubocop:disable CodeReuse/ActiveRecord
}
@@ -301,8 +302,8 @@ module IssuablesHelper
return { groupPath: parent.path } if parent.is_a?(Group)
{
- projectPath: ref_project.path,
- projectNamespace: ref_project.namespace.full_path
+ projectPath: ref_project.path,
+ projectNamespace: ref_project.namespace.full_path
}
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6666a04e71d..83576edb866 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -282,6 +282,9 @@ class ApplicationSetting < ApplicationRecord
validates :hashed_storage_enabled, inclusion: { in: [true], message: _("Hashed storage can't be disabled anymore for new projects") }
+ validates :container_registry_delete_tags_service_timeout,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 5a5fc02c112..82304671e4e 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -163,7 +163,8 @@ module ApplicationSettingImplementation
user_default_external: false,
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
- wiki_page_max_content_bytes: 50.megabytes
+ wiki_page_max_content_bytes: 50.megabytes,
+ container_registry_delete_tags_service_timeout: 100
}
end
diff --git a/app/models/atlassian/identity.rb b/app/models/atlassian/identity.rb
new file mode 100644
index 00000000000..906f2be0fbf
--- /dev/null
+++ b/app/models/atlassian/identity.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Atlassian
+ class Identity < ApplicationRecord
+ self.table_name = 'atlassian_identities'
+
+ belongs_to :user
+
+ validates :extern_uid, presence: true, uniqueness: true
+ validates :user, presence: true, uniqueness: true
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+
+ attr_encrypted :refresh_token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index fd73b0d1e04..618fa06745e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -251,6 +251,15 @@ class MergeRequest < ApplicationRecord
joins(:notes).where(notes: { commit_id: sha })
end
scope :join_project, -> { joins(:target_project) }
+ scope :join_metrics, -> do
+ query = joins(:metrics)
+
+ if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
+ query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
+ end
+
+ query
+ end
scope :references_project, -> { references(:target_project) }
scope :with_api_entity_associations, -> {
preload_routables
@@ -264,6 +273,14 @@ class MergeRequest < ApplicationRecord
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
end
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
+ scope :order_merged_at, ->(direction) do
+ query = join_metrics.order(Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', direction))
+
+ # Add `merge_request_metrics.merged_at` to the `SELECT` in order to make the keyset pagination work.
+ query.select(*query.arel.projections, MergeRequest::Metrics.arel_table[:merged_at].as('"merge_request_metrics.merged_at"'))
+ end
+ scope :order_merged_at_asc, -> { order_merged_at('ASC') }
+ scope :order_merged_at_desc, -> { order_merged_at('DESC') }
scope :preload_source_project, -> { preload(:source_project) }
scope :preload_target_project, -> { preload(:target_project) }
scope :preload_routables, -> do
@@ -320,6 +337,15 @@ class MergeRequest < ApplicationRecord
.pluck(:target_branch)
end
+ def self.sort_by_attribute(method, excluded_labels: [])
+ case method.to_s
+ when 'merged_at', 'merged_at_asc' then order_merged_at_asc.with_order_id_desc
+ when 'merged_at_desc' then order_merged_at_desc.with_order_id_desc
+ else
+ super
+ end
+ end
+
def rebase_in_progress?
rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 148c554119f..262806cd0f0 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -351,10 +351,10 @@ class Service < ApplicationRecord
{ success: result.present?, result: result }
end
- # Disable test for instance-level services.
+ # Disable test for instance-level and group-level services.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test?
- !instance?
+ !instance? && !group_id
end
# Returns a hash of the properties that have been assigned a new value since last save,
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index eb3960ff12b..0179176ba9e 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -345,6 +345,10 @@ class Snippet < ApplicationRecord
repository.ls_files(ref)
end
+ def multiple_files?
+ list_files(repository.root_ref).size > 1
+ end
+
class << self
# Searches for snippets with a matching title, description or file name.
#
diff --git a/app/models/user.rb b/app/models/user.rb
index 300e918513a..355a174ba9a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -181,6 +181,7 @@ class User < ApplicationRecord
has_one :user_detail
has_one :user_highest_role
has_one :user_canonical_email
+ has_one :atlassian_identity, class_name: 'Atlassian::Identity'
has_many :reviews, foreign_key: :author_id, inverse_of: :author
diff --git a/app/services/ci/parse_dotenv_artifact_service.rb b/app/services/ci/parse_dotenv_artifact_service.rb
index fcbdc94c097..71b306864b2 100644
--- a/app/services/ci/parse_dotenv_artifact_service.rb
+++ b/app/services/ci/parse_dotenv_artifact_service.rb
@@ -54,7 +54,7 @@ module Ci
end
def scan_line!(line)
- result = line.scan(/^(.*)=(.*)$/).last
+ result = line.scan(/^(.*?)=(.*)$/).last
raise ParserError, 'Invalid Format' if result.nil?
diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
index 18049648e26..cee94b994a3 100644
--- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -5,6 +5,11 @@ module Projects
module Gitlab
class DeleteTagsService
include BaseServiceUtility
+ include ::Gitlab::Utils::StrongMemoize
+
+ DISABLED_TIMEOUTS = [nil, 0].freeze
+
+ TimeoutError = Class.new(StandardError)
def initialize(container_repository, tag_names)
@container_repository = container_repository
@@ -17,12 +22,42 @@ module Projects
def execute
return success(deleted: []) if @tag_names.empty?
+ delete_tags
+ rescue TimeoutError => e
+ ::Gitlab::ErrorTracking.track_exception(e, tags_count: @tag_names&.size, container_repository_id: @container_repository&.id)
+ error('timeout while deleting tags')
+ end
+
+ private
+
+ def delete_tags
+ start_time = Time.zone.now
+
deleted_tags = @tag_names.select do |name|
+ raise TimeoutError if timeout?(start_time)
+
@container_repository.delete_tag_by_name(name)
end
deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
end
+
+ def timeout?(start_time)
+ return false unless throttling_enabled?
+ return false if service_timeout.in?(DISABLED_TIMEOUTS)
+
+ (Time.zone.now - start_time) > service_timeout
+ end
+
+ def throttling_enabled?
+ strong_memoize(:feature_flag) do
+ Feature.enabled?(:container_registry_expiration_policies_throttling)
+ end
+ end
+
+ def service_timeout
+ ::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
+ end
end
end
end
diff --git a/app/services/projects/container_repository/third_party/delete_tags_service.rb b/app/services/projects/container_repository/third_party/delete_tags_service.rb
index 6504172109e..404642acf72 100644
--- a/app/services/projects/container_repository/third_party/delete_tags_service.rb
+++ b/app/services/projects/container_repository/third_party/delete_tags_service.rb
@@ -15,7 +15,7 @@ module Projects
# This is a hack as the registry doesn't support deleting individual
# tags. This code effectively pushes a dummy image and assigns the tag to it.
# This way when the tag is deleted only the dummy image is affected.
- # This is used to preverse compatibility with third-party registries that
+ # This is used to preserve compatibility with third-party registries that
# don't support fast delete.
# See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
def execute
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index fea3ff4c3ba..8a2de6f53b7 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -14,5 +14,11 @@
.form-text.text-muted
= _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.")
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')
+ - if limit_delete_tags_service?
+ .form-group
+ = f.label :container_registry_delete_tags_service_timeout, _('Cleanup policy maximum processing time (seconds)'), class: 'label-bold'
+ = f.number_field :container_registry_delete_tags_service_timeout, min: 0, class: 'form-control'
+ .form-text.text-muted
+ = _("Tags are deleted until the timeout is reached. Any remaining tags are included the next time the policy runs. To remove the time limit, set it to 0.")
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 746d613934c..735a7fa4fea 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -1,5 +1,5 @@
- @gfm_form = true
-- @content_class = "limit-container-width" unless fluid_layout
+- @content_class = "merge-request-container#{' limit-container-width' unless fluid_layout}"
- add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project)
- breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge Requests")
diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml
index 4d64521f9b0..1fd2194e25b 100644
--- a/app/views/shared/wikis/_form.html.haml
+++ b/app/views/shared/wikis/_form.html.haml
@@ -21,7 +21,7 @@
.col-sm-12
= f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: s_('Wiki|Page title')
%span.d-inline-block.mw-100.gl-mt-2
- = icon('lightbulb-o')
+ = sprite_icon('bulb', size: 12, css_class: 'gl-mr-n1')
- if @page.persisted?
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'),
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index b85c3d862cd..9f8129c8c08 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -17,13 +17,13 @@
= sprite_icon('pencil')
- elsif current_user
- if @user.abuse_report
- %button{ class: link_classes + 'btn btn-danger mr-1', title: s_('UserProfile|Already reported for abuse'),
- data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
- = icon('exclamation-circle')
+ %button{ class: link_classes + 'btn btn-danger', title: s_('UserProfile|Already reported for abuse'),
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }>
+ = sprite_icon('error')
- else
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn',
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('exclamation-circle')
+ = sprite_icon('error')
- if can?(current_user, :read_user_profile, @user)
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-svg btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
= sprite_icon('rss', css_class: 'qa-rss-icon')