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
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-26 21:08:59 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-26 21:08:59 +0300
commit5f825c2edec69e9a23e100c949c6c40e88ae5235 (patch)
treedc510f698f6885ae3656da30eb4c4e5ab4f5dd58
parentc46e0d0c271a21b67a3412faf750d27dd63432bb (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS1
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml1
-rw-r--r--.rubocop_todo/layout/space_in_lambda_literal.yml1
-rw-r--r--.rubocop_todo/performance/active_record_subtransaction_methods.yml2
-rw-r--r--.rubocop_todo/rspec/context_wording.yml2
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml1
-rw-r--r--.rubocop_todo/style/guard_clause.yml1
-rw-r--r--.rubocop_todo/style/if_unless_modifier.yml1
-rw-r--r--.rubocop_todo/style/sole_nested_conditional.yml1
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue2
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js4
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue51
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue24
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue1
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_table_button.vue3
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference.js6
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue6
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue9
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js5
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue433
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue172
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue3
-rw-r--r--app/assets/stylesheets/pages/note_form.scss4
-rw-r--r--app/controllers/concerns/redirects_for_missing_path_on_tree.rb4
-rw-r--r--app/controllers/import/bitbucket_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb14
-rw-r--r--app/controllers/projects/tree_controller.rb34
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/models/ci/external_pull_request.rb106
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/external_pull_request.rb106
-rw-r--r--app/models/integrations/hangouts_chat.rb37
-rw-r--r--app/models/project.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb30
-rw-r--r--app/views/shared/_md_preview.html.haml9
-rw-r--r--app/views/shared/blob/_markdown_buttons.html.haml27
-rw-r--r--app/views/shared/notes/_form.html.haml4
-rw-r--r--app/views/shared/notes/_hints.html.haml12
-rw-r--r--config/feature_flags/development/jira_deployment_issue_keys.yml8
-rw-r--r--config/feature_flags/development/redirect_with_ref_type.yml8
-rw-r--r--doc/development/database/efficient_in_operator_queries.md2
-rw-r--r--doc/integration/jira/development_panel.md6
-rw-r--r--doc/integration/jira/index.md2
-rw-r--r--doc/user/clusters/agent/gitops/flux_tutorial.md3
-rw-r--r--doc/user/clusters/agent/index.md1
-rw-r--r--gems/gitlab-rspec/gitlab-rspec.gemspec2
-rw-r--r--lib/atlassian/jira_connect/serializers/build_entity.rb20
-rw-r--r--lib/atlassian/jira_connect/serializers/deployment_entity.rb46
-rw-r--r--lib/bitbucket/connection.rb2
-rw-r--r--lib/extracts_ref.rb2
-rw-r--r--lib/extracts_ref/requested_ref.rb61
-rw-r--r--lib/gitlab/ci/project_config/repository.rb2
-rw-r--r--lib/gitlab/ci/project_config/source.rb1
-rw-r--r--lib/gitlab/import_export/project/relation_factory.rb5
-rw-r--r--lib/gitlab/legacy_github_import/client.rb2
-rw-r--r--locale/gitlab.pot22
-rw-r--r--qa/qa/service/praefect_manager.rb111
-rw-r--r--qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb96
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb12
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb78
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb171
-rw-r--r--spec/controllers/projects_controller_spec.rb156
-rw-r--r--spec/factories/ci/external_pull_requests.rb (renamed from spec/factories/external_pull_requests.rb)2
-rw-r--r--spec/factories/integrations.rb2
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb4
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb8
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js20
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js8
-rw-r--r--spec/frontend/design_management/components/design_description/description_form_spec.js1
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js13
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js2
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js14
-rw-r--r--spec/frontend/notes/components/note_form_spec.js4
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js24
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js25
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js1
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb20
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb168
-rw-r--r--spec/lib/bitbucket/connection_spec.rb12
-rw-r--r--spec/lib/extracts_ref/requested_ref_spec.rb153
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/gitlab/legacy_github_import/client_spec.rb4
-rw-r--r--spec/models/ci/external_pull_request_spec.rb (renamed from spec/models/external_pull_request_spec.rb)8
-rw-r--r--spec/services/members/invite_service_spec.rb12
-rw-r--r--spec/support/helpers/content_editor_helpers.rb2
-rw-r--r--spec/support/helpers/next_instance_of.rb5
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--spec/support/shared_examples/features/inviting_members_shared_examples.rb34
-rw-r--r--spec/views/shared/notes/_form.html.haml_spec.rb30
-rw-r--r--vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml6
101 files changed, 1470 insertions, 1154 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 5db7ba5b109..9a8c93f5103 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -1236,7 +1236,6 @@ lib/gitlab/checks/**
/app/controllers/projects/triggers_controller.rb
/app/controllers/projects/variables_controller.rb
/app/models/commit_status.rb
-/app/models/external_pull_request.rb
/app/models/generic_commit_status.rb
/app/models/namespace_ci_cd_setting.rb
/app/models/project_ci_cd_setting.rb
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 6d181a59214..9089facb3a8 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -27,7 +27,6 @@
RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler
QA_PATH: qa # sets the optional path for bundler to run from
DYNAMIC_PIPELINE_YML: package-and-test-pipeline.yml # yml files are generated by scripts/generate-e2e-pipeline script
- EXTRA_GITLAB_QA_OPTS: --set-feature-flags super_sidebar_nav_enrolled=enabled
inherit:
variables:
- CHROME_VERSION
diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml
index 3bc63853043..ca6803be7e8 100644
--- a/.rubocop_todo/layout/space_in_lambda_literal.yml
+++ b/.rubocop_todo/layout/space_in_lambda_literal.yml
@@ -46,7 +46,6 @@ Layout/SpaceInLambdaLiteral:
- 'app/models/environment.rb'
- 'app/models/error_tracking/client_key.rb'
- 'app/models/error_tracking/error.rb'
- - 'app/models/external_pull_request.rb'
- 'app/models/group.rb'
- 'app/models/group_group_link.rb'
- 'app/models/incident_management/timeline_event_tag.rb'
diff --git a/.rubocop_todo/performance/active_record_subtransaction_methods.yml b/.rubocop_todo/performance/active_record_subtransaction_methods.yml
index 0c0a527a065..a61bac45eff 100644
--- a/.rubocop_todo/performance/active_record_subtransaction_methods.yml
+++ b/.rubocop_todo/performance/active_record_subtransaction_methods.yml
@@ -9,7 +9,7 @@ Performance/ActiveRecordSubtransactionMethods:
- 'app/models/container_repository.rb'
- 'app/models/design_management/design_collection.rb'
- 'app/models/error_tracking/error.rb'
- - 'app/models/external_pull_request.rb'
+ - 'app/models/ci/external_pull_request.rb'
- 'app/models/plan.rb'
- 'app/models/project.rb'
- 'app/models/shard.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index 1ce003ecd2f..64b2c69e6de 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -1535,7 +1535,6 @@ RSpec/ContextWording:
- 'spec/lib/api/validations/validators/untrusted_regexp_spec.rb'
- 'spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb'
- 'spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb'
- - 'spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb'
- 'spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb'
- 'spec/lib/atlassian/jira_connect_spec.rb'
- 'spec/lib/backup/gitaly_backup_spec.rb'
@@ -2200,7 +2199,6 @@ RSpec/ContextWording:
- 'spec/models/environment_status_spec.rb'
- 'spec/models/error_tracking/error_spec.rb'
- 'spec/models/event_spec.rb'
- - 'spec/models/external_pull_request_spec.rb'
- 'spec/models/gpg_key_spec.rb'
- 'spec/models/grafana_integration_spec.rb'
- 'spec/models/group_label_spec.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index 3cf0ed5c38c..7413252f590 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -4787,7 +4787,6 @@ RSpec/MissingFeatureCategory:
- 'spec/models/event_collection_spec.rb'
- 'spec/models/exported_protected_branch_spec.rb'
- 'spec/models/external_issue_spec.rb'
- - 'spec/models/external_pull_request_spec.rb'
- 'spec/models/fork_network_member_spec.rb'
- 'spec/models/fork_network_spec.rb'
- 'spec/models/generic_commit_status_spec.rb'
diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml
index 8613e240d9c..abd7fe7af98 100644
--- a/.rubocop_todo/style/guard_clause.yml
+++ b/.rubocop_todo/style/guard_clause.yml
@@ -105,7 +105,6 @@ Style/GuardClause:
- 'app/models/diff_viewer/base.rb'
- 'app/models/environment.rb'
- 'app/models/error_tracking/project_error_tracking_setting.rb'
- - 'app/models/external_pull_request.rb'
- 'app/models/generic_commit_status.rb'
- 'app/models/grafana_integration.rb'
- 'app/models/integrations/base_issue_tracker.rb'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index 2ca9b22f0ad..09707e68648 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -155,7 +155,6 @@ Style/IfUnlessModifier:
- 'app/models/diff_viewer/base.rb'
- 'app/models/environment.rb'
- 'app/models/error_tracking/project_error_tracking_setting.rb'
- - 'app/models/external_pull_request.rb'
- 'app/models/generic_commit_status.rb'
- 'app/models/grafana_integration.rb'
- 'app/models/group.rb'
diff --git a/.rubocop_todo/style/sole_nested_conditional.yml b/.rubocop_todo/style/sole_nested_conditional.yml
index 65cac595cde..975da41a69a 100644
--- a/.rubocop_todo/style/sole_nested_conditional.yml
+++ b/.rubocop_todo/style/sole_nested_conditional.yml
@@ -8,7 +8,6 @@ Style/SoleNestedConditional:
- 'app/controllers/projects/blob_controller.rb'
- 'app/helpers/nav_helper.rb'
- 'app/models/concerns/cache_markdown_field.rb'
- - 'app/models/external_pull_request.rb'
- 'app/models/issue.rb'
- 'app/models/network/graph.rb'
- 'app/models/packages/package.rb'
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index e6c3a0cba58..db676b36a0b 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -124,7 +124,7 @@ export default {
:textarea-value="noteData.note"
:markdown-preview-path="getNoteableData.preview_note_path"
:markdown-docs-path="getNotesData.markdownDocsPath"
- :quick-actions-docs-path="getNotesData.quickActionsDocsPath"
+ supports-quick-actions
:restricted-tool-bar-items="$options.restrictedToolbarItems"
:force-autosize="false"
class="js-no-autosize"
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index bcd92d09033..ce77ede9fe4 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -139,7 +139,7 @@ $(document).on('markdown-preview:show', (e, $form) => {
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
- $form.find('.md-header-toolbar, .js-zen-enter').addClass('gl-display-none!');
+ $form.find('.haml-markdown-button, .js-zen-enter').addClass('gl-display-none!');
markdownPreview.showPreview($form);
});
@@ -162,7 +162,7 @@ $(document).on('markdown-preview:hide', (e, $form) => {
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide();
- $form.find('.md-header-toolbar, .js-zen-enter').removeClass('gl-display-none!');
+ $form.find('.haml-markdown-button, .js-zen-enter').removeClass('gl-display-none!');
markdownPreview.hideReferencedCommands($form);
});
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 92f3c3fb8fa..b9e713cee6e 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,8 +1,9 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
-import { GlSprintf, GlLink } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/alert';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
import { createContentEditor } from '../services/create_content_editor';
import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
@@ -17,8 +18,7 @@ import LoadingIndicator from './loading_indicator.vue';
export default {
components: {
- GlSprintf,
- GlLink,
+ GlButton,
LoadingIndicator,
ContentEditorAlert,
ContentEditorProvider,
@@ -29,12 +29,17 @@ export default {
MediaBubbleMenu,
EditorStateObserver,
ReferenceBubbleMenu,
+ EditorModeSwitcher,
},
props: {
renderMarkdown: {
type: Function,
required: true,
},
+ markdownDocsPath: {
+ type: String,
+ required: true,
+ },
uploadsPath: {
type: String,
required: true,
@@ -65,10 +70,10 @@ export default {
default: false,
validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus),
},
- quickActionsDocsPath: {
- type: String,
+ supportsQuickActions: {
+ type: Boolean,
required: false,
- default: '',
+ default: false,
},
drawioEnabled: {
type: Boolean,
@@ -204,11 +209,9 @@ export default {
markdown: this.latestMarkdown,
});
},
- },
- i18n: {
- quickActionsText: s__(
- 'ContentEditor|For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.',
- ),
+ handleEditorModeChanged() {
+ this.$emit('enableMarkdownEditor');
+ },
},
};
</script>
@@ -230,6 +233,7 @@ export default {
>
<formatting-toolbar
ref="toolbar"
+ :supports-quick-actions="supportsQuickActions"
:hide-attachment-button="disableAttachments"
@enableMarkdownEditor="$emit('enableMarkdownEditor')"
/>
@@ -249,21 +253,16 @@ export default {
<reference-bubble-menu />
</div>
<div
- v-if="quickActionsDocsPath"
- class="gl-display-flex gl-align-items-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-4 gl-mx-2 gl-mb-2 gl-bg-gray-10 gl-text-secondary"
+ class="gl-display-flex gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-2 gl-mx-2 gl-mb-2 gl-bg-gray-10 gl-text-secondary"
>
- <div class="gl-w-full gl-line-height-32 gl-font-sm">
- <gl-sprintf :message="$options.i18n.quickActionsText">
- <template #keyboard="{ content }">
- <kbd>{{ content }}</kbd>
- </template>
- <template #quickActionsDocsLink="{ content }">
- <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
+ <editor-mode-switcher size="small" value="richText" @input="handleEditorModeChanged" />
+ <gl-button
+ icon="markdown-mark"
+ :href="markdownDocsPath"
+ target="_blank"
+ category="tertiary"
+ size="small"
+ />
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index c53007b68cf..8bf71a481bd 100644
--- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -1,5 +1,4 @@
<script>
-import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
import trackUIControl from '../services/track_ui_control';
import ToolbarButton from './toolbar_button.vue';
import ToolbarAttachmentButton from './toolbar_attachment_button.vue';
@@ -14,9 +13,13 @@ export default {
ToolbarTableButton,
ToolbarAttachmentButton,
ToolbarMoreDropdown,
- EditorModeSwitcher,
},
props: {
+ supportsQuickActions: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
hideAttachmentButton: {
type: Boolean,
default: false,
@@ -27,9 +30,6 @@ export default {
trackToolbarControlExecution({ contentType, value }) {
trackUIControl({ property: contentType, value });
},
- handleEditorModeChanged() {
- this.$emit('enableMarkdownEditor');
- },
},
};
</script>
@@ -125,11 +125,19 @@ export default {
data-testid="attachment"
@execute="trackToolbarControlExecution"
/>
+ <!-- TODO Add icon and trigger functionality from here -->
+ <toolbar-button
+ v-if="supportsQuickActions"
+ data-testid="quick-actions"
+ content-type="quickAction"
+ icon-name="quick-actions"
+ class="gl-display-none gl-sm-display-inline gl-mr-1!"
+ editor-command="insertQuickAction"
+ :label="__('Add a quick action')"
+ @execute="trackToolbarControlExecution"
+ />
<toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" />
</div>
- <div class="content-editor-switcher gl-display-flex gl-align-items-center gl-ml-auto">
- <editor-mode-switcher size="small" value="richText" @input="handleEditorModeChanged" />
- </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
index 1e13c17bc38..4cf150dd948 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
@@ -47,6 +47,7 @@ export default {
category="tertiary"
icon="paperclip"
size="small"
+ class="gl-mr-3"
lazy
@click="openFileUpload"
/>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
index eb7985f628a..771455dfe66 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
@@ -88,10 +88,11 @@ export default {
size="small"
category="tertiary"
icon="table"
+ no-caret
:aria-label="__('Insert table')"
:toggle-text="__('Insert table')"
positioning-strategy="fixed"
- class="content-editor-table-dropdown"
+ class="content-editor-table-dropdown gl-mr-3"
text-sr-only
:fluid-width="true"
@shown="setFocus(1, 1)"
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
index ef69b9bbda6..fd248709b5a 100644
--- a/app/assets/javascripts/content_editor/extensions/reference.js
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -63,6 +63,12 @@ export default Node.create({
};
},
+ addCommands() {
+ return {
+ insertQuickAction: () => ({ commands }) => commands.insertContent('<p>/</p>'),
+ };
+ },
+
addInputRules() {
const { editor } = this;
const { assetResolver } = this.options;
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index e0bfa1111e8..8493787f075 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -59,6 +59,7 @@ export default {
return {
loading: false,
query: '',
+ originalInput: '',
users: [],
selectedTokens: [],
hasBeenFocused: false,
@@ -67,9 +68,9 @@ export default {
},
computed: {
emailIsValid() {
- const regex = /.+@/;
+ const regex = /^\S+@\S+$/;
- return this.query.match(regex) !== null;
+ return this.originalInput.match(regex) !== null;
},
placeholderText() {
if (this.selectedTokens.length === 0) {
@@ -116,6 +117,7 @@ export default {
methods: {
handleTextInput(inputQuery) {
this.hideDropdownWithNoItems = false;
+ this.originalInput = inputQuery;
this.query = inputQuery.trim();
this.loading = true;
this.retrieveUsers();
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index c8ea8fb7ab2..3969c3b84a7 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -1,7 +1,6 @@
<script>
import { __ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { helpPagePath } from '~/helpers/help_page_helper';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateMixin from '../../mixins/update';
@@ -46,11 +45,6 @@ export default {
},
};
},
- computed: {
- quickActionsDocsPath() {
- return helpPagePath('user/project/quick_actions');
- },
- },
mounted() {
this.focus();
},
@@ -72,7 +66,6 @@ export default {
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:form-field-props="formFieldProps"
- :quick-actions-docs-path="quickActionsDocsPath"
:enable-autocomplete="enableAutocomplete"
supports-quick-actions
autofocus
@@ -85,7 +78,7 @@ export default {
class="gl-mt-3"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
+ supports-quick-actions
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
:textarea-value="value"
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index a2873622682..5fa903568a1 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -376,8 +376,8 @@ export function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select
textArea = $textArea.get(0);
const text = $textArea.val();
const selected = selectedText(text, textArea) || tagContent;
- $textArea.focus();
- return insertMarkdownText({
+ textArea.focus();
+ insertMarkdownText({
textArea,
text,
tag,
@@ -387,6 +387,7 @@ export function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select
wrap,
select,
});
+ textArea.click();
}
/**
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index cba0f960c00..27ec8b55da7 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -146,9 +146,6 @@ export default {
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
- quickActionsDocsPath() {
- return this.getNotesData.quickActionsDocsPath;
- },
markdownPreviewPath() {
return this.getNoteableData.preview_note_path;
},
@@ -366,7 +363,6 @@ export default {
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:add-spacing-classes="false"
- :quick-actions-docs-path="quickActionsDocsPath"
:form-field-props="formFieldProps"
:autosave-key="autosaveKey"
:disabled="isSubmitting"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index fe7967f1ed0..45b70d78225 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -192,9 +192,6 @@ export default {
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
},
- quickActionsDocsPath() {
- return this.getNotesDataByProp('quickActionsDocsPath');
- },
currentUserId() {
return this.getUserDataByProp('id');
},
@@ -359,7 +356,6 @@ export default {
:note="discussionNote"
:form-field-props="formFieldProps"
:show-suggest-popover="showSuggestPopover"
- :quick-actions-docs-path="quickActionsDocsPath"
:autosave-key="autosaveKey"
:autocomplete-data-sources="autocompleteDataSources"
:disabled="isSubmitting"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index 186f5619b87..cc9790279cd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -81,13 +81,14 @@ export default {
:items="filteredSavedReplies"
:toggle-text="__('Insert comment template')"
text-sr-only
+ no-caret
toggle-class="js-comment-template-toggle"
icon="comment-lines"
category="tertiary"
placement="right"
searchable
size="small"
- class="comment-template-dropdown"
+ class="comment-template-dropdown gl-mr-3"
positioning-strategy="fixed"
:searching="$apollo.queries.savedReplies.loading"
@shown="fetchCommentTemplates"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
index 645975ca565..5cf5fbd5323 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
@@ -17,16 +17,20 @@ export default {
return this.value === 'markdown';
},
text() {
- return this.markdownEditorSelected ? __('Switch to rich text') : __('Switch to Markdown');
+ return this.markdownEditorSelected
+ ? __('Switch to rich text editing')
+ : __('Switch to plain text editing');
},
},
};
</script>
<template>
- <gl-button
- class="btn btn-default btn-sm gl-button btn-default-tertiary"
- data-qa-selector="editing_mode_switcher"
- @click="$emit('input')"
- >{{ text }}</gl-button
- >
+ <div class="content-editor-switcher gl-display-inline-flex gl-align-items-center">
+ <gl-button
+ class="btn btn-default btn-sm gl-button btn-default-tertiary"
+ data-qa-selector="editing_mode_switcher"
+ @click="$emit('input')"
+ >{{ text }}</gl-button
+ >
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b5a31f42ec7..268352a9c9c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -68,10 +68,10 @@ export default {
required: false,
default: false,
},
- quickActionsDocsPath: {
- type: String,
+ supportsQuickActions: {
+ type: Boolean,
required: false,
- default: '',
+ default: false,
},
canAttachFile: {
type: Boolean,
@@ -371,13 +371,12 @@ export default {
:uploads-path="uploadsPath"
:markdown-preview-path="markdownPreviewPath"
:drawio-enabled="drawioEnabled"
+ :supports-quick-actions="supportsQuickActions"
data-testid="markdownHeader"
:restricted-tool-bar-items="restrictedToolBarItems"
- :show-content-editor-switcher="showContentEditorSwitcher"
@showPreview="showPreview"
@hidePreview="hidePreview"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
- @enableContentEditor="$emit('enableContentEditor')"
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
@@ -391,9 +390,10 @@ export default {
</a>
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
:show-comment-tool-bar="showCommentToolBar"
+ :show-content-editor-switcher="showContentEditorSwitcher"
+ @enableContentEditor="$emit('enableContentEditor')"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index af0b34f1389..bf070943fe6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -19,7 +19,6 @@ import { updateText } from '~/lib/utils/text_markdown';
import ToolbarButton from './toolbar_button.vue';
import DrawioToolbarButton from './drawio_toolbar_button.vue';
import CommentTemplatesDropdown from './comment_templates_dropdown.vue';
-import EditorModeSwitcher from './editor_mode_switcher.vue';
export default {
components: {
@@ -29,7 +28,6 @@ export default {
DrawioToolbarButton,
CommentTemplatesDropdown,
AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'),
- EditorModeSwitcher,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -91,7 +89,7 @@ export default {
required: false,
default: false,
},
- showContentEditorSwitcher: {
+ supportsQuickActions: {
type: Boolean,
required: false,
default: false,
@@ -126,9 +124,6 @@ export default {
const expandText = s__('MarkdownEditor|Click to expand');
return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n');
},
- showEditorModeSwitcher() {
- return this.showContentEditorSwitcher && !this.previewMarkdown;
- },
},
watch: {
showSuggestPopover() {
@@ -208,9 +203,6 @@ export default {
});
}
},
- handleEditorModeChanged() {
- this.$emit('enableContentEditor');
- },
switchPreview() {
if (this.previewMarkdown) {
this.hideMarkdownPreview();
@@ -236,233 +228,228 @@ export default {
<template>
<div class="md-header gl-bg-gray-50 gl-px-2 gl-rounded-base gl-mx-2 gl-mt-2">
- <div
- class="gl-display-flex gl-align-items-center gl-flex-wrap"
- :class="{
- 'gl-justify-content-end': previewMarkdown,
- 'gl-justify-content-space-between': !previewMarkdown,
- }"
- >
+ <div class="gl-display-flex gl-align-items-center gl-flex-wrap">
<div
data-testid="md-header-toolbar"
- class="md-header-toolbar gl-display-flex gl-py-2 gl-flex-wrap"
- :class="{ 'gl-display-none!': previewMarkdown }"
+ class="md-header-toolbar gl-display-flex gl-py-2 gl-flex-wrap gl-row-gap-3"
>
- <template v-if="canSuggest">
- <toolbar-button
- ref="suggestButton"
- :tag="mdSuggestion"
- :prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- icon="doc-code"
- data-qa-selector="suggestion_button"
- class="js-suggestion-btn"
- @click="handleSuggestDismissed"
- />
- <gl-popover
- v-if="suggestPopoverVisible"
- :target="$refs.suggestButton.$el"
- :css-classes="['diff-suggest-popover']"
- placement="bottom"
- :show="suggestPopoverVisible"
- triggers=""
- >
- <strong>{{ __('New! Suggest changes directly') }}</strong>
- <p class="mb-2">
- {{
- __(
- 'Suggest code changes which can be immediately applied in one click. Try it out!',
- )
- }}
- </p>
- <gl-button
- variant="confirm"
- category="primary"
- size="small"
- data-qa-selector="dismiss_suggestion_popover_button"
- @click="handleSuggestDismissed"
- >
- {{ __('Got it') }}
- </gl-button>
- </gl-popover>
- </template>
- <ai-actions-dropdown
- v-if="editorAiActions.length"
- :actions="editorAiActions"
- @input="insertIntoTextarea"
- />
- <toolbar-button
- tag="**"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.bold"
- icon="bold"
- />
- <toolbar-button
- tag="_"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.italic"
- icon="italic"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('strikethrough')"
- tag="~~"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.strikethrough"
- icon="strikethrough"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('quote')"
- :prepend="true"
- :tag="tag"
- :button-title="__('Insert a quote')"
- icon="quote"
- @click="handleQuote"
- />
- <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
- <toolbar-button
- tag="[{text}](url)"
- tag-select="url"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.link"
- icon="link"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('bullet-list')"
- :prepend="true"
- tag="- "
- :button-title="__('Add a bullet list')"
- icon="list-bulleted"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('numbered-list')"
- :prepend="true"
- tag="1. "
- :button-title="__('Add a numbered list')"
- icon="list-numbered"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('task-list')"
- :prepend="true"
- tag="- [ ] "
- :button-title="__('Add a checklist')"
- icon="list-task"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('indent')"
- class="gl-display-none"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.indent"
- command="indentLines"
- icon="list-indent"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('outdent')"
- class="gl-display-none"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.outdent"
- command="outdentLines"
- icon="list-outdent"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('collapsible-section')"
- :tag="mdCollapsibleSection"
- :prepend="true"
- tag-select="Click to expand"
- :button-title="__('Add a collapsible section')"
- icon="details-block"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('table')"
- :tag="mdTable"
- :prepend="true"
- :button-title="__('Add a table')"
- icon="table"
- />
- <gl-button
- v-if="!restrictedToolBarItems.includes('attach-file')"
- v-gl-tooltip
- :aria-label="__('Attach a file or image')"
- :title="__('Attach a file or image')"
- class="gl-mr-2"
- data-testid="button-attach-file"
- category="tertiary"
- icon="paperclip"
- size="small"
- @click="handleAttachFile"
- />
- <drawio-toolbar-button
- v-if="drawioEnabled"
- :uploads-path="uploadsPath"
- :markdown-preview-path="markdownPreviewPath"
- />
- <comment-templates-dropdown
- v-if="newCommentTemplatePath && glFeatures.savedReplies"
- :new-comment-template-path="newCommentTemplatePath"
- />
- </div>
- <div class="switch-preview gl-py-2 gl-display-flex gl-align-items-center gl-ml-auto">
- <editor-mode-switcher
- v-if="showEditorModeSwitcher"
- size="small"
- class="gl-mr-2"
- value="markdown"
- @input="handleEditorModeChanged"
- />
<gl-button
v-if="enablePreview"
data-testid="preview-toggle"
value="preview"
:label="$options.i18n.previewTabTitle"
- class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!"
+ class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2"
size="small"
category="tertiary"
@click="switchPreview"
>{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button
>
- <gl-button
- v-if="!restrictedToolBarItems.includes('full-screen')"
- v-gl-tooltip
- :class="{ 'gl-display-none!': previewMarkdown }"
- class="js-zen-enter gl-ml-2"
- category="tertiary"
- icon="maximize"
- size="small"
- :title="__('Go full screen')"
- :prepend="true"
- :aria-label="__('Go full screen')"
- />
+ <template v-if="!previewMarkdown">
+ <template v-if="canSuggest">
+ <toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
+ :prepend="true"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ icon="doc-code"
+ data-qa-selector="suggestion_button"
+ class="js-suggestion-btn"
+ @click="handleSuggestDismissed"
+ />
+ <gl-popover
+ v-if="suggestPopoverVisible"
+ :target="$refs.suggestButton.$el"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="suggestPopoverVisible"
+ triggers=""
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{
+ __(
+ 'Suggest code changes which can be immediately applied in one click. Try it out!',
+ )
+ }}
+ </p>
+ <gl-button
+ variant="confirm"
+ category="primary"
+ size="small"
+ data-qa-selector="dismiss_suggestion_popover_button"
+ @click="handleSuggestDismissed"
+ >
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </template>
+ <ai-actions-dropdown
+ v-if="editorAiActions.length"
+ :actions="editorAiActions"
+ @input="insertIntoTextarea"
+ />
+ <toolbar-button
+ tag="**"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.bold"
+ icon="bold"
+ />
+ <toolbar-button
+ tag="_"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.italic"
+ icon="italic"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('strikethrough')"
+ tag="~~"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.strikethrough"
+ icon="strikethrough"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('quote')"
+ :prepend="true"
+ :tag="tag"
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ @click="handleQuote"
+ />
+ <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
+ <toolbar-button
+ tag="[{text}](url)"
+ tag-select="url"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.link"
+ icon="link"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('bullet-list')"
+ :prepend="true"
+ tag="- "
+ :button-title="__('Add a bullet list')"
+ icon="list-bulleted"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('numbered-list')"
+ :prepend="true"
+ tag="1. "
+ :button-title="__('Add a numbered list')"
+ icon="list-numbered"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('task-list')"
+ :prepend="true"
+ tag="- [ ] "
+ :button-title="__('Add a checklist')"
+ icon="list-task"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('indent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.indent"
+ command="indentLines"
+ icon="list-indent"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('outdent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.outdent"
+ command="outdentLines"
+ icon="list-outdent"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('collapsible-section')"
+ :tag="mdCollapsibleSection"
+ :prepend="true"
+ tag-select="Click to expand"
+ :button-title="__('Add a collapsible section')"
+ icon="details-block"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('table')"
+ :tag="mdTable"
+ :prepend="true"
+ :button-title="__('Add a table')"
+ icon="table"
+ />
+ <gl-button
+ v-if="!restrictedToolBarItems.includes('attach-file')"
+ v-gl-tooltip
+ :aria-label="__('Attach a file or image')"
+ :title="__('Attach a file or image')"
+ class="gl-mr-3"
+ data-testid="button-attach-file"
+ category="tertiary"
+ icon="paperclip"
+ size="small"
+ @click="handleAttachFile"
+ />
+ <drawio-toolbar-button
+ v-if="drawioEnabled"
+ :uploads-path="uploadsPath"
+ :markdown-preview-path="markdownPreviewPath"
+ />
+ <!-- TODO Add icon and trigger functionality from here -->
+ <toolbar-button
+ v-if="supportsQuickActions"
+ :prepend="true"
+ tag="/"
+ :button-title="__('Add a quick action')"
+ icon="quick-actions"
+ />
+ <comment-templates-dropdown
+ v-if="newCommentTemplatePath && glFeatures.savedReplies"
+ :new-comment-template-path="newCommentTemplatePath"
+ />
+ <div class="full-screen">
+ <gl-button
+ v-if="!restrictedToolBarItems.includes('full-screen')"
+ v-gl-tooltip
+ class="js-zen-enter"
+ category="tertiary"
+ icon="maximize"
+ size="small"
+ :title="__('Go full screen')"
+ :prepend="true"
+ :aria-label="__('Go full screen')"
+ />
+ </div>
+ </template>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 9fd606d775d..dbbcb8a6424 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -80,11 +80,6 @@ export default {
required: false,
default: '',
},
- quickActionsDocsPath: {
- type: String,
- required: false,
- default: '',
- },
drawioEnabled: {
type: Boolean,
required: false,
@@ -245,7 +240,7 @@ export default {
:enable-autocomplete="enableAutocomplete"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
+ :supports-quick-actions="supportsQuickActions"
:show-content-editor-switcher="enableContentEditor"
:drawio-enabled="drawioEnabled"
:restricted-tool-bar-items="markdownFieldRestrictedToolBarItems"
@@ -272,9 +267,10 @@ export default {
<content-editor
ref="contentEditor"
:render-markdown="renderMarkdown"
+ :markdown-docs-path="markdownDocsPath"
:uploads-path="uploadsPath"
:markdown="markdown"
- :quick-actions-docs-path="quickActionsDocsPath"
+ :supports-quick-actions="supportsQuickActions"
:autofocus="contentEditorAutofocused"
:placeholder="formFieldProps.placeholder"
:drawio-enabled="drawioEnabled"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 4733afb7504..9cf0d0bafb1 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,24 +1,20 @@
<script>
-import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
+import EditorModeSwitcher from './editor_mode_switcher.vue';
export default {
components: {
GlButton,
- GlLink,
GlLoadingIcon,
GlSprintf,
GlIcon,
+ EditorModeSwitcher,
},
props: {
markdownDocsPath: {
type: String,
required: true,
},
- quickActionsDocsPath: {
- type: String,
- required: false,
- default: '',
- },
canAttachFile: {
type: Boolean,
required: false,
@@ -29,10 +25,20 @@ export default {
required: false,
default: true,
},
+ showContentEditorSwitcher: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
- hasQuickActionsDocsPath() {
- return this.quickActionsDocsPath !== '';
+ showEditorModeSwitcher() {
+ return this.showContentEditorSwitcher && !this.previewMarkdown;
+ },
+ },
+ methods: {
+ handleEditorModeChanged() {
+ this.$emit('enableContentEditor');
},
},
};
@@ -41,94 +47,76 @@ export default {
<template>
<div
v-if="showCommentToolBar"
- class="comment-toolbar gl-mx-2 gl-mb-2 gl-px-4 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base clearfix"
+ class="comment-toolbar gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-mx-2 gl-mb-2 gl-px-2 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ :class="{ 'gl-bg-gray-10': showContentEditorSwitcher }"
>
- <div class="toolbar-text gl-font-sm">
- <template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
- <gl-sprintf
- :message="
- s__('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')
- "
- >
- <template #markdownDocsLink="{ content }">
- <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </template>
- <template v-if="hasQuickActionsDocsPath && markdownDocsPath">
- <gl-sprintf
- :message="
- s__(
- 'NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.',
- )
- "
- >
- <template #markdownDocsLink="{ content }">
- <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- <template #keyboard="{ content }">
- <kbd>{{ content }}</kbd>
- </template>
- <template #quickActionsDocsLink="{ content }">
- <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </template>
- </div>
- <span v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32">
- <span class="uploading-progress-container hide">
- <gl-icon name="paperclip" />
- <span class="attaching-file-message"></span>
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- <span class="uploading-progress">0%</span>
- <gl-loading-icon size="sm" inline />
- </span>
- <span class="uploading-error-container hide">
- <span class="uploading-error-icon">
+ <editor-mode-switcher
+ v-if="showEditorModeSwitcher"
+ size="small"
+ value="markdown"
+ @input="handleEditorModeChanged"
+ />
+ <div>
+ <div class="toolbar-text gl-font-sm">
+ <template v-if="markdownDocsPath">
+ <gl-button
+ icon="markdown-mark"
+ :href="markdownDocsPath"
+ target="_blank"
+ category="tertiary"
+ size="small"
+ />
+ </template>
+ </div>
+ <span v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32">
+ <span class="uploading-progress-container hide">
<gl-icon name="paperclip" />
+ <span class="attaching-file-message"></span>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <span class="uploading-progress">0%</span>
+ <gl-loading-icon size="sm" inline />
</span>
- <span class="uploading-error-message"></span>
+ <span class="uploading-error-container hide">
+ <span class="uploading-error-icon">
+ <gl-icon name="paperclip" />
+ </span>
+ <span class="uploading-error-message"></span>
- <gl-sprintf
- :message="
- __(
- '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.',
- )
- "
+ <gl-sprintf
+ :message="
+ __(
+ '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.',
+ )
+ "
+ >
+ <template #retryButton="{ content }">
+ <gl-button
+ variant="link"
+ category="primary"
+ class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!"
+ >
+ {{ content }}
+ </gl-button>
+ </template>
+ <template #newFileButton="{ content }">
+ <gl-button
+ variant="link"
+ category="primary"
+ class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!"
+ >
+ {{ content }}
+ </gl-button>
+ </template>
+ </gl-sprintf>
+ </span>
+ <gl-button
+ variant="link"
+ category="primary"
+ class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!"
>
- <template #retryButton="{ content }">
- <gl-button
- variant="link"
- category="primary"
- class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!"
- >
- {{ content }}
- </gl-button>
- </template>
- <template #newFileButton="{ content }">
- <gl-button
- variant="link"
- category="primary"
- class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!"
- >
- {{ content }}
- </gl-button>
- </template>
- </gl-sprintf>
+ {{ __('Cancel') }}
+ </gl-button>
</span>
- <gl-button
- variant="link"
- category="primary"
- class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!"
- >
- {{ __('Cancel') }}
- </gl-button>
- </span>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 7e062776f98..58bf524f450 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -36,8 +36,7 @@ export default {
required: true,
},
},
- markdownDocsPath: helpPagePath('user/project/quick_actions'),
- quickActionsDocsPath: helpPagePath('user/project/quick_actions'),
+ markdownDocsPath: helpPagePath('user/markdown'),
data() {
return {
workItem: {},
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 322363d7f4b..c7976e11f53 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -348,10 +348,6 @@ table {
.toolbar-text {
font-size: 14px;
line-height: $gl-spacing-scale-7;
-
- @include media-breakpoint-up(md) {
- float: left;
- }
}
.note-form-actions {
diff --git a/app/controllers/concerns/redirects_for_missing_path_on_tree.rb b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb
index 92574dfade9..97c23a2cf3c 100644
--- a/app/controllers/concerns/redirects_for_missing_path_on_tree.rb
+++ b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module RedirectsForMissingPathOnTree
- def redirect_to_tree_root_for_missing_path(project, ref, path)
- redirect_to project_tree_path(project, ref), notice: missing_path_on_ref(path, ref)
+ def redirect_to_tree_root_for_missing_path(project, ref, path, ref_type: nil)
+ redirect_to project_tree_path(project, ref, ref_type: ref_type), notice: missing_path_on_ref(path, ref)
end
private
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index c933b05e0c4..196fadb888d 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -129,7 +129,7 @@ class Import::BitbucketController < Import::BaseController
end
def options
- OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].to_h.deep_symbolize_keys
end
def verify_bitbucket_import_enabled
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 28393e1f365..a60cc5301e2 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -160,6 +160,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def check_for_ambiguous_ref
+ return if Feature.enabled?(:redirect_with_ref_type, @project)
+
@ref_type = ref_type
if @ref_type == ExtractsRef::BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
@@ -169,7 +171,17 @@ class Projects::BlobController < Projects::ApplicationController
end
def commit
- @commit ||= @repository.commit(@ref)
+ if Feature.enabled?(:redirect_with_ref_type, @project)
+ response = ::ExtractsRef::RequestedRef.new(@repository, ref_type: ref_type, ref: @ref).find
+ @commit = response[:commit]
+ @ref_type = response[:ref_type]
+
+ if response[:ambiguous]
+ return redirect_to(project_blob_path(@project, File.join(@ref_type ? @ref : @commit.id, @path), ref_type: @ref_type))
+ end
+ else
+ @commit ||= @repository.commit(@ref)
+ end
return render_404 unless @commit
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index c8f698d6193..d2a820d93b0 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -12,6 +12,7 @@ class Projects::TreeController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
+ before_action :find_requested_ref, only: [:show]
before_action :assign_dir_vars, only: [:create_dir]
before_action :authorize_read_code!
before_action :authorize_edit_tree!, only: [:create_dir]
@@ -28,18 +29,20 @@ class Projects::TreeController < Projects::ApplicationController
def show
return render_404 unless @commit
- @ref_type = ref_type
- if @ref_type == BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
- branch = @project.repository.find_branch(@ref)
- if branch
- redirect_to project_tree_path(@project, branch.target)
- return
+ unless Feature.enabled?(:redirect_with_ref_type, @project)
+ @ref_type = ref_type
+ if @ref_type == BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
+ branch = @project.repository.find_branch(@ref)
+ if branch
+ redirect_to project_tree_path(@project, branch.target)
+ return
+ end
end
end
if tree.entries.empty?
if @repository.blob_at(@commit.id, @path)
- redirect_to project_blob_path(@project, File.join(@ref, @path))
+ redirect_to project_blob_path(@project, File.join(@ref, @path), ref_type: @ref_type)
elsif @path.present?
redirect_to_tree_root_for_missing_path(@project, @ref, @path)
end
@@ -59,6 +62,23 @@ class Projects::TreeController < Projects::ApplicationController
private
+ def find_requested_ref
+ return unless Feature.enabled?(:redirect_with_ref_type, @project)
+
+ @ref_type = ref_type
+ if @ref_type.present?
+ @tree = @repo.tree(@ref, @path, ref_type: @ref_type)
+ else
+ response = ExtractsPath::RequestedRef.new(@repository, ref_type: nil, ref: @ref).find
+ @ref_type = response[:ref_type]
+ @commit = response[:commit]
+
+ if response[:ambiguous]
+ redirect_to(project_tree_path(@project, File.join(@ref_type ? @ref : @commit.id, @path), ref_type: @ref_type))
+ end
+ end
+ end
+
def redirect_renamed_default_branch?
action_name == 'show'
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 71a34a40bd0..bc4831d7772 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -173,7 +173,9 @@ class ProjectsController < Projects::ApplicationController
flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name }
end
- if ambiguous_ref?(@project, @ref)
+ if Feature.enabled?(:redirect_with_ref_type, @project)
+ @ref_type = 'heads'
+ elsif ambiguous_ref?(@project, @ref)
branch = @project.repository.find_branch(@ref)
# The files view would render a ref other than the default branch
diff --git a/app/models/ci/external_pull_request.rb b/app/models/ci/external_pull_request.rb
new file mode 100644
index 00000000000..bd37aa9f85a
--- /dev/null
+++ b/app/models/ci/external_pull_request.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+# This model stores pull requests coming from external providers, such as
+# GitHub, when GitLab project is set as CI/CD only and remote mirror.
+#
+# When setting up a remote mirror with GitHub we subscribe to push and
+# pull_request webhook events. When a pull request is opened on GitHub,
+# a webhook is sent out, we create or update the status of the pull
+# request locally.
+#
+# When the mirror is updated and changes are pushed to branches we check
+# if there are open pull requests for the source and target branch.
+# If so, we create pipelines for external pull requests.
+module Ci
+ class ExternalPullRequest < Ci::ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
+ include ShaAttribute
+ include EachBatch
+
+ belongs_to :project
+
+ sha_attribute :source_sha
+ sha_attribute :target_sha
+
+ validates :source_branch, presence: true
+ validates :target_branch, presence: true
+ validates :source_sha, presence: true
+ validates :target_sha, presence: true
+ validates :source_repository, presence: true
+ validates :target_repository, presence: true
+ validates :status, presence: true
+
+ enum status: {
+ open: 1,
+ closed: 2
+ }
+
+ # We currently don't support pull requests from fork, so
+ # we are going to return an error to the webhook
+ validate :not_from_fork
+
+ scope :by_source_branch, ->(branch) { where(source_branch: branch) }
+ scope :by_source_repository, ->(repository) { where(source_repository: repository) }
+
+ # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix
+ self.table_name = 'external_pull_requests'
+
+ def self.create_or_update_from_params(params)
+ find_params = params.slice(:project_id, :source_branch, :target_branch)
+
+ safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request|
+ yield(pull_request) if block_given?
+ end
+ end
+
+ def actual_branch_head?
+ actual_source_branch_sha == source_sha
+ end
+
+ def from_fork?
+ source_repository != target_repository
+ end
+
+ def source_ref
+ Gitlab::Git::BRANCH_REF_PREFIX + source_branch
+ end
+
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch)
+ end
+ end
+
+ def modified_paths
+ project.repository.diff_stats(target_sha, source_sha).paths
+ end
+
+ private
+
+ def actual_source_branch_sha
+ project.commit(source_ref)&.sha
+ end
+
+ def not_from_fork
+ return unless from_fork?
+
+ errors.add(:base, _('Pull requests from fork are not supported'))
+ end
+
+ def self.safe_find_or_initialize_and_update(find:, update:)
+ safe_ensure_unique(retries: 1) do
+ model = find_or_initialize_by(find)
+
+ yield(model) if model.update(update) && block_given?
+
+ model
+ end
+ end
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 21c58e47a06..69e48cadf97 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -51,7 +51,7 @@ module Ci
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline', inverse_of: :auto_canceled_pipelines
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest'
- belongs_to :external_pull_request
+ belongs_to :external_pull_request, class_name: 'Ci::ExternalPullRequest'
belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines
has_internal_id :iid, scope: :project, presence: false,
diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb
deleted file mode 100644
index 94c242782c1..00000000000
--- a/app/models/external_pull_request.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-# frozen_string_literal: true
-
-# This model stores pull requests coming from external providers, such as
-# GitHub, when GitLab project is set as CI/CD only and remote mirror.
-#
-# When setting up a remote mirror with GitHub we subscribe to push and
-# pull_request webhook events. When a pull request is opened on GitHub,
-# a webhook is sent out, we create or update the status of the pull
-# request locally.
-#
-# When the mirror is updated and changes are pushed to branches we check
-# if there are open pull requests for the source and target branch.
-# If so, we create pipelines for external pull requests.
-class ExternalPullRequest < Ci::ApplicationRecord
- include Gitlab::Utils::StrongMemoize
- include ShaAttribute
- include EachBatch
-
- belongs_to :project
-
- sha_attribute :source_sha
- sha_attribute :target_sha
-
- validates :source_branch, presence: true
- validates :target_branch, presence: true
- validates :source_sha, presence: true
- validates :target_sha, presence: true
- validates :source_repository, presence: true
- validates :target_repository, presence: true
- validates :status, presence: true
-
- enum status: {
- open: 1,
- closed: 2
- }
-
- # We currently don't support pull requests from fork, so
- # we are going to return an error to the webhook
- validate :not_from_fork
-
- scope :by_source_branch, ->(branch) { where(source_branch: branch) }
- scope :by_source_repository, -> (repository) { where(source_repository: repository) }
-
- # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix
- self.table_name = 'external_pull_requests'
-
- def self.create_or_update_from_params(params)
- find_params = params.slice(:project_id, :source_branch, :target_branch)
-
- safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request|
- yield(pull_request) if block_given?
- end
- end
-
- def actual_branch_head?
- actual_source_branch_sha == source_sha
- end
-
- def from_fork?
- source_repository != target_repository
- end
-
- def source_ref
- Gitlab::Git::BRANCH_REF_PREFIX + source_branch
- end
-
- def predefined_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch)
- end
- end
-
- def modified_paths
- project.repository.diff_stats(target_sha, source_sha).paths
- end
-
- private
-
- def actual_source_branch_sha
- project.commit(source_ref)&.sha
- end
-
- def not_from_fork
- if from_fork?
- errors.add(:base, _('Pull requests from fork are not supported'))
- end
- end
-
- def self.safe_find_or_initialize_and_update(find:, update:)
- safe_ensure_unique(retries: 1) do
- model = find_or_initialize_by(find)
-
- if model.update(update)
- yield(model) if block_given?
- end
-
- model
- end
- end
-end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index ad82f1b916f..7ba9bbc38e6 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -2,6 +2,23 @@
module Integrations
class HangoutsChat < BaseChatNotification
+ undef :notify_only_broken_pipelines
+
+ field :webhook,
+ section: SECTION_TYPE_CONNECTION,
+ help: 'https://chat.googleapis.com/v1/spaces…',
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION
+
+ field :branches_to_be_notified,
+ type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
def title
'Google Chat'
end
@@ -19,25 +36,15 @@ module Integrations
s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
- def default_channel_placeholder
+ def fields
+ self.class.fields + build_event_channels
end
- def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
+ def default_channel_placeholder
end
- def default_fields
- [
- { type: 'text', name: 'webhook', help: 'https://chat.googleapis.com/v1/spaces…' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- {
- type: 'select',
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices
- }
- ]
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
end
private
diff --git a/app/models/project.rb b/app/models/project.rb
index 23eb58c6020..e4d8830ec48 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -415,7 +415,7 @@ class Project < ApplicationRecord
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
- has_many :external_pull_requests, inverse_of: :project
+ has_many :external_pull_requests, inverse_of: :project, class_name: 'Ci::ExternalPullRequest'
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index a8da83e84a1..fe0e842f542 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -40,22 +40,22 @@ module Ci
# Create a new pipeline in the specified project.
#
- # @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline
- # creation.
- # @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment
- # is present in the commit body
- # @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an
- # error during creation (e.g. invalid yaml)
- # @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation.
- # @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation.
- # @param [MergeRequest] merge_request The merge request triggers the pipeline creation.
- # @param [ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation.
- # @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation.
- # @param [String] content The content of .gitlab-ci.yml to override the default config
- # contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for
- # generating a dangling pipeline.
+ # @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline
+ # creation.
+ # @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment
+ # is present in the commit body
+ # @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an
+ # error during creation (e.g. invalid yaml)
+ # @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation.
+ # @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation.
+ # @param [MergeRequest] merge_request The merge request triggers the pipeline creation.
+ # @param [Ci::ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation.
+ # @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation.
+ # @param [String] content The content of .gitlab-ci.yml to override the default config
+ # contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for
+ # generating a dangling pipeline.
#
- # @return [Ci::Pipeline] The created Ci::Pipeline object.
+ # @return [Ci::Pipeline] The created Ci::Pipeline object.
# rubocop: disable Metrics/ParameterLists
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
@logger = build_logger
diff --git a/app/views/shared/_md_preview.html.haml b/app/views/shared/_md_preview.html.haml
index dd3a31f5a59..e4fc3ebf23c 100644
--- a/app/views/shared/_md_preview.html.haml
+++ b/app/views/shared/_md_preview.html.haml
@@ -1,4 +1,5 @@
- referenced_users = local_assigns.fetch(:referenced_users, nil)
+- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
- if @merge_request&.discussion_locked?
.issuable-note-warning
@@ -10,12 +11,12 @@
.md-area.position-relative
.md-header.gl-bg-gray-50.gl-px-2.gl-rounded-base.gl-mx-2.gl-mt-2
.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-justify-content-space-between
- .md-header-toolbar.gl-display-flex.gl-py-2.gl-flex-wrap
- = render 'shared/blob/markdown_buttons'
- .switch-preview.gl-py-2.gl-display-flex.gl-align-items-center.gl-ml-auto
+ .md-header-toolbar.gl-display-flex.gl-py-2.gl-flex-wrap.gl-row-gap-3
= render Pajamas::ButtonComponent.new(category: :tertiary, size: :small, button_options: { class: 'js-md-preview-button', value: 'preview' }) do
= _('Preview')
- = render Pajamas::ButtonComponent.new(icon: 'maximize', category: :tertiary, size: :small, button_options: { 'tabindex': -1, 'aria-label': _("Go full screen"), class: 'has-tooltip js-zen-enter gl-ml-2', data: { container: 'body' } })
+ = render 'shared/blob/markdown_buttons', supports_quick_actions: supports_quick_actions
+ .full-screen
+ = render Pajamas::ButtonComponent.new(icon: 'maximize', category: :tertiary, size: :small, button_options: { 'tabindex': -1, 'aria-label': _("Go full screen"), class: 'has-tooltip js-zen-enter', data: { container: 'body' } })
.md-write-holder
= yield
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index a3d3c1c8231..16bffaca810 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -1,32 +1,33 @@
- modifier_key = client_js_flags[:isMac] ? '⌘' : s_('KeyboardKey|Ctrl+')
- supports_file_upload = local_assigns.fetch(:supports_file_upload, true)
+- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
= markdown_toolbar_button({ icon: "bold",
- css_class: 'gl-mr-3',
+ css_class: 'haml-markdown-button gl-mr-3',
data: { "md-tag" => "**", "md-shortcuts": '["mod+b"]' },
title: sprintf(s_("MarkdownEditor|Add bold text (%{modifier_key}B)") % { modifier_key: modifier_key }) })
= markdown_toolbar_button({ icon: "italic",
- css_class: 'gl-mr-3',
+ css_class: 'haml-markdown-button gl-mr-3',
data: { "md-tag" => "_", "md-shortcuts": '["mod+i"]' },
title: sprintf(s_("MarkdownEditor|Add italic text (%{modifier_key}I)") % { modifier_key: modifier_key }) })
= markdown_toolbar_button({ icon: "strikethrough",
- css_class: 'gl-mr-3',
+ css_class: 'haml-markdown-button gl-mr-3',
data: { "md-tag" => "~~", "md-shortcuts": '["mod+shift+x"]' },
title: sprintf(s_("MarkdownEditor|Add strikethrough text (%{modifier_key}⇧X)") % { modifier_key: modifier_key }) })
-= markdown_toolbar_button({ icon: "quote", css_class: 'gl-mr-3', data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") })
-= markdown_toolbar_button({ icon: "code", css_class: 'gl-mr-3', data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") })
+= markdown_toolbar_button({ icon: "quote", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") })
+= markdown_toolbar_button({ icon: "code", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") })
= markdown_toolbar_button({ icon: "link",
- css_class: 'gl-mr-3',
+ css_class: 'haml-markdown-button gl-mr-3',
data: { "md-tag" => "[{text}](url)", "md-select" => "url", "md-shortcuts": '["mod+k"]' },
title: sprintf(s_("MarkdownEditor|Add a link (%{modifier_key}K)") % { modifier_key: modifier_key }) })
-= markdown_toolbar_button({ icon: "list-bulleted", css_class: 'gl-mr-3', data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") })
-= markdown_toolbar_button({ icon: "list-numbered", css_class: 'gl-mr-3', data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
-= markdown_toolbar_button({ icon: "list-task", css_class: 'gl-mr-3', data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") })
+= markdown_toolbar_button({ icon: "list-bulleted", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") })
+= markdown_toolbar_button({ icon: "list-numbered", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
+= markdown_toolbar_button({ icon: "list-task", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") })
= markdown_toolbar_button({ icon: "list-indent",
css_class: 'gl-display-none gl-mr-3',
data: { "md-command" => 'indentLines', "md-shortcuts": '["mod+]"]' },
@@ -36,9 +37,11 @@
data: { "md-command" => 'outdentLines', "md-shortcuts": '["mod+["]' },
title: sprintf(s_("MarkdownEditor|Outdent line (%{modifier_key}[)") % { modifier_key: modifier_key }) })
= markdown_toolbar_button({ icon: "details-block",
- css_class: 'gl-mr-3',
+ css_class: 'haml-markdown-button gl-mr-3',
data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" },
title: _("Add a collapsible section") })
-= markdown_toolbar_button({ icon: "table", css_class: 'gl-mr-3', data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| | |\n| | |", "md-prepend" => true }, title: _("Add a table") })
+= markdown_toolbar_button({ icon: "table", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| | |\n| | |", "md-prepend" => true }, title: _("Add a table") })
- if supports_file_upload
- = render Pajamas::ButtonComponent.new(icon: 'paperclip', category: :tertiary, size: :small, button_options: { 'aria-label': _("Attach a file or image"), class: 'has-tooltip js-attach-file-button gl-mr-3', data: { testid: 'button-attach-file', container: 'body' } })
+ = render Pajamas::ButtonComponent.new(icon: 'paperclip', category: :tertiary, size: :small, button_options: { 'aria-label': _("Attach a file or image"), class: 'has-tooltip js-attach-file-button haml-markdown-button gl-mr-3', data: { testid: 'button-attach-file', container: 'body' } })
+- if supports_quick_actions
+ = markdown_toolbar_button({ icon: "quick-actions", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "/", "md-prepend" => true }, title: _("Add a quick action") })
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 98008fede90..6f4b35266c2 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -25,14 +25,14 @@
= f.hidden_field :position
.discussion-form-container.discussion-with-resolve-btn.flex-column.p-0
- = render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true } do
+ = render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true, supports_quick_actions: supports_quick_actions } do
= render 'shared/zen', f: f, qa_selector: 'note_field',
attr: :note,
classes: 'note-textarea js-note-text',
placeholder: _("Write a comment or drag your files here…"),
supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete
- = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
+ = render 'shared/notes/hints'
.error-alert
.note-form-actions.clearfix.gl-display-flex.gl-flex-wrap
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index d7d6e477ab1..9be87b0a095 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -1,13 +1,7 @@
-- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
- supports_file_upload = local_assigns.fetch(:supports_file_upload, true)
-.comment-toolbar.gl-mx-2.gl-mb-2.gl-px-4.gl-bg-gray-10.gl-rounded-bottom-left-base.gl-rounded-bottom-right-base.clearfix
- .toolbar-text.gl-font-sm
- - markdownLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') }
- - quickActionsLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/quick_actions') }
- - if supports_quick_actions
- = html_escape(s_('NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe, quickActionsDocsLinkStart: quickActionsLinkStart, quickActionsDocsLinkEnd: '</a>'.html_safe, keyboardStart: '<kbd>'.html_safe, keyboardEnd: '</kbd>'.html_safe }
- - else
- = html_escape(s_('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe }
+.comment-toolbar.gl-mx-2.gl-mb-2.gl-px-2.gl-display-flex.gl-justify-content-end.gl-rounded-bottom-left-base.gl-rounded-bottom-right-base.clearfix
+ .toolbar-text
+ = render Pajamas::ButtonComponent.new(category: :tertiary, icon: 'markdown-mark', size: :small, href: help_page_path('user/markdown'), target: '_blank')
- if supports_file_upload
%span.uploading-container.gl-line-height-32.gl-font-sm
%span.uploading-progress-container.hide
diff --git a/config/feature_flags/development/jira_deployment_issue_keys.yml b/config/feature_flags/development/jira_deployment_issue_keys.yml
new file mode 100644
index 00000000000..355d789f338
--- /dev/null
+++ b/config/feature_flags/development/jira_deployment_issue_keys.yml
@@ -0,0 +1,8 @@
+---
+name: jira_deployment_issue_keys
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123455
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/415025
+milestone: '16.2'
+type: development
+group: group::import and integrate
+default_enabled: false
diff --git a/config/feature_flags/development/redirect_with_ref_type.yml b/config/feature_flags/development/redirect_with_ref_type.yml
new file mode 100644
index 00000000000..74a4d31eb2f
--- /dev/null
+++ b/config/feature_flags/development/redirect_with_ref_type.yml
@@ -0,0 +1,8 @@
+---
+name: redirect_with_ref_type
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122237
+rollout_issue_url:
+milestone: '16.2'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/doc/development/database/efficient_in_operator_queries.md b/doc/development/database/efficient_in_operator_queries.md
index a770dfe6531..03a1c442255 100644
--- a/doc/development/database/efficient_in_operator_queries.md
+++ b/doc/development/database/efficient_in_operator_queries.md
@@ -672,7 +672,7 @@ end
#### Ordering by `JOIN` columns
Ordering records by mixed columns where one or more columns are coming from `JOIN` tables
-works with limitations. It requires extra configuration (CTE). The trick is to use a
+works with limitations. It requires extra configuration via Common Table Expression (CTE). The trick is to use a
non-materialized CTE to act as a "fake" table which exposes all required columns.
NOTE:
diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md
index 009e620f121..0a780ee61ee 100644
--- a/doc/integration/jira/development_panel.md
+++ b/doc/integration/jira/development_panel.md
@@ -51,9 +51,9 @@ depends on where you mention the Jira issue ID in GitLab.
| GitLab: where you mention the Jira issue ID | Jira development panel: what information is displayed |
|------------------------------------------------|-------------------------------------------------------|
-| Merge request title or description | Link to the merge request<br>Link to the branch ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11) |
-| Branch name | Link to the branch |
-| Commit message | Link to the commit |
+| Merge request title or description | Link to the merge request<br>Link to the deployment<br>Link to the pipeline by title only and by description ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390888) in GitLab 15.10)<br>Link to the branch ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11) |
+| Branch name | Link to the branch<br>Link to the deployment |
+| Commit message | Link to the commit<br>Link to the deployment from up to 5,000 commits after the last successful deployment to the environment ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300031) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `jira_deployment_issue_keys`. Disabled by default) |
| [Jira Smart Commit](#jira-smart-commits) | Custom comment, logged time, or workflow transition |
## Jira Smart Commits
diff --git a/doc/integration/jira/index.md b/doc/integration/jira/index.md
index 2b6395f437b..dbda2e91dee 100644
--- a/doc/integration/jira/index.md
+++ b/doc/integration/jira/index.md
@@ -46,7 +46,7 @@ This table shows the capabilities available with the Jira issue integration and
| [View a list of Jira issues](issues.md#view-jira-issues) | **{check-circle}** Yes | **{dotted-circle}** No |
| [Create a Jira issue for a vulnerability](../../user/application_security/vulnerabilities/index.md#create-a-jira-issue-for-a-vulnerability) | **{check-circle}** Yes | **{dotted-circle}** No |
| Create a GitLab branch from a Jira issue | **{dotted-circle}** No | **{check-circle}** Yes, in the issue's development panel |
-| Mention a Jira issue ID in a GitLab merge request, and deployments are synced | **{dotted-circle}** No | **{check-circle}** Yes, in the issue's development panel |
+| Sync GitLab deployments to Jira issues | **{dotted-circle}** No | **{check-circle}** Yes, in the issue's development panel. Mention a Jira issue ID in a GitLab merge request, branch name, or any of the last 5,000 commits made to the branch after the last successful deployment to the environment |
## Privacy considerations
diff --git a/doc/user/clusters/agent/gitops/flux_tutorial.md b/doc/user/clusters/agent/gitops/flux_tutorial.md
index c6c9ed9e373..d0780f85201 100644
--- a/doc/user/clusters/agent/gitops/flux_tutorial.md
+++ b/doc/user/clusters/agent/gitops/flux_tutorial.md
@@ -9,6 +9,9 @@ info: A tutorial using Flux
This tutorial teaches you how to set up Flux for GitOps. You'll complete a bootstrap installation,
install `agentk` in your cluster, and deploy a simple `nginx` application.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview of an example Flux
+configuration, see [Flux bootstrap and manifest synchronization with GitLab](https://www.youtube.com/watch?v=EjPVRM-N_PQ).
+
To set up Flux for GitOps:
1. [Create a personal access token](#create-a-personal-access-token)
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index 8b1a55bc7bd..1c999e019b2 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -55,7 +55,6 @@ This workflow has a weaker security model and is not recommended for production
## Supported Kubernetes versions for GitLab features
GitLab supports the following Kubernetes versions. If you want to run
-GitLab in a Kubernetes cluster, you might need a different version of Kubernetes.
GitLab in a Kubernetes cluster, you might need [a different version of Kubernetes](https://docs.gitlab.com/charts/installation/cloud/index.html).
You can upgrade your
Kubernetes version to a supported version at any time:
diff --git a/gems/gitlab-rspec/gitlab-rspec.gemspec b/gems/gitlab-rspec/gitlab-rspec.gemspec
index f9cc83bb497..061647190ec 100644
--- a/gems/gitlab-rspec/gitlab-rspec.gemspec
+++ b/gems/gitlab-rspec/gitlab-rspec.gemspec
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
spec.description = "A set of useful helpers to configure RSpec with various stubs and CI configs."
spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-rspec"
spec.license = "MIT"
- spec.required_ruby_version = ">= 2.7"
+ spec.required_ruby_version = ">= 3.0"
spec.files = Dir['lib/**/*.rb']
spec.test_files = Dir['spec/**/*']
diff --git a/lib/atlassian/jira_connect/serializers/build_entity.rb b/lib/atlassian/jira_connect/serializers/build_entity.rb
index b595d0c2a92..31ab4ece8fe 100644
--- a/lib/atlassian/jira_connect/serializers/build_entity.rb
+++ b/lib/atlassian/jira_connect/serializers/build_entity.rb
@@ -22,13 +22,7 @@ module Atlassian
expose :references
def issue_keys
- commit_message_issue_keys = JiraIssueKeyExtractor.new(pipeline.git_commit_message).issue_keys
-
- # extract Jira issue keys from either the source branch/ref or the merge request title.
- @issue_keys ||= commit_message_issue_keys + pipeline.all_merge_requests.flat_map do |mr|
- src = "#{mr.source_branch} #{mr.title} #{mr.description}"
- JiraIssueKeyExtractor.new(src).issue_keys
- end.uniq
+ @issue_keys ||= (pipeline_commit_issue_keys + pipeline_mrs_issue_keys).uniq
end
private
@@ -89,6 +83,18 @@ module Atlassian
def update_sequence_id
options[:update_sequence_id] || Client.generate_update_sequence_id
end
+
+ def pipeline_commit_issue_keys
+ JiraIssueKeyExtractor.new(pipeline.git_commit_message).issue_keys
+ end
+
+ # Extract Jira issue keys from either the source branch/ref, merge request title or merge request description.
+ def pipeline_mrs_issue_keys
+ pipeline.all_merge_requests.flat_map do |mr|
+ src = "#{mr.source_branch} #{mr.title} #{mr.description}"
+ JiraIssueKeyExtractor.new(src).issue_keys
+ end
+ end
end
end
end
diff --git a/lib/atlassian/jira_connect/serializers/deployment_entity.rb b/lib/atlassian/jira_connect/serializers/deployment_entity.rb
index 9ef1666b61c..96e7b1726cb 100644
--- a/lib/atlassian/jira_connect/serializers/deployment_entity.rb
+++ b/lib/atlassian/jira_connect/serializers/deployment_entity.rb
@@ -6,6 +6,8 @@ module Atlassian
class DeploymentEntity < Grape::Entity
include Gitlab::Routing
+ COMMITS_LIMIT = 5_000
+
format_with(:iso8601, &:iso8601)
expose :schema_version, as: :schemaVersion
@@ -22,9 +24,7 @@ module Atlassian
expose :environment_entity, as: :environment
def issue_keys
- return [] unless build&.pipeline.present?
-
- @issue_keys ||= BuildEntity.new(build.pipeline).issue_keys
+ @issue_keys ||= (issue_keys_from_pipeline + issue_keys_from_commits_since_last_deploy).uniq
end
private
@@ -74,7 +74,7 @@ module Atlassian
end
def pipeline_entity
- PipelineEntity.new(build.pipeline) if build&.pipeline.present?
+ PipelineEntity.new(build.pipeline) if pipeline?
end
def environment_entity
@@ -84,6 +84,44 @@ module Atlassian
def update_sequence_id
options[:update_sequence_id] || Client.generate_update_sequence_id
end
+
+ def pipeline?
+ build&.pipeline.present?
+ end
+
+ def issue_keys_from_pipeline
+ return [] unless pipeline?
+
+ BuildEntity.new(build.pipeline).issue_keys
+ end
+
+ # Extract Jira issue keys from commits made to the deployment's branch or tag
+ # since the last successful deployment was made to the environment.
+ def issue_keys_from_commits_since_last_deploy
+ return [] if Feature.disabled?(:jira_deployment_issue_keys, project)
+
+ last_deployed_commit = environment
+ .successful_deployments
+ .id_not_in(deployment.id)
+ .ordered
+ .find_by_ref(deployment.ref)
+ &.commit
+
+ commits = project.repository.commits(
+ deployment.ref,
+ before: deployment.commit.created_at,
+ after: last_deployed_commit&.created_at,
+ skip_merges: true,
+ limit: COMMITS_LIMIT
+ )
+
+ # Include this deploy's commit, as the `before:` param in `Repository#list_commits_by` excluded it.
+ commits << deployment.commit
+
+ commits.flat_map do |commit|
+ JiraIssueKeyExtractor.new(commit.message).issue_keys
+ end.compact
+ end
end
end
end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index 9937236fc44..64550a0525c 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -63,7 +63,7 @@ module Bitbucket
end
def options
- OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].to_h.deep_symbolize_keys
end
end
end
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index 49ec564eb8d..2a48b66bb5c 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -100,7 +100,7 @@ module ExtractsRef
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def tree
- @tree ||= @repo.tree(@commit.id, @path) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @tree ||= @repo.tree(@commit.id, @path, ref_type: ref_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def extract_ref_path
diff --git a/lib/extracts_ref/requested_ref.rb b/lib/extracts_ref/requested_ref.rb
new file mode 100644
index 00000000000..f20018b5ef4
--- /dev/null
+++ b/lib/extracts_ref/requested_ref.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module ExtractsRef
+ class RequestedRef
+ include Gitlab::Utils::StrongMemoize
+
+ SYMBOLIC_REF_PREFIX = %r{((refs/)?(heads|tags)/)+}
+ def initialize(repository, ref_type:, ref:)
+ @ref_type = ref_type
+ @ref = ref
+ @repository = repository
+ end
+
+ attr_reader :repository, :ref_type, :ref
+
+ def find
+ case ref_type
+ when 'tags'
+ { ref_type: ref_type, commit: tag }
+ when 'heads'
+ { ref_type: ref_type, commit: branch }
+ else
+ commit_without_ref_type
+ end
+ end
+
+ private
+
+ def commit_without_ref_type
+ if commit.nil?
+ { ref_type: nil, commit: nil }
+ elsif commit.id == ref
+ # ref is probably complete 40 character sha
+ { ref_type: nil, commit: commit }
+ elsif tag.present?
+ { ref_type: 'tags', commit: tag, ambiguous: branch.present? }
+ elsif branch.present?
+ { ref_type: 'heads', commit: branch }
+ else
+ { ref_type: nil, commit: commit, ambiguous: ref.match?(SYMBOLIC_REF_PREFIX) }
+ end
+ end
+
+ def commit
+ repository.commit(ref)
+ end
+ strong_memoize_attr :commit
+
+ def tag
+ raw_commit = repository.find_tag(ref)&.dereferenced_target
+ ::Commit.new(raw_commit, repository.container) if raw_commit
+ end
+ strong_memoize_attr :tag
+
+ def branch
+ raw_commit = repository.find_branch(ref)&.dereferenced_target
+ ::Commit.new(raw_commit, repository.container) if raw_commit
+ end
+ strong_memoize_attr :branch
+ end
+end
diff --git a/lib/gitlab/ci/project_config/repository.rb b/lib/gitlab/ci/project_config/repository.rb
index 7dfd528fd6f..a08cf27b74c 100644
--- a/lib/gitlab/ci/project_config/repository.rb
+++ b/lib/gitlab/ci/project_config/repository.rb
@@ -4,6 +4,8 @@ module Gitlab
module Ci
class ProjectConfig
class Repository < Source
+ extend ::Gitlab::Utils::Override
+
def content
strong_memoize(:content) do
next unless file_in_repository?
diff --git a/lib/gitlab/ci/project_config/source.rb b/lib/gitlab/ci/project_config/source.rb
index 68853ca8296..5f37c3bad7b 100644
--- a/lib/gitlab/ci/project_config/source.rb
+++ b/lib/gitlab/ci/project_config/source.rb
@@ -5,7 +5,6 @@ module Gitlab
class ProjectConfig
class Source
include Gitlab::Utils::StrongMemoize
- extend ::Gitlab::Utils::Override
def initialize(project, sha, custom_content, pipeline_source, pipeline_source_bridge)
@project = project
diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb
index 8c673acdd1a..fe9e64a15dd 100644
--- a/lib/gitlab/import_export/project/relation_factory.rb
+++ b/lib/gitlab/import_export/project/relation_factory.rb
@@ -16,6 +16,8 @@ module Gitlab
bridges: 'Ci::Bridge',
runners: 'Ci::Runner',
pipeline_metadata: 'Ci::PipelineMetadata',
+ external_pull_request: 'Ci::ExternalPullRequest',
+ external_pull_requests: 'Ci::ExternalPullRequest',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
@@ -61,8 +63,7 @@ module Gitlab
epic
ProjectCiCdSetting
container_expiration_policy
- external_pull_request
- external_pull_requests
+ Ci::ExternalPullRequest
DesignManagement::Design
MergeRequest::DiffCommitUser
MergeRequestDiffCommit
diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb
index 16c3bc09c4d..e1c3b09d371 100644
--- a/lib/gitlab/legacy_github_import/client.rb
+++ b/lib/gitlab/legacy_github_import/client.rb
@@ -116,7 +116,7 @@ module Gitlab
if config
config["args"]["client_options"].deep_symbolize_keys
else
- OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ OmniAuth::Strategies::GitHub.default_options[:client_options].to_h.symbolize_keys
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e2b72fb0123..f754d328647 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2703,6 +2703,9 @@ msgstr ""
msgid "Add a numbered list"
msgstr ""
+msgid "Add a quick action"
+msgstr ""
+
msgid "Add a related epic"
msgstr ""
@@ -12518,9 +12521,6 @@ msgstr ""
msgid "Content parsed with %{link}."
msgstr ""
-msgid "ContentEditor|For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}."
-msgstr ""
-
msgid "ContentEditor|You have to provide a renderMarkdown function or a custom serializer"
msgstr ""
@@ -15134,9 +15134,15 @@ msgstr ""
msgid "Dependencies|Packager"
msgstr ""
+msgid "Dependencies|Projects"
+msgstr ""
+
msgid "Dependencies|Software Bill of Materials (SBOM) based on the %{linkStart}latest successful%{linkEnd} scan"
msgstr ""
+msgid "Dependencies|Software Bill of Materials (SBOM) based on the latest successful scan of each project."
+msgstr ""
+
msgid "Dependencies|The %{codeStartTag}dependency_scanning%{codeEndTag} job has failed and cannot generate the list. Please ensure the job is running properly and run the pipeline again."
msgstr ""
@@ -27865,9 +27871,6 @@ msgstr ""
msgid "MarkdownEditor|header"
msgstr ""
-msgid "MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}"
-msgstr ""
-
msgid "Marked"
msgstr ""
@@ -30935,9 +30938,6 @@ msgstr ""
msgid "NoteForm|Note"
msgstr ""
-msgid "NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}."
-msgstr ""
-
msgid "Notes"
msgstr ""
@@ -45029,10 +45029,10 @@ msgstr ""
msgid "Switch to GitLab Next"
msgstr ""
-msgid "Switch to Markdown"
+msgid "Switch to plain text editing"
msgstr ""
-msgid "Switch to rich text"
+msgid "Switch to rich text editing"
msgstr ""
msgid "Switch to the source to copy the file contents"
diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb
index 684b18a97a7..1c7c0de09fe 100644
--- a/qa/qa/service/praefect_manager.rb
+++ b/qa/qa/service/praefect_manager.rb
@@ -357,58 +357,6 @@ module QA
result.size >= 5
end
- def list_untracked_repositories
- untracked_repositories = []
- shell "docker exec #{@praefect} bash -c 'gitlab-ctl praefect list-untracked-repositories'" do |line|
- # Results look like this
- # The following repositories were found on disk, but missing from the tracking database:
- # {"relative_path":"@hashed/aa/bb.git","storage":"gitaly1","virtual_storage":"default"}
- # {"relative_path":"@hashed/bb/cc.git","storage":"gitaly3","virtual_storage":"default"}
-
- QA::Runtime::Logger.debug(line.chomp)
- untracked_repositories.append(JSON.parse(line))
- rescue JSON::ParserError
- # Ignore lines that can't be parsed as JSON
- end
-
- QA::Runtime::Logger.debug("list_untracked_repositories --- #{untracked_repositories}")
- untracked_repositories
- end
-
- def track_repository_in_praefect(relative_path, storage, virtual_storage)
- cmd = "gitlab-ctl praefect track-repository --repository-relative-path #{relative_path} --authoritative-storage #{storage} --virtual-storage-name #{virtual_storage}"
- shell "docker exec #{@praefect} bash -c '#{cmd}'"
- end
-
- def remove_tracked_praefect_repository(relative_path, virtual_storage)
- cmd = "gitlab-ctl praefect remove-repository --repository-relative-path #{relative_path} --virtual-storage-name #{virtual_storage} --apply"
- shell "docker exec #{@praefect} bash -c '#{cmd}'"
- end
-
- # set_replication_factor assigns or unassigns random storage nodes as necessary to reach the desired replication factor for a repository
- def set_replication_factor(relative_path, virtual_storage, factor)
- cmd = "/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -repository #{relative_path} -virtual-storage #{virtual_storage} -replication-factor #{factor}"
- shell "docker exec #{@praefect} bash -c '#{cmd}'"
- end
-
- # get_replication_storages retrieves a list of currently assigned storages for a repository
- def get_replication_storages(relative_path, virtual_storage)
- storage_repositories = []
- query = "SELECT storage FROM repository_assignments WHERE relative_path='#{relative_path}' AND virtual_storage='#{virtual_storage}';"
- shell(sql_to_docker_exec_cmd(query)) { |line| storage_repositories << line.strip }
- # Returned data from query will be in format
- # storage
- # --------
- # gitaly1
- # gitaly3
- # gitaly2
- # (3 rows)
- #
-
- # remove 2 header rows and last 2 rows from query response (including blank line)
- storage_repositories[2..-3]
- end
-
def modify_repo_access_time(node, repo_path, update_time)
repo = "/var/opt/gitlab/git-data/repositories/#{repo_path}"
shell(%{
@@ -416,65 +364,6 @@ module QA
})
end
- def add_repo_to_disk(node, repo_path)
- cmd = "GIT_DIR=. git init --initial-branch=main /var/opt/gitlab/git-data/repositories/#{repo_path}"
- shell "docker exec --user git #{node} bash -c '#{cmd}'"
- modify_repo_access_time(node, repo_path, "24 hours ago")
- end
-
- def remove_repo_from_disk(repo_path)
- cmd = "rm -rf /var/opt/gitlab/git-data/repositories/#{repo_path}"
- shell "docker exec #{@primary_node} bash -c '#{cmd}'"
- shell "docker exec #{@secondary_node} bash -c '#{cmd}'"
- shell "docker exec #{@tertiary_node} bash -c '#{cmd}'"
- end
-
- def remove_repository_from_praefect_database(relative_path)
- shell sql_to_docker_exec_cmd("delete from repositories where relative_path = '#{relative_path}';")
- shell sql_to_docker_exec_cmd("delete from storage_repositories where relative_path = '#{relative_path}';")
- end
-
- def praefect_database_tracks_repo?(relative_path)
- storage_repositories = []
- shell sql_to_docker_exec_cmd("SELECT count(*) FROM storage_repositories where relative_path='#{relative_path}';") do |line|
- storage_repositories << line
- end
- QA::Runtime::Logger.debug("storage_repositories count is ---#{storage_repositories}")
-
- repositories = []
- shell sql_to_docker_exec_cmd("SELECT count(*) FROM repositories where relative_path='#{relative_path}';") do |line|
- repositories << line
- end
- QA::Runtime::Logger.debug("repositories count is ---#{repositories}")
-
- (storage_repositories[2].to_i >= 1) && (repositories[2].to_i >= 1)
- end
-
- def repository_replicated_to_disk?(node, relative_path)
- Support::Waiter.wait_until(max_duration: 300, sleep_interval: 1, raise_on_failure: false) do
- result = []
- shell sql_to_docker_exec_cmd("SELECT count(*) FROM storage_repositories where relative_path='#{relative_path}';") do |line|
- result << line
- end
- QA::Runtime::Logger.debug("result is ---#{result}")
- result[2].to_i == 3
- end
-
- repository_exists_on_node_disk?(node, relative_path)
- end
-
- def repository_exists_on_node_disk?(node, relative_path)
- # If the dir does not exist it has a non zero exit code leading to a error being raised
- # Instead we echo a test line if the dir does not exist, which has a zero exit code, with no output
- bash_command = "test -d /var/opt/gitlab/git-data/repositories/#{relative_path} || echo -n 'DIR_DOES_NOT_EXIST'"
- result = []
- shell "docker exec #{node} bash -c '#{bash_command}'" do |line|
- result << line
- end
- QA::Runtime::Logger.debug("result is ---#{result}")
- result.exclude?("DIR_DOES_NOT_EXIST")
- end
-
private
def dataloss_command
diff --git a/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb
deleted file mode 100644
index 4f916300ee3..00000000000
--- a/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- RSpec.describe 'Systems' do
- describe 'Praefect repository commands', :orchestrated, :gitaly_cluster, product_group: :gitaly do
- let(:praefect_manager) { Service::PraefectManager.new }
-
- let(:repo1) do
- { "relative_path" => "@hashed/repo1.git", "storage" => "gitaly1", "virtual_storage" => "default" }
- end
-
- let(:repo2) do
- { "relative_path" => "@hashed/path/to/repo2.git", "storage" => "gitaly3", "virtual_storage" => "default" }
- end
-
- before do
- praefect_manager.start_all_nodes
- praefect_manager.add_repo_to_disk(praefect_manager.primary_node, repo1["relative_path"])
- praefect_manager.add_repo_to_disk(praefect_manager.tertiary_node, repo2["relative_path"])
- end
-
- after do
- praefect_manager.remove_repo_from_disk(repo1["relative_path"])
- praefect_manager.remove_repo_from_disk(repo2["relative_path"])
- praefect_manager.remove_repository_from_praefect_database(repo1["relative_path"])
- praefect_manager.remove_repository_from_praefect_database(repo2["relative_path"])
- end
-
- it 'allows admin to manage difference between praefect database and disk state',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347606' do
- # Some repos are on disk that praefect is not aware of
- untracked_repositories = praefect_manager.list_untracked_repositories
- expect(untracked_repositories).to include(repo1)
- expect(untracked_repositories).to include(repo2)
-
- # admin manually adds the first repo to the praefect database
- praefect_manager
- .track_repository_in_praefect(repo1["relative_path"], repo1["storage"], repo1["virtual_storage"])
- untracked_repositories = praefect_manager.list_untracked_repositories
- expect(untracked_repositories).not_to include(repo1)
- expect(untracked_repositories).to include(repo2)
- expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"]))
- .to be true
- expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be true
-
- # admin manually adds the second repo to the praefect database
- praefect_manager
- .track_repository_in_praefect(repo2["relative_path"], repo2["storage"], repo2["virtual_storage"])
- untracked_repositories = praefect_manager.list_untracked_repositories
- expect(untracked_repositories).not_to include(repo2)
- expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo2["relative_path"]))
- .to be true
- expect(praefect_manager.praefect_database_tracks_repo?(repo2["relative_path"])).to be true
-
- # admin ensures replication to other nodes occurs
- expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo1["relative_path"]))
- .to be true
- expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.tertiary_node, repo1["relative_path"]))
- .to be true
- expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.primary_node, repo2["relative_path"]))
- .to be true
- expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo2["relative_path"]))
- .to be true
-
- # admin chooses to remove the first repo completely from praefect and disk
- praefect_manager.remove_tracked_praefect_repository(repo1["relative_path"], repo1["virtual_storage"])
- expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"]))
- .to be false
- expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager
- .secondary_node, repo1["relative_path"])).to be false
- expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo1["relative_path"]))
- .to be false
- expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be false
-
- untracked_repositories = praefect_manager.list_untracked_repositories
- expect(untracked_repositories).not_to include(repo1)
- end
-
- it 'allows admin to control the number of replicas of data',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347566' do
- praefect_manager
- .track_repository_in_praefect(repo1['relative_path'], repo1['storage'], repo1['virtual_storage'])
-
- praefect_manager.set_replication_factor(repo1['relative_path'], repo1['virtual_storage'], 2)
- replication_storages = praefect_manager
- .get_replication_storages(repo1['relative_path'], repo1['virtual_storage'])
- expect(replication_storages).to have_attributes(size: 2)
-
- praefect_manager.set_replication_factor(repo1['relative_path'], repo1['virtual_storage'], 3)
- replication_storages = praefect_manager
- .get_replication_storages(repo1['relative_path'], repo1['virtual_storage'])
- expect(replication_storages).to eq(%w[gitaly1 gitaly2 gitaly3])
- end
- end
- end
-end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 906cc5cb336..34e8e5af1f4 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -31,6 +31,16 @@ RSpec.describe Import::BitbucketController, feature_category: :importers do
let(:external_bitbucket_auth_url) { "http://fake.bitbucket.host/url" }
it "redirects to external auth url" do
+ expected_client_options = {
+ site: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['site'],
+ authorize_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['authorize_url'],
+ token_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['token_url']
+ }
+
+ expect(OAuth2::Client)
+ .to receive(:new)
+ .with(anything, anything, expected_client_options)
+
allow(SecureRandom).to receive(:base64).and_return(random_key)
allow_next_instance_of(OAuth2::Client) do |client|
allow(client).to receive_message_chain(:auth_code, :authorize_url)
@@ -101,7 +111,7 @@ RSpec.describe Import::BitbucketController, feature_category: :importers do
@invalid_repo = double(name: 'mercurialrepo', slug: 'mercurialrepo', owner: 'asd', full_name: 'asd/mercurialrepo', clone_url: 'http://test.host/demo/mercurialrepo.git', 'valid?' => false)
end
- context "when token does not exists" do
+ context "when token does not exist" do
let(:random_key) { "pure_random" }
let(:external_bitbucket_auth_url) { "http://fake.bitbucket.host/url" }
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index b07cb7a228d..49c1935c4a3 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -5,15 +5,21 @@ require 'spec_helper'
RSpec.describe Projects::BlobController, feature_category: :source_code_management do
include ProjectForksHelper
- let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) }
- let(:previous_default_branch) { nil }
+ let_it_be(:project) { create(:project, :public, :repository) }
describe "GET show" do
- let(:params) { { namespace_id: project.namespace, project_id: project, id: id } }
+ let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
+ let(:ref_type) { nil }
let(:request) do
get(:show, params: params)
end
+ let(:redirect_with_ref_type) { true }
+
+ before do
+ stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
+ end
+
render_views
context 'with file path' do
@@ -24,25 +30,43 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
request
end
+ after do
+ project.repository.rm_tag(project.creator, 'ambiguous_ref')
+ project.repository.rm_branch(project.creator, 'ambiguous_ref')
+ end
+
context 'when the ref is ambiguous' do
let(:ref) { 'ambiguous_ref' }
let(:path) { 'README.md' }
let(:id) { "#{ref}/#{path}" }
- let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
- context 'and explicitly requesting a branch' do
- let(:ref_type) { 'heads' }
+ context 'and the redirect_with_ref_type flag is disabled' do
+ let(:redirect_with_ref_type) { false }
+
+ context 'and explicitly requesting a branch' do
+ let(:ref_type) { 'heads' }
+
+ it 'redirects to blob#show with sha for the branch' do
+ expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}"))
+ end
+ end
+
+ context 'and explicitly requesting a tag' do
+ let(:ref_type) { 'tags' }
- it 'redirects to blob#show with sha for the branch' do
- expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}"))
+ it 'responds with success' do
+ expect(response).to be_ok
+ end
end
end
- context 'and explicitly requesting a tag' do
- let(:ref_type) { 'tags' }
+ context 'and the redirect_with_ref_type flag is enabled' do
+ context 'when the ref_type is nil' do
+ let(:ref_type) { nil }
- it 'responds with success' do
- expect(response).to be_ok
+ it 'redirects to the tag' do
+ expect(response).to redirect_to(project_blob_path(project, id, ref_type: 'tags'))
+ end
end
end
end
@@ -68,18 +92,20 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
it { is_expected.to respond_with(:not_found) }
end
- context "renamed default branch, valid file" do
- let(:id) { 'old-default-branch/README.md' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context 'when default branch was renamed' do
+ let_it_be_with_reload(:project) { create(:project, :public, :repository, previous_default_branch: 'old-default-branch') }
- it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") }
- end
+ context "renamed default branch, valid file" do
+ let(:id) { 'old-default-branch/README.md' }
+
+ it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") }
+ end
- context "renamed default branch, invalid file" do
- let(:id) { 'old-default-branch/invalid-path.rb' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context "renamed default branch, invalid file" do
+ let(:id) { 'old-default-branch/invalid-path.rb' }
- it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") }
+ it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") }
+ end
end
context "binary file" do
@@ -100,7 +126,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
let(:id) { 'master/README.md' }
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: id }, format: :json
+ get :show, params: params, format: :json
end
it do
@@ -114,7 +140,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
let(:id) { 'master/README.md' }
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: id, viewer: 'none' }, format: :json
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id, ref_type: 'heads', viewer: 'none' }, format: :json
end
it do
@@ -127,7 +153,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
context 'with tree path' do
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: id }
+ get :show, params: params
controller.instance_variable_set(:@blob, nil)
end
@@ -414,6 +440,10 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
let(:after_delete_path) { project_tree_path(project, 'master/files') }
it 'redirects to the sub directory' do
+ expect_next_instance_of(Files::DeleteService) do |instance|
+ expect(instance).to receive(:execute).and_return({ status: :success })
+ end
+
delete :destroy, params: default_params
expect(response).to redirect_to(after_delete_path)
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 61998d516e8..ffec670e97d 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Projects::TreeController, feature_category: :source_code_management do
- let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) }
- let(:previous_default_branch) { nil }
+ let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:redirect_with_ref_type) { true }
before do
sign_in(user)
@@ -17,10 +17,14 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
describe "GET show" do
let(:params) do
{
- namespace_id: project.namespace.to_param, project_id: project, id: id
+ namespace_id: project.namespace.to_param, project_id: project, id: id, ref_type: ref_type
}
end
+ let(:request) { get :show, params: params }
+
+ let(:ref_type) { nil }
+
# Make sure any errors accessing the tree in our views bubble up to this spec
render_views
@@ -28,26 +32,79 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
- get :show, params: params
+
+ stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
+ end
+
+ after do
+ project.repository.rm_tag(project.creator, 'ambiguous_ref')
+ project.repository.rm_branch(project.creator, 'ambiguous_ref')
end
- context 'when the ref is ambiguous' do
- let(:id) { 'ambiguous_ref' }
- let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
+ context 'when the redirect_with_ref_type flag is disabled' do
+ let(:redirect_with_ref_type) { false }
- context 'and explicitly requesting a branch' do
- let(:ref_type) { 'heads' }
+ context 'when there is a ref and tag with the same name' do
+ let(:id) { 'ambiguous_ref' }
+ let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
- it 'redirects to blob#show with sha for the branch' do
- expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id))
+ context 'and explicitly requesting a branch' do
+ let(:ref_type) { 'heads' }
+
+ it 'redirects to blob#show with sha for the branch' do
+ request
+ expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id))
+ end
+ end
+
+ context 'and explicitly requesting a tag' do
+ let(:ref_type) { 'tags' }
+
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
end
+ end
- context 'and explicitly requesting a tag' do
- let(:ref_type) { 'tags' }
+ describe 'delegating to ExtractsRef::RequestedRef' do
+ context 'when there is a ref and tag with the same name' do
+ let(:id) { 'ambiguous_ref' }
+ let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
- it 'responds with success' do
- expect(response).to be_ok
+ let(:requested_ref_double) { ExtractsRef::RequestedRef.new(project.repository, ref_type: ref_type, ref: id) }
+
+ before do
+ allow(ExtractsRef::RequestedRef).to receive(:new).with(kind_of(Repository), ref_type: ref_type, ref: id).and_return(requested_ref_double)
+ end
+
+ context 'and not specifying a ref_type' do
+ it 'finds the tags and redirects' do
+ expect(requested_ref_double).to receive(:find).and_call_original
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{id}/?ref_type=tags")
+ end
+ end
+
+ context 'and explicitly requesting a branch' do
+ let(:ref_type) { 'heads' }
+
+ it 'finds the branch' do
+ expect(requested_ref_double).not_to receive(:find)
+ request
+ expect(response).to be_ok
+ end
+ end
+
+ context 'and explicitly requesting a tag' do
+ let(:ref_type) { 'tags' }
+
+ it 'finds the tag' do
+ expect(requested_ref_double).not_to receive(:find)
+ request
+ expect(response).to be_ok
+ end
end
end
end
@@ -55,19 +112,26 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
context "valid branch, no path" do
let(:id) { 'master' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
context "valid branch, valid path" do
let(:id) { 'master/encoding/' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
context "valid branch, invalid path" do
let(:id) { 'master/invalid-path/' }
it 'redirects' do
+ request
expect(subject)
.to redirect_to("/#{project.full_path}/-/tree/master")
end
@@ -76,54 +140,91 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
context "invalid branch, valid path" do
let(:id) { 'invalid-branch/encoding/' }
- it { is_expected.to respond_with(:not_found) }
+ it 'responds with not_found' do
+ request
+ expect(subject).to respond_with(:not_found)
+ end
end
- context "renamed default branch, valid file" do
- let(:id) { 'old-default-branch/encoding/' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context 'when default branch was renamed' do
+ let_it_be_with_reload(:project) { create(:project, :repository, previous_default_branch: 'old-default-branch') }
- it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/") }
- end
+ context "and the file is valid" do
+ let(:id) { 'old-default-branch/encoding/' }
+
+ it 'redirects' do
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/")
+ end
+ end
- context "renamed default branch, invalid file" do
- let(:id) { 'old-default-branch/invalid-path/' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context "and the file is invalid" do
+ let(:id) { 'old-default-branch/invalid-path/' }
- it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/") }
+ it 'redirects' do
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/")
+ end
+ end
end
context "valid empty branch, invalid path" do
let(:id) { 'empty-branch/invalid-path/' }
it 'redirects' do
- expect(subject)
- .to redirect_to("/#{project.full_path}/-/tree/empty-branch")
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/empty-branch")
end
end
context "valid empty branch" do
let(:id) { 'empty-branch' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
context "invalid SHA commit ID" do
let(:id) { 'ff39438/.gitignore' }
- it { is_expected.to respond_with(:not_found) }
+ it 'responds with not_found' do
+ request
+ expect(subject).to respond_with(:not_found)
+ end
end
context "valid SHA commit ID" do
let(:id) { '6d39438' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
+
+ context 'and there is a tag with the same name' do
+ before do
+ project.repository.add_tag(project.creator, id, RepoHelpers.sample_commit.id)
+ end
+
+ it 'responds with success' do
+ request
+
+ # This uses the tag
+ # TODO: Should we redirect in this case?
+ expect(response).to be_ok
+ end
+ end
end
context "valid SHA commit ID with path" do
let(:id) { '6d39438/.gitignore' }
- it { expect(response).to have_gitlab_http_status(:found) }
+ it 'responds with found' do
+ request
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
end
@@ -149,7 +250,7 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
before do
get :show, params: {
- namespace_id: project.namespace.to_param, project_id: project, id: id
+ namespace_id: project.namespace.to_param, project_id: project, id: id, ref_type: 'heads'
}
end
@@ -157,7 +258,7 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
let(:id) { 'master/README.md' }
it 'redirects' do
- redirect_url = "/#{project.full_path}/-/blob/master/README.md"
+ redirect_url = "/#{project.full_path}/-/blob/master/README.md?ref_type=heads"
expect(subject).to redirect_to(redirect_url)
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 6adddccfda7..46913cfa649 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -164,107 +164,113 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
end
end
- context 'when there is a tag with the same name as the default branch' do
- let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) }
- let(:tree_with_default_branch) do
- branch = tagged_project.repository.find_branch(tagged_project.default_branch)
- project_tree_path(tagged_project, branch.target)
- end
-
+ context 'when redirect_with_ref_type is disabled' do
before do
- tagged_project.repository.create_file(
- tagged_project.creator,
- 'file_for_tag',
- 'content for file',
- message: "Automatically created file",
- branch_name: 'branch-to-tag'
- )
-
- tagged_project.repository.add_tag(
- tagged_project.creator,
- tagged_project.default_branch, # tag name
- 'branch-to-tag' # target
- )
- end
-
- it 'redirects to tree view for the default branch' do
- get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project }
- expect(response).to redirect_to(tree_with_default_branch)
- end
- end
-
- context 'when the default branch name is ambiguous' do
- let_it_be(:project_with_default_branch) do
- create(:project, :public, :custom_repo, files: ['somefile'])
+ stub_feature_flags(redirect_with_ref_type: false)
end
- shared_examples 'ambiguous ref redirects' do
- let(:project) { project_with_default_branch }
- let(:branch_ref) { "refs/heads/#{ref}" }
- let(:repo) { project.repository }
+ context 'when there is a tag with the same name as the default branch' do
+ let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) }
+ let(:tree_with_default_branch) do
+ branch = tagged_project.repository.find_branch(tagged_project.default_branch)
+ project_tree_path(tagged_project, branch.target)
+ end
before do
- repo.create_branch(branch_ref, 'master')
- repo.change_head(ref)
+ tagged_project.repository.create_file(
+ tagged_project.creator,
+ 'file_for_tag',
+ 'content for file',
+ message: "Automatically created file",
+ branch_name: 'branch-to-tag'
+ )
+
+ tagged_project.repository.add_tag(
+ tagged_project.creator,
+ tagged_project.default_branch, # tag name
+ 'branch-to-tag' # target
+ )
end
- after do
- repo.change_head('master')
- repo.delete_branch(branch_ref)
+ it 'redirects to tree view for the default branch' do
+ get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project }
+ expect(response).to redirect_to(tree_with_default_branch)
end
+ end
- subject do
- get(
- :show,
- params: {
- namespace_id: project.namespace,
- id: project
- }
- )
+ context 'when the default branch name is ambiguous' do
+ let_it_be(:project_with_default_branch) do
+ create(:project, :public, :custom_repo, files: ['somefile'])
end
- context 'when there is no conflicting ref' do
- let(:other_ref) { 'non-existent-ref' }
+ shared_examples 'ambiguous ref redirects' do
+ let(:project) { project_with_default_branch }
+ let(:branch_ref) { "refs/heads/#{ref}" }
+ let(:repo) { project.repository }
- it { is_expected.to have_gitlab_http_status(:ok) }
- end
+ before do
+ repo.create_branch(branch_ref, 'master')
+ repo.change_head(ref)
+ end
+
+ after do
+ repo.change_head('master')
+ repo.delete_branch(branch_ref)
+ end
- context 'and that other ref exists' do
- let(:other_ref) { 'master' }
+ subject do
+ get(
+ :show,
+ params: {
+ namespace_id: project.namespace,
+ id: project
+ }
+ )
+ end
+
+ context 'when there is no conflicting ref' do
+ let(:other_ref) { 'non-existent-ref' }
- let(:project_default_root_tree_path) do
- sha = repo.find_branch(project.default_branch).target
- project_tree_path(project, sha)
+ it { is_expected.to have_gitlab_http_status(:ok) }
end
- it 'redirects to tree view for the default branch' do
- is_expected.to redirect_to(project_default_root_tree_path)
+ context 'and that other ref exists' do
+ let(:other_ref) { 'master' }
+
+ let(:project_default_root_tree_path) do
+ sha = repo.find_branch(project.default_branch).target
+ project_tree_path(project, sha)
+ end
+
+ it 'redirects to tree view for the default branch' do
+ is_expected.to redirect_to(project_default_root_tree_path)
+ end
end
end
- end
- context 'when ref starts with ref/heads/' do
- let(:ref) { "refs/heads/#{other_ref}" }
+ context 'when ref starts with ref/heads/' do
+ let(:ref) { "refs/heads/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
- end
+ include_examples 'ambiguous ref redirects'
+ end
- context 'when ref starts with ref/tags/' do
- let(:ref) { "refs/tags/#{other_ref}" }
+ context 'when ref starts with ref/tags/' do
+ let(:ref) { "refs/tags/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
- end
+ include_examples 'ambiguous ref redirects'
+ end
- context 'when ref starts with heads/' do
- let(:ref) { "heads/#{other_ref}" }
+ context 'when ref starts with heads/' do
+ let(:ref) { "heads/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
- end
+ include_examples 'ambiguous ref redirects'
+ end
- context 'when ref starts with tags/' do
- let(:ref) { "tags/#{other_ref}" }
+ context 'when ref starts with tags/' do
+ let(:ref) { "tags/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
+ include_examples 'ambiguous ref redirects'
+ end
end
end
end
diff --git a/spec/factories/external_pull_requests.rb b/spec/factories/ci/external_pull_requests.rb
index 470814f4360..9a16e400101 100644
--- a/spec/factories/external_pull_requests.rb
+++ b/spec/factories/ci/external_pull_requests.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :external_pull_request do
+ factory :external_pull_request, class: 'Ci::ExternalPullRequest' do
sequence(:pull_request_iid)
project
source_branch { 'feature' }
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index c3ae0279c05..a89edc19cc7 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -136,6 +136,8 @@ FactoryBot.define do
jira_auth_type: evaluator.jira_auth_type,
jira_issue_transition_automatic: evaluator.jira_issue_transition_automatic,
jira_issue_transition_id: evaluator.jira_issue_transition_id,
+ jira_issue_prefix: evaluator.jira_issue_prefix,
+ jira_issue_regex: evaluator.jira_issue_regex,
username: evaluator.username, password: evaluator.password, issues_enabled: evaluator.issues_enabled,
project_key: evaluator.project_key, vulnerabilities_enabled: evaluator.vulnerabilities_enabled,
vulnerabilities_issuetype: evaluator.vulnerabilities_issuetype, deployment_type: evaluator.deployment_type
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index bc20660d2a0..7e54580b085 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -125,7 +125,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
expect(issuable_form).to have_selector(markdown_field_focused_selector)
page.within issuable_form do
- click_button("Switch to rich text")
+ click_button("Switch to rich text editing")
end
expect(issuable_form).not_to have_selector(content_editor_focused_selector)
@@ -137,7 +137,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
expect(issuable_form).to have_selector(content_editor_focused_selector)
page.within issuable_form do
- click_button("Switch to Markdown")
+ click_button("Switch to plain text editing")
end
expect(issuable_form).not_to have_selector(markdown_field_focused_selector)
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index a749821b083..d31777db42e 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -131,16 +131,16 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_
describe 'when previewing a note' do
it 'shows the toolbar buttons when editing a note' do
- page.within('.js-main-target-form') do
- expect(page).to have_css('.md-header-toolbar')
+ page.within('.js-main-target-form .md-header-toolbar') do
+ expect(page).to have_css('button', count: 16)
end
end
it 'hides the toolbar buttons when previewing a note' do
wait_for_requests
click_button("Preview")
- page.within('.js-main-target-form') do
- expect(page).not_to have_css('.md-header-toolbar')
+ page.within('.js-main-target-form .md-header-toolbar') do
+ expect(page).to have_css('button', count: 1)
end
end
end
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 0b8321ba8eb..816c9458201 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -14,6 +14,7 @@ import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vu
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
jest.mock('~/emoji');
@@ -92,19 +93,6 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
});
- it('renders footer containing quick actions help text if quick actions docs path is defined', () => {
- createWrapper({ quickActionsDocsPath: '/foo/bar' });
-
- expect(wrapper.text()).toContain('For quick actions, type /');
- expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/foo/bar');
- });
-
- it('does not render footer containing quick actions help text if quick actions docs path is not defined', () => {
- createWrapper();
-
- expect(findEditorElement().text()).not.toContain('For quick actions, type /');
- });
-
it('displays an attachment button', () => {
createWrapper();
@@ -286,4 +274,10 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(component).exists()).toBe(true);
});
+
+ it('renders an editor mode dropdown', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index 9d835381ff4..0f0198a6425 100644
--- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -6,7 +6,6 @@ import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
-import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
describe('content_editor/components/formatting_toolbar', () => {
let wrapper;
@@ -17,7 +16,6 @@ describe('content_editor/components/formatting_toolbar', () => {
stubs: {
GlTabs,
GlTab,
- EditorModeSwitcher,
},
propsData: props,
});
@@ -69,12 +67,6 @@ describe('content_editor/components/formatting_toolbar', () => {
});
});
- it('renders an editor mode dropdown', () => {
- buildWrapper();
-
- expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true);
- });
-
describe('when attachment button is hidden', () => {
it('does not show the attachment button', () => {
buildWrapper({ hideAttachmentButton: true });
diff --git a/spec/frontend/design_management/components/design_description/description_form_spec.js b/spec/frontend/design_management/components/design_description/description_form_spec.js
index 8c01023b1a8..1eb0e8898d4 100644
--- a/spec/frontend/design_management/components/design_description/description_form_spec.js
+++ b/spec/frontend/design_management/components/design_description/description_form_spec.js
@@ -139,7 +139,6 @@ describe('Design description form', () => {
mockDesign.id,
)}`,
markdownDocsPath: '/help/user/markdown',
- quickActionsDocsPath: '/help/user/project/quick_actions',
});
});
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index ff0313cc49e..925534edd7c 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -143,12 +143,19 @@ describe('MembersTokenSelect', () => {
});
describe('when input text is an email', () => {
- it('allows user defined tokens', async () => {
- tokenSelector.vm.$emit('text-input', 'foo@bar.com');
+ it.each`
+ email | result
+ ${'foo@bar.com'} | ${true}
+ ${'foo@bar.com '} | ${false}
+ ${' foo@bar.com'} | ${false}
+ ${'foo@ba r.com'} | ${false}
+ ${'fo o@bar.com'} | ${false}
+ `(`with token creation validation on $email`, async ({ email, result }) => {
+ tokenSelector.vm.$emit('text-input', email);
await nextTick();
- expect(tokenSelector.props('allowUserDefinedTokens')).toBe(true);
+ expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result);
});
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index c7116f380a1..36074e07055 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -16,7 +16,6 @@ describe('Description field component', () => {
propsData: {
markdownPreviewPath: '/',
markdownDocsPath: '/',
- quickActionsDocsPath: '/',
value: description,
},
provide: {
@@ -80,7 +79,6 @@ describe('Description field component', () => {
renderMarkdownPath: '/',
autofocus: true,
supportsQuickActions: true,
- quickActionsDocsPath: expect.any(String),
markdownDocsPath: '/',
enableAutocomplete: true,
});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 0179d2afa9e..703f17cc77a 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -294,13 +294,13 @@ describe('issue_comment_form component', () => {
it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } });
- expect(wrapper.text()).not.toContain('Switch to rich text');
+ expect(wrapper.text()).not.toContain('Switch to rich text editing');
});
it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } });
- expect(wrapper.text()).toContain('Switch to rich text');
+ expect(wrapper.text()).toContain('Switch to rich text editing');
});
describe('textarea', () => {
@@ -346,15 +346,7 @@ describe('issue_comment_form component', () => {
const { markdownDocsPath } = notesDataMock;
- expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text()).toBe('Markdown');
- });
-
- it('should link to quick actions docs', () => {
- mountComponent({ mountFunction: mount });
-
- const { quickActionsDocsPath } = notesDataMock;
-
- expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text()).toBe('quick actions');
+ expect(wrapper.find(`[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
it('should resize textarea after note discarded', async () => {
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index b5b33607282..149f7ad3485 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -66,13 +66,13 @@ describe('issue_note_form component', () => {
it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
createComponentWrapper({}, { contentEditorOnIssues: false });
- expect(wrapper.text()).not.toContain('Switch to rich text');
+ expect(wrapper.text()).not.toContain('Switch to rich text editing');
});
it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
createComponentWrapper({}, { contentEditorOnIssues: true });
- expect(wrapper.text()).toContain('Switch to rich text');
+ expect(wrapper.text()).toContain('Switch to rich text editing');
});
describe('conflicts editing', () => {
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 0f70b264326..0b38f7ffcb7 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -250,15 +250,7 @@ describe('note_app', () => {
it('should render markdown docs url', () => {
const { markdownDocsPath } = mockData.notesDataMock;
- expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text().trim()).toEqual('Markdown');
- });
-
- it('should render quick action docs url', () => {
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual(
- 'quick actions',
- );
+ expect(wrapper.find(`a[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
});
@@ -274,19 +266,7 @@ describe('note_app', () => {
const { markdownDocsPath } = mockData.notesDataMock;
await nextTick();
- expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).text().trim()).toEqual(
- 'Markdown',
- );
- });
-
- it('should render quick actions docs url', async () => {
- wrapper.find('.js-note-edit').trigger('click');
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- await nextTick();
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual(
- 'quick actions',
- );
+ expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 9f2b91cb7fd..ec3d028db1a 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -83,7 +83,6 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<markdown-toolbar-stub
canattachfile="true"
markdowndocspath="help/"
- quickactionsdocspath=""
showcommenttoolbar="true"
/>
</div>
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
index 693353ed604..f8d5faf317c 100644
--- a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
@@ -17,8 +17,8 @@ describe('vue_shared/component/markdown/editor_mode_switcher', () => {
describe.each`
modeText | value | buttonText
- ${'Rich text'} | ${'richText'} | ${'Switch to Markdown'}
- ${'Markdown'} | ${'markdown'} | ${'Switch to rich text'}
+ ${'Rich text'} | ${'richText'} | ${'Switch to plain text editing'}
+ ${'Markdown'} | ${'markdown'} | ${'Switch to rich text editing'}
`('when $modeText', ({ modeText, value, buttonText }) => {
beforeEach(() => {
createComponent({ value });
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index b29f0d58d77..37d18455bf2 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -65,6 +65,7 @@ describe('Markdown field component', () => {
enablePreview,
restrictedToolBarItems,
showContentEditorSwitcher,
+ supportsQuickActions: true,
},
},
);
@@ -206,12 +207,12 @@ describe('Markdown field component', () => {
expect(findMarkdownToolbar().props()).toEqual({
canAttachFile: true,
markdownDocsPath,
- quickActionsDocsPath: '',
showCommentToolBar: true,
+ showContentEditorSwitcher: false,
});
expect(findMarkdownHeader().props()).toMatchObject({
- showContentEditorSwitcher: false,
+ supportsQuickActions: true,
});
});
});
@@ -368,13 +369,13 @@ describe('Markdown field component', () => {
it('defaults to false', () => {
createSubject();
- expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(false);
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false);
});
it('passes showContentEditorSwitcher', () => {
createSubject({ showContentEditorSwitcher: true });
- expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(true);
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 48fe5452e74..0d973bb9afc 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -1,11 +1,10 @@
import $ from 'jquery';
import { nextTick } from 'vue';
-import { GlToggle } from '@gitlab/ui';
+import { GlToggle, GlButton } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
describe('Markdown field header component', () => {
let wrapper;
@@ -100,7 +99,8 @@ describe('Markdown field header component', () => {
it('hides toolbar in preview mode', () => {
createWrapper({ previewMarkdown: true });
- expect(findToolbar().classes().includes('gl-display-none!')).toBe(true);
+ // only one button is rendered in preview mode
+ expect(findToolbar().findAllComponents(GlButton)).toHaveLength(1);
});
it('emits toggle markdown event when clicking preview toggle', async () => {
@@ -205,18 +205,4 @@ describe('Markdown field header component', () => {
});
});
});
-
- describe('with content editor switcher', () => {
- beforeEach(() => {
- createWrapper({
- showContentEditorSwitcher: true,
- });
- });
-
- it('re-emits event from switcher', () => {
- wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText');
-
- expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index e54e261b8e4..2119bd1cd27 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -29,7 +29,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const value = 'test markdown';
const renderMarkdownPath = '/api/markdown';
const markdownDocsPath = '/help/markdown';
- const quickActionsDocsPath = '/help/quickactions';
const enableAutocomplete = true;
const enablePreview = false;
const formFieldId = 'markdown_field';
@@ -43,7 +42,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
value,
renderMarkdownPath,
markdownDocsPath,
- quickActionsDocsPath,
enableAutocomplete,
autocompleteDataSources,
enablePreview,
@@ -110,7 +108,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findMarkdownField().props()).toMatchObject({
autocompleteDataSources,
markdownPreviewPath: renderMarkdownPath,
- quickActionsDocsPath,
+ supportsQuickActions: true,
canAttachFile: true,
enableAutocomplete,
textareaValue: value,
@@ -145,13 +143,13 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('enables content editor switcher when contentEditorEnabled prop is true', () => {
buildWrapper({ propsData: { enableContentEditor: true } });
- expect(findMarkdownField().text()).toContain('Switch to rich text');
+ expect(findMarkdownField().text()).toContain('Switch to rich text editing');
});
it('hides content editor switcher when contentEditorEnabled prop is false', () => {
buildWrapper({ propsData: { enableContentEditor: false } });
- expect(findMarkdownField().text()).not.toContain('Switch to rich text');
+ expect(findMarkdownField().text()).not.toContain('Switch to rich text editing');
});
it('passes down any additional props to markdown field component', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index 2489421b697..28dc6fcde74 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -1,10 +1,11 @@
import { mount } from '@vue/test-utils';
import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
describe('toolbar', () => {
let wrapper;
- const createMountedWrapper = (props = {}) => {
+ const createWrapper = (props = {}) => {
wrapper = mount(Toolbar, {
propsData: { markdownDocsPath: '', ...props },
});
@@ -12,7 +13,7 @@ describe('toolbar', () => {
describe('user can attach file', () => {
beforeEach(() => {
- createMountedWrapper();
+ createWrapper();
});
it('should render uploading-container', () => {
@@ -22,7 +23,7 @@ describe('toolbar', () => {
describe('user cannot attach file', () => {
beforeEach(() => {
- createMountedWrapper({ canAttachFile: false });
+ createWrapper({ canAttachFile: false });
});
it('should not render uploading-container', () => {
@@ -32,15 +33,29 @@ describe('toolbar', () => {
describe('comment tool bar settings', () => {
it('does not show comment tool bar div', () => {
- createMountedWrapper({ showCommentToolBar: false });
+ createWrapper({ showCommentToolBar: false });
expect(wrapper.find('.comment-toolbar').exists()).toBe(false);
});
it('shows comment tool bar by default', () => {
- createMountedWrapper();
+ createWrapper();
expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
});
});
+
+ describe('with content editor switcher', () => {
+ beforeEach(() => {
+ createWrapper({
+ showContentEditorSwitcher: true,
+ });
+ });
+
+ it('re-emits event from switcher', () => {
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText');
+
+ expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 2b3fdd1b31c..8b9963b2476 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -91,7 +91,6 @@ describe('WorkItemDescription', () => {
expect(findMarkdownEditor().props()).toMatchObject({
supportsQuickActions: true,
renderMarkdownPath: markdownPreviewPath(fullPath, iid),
- quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
autocompleteDataSources: autocompleteDataSources(fullPath, iid),
});
});
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index 66ae3658a92..f7597579e7a 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -214,13 +214,7 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d
end
describe '#store_deploy_info' do
- let_it_be(:environment) { create(:environment, name: 'DEV', project: project) }
- let_it_be(:deployments) do
- pipelines.map do |p|
- build = create(:ci_build, environment: environment.name, pipeline: p, project: project)
- create(:deployment, deployable: build, environment: environment)
- end
- end
+ let_it_be(:deployments) { create_list(:deployment, 1) }
let(:schema) do
Atlassian::Schemata.deploy_info_payload
@@ -252,18 +246,22 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d
subject.send(:store_deploy_info, project: project, deployments: deployments)
end
- it 'only sends information about relevant MRs' do
+ it 'calls the API if issue keys are found' do
expect(subject).to receive(:post).with(
- '/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 8) }
+ '/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 1) }
).and_call_original
subject.send(:store_deploy_info, project: project, deployments: deployments)
end
- it 'does not call the API if there is nothing to report' do
+ it 'does not call the API if no issue keys are found' do
+ allow_next_instances_of(Atlassian::JiraConnect::Serializers::DeploymentEntity, nil) do |entity|
+ allow(entity).to receive(:issue_keys).and_return([])
+ end
+
expect(subject).not_to receive(:post)
- subject.send(:store_deploy_info, project: project, deployments: deployments.take(1))
+ subject.send(:store_deploy_info, project: project, deployments: deployments)
end
context 'when there are errors' do
diff --git a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
index 523b7ddaa09..57e0b67e9e6 100644
--- a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
@@ -6,18 +6,16 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create_default(:project, :repository) }
let_it_be(:environment) { create(:environment, name: 'prod', project: project) }
- let_it_be_with_reload(:deployment) { create(:deployment, environment: environment) }
+ let_it_be_with_refind(:deployment) { create(:deployment, environment: environment) }
subject { described_class.represent(deployment) }
- context 'when the deployment does not belong to any Jira issue' do
- describe '#issue_keys' do
- it 'is empty' do
- expect(subject.issue_keys).to be_empty
+ describe '#to_json' do
+ context 'when the deployment does not belong to any Jira issue' do
+ before do
+ allow(subject).to receive(:issue_keys).and_return([])
end
- end
- describe '#to_json' do
it 'can encode the object' do
expect(subject.to_json).to be_valid_json
end
@@ -26,9 +24,19 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
expect(subject.to_json).not_to match_schema(Atlassian::Schemata.deployment_info)
end
end
+
+ context 'when the deployment belongs to Jira issue' do
+ before do
+ allow(subject).to receive(:issue_keys).and_return(['JIRA-1'])
+ end
+
+ it 'is valid according to the deployment info schema' do
+ expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info)
+ end
+ end
end
- context 'this is an external deployment' do
+ context 'when deployment is an external deployment' do
before do
deployment.update!(deployable: nil)
end
@@ -36,10 +44,6 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
it 'does not raise errors when serializing' do
expect { subject.to_json }.not_to raise_error
end
-
- it 'returns an empty list of issue keys' do
- expect(subject.issue_keys).to be_empty
- end
end
describe 'environment type' do
@@ -62,27 +66,137 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
end
end
- context 'when the deployment can be linked to a Jira issue' do
- let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
-
+ describe '#issue_keys' do
+ # For these tests, use a Jira issue key regex that matches a set of commit messages
+ # in the test repo.
+ #
+ # Relevant commits in this test from https://gitlab.com/gitlab-org/gitlab-test/-/commits/master:
+ #
+ # 1) 5f923865dde3436854e9ceb9cdb7815618d4e849 GitLab currently doesn't support patches [...]: add a commit here
+ # 2) 4cd80ccab63c82b4bad16faa5193fbd2aa06df40 add directory structure for tree_helper spec
+ # 3) ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added
+ # 4) 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added
before do
- subject.deployable.update!(pipeline: pipeline)
+ allow(Gitlab::Regex).to receive(:jira_issue_key_regex).and_return(/add.[a-d]/)
+ end
+
+ let(:expected_issue_keys) { ['add a', 'add d', 'added'] }
+
+ it 'extracts issue keys from the commits' do
+ expect(subject.issue_keys).to contain_exactly(*expected_issue_keys)
+ end
+
+ it 'limits the number of commits scanned' do
+ stub_const("#{described_class}::COMMITS_LIMIT", 10)
+
+ expect(subject.issue_keys).to contain_exactly('add a')
+ end
+
+ context 'when `jira_deployment_issue_keys` flag is disabled' do
+ before do
+ stub_feature_flags(jira_deployment_issue_keys: false)
+ end
+
+ it 'does not extract issue keys from commits' do
+ expect(subject.issue_keys).to be_empty
+ end
+ end
+
+ context 'when deploy happened at an older commit' do
+ before do
+ # SHA is from a commit between 1) and 2) in the commit list above.
+ deployment.update!(sha: 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd')
+ end
+
+ it 'extracts only issue keys from that commit or older' do
+ expect(subject.issue_keys).to contain_exactly('add d', 'added')
+ end
end
- %i[jira_branch jira_title jira_description].each do |trait|
- context "because it belongs to an MR with a #{trait}" do
- let(:merge_request) { create(:merge_request, trait) }
+ context 'when the deployment has an associated merge request' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline,
+ merge_request: create(:merge_request,
+ title: 'Title addxa',
+ description: "Description\naddxa\naddya",
+ source_branch: 'feature/addza'
+ )
+ )
+ end
+
+ before do
+ subject.deployable.update!(pipeline: pipeline)
+ end
+
+ it 'includes issue keys extracted from the merge request' do
+ expect(subject.issue_keys).to contain_exactly(
+ *(expected_issue_keys + %w[addxa addya addza])
+ )
+ end
+ end
+
+ context 'when there was a successful deploy to the environment' do
+ let_it_be_with_reload(:last_deploy) do
+ # SHA is from a commit between 2) and 3) in the commit list above.
+ sha = '5937ac0a7beb003549fc5fd26fc247adbce4a52e'
+ create(:deployment, :success, sha: sha, environment: environment, finished_at: 1.hour.ago)
+ end
+
+ shared_examples 'extracts only issue keys from commits made since that deployment' do
+ specify do
+ expect(subject.issue_keys).to contain_exactly('add a', 'add d')
+ end
+ end
+
+ shared_examples 'ignores that deployment' do
+ specify do
+ expect(subject.issue_keys).to contain_exactly(*expected_issue_keys)
+ end
+ end
+
+ it_behaves_like 'extracts only issue keys from commits made since that deployment'
+
+ context 'when the deploy was for a different environment' do
+ before do
+ last_deploy.update!(environment: create(:environment))
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
+
+ context 'when the deploy was for a different branch or tag' do
+ before do
+ last_deploy.update!(ref: 'foo')
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
+
+ context 'when the deploy was not successful' do
+ before do
+ last_deploy.drop!
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
+
+ context 'when the deploy commit cannot be found' do
+ before do
+ last_deploy.update!(sha: 'foo')
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
- describe '#issue_keys' do
- it 'is not empty' do
- expect(subject.issue_keys).not_to be_empty
- end
+ context 'when there is a more recent deployment' do
+ let_it_be(:more_recent_last_deploy) do
+ # SHA is from a commit between 1) and 2) in the commit list above.
+ sha = 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd'
+ create(:deployment, :success, sha: sha, environment: environment, finished_at: 1.minute.ago)
end
- describe '#to_json' do
- it 'is valid according to the deployment info schema' do
- expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info)
- end
+ it 'extracts only issue keys from commits made since that deployment' do
+ expect(subject.issue_keys).to contain_exactly('add a')
end
end
end
diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb
index 58a05c52b9f..2b35a37558c 100644
--- a/spec/lib/bitbucket/connection_spec.rb
+++ b/spec/lib/bitbucket/connection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Bitbucket::Connection do
+RSpec.describe Bitbucket::Connection, feature_category: :integrations do
let(:token) { 'token' }
before do
@@ -13,6 +13,16 @@ RSpec.describe Bitbucket::Connection do
describe '#get' do
it 'calls OAuth2::AccessToken::get' do
+ expected_client_options = {
+ site: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['site'],
+ authorize_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['authorize_url'],
+ token_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['token_url']
+ }
+
+ expect(OAuth2::Client)
+ .to receive(:new)
+ .with(anything, anything, expected_client_options)
+
expect_next_instance_of(OAuth2::AccessToken) do |instance|
expect(instance).to receive(:get).and_return(double(parsed: true))
end
diff --git a/spec/lib/extracts_ref/requested_ref_spec.rb b/spec/lib/extracts_ref/requested_ref_spec.rb
new file mode 100644
index 00000000000..80d3575b360
--- /dev/null
+++ b/spec/lib/extracts_ref/requested_ref_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExtractsRef::RequestedRef, feature_category: :source_code_management do
+ describe '#find' do
+ subject { described_class.new(project.repository, ref_type: ref_type, ref: ref).find }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let(:ref_type) { nil }
+
+ # Create branches and tags consistently with the same shas to make comparison easier to follow
+ let(:tag_sha) { RepoHelpers.sample_commit.id }
+ let(:branch_sha) { RepoHelpers.another_sample_commit.id }
+
+ shared_context 'when a branch exists' do
+ before do
+ project.repository.create_branch(branch_name, branch_sha)
+ end
+
+ after do
+ project.repository.rm_branch(project.owner, branch_name)
+ end
+ end
+
+ shared_context 'when a tag exists' do
+ before do
+ project.repository.add_tag(project.owner, tag_name, tag_sha)
+ end
+
+ after do
+ project.repository.rm_tag(project.owner, tag_name)
+ end
+ end
+
+ shared_examples 'RequestedRef when ref_type is specified' do |branch_sha, tag_sha|
+ context 'when ref_type is heads' do
+ let(:ref_type) { 'heads' }
+
+ it 'returns the branch commit' do
+ expect(subject[:ref_type]).to eq('heads')
+ expect(subject[:commit].id).to eq(branch_sha)
+ end
+ end
+
+ context 'when ref_type is tags' do
+ let(:ref_type) { 'tags' }
+
+ it 'returns the tag commit' do
+ expect(subject[:ref_type]).to eq('tags')
+ expect(subject[:commit].id).to eq(tag_sha)
+ end
+ end
+ end
+
+ context 'when the ref is the sha for a commit' do
+ let(:ref) { branch_sha }
+
+ context 'and a tag and branch with that sha as a name' do
+ include_context 'when a branch exists' do
+ let(:branch_name) { ref }
+ end
+
+ include_context 'when a tag exists' do
+ let(:tag_name) { ref }
+ end
+
+ it_behaves_like 'RequestedRef when ref_type is specified',
+ RepoHelpers.another_sample_commit.id,
+ RepoHelpers.sample_commit.id
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit].id).to eq(ref)
+ end
+ end
+ end
+
+ context 'when ref is for a tag' do
+ include_context 'when a tag exists' do
+ let(:tag_name) { SecureRandom.uuid }
+ end
+
+ let(:ref) { tag_name }
+
+ it 'returns the tag commit' do
+ expect(subject[:ref_type]).to eq('tags')
+ expect(subject[:commit].id).to eq(tag_sha)
+ end
+
+ context 'and there is a branch with the same name' do
+ include_context 'when a branch exists' do
+ let(:branch_name) { ref }
+ end
+
+ it_behaves_like 'RequestedRef when ref_type is specified',
+ RepoHelpers.another_sample_commit.id,
+ RepoHelpers.sample_commit.id
+
+ it 'returns the tag commit' do
+ expect(subject[:ref_type]).to eq('tags')
+ expect(subject[:commit].id).to eq(tag_sha)
+ expect(subject[:ambiguous]).to be_truthy
+ end
+ end
+ end
+
+ context 'when ref is only for a branch' do
+ let(:ref) { SecureRandom.uuid }
+
+ include_context 'when a branch exists' do
+ let(:branch_name) { ref }
+ end
+
+ it 'returns the branch commit' do
+ expect(subject[:ref_type]).to eq('heads')
+ expect(subject[:commit].id).to eq(branch_sha)
+ end
+ end
+
+ context 'when ref is an abbreviated commit sha' do
+ let(:ref) { branch_sha.first(8) }
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit].id).to eq(branch_sha)
+ end
+ end
+
+ context 'when ref does not exist' do
+ let(:ref) { SecureRandom.uuid }
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit]).to be_nil
+ end
+ end
+
+ context 'when ref is symbolic' do
+ let(:ref) { "heads/#{branch_name}" }
+
+ include_context 'when a branch exists' do
+ let(:branch_name) { SecureRandom.uuid }
+ end
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit].id).to eq(branch_sha)
+ expect(subject[:ambiguous]).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index abdd8741377..e1825342ebd 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -905,7 +905,7 @@ List:
- max_issue_count
- max_issue_weight
- limit_metric
-ExternalPullRequest:
+Ci::ExternalPullRequest:
- id
- created_at
- updated_at
diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb
index d0f63d11469..757bdd8dd6c 100644
--- a/spec/lib/gitlab/legacy_github_import/client_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::LegacyGithubImport::Client do
+RSpec.describe Gitlab::LegacyGithubImport::Client, feature_category: :importers do
let(:token) { '123456' }
let(:github_provider) { GitlabSettings::Options.build('app_id' => 'asd123', 'app_secret' => 'asd123', 'name' => 'github', 'args' => { 'client_options' => {} }) }
let(:wait_for_rate_limit_reset) { true }
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::LegacyGithubImport::Client do
end
describe '#api_endpoint' do
- context 'when provider does not specity an API endpoint' do
+ context 'when provider does not specify an API endpoint' do
it 'uses GitHub root API endpoint' do
expect(client.api.api_endpoint).to eq 'https://api.github.com/'
end
diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/ci/external_pull_request_spec.rb
index 10136dd0bdb..2a273146626 100644
--- a/spec/models/external_pull_request_spec.rb
+++ b/spec/models/ci/external_pull_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ExternalPullRequest do
+RSpec.describe Ci::ExternalPullRequest, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let(:source_branch) { 'the-branch' }
@@ -228,12 +228,12 @@ RSpec.describe ExternalPullRequest do
it 'returns modified paths' do
expect(modified_paths).to eq ['bar/branch-test.txt',
- 'files/js/commit.coffee',
- 'with space/README.md']
+ 'files/js/commit.coffee',
+ 'with space/README.md']
end
end
- context 'loose foreign key on external_pull_requests.project_id' do
+ context 'with a loose foreign key on external_pull_requests.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
let!(:model) { create(:external_pull_request, project: parent) }
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 1c0466980f4..76cd5d6c89e 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -219,6 +219,18 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
expect(result[:message][params[:email]]).to eq("Invite email is invalid")
end
end
+
+ context 'with email that has trailing spaces' do
+ let(:params) { { email: ' foo@bar.com ' } }
+
+ it 'returns an error' do
+ expect_not_to_create_members
+ expect(result[:status]).to eq(:error)
+ expect(result[:message][params[:email]]).to eq("Invite email is invalid")
+ end
+
+ it_behaves_like 'does not record an onboarding progress action'
+ end
end
context 'with duplicate invites' do
diff --git a/spec/support/helpers/content_editor_helpers.rb b/spec/support/helpers/content_editor_helpers.rb
index 83c18f8073f..a3ecafbbec6 100644
--- a/spec/support/helpers/content_editor_helpers.rb
+++ b/spec/support/helpers/content_editor_helpers.rb
@@ -2,7 +2,7 @@
module ContentEditorHelpers
def switch_to_content_editor
- click_button("Switch to rich text")
+ click_button("Switch to rich text editing")
end
def type_in_content_editor(keys)
diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb
index 3c88715615d..5cc63fe5c6e 100644
--- a/spec/support/helpers/next_instance_of.rb
+++ b/spec/support/helpers/next_instance_of.rb
@@ -31,8 +31,9 @@ module NextInstanceOf
receive_new.exactly(number).times
end
- target.to receive_new.and_wrap_original do |method, *original_args|
- method.call(*original_args).tap(&blk)
+ target.to receive_new.and_wrap_original do |*original_args, **original_kwargs|
+ method, *original_args = original_args
+ method.call(*original_args, **original_kwargs).tap(&blk)
end
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 3f8e504a296..0c4ae894de7 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -5248,7 +5248,6 @@
- './spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb'
-- './spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb'
diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
index 2eca2a72997..178f85cb85b 100644
--- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
@@ -159,6 +159,40 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
end
end
+ context 'when a user already exists, and private email is used' do
+ it 'fails with an error', :js do
+ visit subentity_members_page_path
+
+ invite_member(user2.email, role: role)
+
+ invite_modal = page.find(invite_modal_selector)
+ expect(invite_modal).to have_content "#{user2.email}: Access level should be greater than or equal to " \
+ "Developer inherited membership from group #{group.name}"
+
+ page.refresh
+
+ page.within find_invited_member_row(user2.name) do
+ expect(page).to have_content('Developer')
+ expect(page).not_to have_button('Developer')
+ end
+ end
+
+ it 'does not allow inviting of an email that has spaces', :js do
+ visit subentity_members_page_path
+
+ click_on _('Invite members')
+
+ page.within invite_modal_selector do
+ choose_options(role, nil)
+ find(member_dropdown_selector).set("#{user2.email} ")
+ wait_for_requests
+
+ expect(page).to have_content('No matches found')
+ expect(page).not_to have_button("#{user2.email} ")
+ end
+ end
+ end
+
context 'when there are multiple users invited with errors' do
let_it_be(:user3) { create(:user) }
diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb
deleted file mode 100644
index ccf1e08b7e7..00000000000
--- a/spec/views/shared/notes/_form.html.haml_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'shared/notes/_form' do
- include Devise::Test::ControllerHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- before do
- project.add_maintainer(user)
- assign(:project, project)
- assign(:note, note)
-
- allow(view).to receive(:current_user).and_return(user)
-
- render
- end
-
- %w[issue merge_request commit].each do |noteable|
- context "with a note on #{noteable}" do
- let(:note) { build(:"note_on_#{noteable}", project: project) }
-
- it 'says that markdown and quick actions are supported' do
- expect(rendered).to have_content('Supports Markdown. For quick actions, type /.')
- end
- end
- end
-end
diff --git a/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml b/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml
index b87a454bccc..fdf9ccdeb55 100644
--- a/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml
+++ b/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml
@@ -50,12 +50,6 @@ integration_reliable:
variables:
JOB_FETCHER: reliable
-integration_basic:
- extends: .integration
- allow_failure: yes
- variables:
- JOB_FETCHER: basic
-
kill_interruption:
stage: test
script: