Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-22 15:09:48 +00:00
parent 421f6c92d5
commit 640007842a
39 changed files with 433 additions and 157 deletions

View File

@ -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}

View File

@ -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>

View File

@ -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');

View File

@ -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;

View File

@ -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];

View File

@ -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);

View File

@ -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,
);

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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})

View File

@ -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 })

View File

@ -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,

View File

@ -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.")

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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 (`` ` ``).

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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`);
});
});
});

View File

@ -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">

View File

@ -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);
},
);
});
});
});

View File

@ -67,11 +67,6 @@ describe('Markdown field component', () => {
enablePreview,
restrictedToolBarItems,
},
provide: {
glFeatures: {
contactsAutocomplete: true,
},
},
},
);
}

View File

@ -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

View File

@ -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') }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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