Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
118083ac69
commit
abd2c6b32a
|
|
@ -277,7 +277,6 @@ Layout/SpaceInLambdaLiteral:
|
|||
- 'ee/lib/ee/api/entities/list.rb'
|
||||
- 'ee/lib/ee/api/entities/member.rb'
|
||||
- 'ee/lib/ee/api/entities/project_approval_rule.rb'
|
||||
- 'ee/lib/ee/api/entities/user_basic.rb'
|
||||
- 'ee/lib/ee/api/entities/vulnerability_issue_link.rb'
|
||||
- 'ee/lib/ee/gitlab/background_migration/backfill_epic_cache_counts.rb'
|
||||
- 'ee/lib/ee/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb'
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import $ from 'jquery';
|
||||
import initVueAlerts from '~/vue_alerts';
|
||||
import NoEmojiValidator from '~/emoji/no_emoji_validator';
|
||||
import { initLanguageSwitcher } from '~/language_switcher';
|
||||
import LengthValidator from '~/validators/length_validator';
|
||||
import mountEmailVerificationApplication from '~/sessions/new';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import OAuthRememberMe from './oauth_remember_me';
|
||||
import preserveUrlFragment from './preserve_url_fragment';
|
||||
import {
|
||||
appendUrlFragment,
|
||||
appendRedirectQuery,
|
||||
toggleRememberMeQuery,
|
||||
} from './preserve_url_fragment';
|
||||
import SigninTabsMemoizer from './signin_tabs_memoizer';
|
||||
import UsernameValidator from './username_validator';
|
||||
|
||||
|
|
@ -15,13 +17,9 @@ new LengthValidator(); // eslint-disable-line no-new
|
|||
new SigninTabsMemoizer(); // eslint-disable-line no-new
|
||||
new NoEmojiValidator(); // eslint-disable-line no-new
|
||||
|
||||
new OAuthRememberMe({
|
||||
container: $('.js-oauth-login'),
|
||||
}).bindEvents();
|
||||
|
||||
// Save the URL fragment from the current window location. This will be present if the user was
|
||||
// redirected to sign-in after attempting to access a protected URL that included a fragment.
|
||||
preserveUrlFragment(window.location.hash);
|
||||
appendUrlFragment();
|
||||
appendRedirectQuery();
|
||||
toggleRememberMeQuery();
|
||||
initVueAlerts();
|
||||
initLanguageSwitcher();
|
||||
mountEmailVerificationApplication();
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
|
||||
|
||||
/**
|
||||
* OAuth-based login buttons have a separate "remember me" checkbox.
|
||||
*
|
||||
* Toggling this checkbox adds/removes a `remember_me` parameter to the
|
||||
* login buttons' parent form action, which is passed on to the omniauth callback.
|
||||
*/
|
||||
|
||||
export default class OAuthRememberMe {
|
||||
constructor(opts = {}) {
|
||||
this.container = opts.container || '';
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
$('#remember_me_omniauth', this.container).on('click', this.toggleRememberMe);
|
||||
}
|
||||
|
||||
toggleRememberMe(event) {
|
||||
const rememberMe = $(event.target).is(':checked');
|
||||
|
||||
$('.js-oauth-login form', this.container).each((_, form) => {
|
||||
const $form = $(form);
|
||||
const href = $form.attr('action');
|
||||
|
||||
if (rememberMe) {
|
||||
$form.attr('action', mergeUrlParams({ remember_me: 1 }, href));
|
||||
} else {
|
||||
$form.attr('action', removeParams(['remember_me'], href));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,72 @@
|
|||
import { mergeUrlParams, setUrlFragment } from '~/lib/utils/url_utility';
|
||||
import { mergeUrlParams, removeParams, setUrlFragment } from '~/lib/utils/url_utility';
|
||||
|
||||
/**
|
||||
* Ensure the given URL fragment is preserved by appending it to sign-in/sign-up form actions and
|
||||
* OAuth/SAML login links.
|
||||
* Append the fragment to all non-OAuth login form actions so it is preserved
|
||||
* when the user is eventually redirected back to the originally requested URL.
|
||||
*
|
||||
* @param fragment {string} - url fragment to be preserved
|
||||
*/
|
||||
export default function preserveUrlFragment(fragment = '') {
|
||||
if (fragment) {
|
||||
const normalFragment = fragment.replace(/^#/, '');
|
||||
export function appendUrlFragment(fragment = document.location.hash) {
|
||||
if (!fragment) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is
|
||||
// eventually redirected back to the originally requested URL.
|
||||
const forms = document.querySelectorAll('.js-non-oauth-login form');
|
||||
Array.prototype.forEach.call(forms, (form) => {
|
||||
const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`);
|
||||
form.setAttribute('action', actionWithFragment);
|
||||
});
|
||||
const normalFragment = fragment.replace(/^#/, '');
|
||||
const forms = document.querySelectorAll('.js-non-oauth-login form');
|
||||
forms.forEach((form) => {
|
||||
const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`);
|
||||
form.setAttribute('action', actionWithFragment);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a redirect_fragment query param to all OAuth login form actions. The
|
||||
* redirect_fragment query param will be available in the omniauth callback upon
|
||||
* successful authentication.
|
||||
*
|
||||
* @param {string} fragment - url fragment to be preserved
|
||||
*/
|
||||
export function appendRedirectQuery(fragment = document.location.hash) {
|
||||
if (!fragment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalFragment = fragment.replace(/^#/, '');
|
||||
const oauthForms = document.querySelectorAll('.js-oauth-login form');
|
||||
oauthForms.forEach((oauthForm) => {
|
||||
const newHref = mergeUrlParams(
|
||||
{ redirect_fragment: normalFragment },
|
||||
oauthForm.getAttribute('action'),
|
||||
);
|
||||
oauthForm.setAttribute('action', newHref);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth login buttons have a separate "remember me" checkbox.
|
||||
*
|
||||
* Toggling this checkbox adds/removes a `remember_me` parameter to the
|
||||
* login form actions, which is passed on to the omniauth callback.
|
||||
*/
|
||||
export function toggleRememberMeQuery() {
|
||||
const oauthForms = document.querySelectorAll('.js-oauth-login form');
|
||||
const checkbox = document.querySelector('#js-remember-me-omniauth');
|
||||
|
||||
if (oauthForms.length === 0 || !checkbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.addEventListener('change', ({ currentTarget }) => {
|
||||
oauthForms.forEach((oauthForm) => {
|
||||
const href = oauthForm.getAttribute('action');
|
||||
let newHref;
|
||||
if (currentTarget.checked) {
|
||||
newHref = mergeUrlParams({ remember_me: '1' }, href);
|
||||
} else {
|
||||
newHref = removeParams(['remember_me'], href);
|
||||
}
|
||||
|
||||
// Append a redirect_fragment query param to all oauth provider links. The redirect_fragment
|
||||
// query param will be available in the omniauth callback upon successful authentication
|
||||
const oauthForms = document.querySelectorAll('.js-oauth-login form');
|
||||
Array.prototype.forEach.call(oauthForms, (oauthForm) => {
|
||||
const newHref = mergeUrlParams(
|
||||
{ redirect_fragment: normalFragment },
|
||||
oauthForm.getAttribute('action'),
|
||||
);
|
||||
oauthForm.setAttribute('action', newHref);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
span: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
prevBlameLink: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -43,6 +48,9 @@ export default {
|
|||
avatarLinkAltText() {
|
||||
return sprintf(__(`%{username}'s avatar`), { username: this.commit.authorName });
|
||||
},
|
||||
truncateAuthorName() {
|
||||
return typeof this.span === 'number' && this.span < 3;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleShowDescription() {
|
||||
|
|
@ -102,18 +110,23 @@ export default {
|
|||
@click="toggleShowDescription"
|
||||
/>
|
||||
</div>
|
||||
<div class="committer gl-flex-basis-full">
|
||||
<div
|
||||
class="committer gl-flex-basis-full"
|
||||
:class="truncateAuthorName ? 'gl-display-inline-flex' : ''"
|
||||
data-testid="committer"
|
||||
>
|
||||
<gl-link
|
||||
v-if="commit.author"
|
||||
:href="commit.author.webPath"
|
||||
class="commit-author-link js-user-link"
|
||||
:class="truncateAuthorName ? 'gl-display-inline-block gl-text-truncate' : ''"
|
||||
>
|
||||
{{ commit.author.name }}</gl-link
|
||||
>
|
||||
<template v-else>
|
||||
{{ commit.authorName }}
|
||||
</template>
|
||||
{{ $options.i18n.authored }}
|
||||
{{ $options.i18n.authored }}
|
||||
<timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
|
||||
</div>
|
||||
<pre
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ const InternalEvents = {
|
|||
/**
|
||||
*
|
||||
* @param {string} event
|
||||
* @param {string} category - The category of the event. This is optional and
|
||||
* defaults to the page name where the event was triggered. It's advised not to use
|
||||
* this parameter for new events unless absolutely necessary.
|
||||
*/
|
||||
trackEvent(event) {
|
||||
trackEvent(event, category = undefined) {
|
||||
API.trackInternalEvent(event);
|
||||
Tracking.event(undefined, event, {
|
||||
Tracking.event(category, event, {
|
||||
context: {
|
||||
schema: SERVICE_PING_SCHEMA,
|
||||
data: {
|
||||
|
|
@ -30,8 +33,8 @@ const InternalEvents = {
|
|||
mixin() {
|
||||
return {
|
||||
methods: {
|
||||
trackEvent(event) {
|
||||
InternalEvents.trackEvent(event);
|
||||
trackEvent(event, category = undefined) {
|
||||
InternalEvents.trackEvent(event, category);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default {
|
|||
class="gl-display-flex gl-absolute gl-px-3"
|
||||
:style="{ top: blame.blameOffset }"
|
||||
:commit="blame.commit"
|
||||
:span="blame.span"
|
||||
:prev-blame-link="blame.commitData && blame.commitData.projectBlameLink"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@
|
|||
.commit-author-link {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.commit-author-link.gl-text-truncate {
|
||||
max-width: 20ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
|
|||
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/388541
|
||||
# type_id is a misnomer. QuickActions::TargetService actually requires an iid.
|
||||
QuickActions::TargetService
|
||||
.new(nil, current_user, group: @group)
|
||||
.new(container: @group, current_user: current_user)
|
||||
.execute(params[:type], params[:type_id])
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
|||
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/388541
|
||||
# type_id is a misnomer. QuickActions::TargetService actually requires an iid.
|
||||
QuickActions::TargetService
|
||||
.new(project, current_user)
|
||||
.new(container: project, current_user: current_user)
|
||||
.execute(target_type, params[:type_id])
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module Projects
|
||||
class IsForkedResolver < BaseResolver
|
||||
type GraphQL::Types::Boolean, null: false
|
||||
|
||||
def resolve
|
||||
lazy_fork_network_members = BatchLoader::GraphQL.for(object.id).batch do |ids, loader|
|
||||
ForkNetworkMember.by_projects(ids)
|
||||
.with_fork_network
|
||||
.find_each do |fork_network_member|
|
||||
loader.call(fork_network_member.project_id, fork_network_member)
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::Graphql::Lazy.with_value(lazy_fork_network_members) do |fork_network_member|
|
||||
next false if fork_network_member.nil?
|
||||
|
||||
fork_network_member.fork_network.root_project_id != object.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -685,6 +685,12 @@ module Types
|
|||
description: 'Project allows assigning multiple reviewers to a merge request.',
|
||||
null: false
|
||||
|
||||
field :is_forked,
|
||||
GraphQL::Types::Boolean,
|
||||
resolver: Resolvers::Projects::IsForkedResolver,
|
||||
description: 'Project is forked.',
|
||||
null: false
|
||||
|
||||
def timelog_categories
|
||||
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ module Ci
|
|||
include FileStoreMounter
|
||||
include Lockable
|
||||
include Presentable
|
||||
include SafelyChangeColumnDefault
|
||||
|
||||
columns_changing_default :partition_id
|
||||
|
||||
FILE_SIZE_LIMIT = 10.megabytes.freeze
|
||||
EXPIRATION_DATE = 1.week.freeze
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
module Ci
|
||||
class PipelineConfig < Ci::ApplicationRecord
|
||||
include Ci::Partitionable
|
||||
include SafelyChangeColumnDefault
|
||||
|
||||
columns_changing_default :partition_id
|
||||
|
||||
self.table_name = 'ci_pipelines_config'
|
||||
self.primary_key = :pipeline_id
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ module Ci
|
|||
class PipelineMetadata < Ci::ApplicationRecord
|
||||
include Ci::Partitionable
|
||||
include Importable
|
||||
include SafelyChangeColumnDefault
|
||||
|
||||
columns_changing_default :partition_id
|
||||
|
||||
self.primary_key = :pipeline_id
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ class ForkNetworkMember < ApplicationRecord
|
|||
|
||||
after_destroy :cleanup_fork_network
|
||||
|
||||
scope :by_projects, ->(ids) { where(project_id: ids) }
|
||||
scope :with_fork_network, -> { joins(:fork_network).includes(:fork_network) }
|
||||
|
||||
private
|
||||
|
||||
def cleanup_fork_network
|
||||
|
|
|
|||
|
|
@ -631,6 +631,8 @@ class User < MainClusterwide::ApplicationRecord
|
|||
.trusted_with_spam)
|
||||
end
|
||||
|
||||
scope :preload_user_detail, -> { preload(:user_detail) }
|
||||
|
||||
def self.supported_keyset_orderings
|
||||
{
|
||||
id: [:asc, :desc],
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class PreviewMarkdownService < BaseService
|
|||
|
||||
def find_commands_target
|
||||
QuickActions::TargetService
|
||||
.new(project, current_user, group: params[:group])
|
||||
.new(container: project, current_user: current_user, params: { group: params[:group] })
|
||||
.execute(target_type, target_id)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QuickActions
|
||||
class TargetService < BaseService
|
||||
class TargetService < BaseContainerService
|
||||
def execute(type, type_iid)
|
||||
case type&.downcase
|
||||
when 'workitem'
|
||||
|
|
@ -19,15 +19,15 @@ module QuickActions
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def work_item(type_iid)
|
||||
WorkItems::WorkItemsFinder.new(current_user, project_id: project.id).find_by(iid: type_iid)
|
||||
WorkItems::WorkItemsFinder.new(current_user, **parent_params).find_by(iid: type_iid)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def issue(type_iid)
|
||||
return project.issues.build if type_iid.nil?
|
||||
return container.issues.build if type_iid.nil?
|
||||
|
||||
IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_iid) || project.issues.build
|
||||
IssuesFinder.new(current_user, **parent_params).find_by(iid: type_iid) || container.issues.build
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
@ -42,7 +42,11 @@ module QuickActions
|
|||
def commit(type_iid)
|
||||
project.commit(type_iid)
|
||||
end
|
||||
|
||||
def parent_params
|
||||
group_container? ? { group_id: group.id } : { project_id: project.id }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
QuickActions::TargetService.prepend_mod_with('QuickActions::TargetService')
|
||||
QuickActions::TargetService.prepend_mod
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@
|
|||
data: { testid: test_id_for_provider(provider) },
|
||||
id: "oauth-login-#{provider}"
|
||||
- if render_remember_me
|
||||
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c|
|
||||
= render Pajamas::CheckboxTagComponent.new(name: 'js-remember-me-omniauth', value: nil) do |c|
|
||||
- c.with_label do
|
||||
= _('Remember me')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemovePartitionIdDefaultValueForCiPipelineMetadata < Gitlab::Database::Migration[2.2]
|
||||
milestone '16.9'
|
||||
enable_lock_retries!
|
||||
|
||||
TABLE_NAME = :ci_pipeline_metadata
|
||||
COLUM_NAME = :partition_id
|
||||
|
||||
def change
|
||||
change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemovePartitionIdDefaultValueForCiPipelineArtifact < Gitlab::Database::Migration[2.2]
|
||||
milestone '16.9'
|
||||
enable_lock_retries!
|
||||
|
||||
TABLE_NAME = :ci_pipeline_artifacts
|
||||
COLUM_NAME = :partition_id
|
||||
|
||||
def change
|
||||
change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemovePartitionIdDefaultValueForCiPipelineConfig < Gitlab::Database::Migration[2.2]
|
||||
milestone '16.9'
|
||||
enable_lock_retries!
|
||||
|
||||
TABLE_NAME = :ci_pipelines_config
|
||||
COLUM_NAME = :partition_id
|
||||
|
||||
def change
|
||||
change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
43ff332582062a104cef5449444034363c1a71d288bcae7dfdeefbd69500186e
|
||||
|
|
@ -0,0 +1 @@
|
|||
29392953f2fce7fb1a24dbc49f1ea30c49b1006551599bff98edc4de8061106b
|
||||
|
|
@ -0,0 +1 @@
|
|||
d14905475e591b7fa855097434d0e810fbb5a0890d7feb7b4fe8a22d5d75335f
|
||||
|
|
@ -14603,7 +14603,7 @@ CREATE TABLE ci_pipeline_artifacts (
|
|||
verification_checksum bytea,
|
||||
verification_failure text,
|
||||
locked smallint DEFAULT 2,
|
||||
partition_id bigint DEFAULT 100 NOT NULL,
|
||||
partition_id bigint NOT NULL,
|
||||
CONSTRAINT check_191b5850ec CHECK ((char_length(file) <= 255)),
|
||||
CONSTRAINT check_abeeb71caf CHECK ((file IS NOT NULL)),
|
||||
CONSTRAINT ci_pipeline_artifacts_verification_failure_text_limit CHECK ((char_length(verification_failure) <= 255))
|
||||
|
|
@ -14658,7 +14658,7 @@ CREATE TABLE ci_pipeline_metadata (
|
|||
name text,
|
||||
auto_cancel_on_new_commit smallint DEFAULT 0 NOT NULL,
|
||||
auto_cancel_on_job_failure smallint DEFAULT 0 NOT NULL,
|
||||
partition_id bigint DEFAULT 100 NOT NULL,
|
||||
partition_id bigint NOT NULL,
|
||||
CONSTRAINT check_9d3665463c CHECK ((char_length(name) <= 255))
|
||||
);
|
||||
|
||||
|
|
@ -14782,7 +14782,7 @@ CREATE TABLE ci_pipelines (
|
|||
CREATE TABLE ci_pipelines_config (
|
||||
pipeline_id bigint NOT NULL,
|
||||
content text NOT NULL,
|
||||
partition_id bigint DEFAULT 100 NOT NULL
|
||||
partition_id bigint NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ci_pipelines_id_seq
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ swap:
|
|||
raketask: Rake task
|
||||
raketasks: Rake tasks
|
||||
rspec: RSpec
|
||||
self hosted: self-managed
|
||||
self-hosted: self-managed
|
||||
GitLab self hosted: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
|
||||
GitLab self-hosted: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
|
||||
self hosted GitLab: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
|
||||
self-hosted GitLab: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed
|
||||
styleguide: style guide
|
||||
to login: to log in
|
||||
can login: can log in
|
||||
|
|
|
|||
|
|
@ -24351,6 +24351,7 @@ Represents vulnerability finding of a security report on the pipeline.
|
|||
| <a id="projectimportstatus"></a>`importStatus` | [`String`](#string) | Status of import background job of the project. |
|
||||
| <a id="projectincidentmanagementtimelineeventtags"></a>`incidentManagementTimelineEventTags` | [`[TimelineEventTagType!]`](#timelineeventtagtype) | Timeline event tags for the project. |
|
||||
| <a id="projectiscatalogresource"></a>`isCatalogResource` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Indicates if a project is a catalog resource. |
|
||||
| <a id="projectisforked"></a>`isForked` | [`Boolean!`](#boolean) | Project is forked. |
|
||||
| <a id="projectissuesaccesslevel"></a>`issuesAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for issues access. |
|
||||
| <a id="projectissuesenabled"></a>`issuesEnabled` | [`Boolean`](#boolean) | Indicates if Issues are enabled for the current user. |
|
||||
| <a id="projectjiraimportstatus"></a>`jiraImportStatus` | [`String`](#string) | Status of Jira import background job of the project. |
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ In GitLab 14.8 and earlier, projects in personal namespaces have an `access_leve
|
|||
|
||||
The `group_saml_identity` attribute is only visible to group owners for [SSO-enabled groups](../user/group/saml_sso/index.md).
|
||||
|
||||
The `email` attribute is only visible to group owners for users provisioned by the group with [SCIM](../user/group/saml_sso/scim_setup.md).
|
||||
[Issue 391453](https://gitlab.com/gitlab-org/gitlab/-/issues/391453) proposes to change the criteria for access to the `email` attribute from provisioned users to [enterprise users](../user/enterprise_user/index.md).
|
||||
The `email` attribute is only visible to group owners for [enterprise users](../user/enterprise_user/index.md) of the group when an API request is sent to the group itself, or that group's subgroups or projects.
|
||||
|
||||
## List all members of a group or project
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ Ideally, you should use [CI/CD variables](../variables/predefined_variables.md)
|
|||
to replace those values at runtime when each review app is created:
|
||||
|
||||
- `data-project-id` is the project ID, which can be found by the `CI_PROJECT_ID`
|
||||
variable.
|
||||
variable or on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
|
||||
- `data-merge-request-id` is the merge request ID, which can be found by the
|
||||
`CI_MERGE_REQUEST_IID` variable. `CI_MERGE_REQUEST_IID` is available only if
|
||||
[`rules:if: $CI_PIPELINE_SOURCE == "merge_request_event`](../pipelines/merge_request_pipelines.md#use-rules-to-add-jobs)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ In each example, replace:
|
|||
- `<token>` with your trigger token.
|
||||
- `<ref_name>` with a branch or tag name, like `main`.
|
||||
- `<project_id>` with your project ID, like `123456`. The project ID is displayed
|
||||
at the top of every project's landing page.
|
||||
on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
|
||||
|
||||
### Use a CI/CD job
|
||||
|
||||
|
|
@ -100,8 +100,8 @@ trigger_pipeline:
|
|||
|
||||
In this example:
|
||||
|
||||
- `1234` is the project ID for `project-B`. The project ID is displayed at the top
|
||||
of every project's landing page.
|
||||
- `1234` is the project ID for `project-B`. The project ID is displayed on the
|
||||
[project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
|
||||
- The [`rules`](../yaml/index.md#rules) cause the job to run every time a tag is added to `project-A`.
|
||||
- `MY_TRIGGER_TOKEN` is a [masked CI/CD variables](../variables/index.md#mask-a-cicd-variable)
|
||||
that contains the trigger token.
|
||||
|
|
@ -119,7 +119,7 @@ Replace:
|
|||
|
||||
- The URL with `https://gitlab.com` or the URL of your instance.
|
||||
- `<project_id>` with your project ID, like `123456`. The project ID is displayed
|
||||
at the top of the project's landing page.
|
||||
on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id).
|
||||
- `<ref_name>` with a branch or tag name, like `main`. This value takes precedence over the `ref_name` in the webhook payload.
|
||||
The payload's `ref` is the branch that fired the trigger in the source repository.
|
||||
You must URL-encode the `ref_name` if it contains slashes.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ If you are already tracking events in Snowplow, you can also start collecting me
|
|||
The event triggered by Internal Events has some special properties compared to previously tracking with Snowplow directly:
|
||||
|
||||
1. The `label`, `property` and `value` attributes are not used within Internal Events and are always empty.
|
||||
1. The `category` is automatically set to `InternalEventTracking`
|
||||
1. The `category` is automatically set to the location where the event happened. For Frontend events it is the page name and for Backend events it is a class name. If the page name or class name is not used, the default value of `"InternalEventTracking"` will be used.
|
||||
|
||||
Make sure that you are okay with this change before you migrate and dashboards are changed accordingly.
|
||||
|
||||
|
|
@ -73,9 +73,11 @@ import { InternalEvents } from '~/tracking';
|
|||
mixins: [InternalEvents.mixin()]
|
||||
...
|
||||
...
|
||||
this.trackEvent('action')
|
||||
this.trackEvent('action', 'category')
|
||||
```
|
||||
|
||||
If you are currently passing `category` and need to keep it, it can be passed as the second argument in the `trackEvent` method, as illustrated in the previous example. Nonetheless, it is strongly advised against using the `category` parameter for new events. This is because, by default, the category field is populated with information about where the event was triggered.
|
||||
|
||||
You can use [this MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123901/diffs) as an example. It migrates the `devops_adoption_app` component to use Internal Events Tracking.
|
||||
|
||||
If you are using `data-track-action` in the component, you have to change it to `data-event-tracking` to migrate to Internal Events Tracking.
|
||||
|
|
|
|||
|
|
@ -81,11 +81,10 @@ For more information about our plans for language support in SAST, see the [cate
|
|||
| TypeScript | [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep/#sast-rules) | 13.10 |
|
||||
|
||||
<html>
|
||||
<small>Footnotes:
|
||||
Footnotes:
|
||||
<ol>
|
||||
<li>The SpotBugs-based analyzer supports [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/). It can also be used with variants like the [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), [Grails](https://grails.org/), and the [Maven wrapper](https://github.com/takari/maven-wrapper). However, SpotBugs has [limitations](https://gitlab.com/gitlab-org/gitlab/-/issues/350801) when used against [Ant](https://ant.apache.org/)-based projects. We recommend using the Semgrep-based analyzer for Ant-based Java or Scala projects.</li>
|
||||
<li>The SpotBugs-based analyzer supports <a href="https://gradle.org/">Gradle</a>, <a href="https://maven.apache.org/">Maven</a>, and <a href="https://www.scala-sbt.org/">SBT</a>. It can also be used with variants like the <a href="https://docs.gradle.org/current/userguide/gradle_wrapper.html">Gradle wrapper</a>, <a href="https://grails.org/">Grails</a>, and the <a href="https://github.com/takari/maven-wrapper">Maven wrapper</a>. However, SpotBugs has <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/350801">limitations</a> when used against <a href="https://ant.apache.org/">Ant</a>-based projects. You should use the Semgrep-based analyzer for Ant-based Java or Scala projects.</li>
|
||||
</ol>
|
||||
</small>
|
||||
</html>
|
||||
|
||||
## End of supported analyzers
|
||||
|
|
|
|||
|
|
@ -203,10 +203,7 @@ A top-level group Owner can [set up verified domains to bypass confirmation emai
|
|||
### Get users' email addresses through the API
|
||||
|
||||
A top-level group Owner can use the [group and project members API](../../api/members.md) to access
|
||||
users' information. For users provisioned by the group with [SCIM](../group/saml_sso/scim_setup.md),
|
||||
this information includes users' email addresses.
|
||||
|
||||
[Issue 391453](https://gitlab.com/gitlab-org/gitlab/-/issues/391453) proposes to change the criteria for access to email addresses from provisioned users to enterprise users.
|
||||
users' information. For enterprise users of the group this information includes users' email addresses.
|
||||
|
||||
### Remove enterprise management features from an account
|
||||
|
||||
|
|
|
|||
|
|
@ -738,6 +738,12 @@ module API
|
|||
namespace: namespace,
|
||||
project: project
|
||||
)
|
||||
rescue Gitlab::InternalEvents::UnknownEventError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, event_name: event_name)
|
||||
|
||||
# We want to keep the error silent on production to keep the behavior
|
||||
# consistent with StandardError rescue
|
||||
unprocessable_entity!(e.message) if Gitlab.dev_or_test_env?
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, event_name: event_name)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ module API
|
|||
end
|
||||
|
||||
desc 'Updates a group or project invitation.' do
|
||||
success Entities::Member
|
||||
success Entities::Invitation
|
||||
tags %w[invitations]
|
||||
end
|
||||
params do
|
||||
|
|
@ -103,7 +103,7 @@ module API
|
|||
updated_member = result[:members].first
|
||||
|
||||
if result[:status] == :success
|
||||
present_members updated_member
|
||||
present_member_invitations updated_member
|
||||
else
|
||||
render_validation_error!(updated_member)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ module BulkImports
|
|||
return if user_membership && user_membership[:access_level] >= data[:access_level]
|
||||
|
||||
# Create new membership for any other access level
|
||||
portable.members.create!(data)
|
||||
member = portable.members.new(data)
|
||||
member.importing = true # avoid sending new member notification to the invited user
|
||||
member.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -2918,6 +2918,9 @@ msgstr ""
|
|||
msgid "Add an impersonation token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add another branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add another link"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44054,6 +44057,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Add new approver"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Add project full path after @ to following branches: %{branches}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Add protected branches"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44123,6 +44129,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Choose approver type"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Choose exception branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Choose specific role"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44192,6 +44201,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Every time a pipeline runs for %{branches}%{branchExceptionsString}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Exception branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Exceptions"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44207,6 +44219,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Failed to load images."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Fill in branch name with project name in the format of %{boldStart}branch-name@project-path,%{boldEnd} separate with `,`"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Following projects:"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44276,6 +44291,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|No actions defined - policy will not run."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|No branches yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|No compliance frameworks"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44320,6 +44338,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Overwrite the current CI/CD code with the new file's content?"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Please remove duplicated values"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Policies"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'gitlab-qa', '~> 13', '>= 13.1.0', require: 'gitlab/qa'
|
||||
gem 'gitlab-qa', '~> 14', require: 'gitlab/qa'
|
||||
gem 'gitlab_quality-test_tooling', '~> 1.11.0', require: false
|
||||
gem 'gitlab-utils', path: '../gems/gitlab-utils'
|
||||
gem 'activesupport', '~> 7.0.8' # This should stay in sync with the root's Gemfile
|
||||
|
|
|
|||
|
|
@ -118,8 +118,8 @@ GEM
|
|||
gitlab (4.19.0)
|
||||
httparty (~> 0.20)
|
||||
terminal-table (>= 1.5.1)
|
||||
gitlab-qa (13.1.0)
|
||||
activesupport (>= 6.1, < 7.1)
|
||||
gitlab-qa (14.0.0)
|
||||
activesupport (>= 6.1, < 7.2)
|
||||
gitlab (~> 4.19)
|
||||
http (~> 5.0)
|
||||
nokogiri (~> 1.10)
|
||||
|
|
@ -354,7 +354,7 @@ DEPENDENCIES
|
|||
faraday-retry (~> 2.2)
|
||||
fog-core (= 2.1.0)
|
||||
fog-google (~> 1.19)
|
||||
gitlab-qa (~> 13, >= 13.1.0)
|
||||
gitlab-qa (~> 14)
|
||||
gitlab-utils!
|
||||
gitlab_quality-test_tooling (~> 1.11.0)
|
||||
influxdb-client (~> 3.0)
|
||||
|
|
@ -380,4 +380,4 @@ DEPENDENCIES
|
|||
zeitwerk (~> 2.6, >= 2.6.12)
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.4
|
||||
2.5.5
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
|
|||
it 'displays the remember me checkbox' do
|
||||
visit new_user_session_path
|
||||
|
||||
expect(page).to have_field('remember_me_omniauth')
|
||||
expect(page).to have_field('js-remember-me-omniauth')
|
||||
end
|
||||
|
||||
context 'when remember me is not enabled' do
|
||||
|
|
@ -413,7 +413,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
|
|||
it 'does not display the remember me checkbox' do
|
||||
visit new_user_session_path
|
||||
|
||||
expect(page).not_to have_field('remember_me_omniauth')
|
||||
expect(page).not_to have_field('js-remember-me-omniauth')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<div class="js-oauth-login">
|
||||
<input id="remember_me_omniauth" type="checkbox" />
|
||||
|
||||
<form method="post" action="http://example.com/">
|
||||
<button class="twitter" type="submit">
|
||||
<span>Twitter</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="http://example.com/">
|
||||
<button class="github" type="submit">
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="http://example.com/?redirect_fragment=L1">
|
||||
<button class="facebook" type="submit">
|
||||
<span>Facebook</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import htmlOauthRememberMe from 'test_fixtures_static/oauth_remember_me.html';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me';
|
||||
|
||||
describe('OAuthRememberMe', () => {
|
||||
const findFormAction = (selector) => {
|
||||
return $(`.js-oauth-login ${selector}`).parent('form').attr('action');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(htmlOauthRememberMe);
|
||||
|
||||
new OAuthRememberMe({ container: $('.js-oauth-login') }).bindEvents();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('adds and removes the "remember_me" query parameter from all OAuth login buttons', () => {
|
||||
$('.js-oauth-login #remember_me_omniauth').click();
|
||||
|
||||
expect(findFormAction('.twitter')).toBe('http://example.com/?remember_me=1');
|
||||
expect(findFormAction('.github')).toBe('http://example.com/?remember_me=1');
|
||||
expect(findFormAction('.facebook')).toBe(
|
||||
'http://example.com/?redirect_fragment=L1&remember_me=1',
|
||||
);
|
||||
|
||||
$('.js-oauth-login #remember_me_omniauth').click();
|
||||
|
||||
expect(findFormAction('.twitter')).toBe('http://example.com/');
|
||||
expect(findFormAction('.github')).toBe('http://example.com/');
|
||||
expect(findFormAction('.facebook')).toBe('http://example.com/?redirect_fragment=L1');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import $ from 'jquery';
|
||||
import htmlSessionsNew from 'test_fixtures/sessions/new.html';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment';
|
||||
import {
|
||||
appendUrlFragment,
|
||||
appendRedirectQuery,
|
||||
toggleRememberMeQuery,
|
||||
} from '~/pages/sessions/new/preserve_url_fragment';
|
||||
|
||||
describe('preserve_url_fragment', () => {
|
||||
const findFormAction = (selector) => {
|
||||
return $(`.js-oauth-login ${selector}`).parent('form').attr('action');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(htmlSessionsNew);
|
||||
});
|
||||
|
|
@ -16,41 +15,74 @@ describe('preserve_url_fragment', () => {
|
|||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('adds the url fragment to the login form actions', () => {
|
||||
preserveUrlFragment('#L65');
|
||||
describe('non-OAuth login forms', () => {
|
||||
describe('appendUrlFragment', () => {
|
||||
const findFormAction = () => document.querySelector('.js-non-oauth-login form').action;
|
||||
|
||||
expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in#L65');
|
||||
it('adds the url fragment to the login form actions', () => {
|
||||
appendUrlFragment('#L65');
|
||||
|
||||
expect(findFormAction()).toBe('http://test.host/users/sign_in#L65');
|
||||
});
|
||||
|
||||
it('does not add an empty url fragment to the login form actions', () => {
|
||||
appendUrlFragment();
|
||||
|
||||
expect(findFormAction()).toBe('http://test.host/users/sign_in');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add an empty url fragment to the login form actions', () => {
|
||||
preserveUrlFragment();
|
||||
describe('OAuth login forms', () => {
|
||||
const findFormAction = (selector) =>
|
||||
document.querySelector(`.js-oauth-login #oauth-login-${selector}`).parentElement.action;
|
||||
|
||||
expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in');
|
||||
});
|
||||
describe('appendRedirectQuery', () => {
|
||||
it('does not add an empty query parameter to the login form actions', () => {
|
||||
appendRedirectQuery();
|
||||
|
||||
it('does not add an empty query parameter to OmniAuth login buttons', () => {
|
||||
preserveUrlFragment();
|
||||
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0');
|
||||
});
|
||||
|
||||
expect(findFormAction('#oauth-login-auth0')).toBe('http://test.host/users/auth/auth0');
|
||||
});
|
||||
describe('adds "redirect_fragment" query parameter to the login form actions', () => {
|
||||
it('when "remember_me" is not present', () => {
|
||||
appendRedirectQuery('#L65');
|
||||
|
||||
describe('adds "redirect_fragment" query parameter to OmniAuth login buttons', () => {
|
||||
it('when "remember_me" is not present', () => {
|
||||
preserveUrlFragment('#L65');
|
||||
expect(findFormAction('auth0')).toBe(
|
||||
'http://test.host/users/auth/auth0?redirect_fragment=L65',
|
||||
);
|
||||
});
|
||||
|
||||
expect(findFormAction('#oauth-login-auth0')).toBe(
|
||||
'http://test.host/users/auth/auth0?redirect_fragment=L65',
|
||||
);
|
||||
it('when "remember_me" is present', () => {
|
||||
document
|
||||
.querySelectorAll('form')
|
||||
.forEach((form) => form.setAttribute('action', `${form.action}?remember_me=1`));
|
||||
|
||||
appendRedirectQuery('#L65');
|
||||
|
||||
expect(findFormAction('auth0')).toBe(
|
||||
'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('when "remember-me" is present', () => {
|
||||
$('.js-oauth-login form').attr('action', (i, href) => `${href}?remember_me=1`);
|
||||
describe('toggleRememberMeQuery', () => {
|
||||
const rememberMe = () => document.querySelector('#js-remember-me-omniauth');
|
||||
|
||||
preserveUrlFragment('#L65');
|
||||
it('toggles "remember_me" query parameter', () => {
|
||||
toggleRememberMeQuery();
|
||||
|
||||
expect(findFormAction('#oauth-login-auth0')).toBe(
|
||||
'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65',
|
||||
);
|
||||
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0');
|
||||
|
||||
rememberMe().click();
|
||||
|
||||
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0?remember_me=1');
|
||||
|
||||
rememberMe().click();
|
||||
|
||||
expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,14 +16,15 @@ const commit = {
|
|||
|
||||
const findTextExpander = () => wrapper.findComponent(GlButton);
|
||||
const findUserLink = () => wrapper.findByText(commit.author.name);
|
||||
const findCommitterWrapper = () => wrapper.findByTestId('committer');
|
||||
const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
|
||||
const findAuthorName = () => wrapper.findByText(`${commit.authorName} authored`);
|
||||
const findCommitRowDescription = () => wrapper.find('pre');
|
||||
const findTitleHtml = () => wrapper.findByText(commit.titleHtml);
|
||||
|
||||
const createComponent = async ({ commitMock = {}, prevBlameLink } = {}) => {
|
||||
const createComponent = async ({ commitMock = {}, prevBlameLink, span = 3 } = {}) => {
|
||||
wrapper = shallowMountExtended(CommitInfo, {
|
||||
propsData: { commit: { ...commit, ...commitMock }, prevBlameLink },
|
||||
propsData: { commit: { ...commit, ...commitMock }, prevBlameLink, span },
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
|
@ -46,6 +47,22 @@ describe('Repository last commit component', () => {
|
|||
expect(findAuthorName().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('truncates author name when commit spans less than 3 lines', () => {
|
||||
createComponent({ span: 2 });
|
||||
|
||||
expect(findCommitterWrapper().classes()).toEqual([
|
||||
'committer',
|
||||
'gl-flex-basis-full',
|
||||
'gl-display-inline-flex',
|
||||
]);
|
||||
expect(findUserLink().classes()).toEqual([
|
||||
'commit-author-link',
|
||||
'js-user-link',
|
||||
'gl-display-inline-block',
|
||||
'gl-text-truncate',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not render description expander when description is null', () => {
|
||||
createComponent();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import InternalEvents from '~/tracking/internal_events';
|
|||
import { LOAD_INTERNAL_EVENTS_SELECTOR } from '~/tracking/constants';
|
||||
import * as utils from '~/tracking/utils';
|
||||
import { Tracker } from '~/tracking/tracker';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
jest.mock('~/api', () => ({
|
||||
trackInternalEvent: jest.fn(),
|
||||
|
|
@ -20,13 +21,23 @@ const event = 'TestEvent';
|
|||
|
||||
describe('InternalEvents', () => {
|
||||
describe('trackEvent', () => {
|
||||
const category = 'TestCategory';
|
||||
|
||||
it('trackEvent calls API.trackInternalEvent with correct arguments', () => {
|
||||
InternalEvents.trackEvent(event);
|
||||
InternalEvents.trackEvent(event, category);
|
||||
|
||||
expect(API.trackInternalEvent).toHaveBeenCalledTimes(1);
|
||||
expect(API.trackInternalEvent).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('trackEvent calls Tracking.event with correct arguments including category', () => {
|
||||
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
|
||||
|
||||
InternalEvents.trackEvent(event, category);
|
||||
|
||||
expect(Tracking.event).toHaveBeenCalledWith(category, event, expect.any(Object));
|
||||
});
|
||||
|
||||
it('trackEvent calls trackBrowserSDK with correct arguments', () => {
|
||||
jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
|
||||
|
||||
|
|
@ -63,7 +74,7 @@ describe('InternalEvents', () => {
|
|||
await wrapper.findByTestId('button').trigger('click');
|
||||
|
||||
expect(trackEventSpy).toHaveBeenCalledTimes(1);
|
||||
expect(trackEventSpy).toHaveBeenCalledWith(event);
|
||||
expect(trackEventSpy).toHaveBeenCalledWith(event, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages
|
||||
incident_management_timeline_event_tags visible_forks inherited_ci_variables autocomplete_users
|
||||
ci_cd_settings detailed_import_status value_streams ml_models
|
||||
allows_multiple_merge_request_assignees allows_multiple_merge_request_reviewers
|
||||
allows_multiple_merge_request_assignees allows_multiple_merge_request_reviewers is_forked
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
@ -771,6 +771,57 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
end
|
||||
end
|
||||
|
||||
describe 'is_forked' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:unforked_project) { create(:project, :public) }
|
||||
let!(:forked_project) { fork_project(unforked_project) }
|
||||
let(:project) { nil }
|
||||
|
||||
let(:query) do
|
||||
%(
|
||||
query {
|
||||
project(fullPath: "#{project.full_path}") {
|
||||
isForked
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:response) { GitlabSchema.execute(query).as_json }
|
||||
|
||||
subject(:is_forked) { response.dig('data', 'project', 'isForked') }
|
||||
|
||||
context 'when project has a fork network' do
|
||||
context 'when fork is itself' do
|
||||
let(:project) { unforked_project }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when fork is not itself' do
|
||||
let(:project) { forked_project }
|
||||
|
||||
it { is_expected.to be true }
|
||||
|
||||
it 'avoids N+1 queries' do
|
||||
query_count = ActiveRecord::QueryRecorder.new { response }
|
||||
|
||||
expect(query_count).not_to exceed_query_limit(8)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project does not have a fork network' do
|
||||
let(:project) { unforked_project }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:fork_network).and_return(nil)
|
||||
end
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'branch_rules' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project, reload: true) { create(:project, :public) }
|
||||
|
|
|
|||
|
|
@ -860,13 +860,30 @@ RSpec.describe API::Helpers, feature_category: :shared do
|
|||
)
|
||||
end
|
||||
|
||||
it 'logs an exception for unknown event' do
|
||||
it 'tracks an exception and renders 422 for unknown event', :aggregate_failures do
|
||||
expect(Gitlab::InternalEvents).to receive(:track_event).and_raise(Gitlab::InternalEvents::UnknownEventError, "Unknown event: #{unknown_event}")
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
.with(
|
||||
instance_of(Gitlab::InternalEvents::UnknownEventError),
|
||||
event_name: unknown_event
|
||||
)
|
||||
expect(helper).to receive(:unprocessable_entity!).with("Unknown event: #{unknown_event}")
|
||||
|
||||
helper.track_event(unknown_event,
|
||||
user: user,
|
||||
namespace_id: namespace.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
it 'logs an exception for tracking errors' do
|
||||
expect(Gitlab::InternalEvents).to receive(:track_event).and_raise(ArgumentError, "Error message")
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
.with(
|
||||
instance_of(ArgumentError),
|
||||
event_name: unknown_event
|
||||
)
|
||||
|
||||
helper.track_event(unknown_event,
|
||||
user: user,
|
||||
|
|
|
|||
|
|
@ -87,6 +87,12 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
|
|||
expect(member.expires_at).to eq(nil)
|
||||
end
|
||||
|
||||
it 'does not send new member notification' do
|
||||
expect(NotificationService).not_to receive(:new)
|
||||
|
||||
subject.load(context, member_data)
|
||||
end
|
||||
|
||||
context 'when user_id is current user id' do
|
||||
it 'does not create new membership' do
|
||||
data = { user_id: user.id }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLockedStatus do
|
||||
RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLockedStatus, feature_category: :build_artifacts do
|
||||
describe '#perform' do
|
||||
let(:batch_table) { :ci_pipeline_artifacts }
|
||||
let(:batch_column) { :id }
|
||||
|
|
@ -30,11 +30,11 @@ RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLock
|
|||
let(:locked_pipeline) { pipelines.create!(locked: locked, partition_id: 100) }
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
let!(:locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 1024, file_type: 0, file_format: 'gzip', file: 'a.gz', locked: unknown) }
|
||||
let!(:unlocked_artifact_1) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 2048, file_type: 1, file_format: 'raw', file: 'b', locked: unknown) }
|
||||
let!(:unlocked_artifact_2) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 4096, file_type: 2, file_format: 'gzip', file: 'c.gz', locked: unknown) }
|
||||
let!(:already_unlocked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: unlocked) }
|
||||
let!(:already_locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: locked) }
|
||||
let!(:locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 1024, file_type: 0, file_format: 'gzip', file: 'a.gz', locked: unknown, partition_id: 100) }
|
||||
let!(:unlocked_artifact_1) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 2048, file_type: 1, file_format: 'raw', file: 'b', locked: unknown, partition_id: 100) }
|
||||
let!(:unlocked_artifact_2) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 4096, file_type: 2, file_format: 'gzip', file: 'c.gz', locked: unknown, partition_id: 100) }
|
||||
let!(:already_unlocked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: unlocked, partition_id: 100) }
|
||||
let!(:already_locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: locked, partition_id: 100) }
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
subject do
|
||||
|
|
|
|||
|
|
@ -25,4 +25,30 @@ RSpec.describe ForkNetworkMember do
|
|||
expect(ForkNetwork.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#by_projects' do
|
||||
let_it_be(:fork_network_member_1) { create(:fork_network_member) }
|
||||
let_it_be(:fork_network_member_2) { create(:fork_network_member) }
|
||||
|
||||
it 'returns fork network members by project ids' do
|
||||
expect(
|
||||
described_class.by_projects(
|
||||
[fork_network_member_1.project_id, fork_network_member_2.project_id]
|
||||
)
|
||||
).to match_array([fork_network_member_1, fork_network_member_2])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#with_fork_network' do
|
||||
let_it_be(:fork_network_member_1) { create(:fork_network_member) }
|
||||
let_it_be(:fork_network_member_2) { create(:fork_network_member) }
|
||||
|
||||
it 'avoids N+1 queries' do
|
||||
query_count = ActiveRecord::QueryRecorder.new do
|
||||
described_class.all.with_fork_network.find_each(&:fork_network)
|
||||
end
|
||||
|
||||
expect(query_count).not_to exceed_query_limit(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,42 @@ RSpec.describe 'groups autocomplete', feature_category: :groups_and_projects do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
describe '#members' do
|
||||
context 'when type is WorkItem' do
|
||||
let(:type) { 'Workitem' }
|
||||
|
||||
it 'returns the correct response', :aggregate_failures do
|
||||
work_item = create(:work_item, :group_level, namespace: group, author: user)
|
||||
|
||||
get members_group_autocomplete_sources_path(group, type_id: work_item.iid, type: type)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_an(Array)
|
||||
expect(json_response).to contain_exactly(
|
||||
hash_including('type' => 'User', 'username' => user.username),
|
||||
hash_including('type' => 'Group', 'username' => group.full_path)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when type is Issue' do
|
||||
let(:type) { 'Issue' }
|
||||
|
||||
it 'returns the correct response', :aggregate_failures do
|
||||
issue = create(:issue, :group_level, namespace: group, author: user)
|
||||
|
||||
get members_group_autocomplete_sources_path(group, type_id: issue.iid, type: type)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_an(Array)
|
||||
expect(json_response).to contain_exactly(
|
||||
hash_including('type' => 'User', 'username' => user.username),
|
||||
hash_including('type' => 'Group', 'username' => group.full_path)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issues' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:service) { described_class.new(project, user) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be_with_reload(:project) { create(:project, group: group) }
|
||||
let_it_be(:user) { create(:user).tap { |u| project.add_maintainer(u) } }
|
||||
let(:container) { project }
|
||||
let(:service) { described_class.new(container: container, current_user: user) }
|
||||
|
||||
describe '#execute' do
|
||||
shared_examples 'no target' do |type_iid:|
|
||||
|
|
@ -32,7 +30,7 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
|
|||
it 'builds a new target' do
|
||||
target = service.execute(type, type_iid)
|
||||
|
||||
expect(target.project).to eq(project)
|
||||
expect(target.resource_parent).to eq(container)
|
||||
expect(target).to be_new_record
|
||||
end
|
||||
end
|
||||
|
|
@ -45,6 +43,15 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
|
|||
it_behaves_like 'find target'
|
||||
it_behaves_like 'build target', type_iid: nil
|
||||
it_behaves_like 'build target', type_iid: -1
|
||||
|
||||
context 'when issue belongs to a group' do
|
||||
let(:container) { group }
|
||||
let(:target) { create(:issue, :group_level, namespace: group) }
|
||||
|
||||
it_behaves_like 'find target'
|
||||
it_behaves_like 'build target', type_iid: nil
|
||||
it_behaves_like 'build target', type_iid: -1
|
||||
end
|
||||
end
|
||||
|
||||
context 'for work item' do
|
||||
|
|
@ -53,6 +60,13 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
|
|||
let(:type) { 'WorkItem' }
|
||||
|
||||
it_behaves_like 'find target'
|
||||
|
||||
context 'when work item belongs to a group' do
|
||||
let(:container) { group }
|
||||
let(:target) { create(:work_item, :group_level, namespace: group) }
|
||||
|
||||
it_behaves_like 'find target'
|
||||
end
|
||||
end
|
||||
|
||||
context 'for merge request' do
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ module LoginHelpers
|
|||
visit new_user_session_path
|
||||
expect(page).to have_css('.js-oauth-login')
|
||||
|
||||
check 'remember_me_omniauth' if remember_me
|
||||
check 'js-remember-me-omniauth' if remember_me
|
||||
|
||||
click_button "oauth-login-#{provider}"
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue