diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-22 18:09:48 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-22 18:09:48 +0300 |
commit | 640007842a876dfa551578feccfd0fe2307c522a (patch) | |
tree | 4204c45a13b9beac3040df00572ffe0ecdb0ca40 | |
parent | 421f6c92d5984d035a7a6687d70277ba88f5f92b (diff) |
Add latest changes from gitlab-org/gitlab@master
39 files changed, 433 insertions, 157 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 26c7306c880..86f5ef48522 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -5,7 +5,7 @@ review-cleanup: extends: - .default-retry - .review:rules:review-cleanup - image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:gitlab-helm3.5-kubectl1.17 + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:gitlab-gcloud-helm3.5-kubectl1.17 stage: prepare environment: name: review/${CI_COMMIT_REF_SLUG}${FREQUENCY} diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue index f839056daf8..770a5b58276 100644 --- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue @@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { setUrlParams, visitUrl } from '~/lib/utils/url_utility'; import PreviewItem from './preview_item.vue'; import DraftsCount from './drafts_count.vue'; @@ -17,6 +18,7 @@ export default { computed: { ...mapState('diffs', ['viewDiffsFileByFile']), ...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']), + ...mapGetters(['getNoteableData']), }, methods: { ...mapActions('diffs', ['setCurrentFileHash']), @@ -24,12 +26,21 @@ export default { isLast(index) { return index === this.sortedDrafts.length - 1; }, + isOnLatestDiff(draft) { + return draft.position?.head_sha === this.getNoteableData.diff_head_sha; + }, async onClickDraft(draft) { if (this.viewDiffsFileByFile && draft.file_hash) { await this.setCurrentFileHash(draft.file_hash); } - await this.scrollToDraft(draft); + if (draft.position && !this.isOnLatestDiff(draft)) { + const url = new URL(setUrlParams({ commit_id: draft.position.head_sha })); + url.hash = `note_${draft.id}`; + visitUrl(url.toString()); + } else { + await this.scrollToDraft(draft); + } }, }, }; @@ -52,6 +63,7 @@ export default { data-testid="preview-item" @click="onClickDraft(draft)" > + {{ isOnLatestDiff(draft) }} <preview-item :draft="draft" :is-last="isLast(index)" /> </gl-dropdown-item> </gl-dropdown> diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 1ed0cc3130b..eb50ea62d06 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -11,6 +11,8 @@ import { convertToCamelCase, convertToSnakeCase } from './text_utility'; import { isObject } from './type_utility'; import { getLocationHash } from './url_utility'; +export const NO_SCROLL_TO_HASH_CLASS = 'js-no-scroll-to-hash'; + export const getPagePath = (index = 0) => { const { page = '' } = document.body.dataset; return page.split(':')[index]; @@ -68,6 +70,10 @@ export const handleLocationHash = () => { hash = decodeURIComponent(hash); const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`); + + // Allow targets to opt out of scroll behavior + if (target?.classList.contains(NO_SCROLL_TO_HASH_CLASS)) return; + const fixedTabs = document.querySelector('.js-tabs-affix'); const fixedDiffStats = document.querySelector('.js-diff-files-changed'); const fixedNav = document.querySelector('.navbar-gitlab'); diff --git a/app/assets/javascripts/milestones/milestone.js b/app/assets/javascripts/milestones/milestone.js index 05102f73f92..8f2721c2a5b 100644 --- a/app/assets/javascripts/milestones/milestone.js +++ b/app/assets/javascripts/milestones/milestone.js @@ -1,33 +1,27 @@ import createFlash from '~/flash'; import { sanitize } from '~/lib/dompurify'; import axios from '~/lib/utils/axios_utils'; -import { historyPushState } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; -import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs'; +import { GlTabsBehavior, TAB_SHOWN_EVENT, HISTORY_TYPE_HASH } from '~/tabs'; export default class Milestone { constructor() { this.tabsEl = document.querySelector('.js-milestone-tabs'); - this.glTabs = new GlTabsBehavior(this.tabsEl); this.loadedTabs = new WeakSet(); this.bindTabsSwitching(); - this.loadInitialTab(); + // eslint-disable-next-line no-new + new GlTabsBehavior(this.tabsEl, { history: HISTORY_TYPE_HASH }); } bindTabsSwitching() { this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => { const tab = event.target; const { activeTabPanel } = event.detail; - historyPushState(tab.getAttribute('href')); this.loadTab(tab, activeTabPanel); }); } - loadInitialTab() { - const tab = this.tabsEl.querySelector(`a[href="${window.location.hash}"]`); - this.glTabs.activateTab(tab || this.glTabs.activeTab); - } loadTab(tab, tabPanel) { const { endpoint } = tab.dataset; diff --git a/app/assets/javascripts/tabs/constants.js b/app/assets/javascripts/tabs/constants.js index 90c9a89d652..0c227ab7afc 100644 --- a/app/assets/javascripts/tabs/constants.js +++ b/app/assets/javascripts/tabs/constants.js @@ -14,3 +14,6 @@ export const ATTR_ROLE = 'role'; export const ATTR_TABINDEX = 'tabindex'; export const TAB_SHOWN_EVENT = 'gl-tab-shown'; + +export const HISTORY_TYPE_HASH = 'hash'; +export const ALLOWED_HISTORY_TYPES = [HISTORY_TYPE_HASH]; diff --git a/app/assets/javascripts/tabs/index.js b/app/assets/javascripts/tabs/index.js index 44937e593e0..9230b7361a5 100644 --- a/app/assets/javascripts/tabs/index.js +++ b/app/assets/javascripts/tabs/index.js @@ -1,4 +1,5 @@ import { uniqueId } from 'lodash'; +import { historyReplaceState, NO_SCROLL_TO_HASH_CLASS } from '~/lib/utils/common_utils'; import { ACTIVE_TAB_CLASSES, ATTR_ROLE, @@ -12,9 +13,11 @@ import { KEY_CODE_RIGHT, KEY_CODE_DOWN, TAB_SHOWN_EVENT, + HISTORY_TYPE_HASH, + ALLOWED_HISTORY_TYPES, } from './constants'; -export { TAB_SHOWN_EVENT }; +export { TAB_SHOWN_EVENT, HISTORY_TYPE_HASH }; /** * The `GlTabsBehavior` class adds interactivity to tabs created by the `gl_tabs_nav` and @@ -88,9 +91,13 @@ export class GlTabsBehavior { /** * Create a GlTabsBehavior instance. * - * @param {HTMLElement} el The element created by the Rails `gl_tabs_nav` helper. + * @param {HTMLElement} el - The element created by the Rails `gl_tabs_nav` helper. + * @param {Object} [options] + * @param {'hash' | null} [options.history=null] - Sets the type of routing GlTabs will use when navigating between tabs. + * 'hash': Updates the URL hash with the current tab ID. + * null: No routing mechanism will be used. */ - constructor(el) { + constructor(el, { history = null } = {}) { if (!el) { throw new Error('Cannot instantiate GlTabsBehavior without an element'); } @@ -100,8 +107,11 @@ export class GlTabsBehavior { this.tabs = this.getTabs(); this.activeTab = null; + this.history = ALLOWED_HISTORY_TYPES.includes(history) ? history : null; + this.setAccessibilityAttrs(); this.bindEvents(); + if (this.history === HISTORY_TYPE_HASH) this.loadInitialTab(); } setAccessibilityAttrs() { @@ -128,6 +138,7 @@ export class GlTabsBehavior { tab.setAttribute(ATTR_ARIA_CONTROLS, tabPanel.id); } + tabPanel.classList.add(NO_SCROLL_TO_HASH_CLASS); tabPanel.setAttribute(ATTR_ROLE, 'tabpanel'); tabPanel.setAttribute(ATTR_ARIA_LABELLEDBY, tab.id); }); @@ -164,6 +175,11 @@ export class GlTabsBehavior { }); } + loadInitialTab() { + const tab = this.tabList.querySelector(`a[href="${CSS.escape(window.location.hash)}"]`); + this.activateTab(tab || this.activeTab); + } + activatePreviousTab() { const currentTabIndex = this.tabs.indexOf(this.activeTab); @@ -216,6 +232,7 @@ export class GlTabsBehavior { const tabPanel = this.getPanelForTab(tabToActivate); tabPanel.classList.add(ACTIVE_PANEL_CLASS); + if (this.history === HISTORY_TYPE_HASH) historyReplaceState(tabToActivate.getAttribute('href')); this.activeTab = tabToActivate; this.dispatchTabShown(tabToActivate, tabPanel); diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 1f309a19b14..32b3a0e22c2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -248,7 +248,7 @@ export default { labels: this.enableAutocomplete, snippets: this.enableAutocomplete, vulnerabilities: this.enableAutocomplete, - contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete, + contacts: this.enableAutocomplete, }, true, ); diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index f974b16468c..255d77988f2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -41,7 +41,6 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_download_code!, only: [:related_branches] before_action do - push_frontend_feature_flag(:contacts_autocomplete, project&.group) push_frontend_feature_flag(:incident_timeline, project) end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index e46aa6a446c..4ea2512bc67 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -15,7 +15,7 @@ module UsersHelper end def user_email_help_text(user) - return 'We also use email for avatar detection if no avatar is uploaded' unless user.unconfirmed_email.present? + return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present? confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post diff --git a/app/services/bulk_imports/create_pipeline_trackers_service.rb b/app/services/bulk_imports/create_pipeline_trackers_service.rb index 5c9c68e62b5..af97aec09b5 100644 --- a/app/services/bulk_imports/create_pipeline_trackers_service.rb +++ b/app/services/bulk_imports/create_pipeline_trackers_service.rb @@ -47,7 +47,7 @@ module BulkImports end def non_patch_source_version - Gitlab::VersionInfo.new(source_version.major, source_version.minor, 0) + source_version.without_patch end def log_skipped_pipeline(pipeline, minimum_version, maximum_version) diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index edf6d75b632..d4fbbf67420 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -13,6 +13,7 @@ module Issues # in the caller (for example, an issue created via email) and the required arguments to the # SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil. def initialize(project:, current_user: nil, params: {}, spam_params:, build_service: nil) + @extra_params = params.delete(:extra_params) || {} super(project: project, current_user: current_user, params: params) @spam_params = spam_params @build_service = build_service || BuildService.new(project: project, current_user: current_user, params: params) @@ -56,7 +57,7 @@ module Issues handle_add_related_issue(issue) resolve_discussions_with_issue(issue) create_escalation_status(issue) - try_to_associate_contact(issue) + try_to_associate_contacts(issue) super end @@ -85,7 +86,7 @@ module Issues private - attr_reader :spam_params + attr_reader :spam_params, :extra_params def create_escalation_status(issue) ::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute if issue.supports_escalation? @@ -101,11 +102,14 @@ module Issues IssueLinks::CreateService.new(issue, issue.author, { target_issuable: @add_related_issue }).execute end - def try_to_associate_contact(issue) + def try_to_associate_contacts(issue) return unless issue.external_author return unless current_user.can?(:set_issue_crm_contacts, issue) - set_crm_contacts(issue, [issue.external_author]) + contacts = [issue.external_author] + contacts.concat extra_params[:cc] unless extra_params[:cc].nil? + + set_crm_contacts(issue, contacts) end end end diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml index ad0780e869c..3e3de5d60e0 100644 --- a/app/views/groups/settings/_general.html.haml +++ b/app/views/groups/settings/_general.html.haml @@ -1,6 +1,6 @@ = form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-settings-form' }, authenticity_token: true do |f| %input{ type: 'hidden', name: 'update_section', value: 'js-general-settings' } - = form_errors(@group) + = form_errors(@group, pajamas_alert: true) %fieldset .row diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml index 4ab40ff2659..54e51e07c86 100644 --- a/app/views/notify/_reassigned_issuable_email.html.haml +++ b/app/views/notify/_reassigned_issuable_email.html.haml @@ -1,10 +1,7 @@ +- to_names = content_tag(:strong, issuable.assignees.any? ? sanitize_name(issuable.assignee_list) : s_('Unassigned')) + %p - Assignee changed - if previous_assignees.any? - from - %strong= sanitize_name(previous_assignees.map(&:name).to_sentence) - to - - if issuable.assignees.any? - %strong= sanitize_name(issuable.assignee_list) + = html_escape(s_('Notify|Assignee changed from %{fromNames} to %{toNames}').html_safe % { fromNames: content_tag(:strong, sanitize_name(previous_assignees.map(&:name).to_sentence)), toNames: to_names }) - else - %strong Unassigned + = html_escape(s_('Notify|Assignee changed to %{toNames}').html_safe % { toNames: to_names}) diff --git a/app/views/notify/_relabeled_issuable_email.html.haml b/app/views/notify/_relabeled_issuable_email.html.haml index 80a0de255be..41d3a63845f 100644 --- a/app/views/notify/_relabeled_issuable_email.html.haml +++ b/app/views/notify/_relabeled_issuable_email.html.haml @@ -1,3 +1,2 @@ %p - #{'Label'.pluralize(@label_names.size)} added: - %em= @label_names.to_sentence + = html_escape(n_('Label added: %{labels}', 'Labels added: %{labels}', @label_names.size).html_safe % { labels: content_tag(:em, @label_names.to_sentence).html_safe }) diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml index 457d6690a78..0ca9acba2de 100644 --- a/app/views/profiles/_email_settings.html.haml +++ b/app/views/profiles/_email_settings.html.haml @@ -22,12 +22,12 @@ { include_blank: s_("Profiles|Do not show on profile") }, { class: 'gl-form-select custom-select', disabled: email_change_disabled } %small.form-text.text-gl-muted - = s_("Profiles|This email will be displayed on your public profile") + = s_("Profiles|This email will be displayed on your public profile.") .form-group.gl-form-group - commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank') - commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url } - - commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe } + - commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more.%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe } = form.label :commit_email, s_('Profiles|Commit email') .gl-md-form-input-lg = form.select :commit_email, diff --git a/app/views/profiles/_name.html.haml b/app/views/profiles/_name.html.haml index 5af4fe24d62..d798eab7635 100644 --- a/app/views/profiles/_name.html.haml +++ b/app/views/profiles/_name.html.haml @@ -2,8 +2,8 @@ - if user.read_only_attribute?(:name) = form.text_field :name, class: 'gl-form-input form-control', required: true, readonly: true %small.form-text.text-gl-muted - = s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) } + = s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) } - else = form.text_field :name, class: 'gl-form-input form-control', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead") %small.form-text.text-gl-muted - = s_("Profiles|Enter your name, so people you know can recognize you") + = s_("Profiles|Enter your name, so people you know can recognize you.") diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 9c467bf5485..343a6a39b24 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -74,7 +74,7 @@ .form-group.gl-form-group = status_form.gitlab_ui_checkbox_component :availability, s_("Profiles|Busy"), - help_text: s_('Profiles|An indicator appears next to your name and avatar'), + help_text: s_('Profiles|An indicator appears next to your name and avatar.'), checkbox_options: { data: { testid: "user-availability-checkbox" } }, checked_value: availability["busy"], unchecked_value: availability["not_set"] @@ -83,7 +83,7 @@ .row.user-time-preferences.js-search-settings-section .col-lg-4.profile-settings-sidebar %h4.gl-mt-0= s_("Profiles|Time settings") - %p= s_("Profiles|Set your local time zone") + %p= s_("Profiles|Set your local time zone.") .col-lg-8 %h5= _("Time zone") = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg gl-w-full!', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) @@ -95,7 +95,7 @@ %h4.gl-mt-0 = s_("Profiles|Main settings") %p - = s_("Profiles|This information will appear on your profile") + = s_("Profiles|This information will appear on your profile.") - if current_user.ldap_user? = s_("Profiles|Some options are unavailable for LDAP accounts") .col-lg-8 @@ -109,12 +109,12 @@ = f.label :pronouns, s_('Profiles|Pronouns') = f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg' %small.form-text.text-gl-muted - = s_("Profiles|Enter your pronouns to let people know how to refer to you") + = s_("Profiles|Enter your pronouns to let people know how to refer to you.") .form-group.gl-form-group = f.label :pronunciation, s_('Profiles|Pronunciation') = f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg' %small.form-text.text-gl-muted - = s_("Profiles|Enter how your name is pronounced to help people address you correctly") + = s_("Profiles|Enter how your name is pronounced to help people address you correctly.") = render_if_exists 'profiles/extra_settings', form: f = render_if_exists 'profiles/email_settings', form: f .form-group.gl-form-group @@ -146,17 +146,17 @@ = f.label :organization, s_('Profiles|Organization') = f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg' %small.form-text.text-gl-muted - = s_("Profiles|Who you represent or work for") + = s_("Profiles|Who you represent or work for.") .form-group.gl-form-group = f.label :bio, s_('Profiles|Bio') = f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250 %small.form-text.text-gl-muted - = s_("Profiles|Tell us about yourself in fewer than 250 characters") + = s_("Profiles|Tell us about yourself in fewer than 250 characters.") %hr %fieldset.form-group.gl-form-group %legend.col-form-label.col-form-label = _('Private profile') - - private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile") + - private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.") - private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private') = f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe } %fieldset.form-group.gl-form-group @@ -164,7 +164,7 @@ = s_("Profiles|Private contributions") = f.gitlab_ui_checkbox_component :include_private_contributions, s_('Profiles|Include private contributions on my profile'), - help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information") + help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.") %hr = f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn' = link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel' diff --git a/config/feature_flags/development/contacts_autocomplete.yml b/config/feature_flags/development/contacts_autocomplete.yml deleted file mode 100644 index 2b0908e7343..00000000000 --- a/config/feature_flags/development/contacts_autocomplete.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: contacts_autocomplete -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79639 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352123 -milestone: '14.8' -type: development -group: group::product planning -default_enabled: true diff --git a/config/routes/project.rb b/config/routes/project.rb index afc06780471..ce642ecf9f6 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -221,20 +221,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - # Legacy routes for `/-/integrations` which are now in `/-/settings/integrations`. - # Can be removed in 15.2, see https://gitlab.com/gitlab-org/gitlab/-/issues/334846 - resources :integrations, controller: 'settings/integrations', constraints: { id: %r{[^/]+} }, only: [:edit, :update] do - member do - put :test - end - - resources :hook_logs, only: [:show], controller: 'settings/integration_hook_logs' do - member do - post :retry - end - end - end - resources :boards, only: [:index, :show, :create, :update, :destroy], constraints: { id: /\d+/ } do collection do get :recent diff --git a/doc/development/documentation/restful_api_styleguide.md b/doc/development/documentation/restful_api_styleguide.md index 1f270a2b5ee..b083f68e52d 100644 --- a/doc/development/documentation/restful_api_styleguide.md +++ b/doc/development/documentation/restful_api_styleguide.md @@ -66,7 +66,8 @@ Supported attributes: | `attribute` | datatype | **{dotted-circle}** No | Detailed description. | | `attribute` | datatype | **{dotted-circle}** No | Detailed description. | -Response body attributes: +If successful, returns [`<status_code>`](../../api/index.md#status-codes) and the following +response attributes: | Attribute | Type | Description | |:-------------------------|:---------|:----------------------| @@ -151,6 +152,14 @@ For information about writing attribute descriptions, see the [GraphQL API descr ## Response body description +Start the description with the following sentence, replacing `status code` with the +relevant [HTTP status code](../../api/index.md#status-codes), for example: + +```markdown +If successful, returns [`200 OK`](../../api/index.md#status-codes) and the +following response attributes: +``` + Use the following table headers to describe the response bodies. Attributes should always be in code blocks using backticks (`` ` ``). diff --git a/doc/user/crm/index.md b/doc/user/crm/index.md index b5287816052..04bb3e582b1 100644 --- a/doc/user/crm/index.md +++ b/doc/user/crm/index.md @@ -118,6 +118,9 @@ organizations using the GraphQL API. ## Issues +If you use [Service Desk](../project/service_desk.md) and create issues from emails, +issues are linked to contacts matching the email addresses in the sender and CC of the email. + ### View issues linked to a contact To view a contact's issues, select a contact from the issue sidebar, or: @@ -170,10 +173,7 @@ API. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `contacts_autocomplete`. Disabled by default. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) in GitLab 15.0. - -FLAG: -On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `contacts_autocomplete`. -On GitLab.com, this feature is available. +> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) in GitLab 15.2. [Feature flag `contacts_autocomplete`](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) removed. When you use the `/add_contacts` or `/remove_contacts` quick actions, follow them with `[contact:` and an autocomplete list appears: diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb index 077eabdd131..a995f308c2b 100644 --- a/lib/backup/gitaly_backup.rb +++ b/lib/backup/gitaly_backup.rb @@ -96,8 +96,8 @@ module Backup def build_env { - 'SSL_CERT_FILE' => OpenSSL::X509::DEFAULT_CERT_FILE, - 'SSL_CERT_DIR' => OpenSSL::X509::DEFAULT_CERT_DIR + 'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file, + 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir }.merge(ENV) end diff --git a/lib/gitlab/ci/runner_upgrade_check.rb b/lib/gitlab/ci/runner_upgrade_check.rb index 0808290fe5b..f265a40eb8b 100644 --- a/lib/gitlab/ci/runner_upgrade_check.rb +++ b/lib/gitlab/ci/runner_upgrade_check.rb @@ -25,18 +25,16 @@ module Gitlab raise ArgumentError, "'#{orig_runner_version}' is not a valid version" unless runner_version.valid? - gitlab_minor_version = version_without_patch(@gitlab_version) + gitlab_minor_version = @gitlab_version.without_patch available_releases = releases - .reject { |release| release.major > @gitlab_version.major } - .reject do |release| - release_minor_version = version_without_patch(release) + .reject { |release| release.major > @gitlab_version.major } + .reject do |release| + # Do not reject a patch update, even if the runner is ahead of the instance version + next false if release.same_minor_version?(runner_version) - # Do not reject a patch update, even if the runner is ahead of the instance version - next false if version_without_patch(runner_version) == release_minor_version - - release_minor_version > gitlab_minor_version - end + release.without_patch > gitlab_minor_version + end return :recommended if available_releases.any? { |available_rel| patch_update?(available_rel, runner_version) } return :recommended if outside_backport_window?(runner_version, releases) @@ -63,19 +61,15 @@ module Gitlab def outside_backport_window?(runner_version, releases) return false if runner_version >= releases.last # return early if runner version is too new - latest_minor_releases = releases.map { |r| version_without_patch(r) }.uniq { |v| v.to_s } + latest_minor_releases = releases.map(&:without_patch).uniq latest_version_position = latest_minor_releases.count - 1 - runner_version_position = latest_minor_releases.index(version_without_patch(runner_version)) + runner_version_position = latest_minor_releases.index(runner_version.without_patch) return true if runner_version_position.nil? # consider outside if version is too old # https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases latest_version_position - runner_version_position > 2 end - - def version_without_patch(version) - ::Gitlab::VersionInfo.new(version.major, version.minor, 0) - end end end end diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb index 71b1d4ed8f9..8e2c7559bc1 100644 --- a/lib/gitlab/email/handler/service_desk_handler.rb +++ b/lib/gitlab/email/handler/service_desk_handler.rb @@ -98,7 +98,10 @@ module Gitlab title: mail.subject, description: message_including_template, confidential: true, - external_author: from_address + external_author: from_address, + extra_params: { + cc: mail.cc + } }, spam_params: nil ).execute diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb index aa6d5310161..88a5b735d8c 100644 --- a/lib/gitlab/version_info.rb +++ b/lib/gitlab/version_info.rb @@ -52,5 +52,21 @@ module Gitlab def valid? @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 end + + def hash + [self.class, to_s].hash + end + + def eql?(other) + (self <=> other) == 0 + end + + def same_minor_version?(other) + @major == other.major && @minor == other.minor + end + + def without_patch + self.class.new(@major, @minor, 0) + end end end diff --git a/lib/gitlab/x509/certificate.rb b/lib/gitlab/x509/certificate.rb index 752f3c6b004..98688f504eb 100644 --- a/lib/gitlab/x509/certificate.rb +++ b/lib/gitlab/x509/certificate.rb @@ -23,6 +23,18 @@ module Gitlab include ::Gitlab::Utils::StrongMemoize end + def self.default_cert_dir + strong_memoize(:default_cert_dir) do + ENV.fetch('SSL_CERT_DIR', OpenSSL::X509::DEFAULT_CERT_DIR) + end + end + + def self.default_cert_file + strong_memoize(:default_cert_file) do + ENV.fetch('SSL_CERT_FILE', OpenSSL::X509::DEFAULT_CERT_FILE) + end + end + def self.from_strings(key_string, cert_string, ca_certs_string = nil) key = OpenSSL::PKey::RSA.new(key_string) cert = OpenSSL::X509::Certificate.new(cert_string) @@ -39,10 +51,10 @@ module Gitlab # Returns all top-level, readable files in the default CA cert directory def self.ca_certs_paths - cert_paths = Dir["#{OpenSSL::X509::DEFAULT_CERT_DIR}/*"].select do |path| + cert_paths = Dir["#{default_cert_dir}/*"].select do |path| !File.directory?(path) && File.readable?(path) end - cert_paths << OpenSSL::X509::DEFAULT_CERT_FILE if File.exist? OpenSSL::X509::DEFAULT_CERT_FILE + cert_paths << default_cert_file if File.exist? default_cert_file cert_paths end @@ -61,6 +73,11 @@ module Gitlab clear_memoization(:ca_certs_bundle) end + def self.reset_default_cert_paths + clear_memoization(:default_cert_dir) + clear_memoization(:default_cert_file) + end + # Returns an array of OpenSSL::X509::Certificate objects, empty array if none found # # Ruby OpenSSL::X509::Certificate.new will only load the first diff --git a/lib/gitlab/x509/signature.rb b/lib/gitlab/x509/signature.rb index a6761e211fa..8acbfc144e9 100644 --- a/lib/gitlab/x509/signature.rb +++ b/lib/gitlab/x509/signature.rb @@ -59,7 +59,7 @@ module Gitlab if Feature.enabled?(:x509_forced_cert_loading, type: :ops) # Forcibly load the default cert file because the OpenSSL library seemingly ignores it - store.add_file(OpenSSL::X509::DEFAULT_CERT_FILE) if File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) + store.add_file(Gitlab::X509::Certificate.default_cert_file) if File.exist?(Gitlab::X509::Certificate.default_cert_file) # rubocop:disable Layout/LineLength end # valid_signing_time? checks the time attributes already diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4d740b154b5..bcc03be2aaa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22411,6 +22411,11 @@ msgstr "" msgid "Label actions dropdown" msgstr "" +msgid "Label added: %{labels}" +msgid_plural "Labels added: %{labels}" +msgstr[0] "" +msgstr[1] "" + msgid "Label priority" msgstr "" @@ -26188,6 +26193,12 @@ msgstr "" msgid "Notify|%{author_link}'s issue %{issue_reference_link} is due soon." msgstr "" +msgid "Notify|Assignee changed from %{fromNames} to %{toNames}" +msgstr "" + +msgid "Notify|Assignee changed to %{toNames}" +msgstr "" + msgid "Notify|Author: %{author_name}" msgstr "" @@ -29204,7 +29215,7 @@ msgstr "" msgid "Profiles|An error occurred while updating your username, please try again." msgstr "" -msgid "Profiles|An indicator appears next to your name and avatar" +msgid "Profiles|An indicator appears next to your name and avatar." msgstr "" msgid "Profiles|Avatar cropper" @@ -29231,7 +29242,7 @@ msgstr "" msgid "Profiles|Choose file..." msgstr "" -msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information" +msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information." msgstr "" msgid "Profiles|City, country" @@ -29273,7 +29284,7 @@ msgstr "" msgid "Profiles|Do not show on profile" msgstr "" -msgid "Profiles|Don't display activity-related personal information on your profile" +msgid "Profiles|Don't display activity-related personal information on your profile." msgstr "" msgid "Profiles|Edit Profile" @@ -29282,16 +29293,16 @@ msgstr "" msgid "Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place." msgstr "" -msgid "Profiles|Enter how your name is pronounced to help people address you correctly" +msgid "Profiles|Enter how your name is pronounced to help people address you correctly." msgstr "" -msgid "Profiles|Enter your name, so people you know can recognize you" +msgid "Profiles|Enter your name, so people you know can recognize you." msgstr "" msgid "Profiles|Enter your password to confirm the email change" msgstr "" -msgid "Profiles|Enter your pronouns to let people know how to refer to you" +msgid "Profiles|Enter your pronouns to let people know how to refer to you." msgstr "" msgid "Profiles|Example: MacBook key" @@ -29414,7 +29425,7 @@ msgstr "" msgid "Profiles|Set new profile picture" msgstr "" -msgid "Profiles|Set your local time zone" +msgid "Profiles|Set your local time zone." msgstr "" msgid "Profiles|Social sign-in" @@ -29426,7 +29437,7 @@ msgstr "" msgid "Profiles|Static object token was successfully reset" msgstr "" -msgid "Profiles|Tell us about yourself in fewer than 250 characters" +msgid "Profiles|Tell us about yourself in fewer than 250 characters." msgstr "" msgid "Profiles|The ability to update your name has been disabled by your administrator." @@ -29435,16 +29446,16 @@ msgstr "" msgid "Profiles|The maximum file size allowed is 200KB." msgstr "" -msgid "Profiles|This email will be displayed on your public profile" +msgid "Profiles|This email will be displayed on your public profile." msgstr "" -msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}" +msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more.%{commit_email_link_end}" msgstr "" msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." msgstr "" -msgid "Profiles|This information will appear on your profile" +msgid "Profiles|This information will appear on your profile." msgstr "" msgid "Profiles|Time settings" @@ -29489,7 +29500,7 @@ msgstr "" msgid "Profiles|What's your status?" msgstr "" -msgid "Profiles|Who you represent or work for" +msgid "Profiles|Who you represent or work for." msgstr "" msgid "Profiles|You can change your avatar here" @@ -29531,6 +29542,9 @@ msgstr "" msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you" msgstr "" +msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you." +msgstr "" + msgid "Profiles|Your status" msgstr "" diff --git a/spec/fixtures/emails/service_desk.eml b/spec/fixtures/emails/service_desk.eml index 0db1270bc64..102f29542ae 100644 --- a/spec/fixtures/emails/service_desk.eml +++ b/spec/fixtures/emails/service_desk.eml @@ -6,6 +6,7 @@ Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 Date: Thu, 13 Jun 2013 17:03:48 -0400 From: Jake the Dog <jake@adventuretime.ooo> To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo +Cc: Carbon Copy <cc@example.com>, kk@example.org Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Subject: The message subject! @all Mime-Version: 1.0 diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js index bf3bbf4de26..079b64225e4 100644 --- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js @@ -1,8 +1,15 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { TEST_HOST } from 'helpers/test_constants'; +import { visitUrl } from '~/lib/utils/url_utility'; import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue'; +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), + setUrlParams: jest.requireActual('~/lib/utils/url_utility').setUrlParams, +})); + Vue.use(Vuex); let wrapper; @@ -27,6 +34,11 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts = actions: { scrollToDraft }, getters: { draftsCount: () => draftsCount, sortedDrafts: () => sortedDrafts }, }, + notes: { + getters: { + getNoteableData: () => ({ diff_head_sha: '123' }), + }, + }, }, }); @@ -67,5 +79,19 @@ describe('Batch comments preview dropdown', () => { expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1 }); }); + + it('changes window location to navigate to commit', async () => { + factory({ + viewDiffsFileByFile: false, + sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }], + }); + + wrapper.findByTestId('preview-item').vm.$emit('click'); + + await nextTick(); + + expect(scrollToDraft).not.toHaveBeenCalled(); + expect(visitUrl).toHaveBeenCalledWith(`${TEST_HOST}/?commit_id=1234#note_1`); + }); }); }); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 8e499844406..7cf101a5e59 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -88,6 +88,28 @@ describe('common_utils', () => { expectGetElementIdToHaveBeenCalledWith('user-content-definição'); }); + it(`does not scroll when ${commonUtils.NO_SCROLL_TO_HASH_CLASS} is set on target`, () => { + jest.spyOn(window, 'scrollBy'); + + document.body.innerHTML += ` + <div id="parent"> + <a href="#test">Link</a> + <div style="height: 2000px;"></div> + <div id="test" style="height: 2000px;" class="${commonUtils.NO_SCROLL_TO_HASH_CLASS}"></div> + </div> + `; + + window.history.pushState({}, null, '#test'); + commonUtils.handleLocationHash(); + jest.runOnlyPendingTimers(); + + try { + expect(window.scrollBy).not.toHaveBeenCalled(); + } finally { + document.getElementById('parent').remove(); + } + }); + it('scrolls element into view', () => { document.body.innerHTML += ` <div id="parent"> diff --git a/spec/frontend/tabs/index_spec.js b/spec/frontend/tabs/index_spec.js index 67e3d707adb..1d61d38a488 100644 --- a/spec/frontend/tabs/index_spec.js +++ b/spec/frontend/tabs/index_spec.js @@ -1,9 +1,16 @@ -import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs'; +import { GlTabsBehavior, TAB_SHOWN_EVENT, HISTORY_TYPE_HASH } from '~/tabs'; import { ACTIVE_PANEL_CLASS, ACTIVE_TAB_CLASSES } from '~/tabs/constants'; +import { getLocationHash } from '~/lib/utils/url_utility'; +import { NO_SCROLL_TO_HASH_CLASS } from '~/lib/utils/common_utils'; import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import setWindowLocation from 'helpers/set_window_location_helper'; const tabsFixture = getFixture('tabs/tabs.html'); +global.CSS = { + escape: (val) => val, +}; + describe('GlTabsBehavior', () => { let glTabs; let tabShownEventSpy; @@ -41,6 +48,7 @@ describe('GlTabsBehavior', () => { }); expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(true); + expect(panel.classList.contains(NO_SCROLL_TO_HASH_CLASS)).toBe(true); }; const expectInactiveTabAndPanel = (name) => { @@ -67,6 +75,7 @@ describe('GlTabsBehavior', () => { }); expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(false); + expect(panel.classList.contains(NO_SCROLL_TO_HASH_CLASS)).toBe(true); }; const expectGlTabShownEvent = (name) => { @@ -263,4 +272,98 @@ describe('GlTabsBehavior', () => { expectInactiveTabAndPanel('foo'); }); }); + + describe('using history=hash', () => { + const defaultTab = 'foo'; + let tab; + let tabsEl; + + beforeEach(() => { + setHTMLFixture(tabsFixture); + tabsEl = findByTestId('tabs'); + }); + + afterEach(() => { + glTabs.destroy(); + resetHTMLFixture(); + }); + + describe('when a hash exists onInit', () => { + beforeEach(() => { + tab = 'bar'; + setWindowLocation(`http://foo.com/index#${tab}`); + glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH }); + }); + + it('sets the active tab to the hash and preserves hash', () => { + expectActiveTabAndPanel(tab); + expect(getLocationHash()).toBe(tab); + }); + }); + + describe('when a hash does not exist onInit', () => { + beforeEach(() => { + setWindowLocation(`http://foo.com/index`); + glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH }); + }); + + it('sets the active tab to the first tab and sets hash', () => { + expectActiveTabAndPanel(defaultTab); + expect(getLocationHash()).toBe(defaultTab); + }); + }); + + describe('clicking on an inactive tab', () => { + beforeEach(() => { + tab = 'qux'; + setWindowLocation(`http://foo.com/index`); + glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH }); + + findTab(tab).click(); + }); + + it('changes the tabs and updates the hash', () => { + expectInactiveTabAndPanel(defaultTab); + expectActiveTabAndPanel(tab); + expect(getLocationHash()).toBe(tab); + }); + }); + + describe('keyboard navigation', () => { + const secondTab = 'bar'; + + beforeEach(() => { + setWindowLocation(`http://foo.com/index`); + glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH }); + }); + + it.each(['ArrowRight', 'ArrowDown'])( + 'pressing %s moves to next tab and updates hash', + (code) => { + expectActiveTabAndPanel(defaultTab); + + triggerKeyDown(code, glTabs.activeTab); + + expectInactiveTabAndPanel(defaultTab); + expectActiveTabAndPanel(secondTab); + expect(getLocationHash()).toBe(secondTab); + }, + ); + + it.each(['ArrowLeft', 'ArrowUp'])( + 'pressing %s moves to previous tab and updates hash', + (code) => { + // First, make the 2nd tab active + findTab(secondTab).click(); + expectActiveTabAndPanel(secondTab); + + triggerKeyDown(code, glTabs.activeTab); + + expectInactiveTabAndPanel(secondTab); + expectActiveTabAndPanel(defaultTab); + expect(getLocationHash()).toBe(defaultTab); + }, + ); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index b3376f26a25..85a135d2b89 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -67,11 +67,6 @@ describe('Markdown field component', () => { enablePreview, restrictedToolBarItems, }, - provide: { - glFeatures: { - contactsAutocomplete: true, - }, - }, }, ); } diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb index ab198fcbe1f..3a9c4dfe3fb 100644 --- a/spec/lib/backup/gitaly_backup_spec.rb +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -16,8 +16,8 @@ RSpec.describe Backup::GitalyBackup do let(:expected_env) do { - 'SSL_CERT_FILE' => OpenSSL::X509::DEFAULT_CERT_FILE, - 'SSL_CERT_DIR' => OpenSSL::X509::DEFAULT_CERT_DIR + 'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file, + 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir }.merge(ENV) end diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb index 6e7806c5d53..d0aba70081b 100644 --- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb @@ -52,14 +52,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do expect(new_issue.issue_email_participants.first.email).to eq(author_email) end - it 'attaches existing CRM contact' do - contact = create(:contact, group: group, email: author_email) - receiver.execute - new_issue = Issue.last - - expect(new_issue.issue_customer_relations_contacts.last.contact).to eq(contact) - end - it 'sends thank you email' do expect { receiver.execute }.to have_enqueued_job.on_queue('mailers') end @@ -77,6 +69,16 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do context 'when everything is fine' do it_behaves_like 'a new issue request' + it 'attaches existing CRM contacts' do + contact = create(:contact, group: group, email: author_email) + contact2 = create(:contact, group: group, email: "cc@example.com") + contact3 = create(:contact, group: group, email: "kk@example.org") + receiver.execute + new_issue = Issue.last + + expect(new_issue.issue_customer_relations_contacts.map(&:contact)).to contain_exactly(contact, contact2, contact3) + end + context 'with legacy incoming email address' do let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') } diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index f81e3aa070a..a77d51fab88 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe 'Gitlab::VersionInfo' do before do @@ -13,7 +13,7 @@ RSpec.describe 'Gitlab::VersionInfo' do @v2_0_0 = Gitlab::VersionInfo.new(2, 0, 0) end - context '>' do + describe '>' do it { expect(@v2_0_0).to be > @v1_1_0 } it { expect(@v1_1_0).to be > @v1_0_1 } it { expect(@v1_0_1).to be > @v1_0_0 } @@ -21,12 +21,12 @@ RSpec.describe 'Gitlab::VersionInfo' do it { expect(@v0_1_0).to be > @v0_0_1 } end - context '>=' do + describe '>=' do it { expect(@v2_0_0).to be >= Gitlab::VersionInfo.new(2, 0, 0) } it { expect(@v2_0_0).to be >= @v1_1_0 } end - context '<' do + describe '<' do it { expect(@v0_0_1).to be < @v0_1_0 } it { expect(@v0_1_0).to be < @v1_0_0 } it { expect(@v1_0_0).to be < @v1_0_1 } @@ -34,29 +34,29 @@ RSpec.describe 'Gitlab::VersionInfo' do it { expect(@v1_1_0).to be < @v2_0_0 } end - context '<=' do + describe '<=' do it { expect(@v0_0_1).to be <= Gitlab::VersionInfo.new(0, 0, 1) } it { expect(@v0_0_1).to be <= @v0_1_0 } end - context '==' do + describe '==' do it { expect(@v0_0_1).to eq(Gitlab::VersionInfo.new(0, 0, 1)) } it { expect(@v0_1_0).to eq(Gitlab::VersionInfo.new(0, 1, 0)) } it { expect(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) } end - context '!=' do + describe '!=' do it { expect(@v0_0_1).not_to eq(@v0_1_0) } end - context 'unknown' do + describe '.unknown' do it { expect(@unknown).not_to be @v0_0_1 } it { expect(@unknown).not_to be Gitlab::VersionInfo.new } it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) } it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) } end - context 'parse' do + describe '.parse' do it { expect(Gitlab::VersionInfo.parse("1.0.0")).to eq(@v1_0_0) } it { expect(Gitlab::VersionInfo.parse("1.0.0.1")).to eq(@v1_0_0) } it { expect(Gitlab::VersionInfo.parse("1.0.0-ee")).to eq(@v1_0_0) } @@ -66,8 +66,37 @@ RSpec.describe 'Gitlab::VersionInfo' do it { expect(Gitlab::VersionInfo.parse("git 1.0b1")).not_to be_valid } end - context 'to_s' do + describe '.to_s' do it { expect(@v1_0_0.to_s).to eq("1.0.0") } it { expect(@unknown.to_s).to eq("Unknown") } end + + describe '.hash' do + it { expect(Gitlab::VersionInfo.parse("1.0.0").hash).to eq(@v1_0_0.hash) } + it { expect(Gitlab::VersionInfo.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) } + it { expect(Gitlab::VersionInfo.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) } + end + + describe '.eql?' do + it { expect(Gitlab::VersionInfo.parse("1.0.0").eql?(@v1_0_0)).to be_truthy } + it { expect(Gitlab::VersionInfo.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy } + it { expect([@v1_0_0, @v1_1_0, @v1_0_0].uniq).to eq [@v1_0_0, @v1_1_0] } + end + + describe '.same_minor_version?' do + it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey } + it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy } + it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey } + it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey } + end + + describe '.without_patch' do + it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) } + it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) } + end end diff --git a/spec/lib/gitlab/x509/certificate_spec.rb b/spec/lib/gitlab/x509/certificate_spec.rb index 2dc30cc871d..d919b99de2a 100644 --- a/spec/lib/gitlab/x509/certificate_spec.rb +++ b/spec/lib/gitlab/x509/certificate_spec.rb @@ -116,9 +116,69 @@ RSpec.describe Gitlab::X509::Certificate do end end + describe '.default_cert_dir' do + before do + described_class.reset_default_cert_paths + end + + after(:context) do + described_class.reset_default_cert_paths + end + + context 'when SSL_CERT_DIR env variable is not set' do + before do + stub_env('SSL_CERT_DIR', nil) + end + + it 'returns default directory from OpenSSL' do + expect(described_class.default_cert_dir).to eq(OpenSSL::X509::DEFAULT_CERT_DIR) + end + end + + context 'when SSL_CERT_DIR env variable is set' do + before do + stub_env('SSL_CERT_DIR', '/tmp/foo/certs') + end + + it 'returns specified directory' do + expect(described_class.default_cert_dir).to eq('/tmp/foo/certs') + end + end + end + + describe '.default_cert_file' do + before do + described_class.reset_default_cert_paths + end + + after(:context) do + described_class.reset_default_cert_paths + end + + context 'when SSL_CERT_FILE env variable is not set' do + before do + stub_env('SSL_CERT_FILE', nil) + end + + it 'returns default file from OpenSSL' do + expect(described_class.default_cert_file).to eq(OpenSSL::X509::DEFAULT_CERT_FILE) + end + end + + context 'when SSL_CERT_FILE env variable is set' do + before do + stub_env('SSL_CERT_FILE', '/tmp/foo/cert.pem') + end + + it 'returns specified file' do + expect(described_class.default_cert_file).to eq('/tmp/foo/cert.pem') + end + end + end + describe '.ca_certs_paths' do it 'returns all files specified by OpenSSL defaults' do - cert_paths = Dir["#{OpenSSL::X509::DEFAULT_CERT_DIR}/*"] + cert_paths = Dir["#{described_class.default_cert_dir}/*"] expect(described_class.ca_certs_paths).to match_array(cert_paths + [sample_cert]) end diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb index 0e34d5393d6..5626e49bfe1 100644 --- a/spec/lib/gitlab/x509/signature_spec.rb +++ b/spec/lib/gitlab/x509/signature_spec.rb @@ -107,7 +107,7 @@ RSpec.describe Gitlab::X509::Signature do f.print certificate.to_pem end - stub_const("OpenSSL::X509::DEFAULT_CERT_FILE", file_path) + allow(Gitlab::X509::Certificate).to receive(:default_cert_file).and_return(file_path) allow(OpenSSL::X509::Store).to receive(:new).and_return(store) end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 47fd1622306..1d58a31bd6e 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -788,20 +788,6 @@ RSpec.describe 'project routing' do it 'to #test' do expect(put('/gitlab/gitlabhq/-/settings/integrations/acme/test')).to route_to('projects/settings/integrations#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') end - - context 'legacy routes' do - it 'to #edit' do - expect(get('/gitlab/gitlabhq/-/integrations/acme/edit')).to route_to('projects/settings/integrations#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') - end - - it 'to #update' do - expect(put('/gitlab/gitlabhq/-/integrations/acme')).to route_to('projects/settings/integrations#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') - end - - it 'to #test' do - expect(put('/gitlab/gitlabhq/-/integrations/acme/test')).to route_to('projects/settings/integrations#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') - end - end end describe Projects::Settings::IntegrationHookLogsController do @@ -812,16 +798,6 @@ RSpec.describe 'project routing' do it 'to #retry' do expect(post('/gitlab/gitlabhq/-/settings/integrations/acme/hook_logs/log/retry')).to route_to('projects/settings/integration_hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') end - - context 'legacy routes' do - it 'to #show' do - expect(get('/gitlab/gitlabhq/-/integrations/acme/hook_logs/log')).to route_to('projects/settings/integration_hook_logs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') - end - - it 'to #retry' do - expect(post('/gitlab/gitlabhq/-/integrations/acme/hook_logs/log/retry')).to route_to('projects/settings/integration_hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') - end - end end describe Projects::TemplatesController, 'routing' do |