Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-13 09:07:54 +00:00
parent 1a397155d6
commit 2705a15dea
67 changed files with 979 additions and 531 deletions

View File

@ -11,12 +11,6 @@ Gitlab/PolicyRuleBoolean:
Exclude:
- 'ee/app/policies/ee/identity_provider_policy.rb'
# Offense count: 218
# Cop supports --auto-correct.
# Configuration parameters: PreferredName.
Naming/RescuedExceptionsVariableName:
Enabled: false
# Offense count: 29
# Configuration parameters: MinSize.
Performance/CollectionLiteralInLoop:

View File

@ -0,0 +1,222 @@
---
# Cop supports --auto-correct.
Naming/RescuedExceptionsVariableName:
# Offense count: 269
# Temporarily disabled due to too many offenses
Enabled: false
Exclude:
- 'app/controllers/admin/projects_controller.rb'
- 'app/controllers/projects/google_cloud/deployments_controller.rb'
- 'app/controllers/projects/google_cloud/service_accounts_controller.rb'
- 'app/controllers/projects/merge_requests/drafts_controller.rb'
- 'app/controllers/projects/milestones_controller.rb'
- 'app/controllers/projects/mirrors_controller.rb'
- 'app/controllers/projects/repositories_controller.rb'
- 'app/controllers/projects_controller.rb'
- 'app/finders/repositories/changelog_tag_finder.rb'
- 'app/graphql/mutations/issues/move.rb'
- 'app/graphql/resolvers/ci/config_resolver.rb'
- 'app/graphql/resolvers/environments_resolver.rb'
- 'app/helpers/application_helper.rb'
- 'app/models/application_setting.rb'
- 'app/models/blob_viewer/metrics_dashboard_yml.rb'
- 'app/models/ci/build.rb'
- 'app/models/ci/deleted_object.rb'
- 'app/models/clusters/concerns/elasticsearch_client.rb'
- 'app/models/concerns/prometheus_adapter.rb'
- 'app/models/concerns/repository_storage_movable.rb'
- 'app/models/concerns/x509_serial_number_attribute.rb'
- 'app/models/integrations/base_issue_tracker.rb'
- 'app/models/integrations/discord.rb'
- 'app/models/integrations/jenkins.rb'
- 'app/models/integrations/jira.rb'
- 'app/models/integrations/packagist.rb'
- 'app/models/integrations/pipelines_email.rb'
- 'app/models/integrations/prometheus.rb'
- 'app/models/performance_monitoring/prometheus_dashboard.rb'
- 'app/models/personal_access_token.rb'
- 'app/models/project.rb'
- 'app/models/repository.rb'
- 'app/models/snippet_repository.rb'
- 'app/models/u2f_registration.rb'
- 'app/models/wiki.rb'
- 'app/services/branches/delete_service.rb'
- 'app/services/branches/validate_new_service.rb'
- 'app/services/ci/job_artifacts/create_service.rb'
- 'app/services/ci/parse_dotenv_artifact_service.rb'
- 'app/services/ci/register_job_service.rb'
- 'app/services/ci/stuck_builds/drop_helpers.rb'
- 'app/services/clusters/applications/prometheus_update_service.rb'
- 'app/services/commits/change_service.rb'
- 'app/services/commits/create_service.rb'
- 'app/services/dependency_proxy/head_manifest_service.rb'
- 'app/services/dependency_proxy/request_token_service.rb'
- 'app/services/design_management/copy_design_collection/copy_service.rb'
- 'app/services/git/base_hooks_service.rb'
- 'app/services/grafana/proxy_service.rb'
- 'app/services/groups/update_shared_runners_service.rb'
- 'app/services/issues/relative_position_rebalancing_service.rb'
- 'app/services/jira/requests/base.rb'
- 'app/services/jira_import/start_import_service.rb'
- 'app/services/jira_import/users_importer.rb'
- 'app/services/lfs/lock_file_service.rb'
- 'app/services/lfs/locks_finder_service.rb'
- 'app/services/lfs/push_service.rb'
- 'app/services/lfs/unlock_file_service.rb'
- 'app/services/merge_requests/merge_to_ref_service.rb'
- 'app/services/merge_requests/mergeability_check_service.rb'
- 'app/services/metrics/dashboard/base_service.rb'
- 'app/services/metrics/dashboard/panel_preview_service.rb'
- 'app/services/projects/cleanup_service.rb'
- 'app/services/projects/destroy_service.rb'
- 'app/services/projects/hashed_storage/base_repository_service.rb'
- 'app/services/projects/transfer_service.rb'
- 'app/services/prometheus/proxy_service.rb'
- 'app/services/resource_access_tokens/revoke_service.rb'
- 'app/services/tags/create_service.rb'
- 'app/services/tags/destroy_service.rb'
- 'app/services/users/validate_manual_otp_service.rb'
- 'app/services/users/validate_push_otp_service.rb'
- 'app/services/verify_pages_domain_service.rb'
- 'app/validators/js_regex_validator.rb'
- 'app/workers/concerns/limited_capacity/worker.rb'
- 'app/workers/gitlab/jira_import/import_issue_worker.rb'
- 'app/workers/issuable_export_csv_worker.rb'
- 'app/workers/namespaces/root_statistics_worker.rb'
- 'app/workers/namespaces/schedule_aggregation_worker.rb'
- 'app/workers/packages/go/sync_packages_worker.rb'
- 'app/workers/project_destroy_worker.rb'
- 'app/workers/project_service_worker.rb'
- 'app/workers/projects/git_garbage_collect_worker.rb'
- 'app/workers/remove_expired_members_worker.rb'
- 'app/workers/users/create_statistics_worker.rb'
- 'config/initializers/rspec_profiling.rb'
- 'config/initializers/wikicloth_redos_patch.rb'
- 'db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb'
- 'db/post_migrate/20211206162601_cleanup_after_add_primary_email_to_emails_if_user_confirmed.rb'
- 'db/post_migrate/20220318111729_cleanup_after_fixing_issue_when_admin_changed_primary_email.rb'
- 'db/post_migrate/20220504083836_cleanup_after_fixing_regression_with_new_users_emails.rb'
- 'ee/app/finders/projects/integrations/jira/by_ids_finder.rb'
- 'ee/app/graphql/mutations/issues/promote_to_epic.rb'
- 'ee/app/graphql/mutations/issues/set_epic.rb'
- 'ee/app/helpers/ee/kerberos_spnego_helper.rb'
- 'ee/app/models/concerns/geo/replicable_model.rb'
- 'ee/app/models/integrations/github.rb'
- 'ee/app/services/app_sec/dast/profiles/create_service.rb'
- 'ee/app/services/app_sec/dast/profiles/update_service.rb'
- 'ee/app/services/app_sec/dast/scans/create_service.rb'
- 'ee/app/services/app_sec/dast/site_validations/find_or_create_service.rb'
- 'ee/app/services/app_sec/dast/site_validations/revoke_service.rb'
- 'ee/app/services/app_sec/fuzzing/coverage/corpuses/create_service.rb'
- 'ee/app/services/arkose/user_verification_service.rb'
- 'ee/app/services/ci/sync_reports_to_approval_rules_service.rb'
- 'ee/app/services/elastic/process_bookkeeping_service.rb'
- 'ee/app/services/geo/file_registry_removal_service.rb'
- 'ee/app/services/geo/framework_repository_sync_service.rb'
- 'ee/app/services/geo/move_repository_service.rb'
- 'ee/app/services/geo/repository_base_sync_service.rb'
- 'ee/app/services/incident_management/oncall_rotations/create_service.rb'
- 'ee/app/services/incident_management/oncall_rotations/edit_service.rb'
- 'ee/app/services/namespaces/deactivate_members_over_limit_service.rb'
- 'ee/app/services/namespaces/remove_project_group_links_outside_hierarchy_service.rb'
- 'ee/app/services/namespaces/update_prevent_sharing_outside_hierarchy_service.rb'
- 'ee/app/services/projects/licenses/create_policy_service.rb'
- 'ee/app/services/projects/licenses/update_policy_service.rb'
- 'ee/app/services/security/ingestion/ingest_report_service.rb'
- 'ee/app/services/security/orchestration/assign_service.rb'
- 'ee/app/services/security/store_grouped_scans_service.rb'
- 'ee/app/services/security/store_scan_service.rb'
- 'ee/app/services/security/token_revocation_service.rb'
- 'ee/app/services/software_license_policies/create_service.rb'
- 'ee/app/services/software_license_policies/update_service.rb'
- 'ee/app/workers/adjourned_project_deletion_worker.rb'
- 'ee/app/workers/geo/file_removal_worker.rb'
- 'ee/app/workers/geo/repositories_clean_up_worker.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- 'ee/app/workers/namespaces/free_user_cap_worker.rb'
- 'ee/app/workers/refresh_license_compliance_checks_worker.rb'
- 'ee/app/workers/repository_update_mirror_worker.rb'
- 'ee/app/workers/sync_seat_link_request_worker.rb'
- 'ee/lib/ee/gitlab/background_migration/populate_uuids_for_security_findings.rb'
- 'ee/lib/elastic/instance_proxy_util.rb'
- 'ee/lib/gitlab/audit/auditor.rb'
- 'ee/lib/gitlab/auth/smartcard/base.rb'
- 'ee/lib/gitlab/ci/parsers/license_compliance/license_scanning.rb'
- 'ee/lib/gitlab/elastic/bulk_indexer.rb'
- 'ee/lib/gitlab/spdx/catalogue_gateway.rb'
- 'ee/lib/tasks/gitlab/seed/metrics.rake'
- 'lib/api/environments.rb'
- 'lib/api/helpers.rb'
- 'lib/api/helpers/label_helpers.rb'
- 'lib/api/issues.rb'
- 'lib/api/project_milestones.rb'
- 'lib/api/projects.rb'
- 'lib/api/repositories.rb'
- 'lib/api/v3/github.rb'
- 'lib/gitaly/server.rb'
- 'lib/gitlab/auth/ldap/adapter.rb'
- 'lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb'
- 'lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp.rb'
- 'lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans.rb'
- 'lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb'
- 'lib/gitlab/ci/config/external/file/artifact.rb'
- 'lib/gitlab/ci/pipeline/chain/config/process.rb'
- 'lib/gitlab/ci/pipeline/chain/validate/external.rb'
- 'lib/gitlab/ci/reports/codequality_reports.rb'
- 'lib/gitlab/database/background_migration/batched_job.rb'
- 'lib/gitlab/database/background_migration/batched_migration_wrapper.rb'
- 'lib/gitlab/database/batch_counter.rb'
- 'lib/gitlab/database/load_balancing/load_balancer.rb'
- 'lib/gitlab/database/load_balancing/service_discovery.rb'
- 'lib/gitlab/database/reindexing/grafana_notifier.rb'
- 'lib/gitlab/git/keep_around.rb'
- 'lib/gitlab/gitaly_client/call.rb'
- 'lib/gitlab/gitaly_client/commit_service.rb'
- 'lib/gitlab/gitaly_client/operation_service.rb'
- 'lib/gitlab/gitaly_client/ref_service.rb'
- 'lib/gitlab/gitaly_client/repository_service.rb'
- 'lib/gitlab/hashed_storage/migrator.rb'
- 'lib/gitlab/health_checks/base_abstract_check.rb'
- 'lib/gitlab/import_export/merge_request_parser.rb'
- 'lib/gitlab/instrumentation/redis_interceptor.rb'
- 'lib/gitlab/issuables_count_for_state.rb'
- 'lib/gitlab/jira_import/issues_importer.rb'
- 'lib/gitlab/json.rb'
- 'lib/gitlab/jwt_token.rb'
- 'lib/gitlab/kubernetes/namespace.rb'
- 'lib/gitlab/metrics/dashboard/stages/panel_ids_inserter.rb'
- 'lib/gitlab/metrics/rack_middleware.rb'
- 'lib/gitlab/middleware/handle_ip_spoof_attack_error.rb'
- 'lib/gitlab/prometheus/queries/validate_query.rb'
- 'lib/gitlab/prometheus_client.rb'
- 'lib/gitlab/sanitizers/exif.rb'
- 'lib/gitlab/sidekiq_logging/structured_logger.rb'
- 'lib/gitlab/tcp_checker.rb'
- 'lib/gitlab/template_parser/parser.rb'
- 'lib/gitlab/tracking.rb'
- 'lib/gitlab/url_blocker.rb'
- 'lib/gitlab/usage/metrics/aggregates/aggregate.rb'
- 'lib/gitlab/usage_data.rb'
- 'lib/gitlab/utils/usage_data.rb'
- 'lib/gitlab/verify/batch_verifier.rb'
- 'lib/gitlab/wiki_pages/front_matter_parser.rb'
- 'lib/microsoft_teams/notifier.rb'
- 'lib/system_check/incoming_email/imap_authentication_check.rb'
- 'lib/tasks/gitlab/db/validate_config.rake'
- 'lib/tasks/gitlab/setup.rake'
- 'lib/tasks/gitlab/storage.rake'
- 'lib/tasks/lint.rake'
- 'qa/qa/resource/user_gpg.rb'
- 'scripts/review_apps/automated_cleanup.rb'
- 'scripts/trigger-build.rb'
- 'spec/commands/metrics_server/metrics_server_spec.rb'
- 'spec/db/docs_spec.rb'
- 'spec/lib/bulk_imports/network_error_spec.rb'
- 'spec/lib/gitlab/database/load_balancing/host_spec.rb'
- 'spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
- 'spec/lib/gitlab/error_tracking_spec.rb'
- 'spec/lib/gitlab/sanitizers/exception_message_spec.rb'
- 'spec/support/capybara.rb'
- 'spec/support/helpers/capybara_helpers.rb'
- 'tooling/lib/tooling/helm3_client.rb'
- 'tooling/lib/tooling/kubernetes_client.rb'

View File

@ -1,9 +1,12 @@
## Contributor license agreement
## Contributor License Agreement and Developer Certificate of Origin
By submitting code as an individual you agree to the
[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
By submitting code as an entity you agree to the
[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
Contributions to this repository are subject to the [Developer Certificate of Origin](https://docs.gitlab.com/ee/legal/developer_certificate_of_origin.html#developer-certificate-of-origin-version-11), or the [Individual](https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html) or [Corporate](https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html) Contributor License Agreement, depending on where the contribution is made and on whose behalf:
- By submitting code contributions as an individual to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Individual Contributor License Agreement](https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html).
- By submitting code contributions on behalf of a corporation to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Corporate Contributor License Agreement](https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html).
- By submitting code contributions as an individual or on behalf of a corporation to any directory in this repository outside of the [`/ee` subdirectory](/ee), you agree to the [Developer Certificate of Origin](https://docs.gitlab.com/ee/legal/developer_certificate_of_origin.html#developer-certificate-of-origin-version-11).
All Documentation content that resides under the [`doc/` directory](/doc) of this
repository is licensed under Creative Commons:

View File

@ -55,7 +55,7 @@ export default {
return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
return `${path}?crm_contact_id=${value}`;
},
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };

View File

@ -55,7 +55,7 @@ export default {
return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_organization_id=${value}`;
return `${path}?crm_organization_id=${value}`;
},
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };

View File

@ -1,37 +1,48 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { GlBadge, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import { __ } from '~/locale';
import { IssuableType } from '~/issues/constants';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
export const statusBoxState = Vue.observable({
export const badgeState = Vue.observable({
state: '',
updateStatus: null,
});
const CLASSES = {
opened: 'status-box-open',
merge_request_opened: 'badge-success',
locked: 'status-box-open',
merge_request_locked: 'badge-success',
closed: 'status-box-mr-closed',
merge_request_closed: 'badge-danger',
merged: 'badge-info',
opened: 'issuable-status-badge-open',
locked: 'issuable-status-badge-open',
closed: 'issuable-status-badge-closed',
merged: 'issuable-status-badge-merged',
};
const ISSUE_ICONS = {
opened: 'issues',
locked: 'issues',
closed: 'issue-closed',
};
const MERGE_REQUEST_ICONS = {
opened: 'merge-request-open',
locked: 'merge-request-open',
closed: 'merge-request-close',
merged: 'merge',
};
const STATUS = {
opened: [__('Open'), 'issue-open-m'],
locked: [__('Open'), 'issue-open-m'],
closed: [__('Closed'), 'issue-close'],
merged: [__('Merged'), 'git-merge'],
opened: __('Open'),
locked: __('Open'),
closed: __('Closed'),
merged: __('Merged'),
};
export default {
components: {
GlBadge,
GlIcon,
},
mixins: [glFeatureFlagMixin()],
inject: {
query: { default: null },
projectPath: { default: null },
@ -51,39 +62,41 @@ export default {
},
data() {
if (this.initialState) {
statusBoxState.state = this.initialState;
badgeState.state = this.initialState;
}
return statusBoxState;
return badgeState;
},
computed: {
isMergeRequest() {
return this.issuableType === 'merge_request' && this.glFeatures.updatedMrHeader;
badgeClass() {
return CLASSES[this.state];
},
statusBoxClass() {
return [
CLASSES[`${this.issuableType}_${this.state}`] || CLASSES[this.state],
{
'badge badge-pill gl-badge gl-mr-3': this.isMergeRequest,
'issuable-status-box status-box': !this.isMergeRequest,
},
];
badgeVariant() {
if (this.state === IssuableStates.Opened) {
return 'success';
} else if (this.state === IssuableStates.Closed) {
return this.issuableType === IssuableType.MergeRequest ? 'danger' : 'info';
}
return 'info';
},
statusHumanName() {
return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[0];
badgeText() {
return STATUS[this.state];
},
statusIconName() {
return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[1];
badgeIcon() {
if (this.issuableType === IssuableType.Issue) {
return ISSUE_ICONS[this.state];
}
return MERGE_REQUEST_ICONS[this.state];
},
},
created() {
if (!statusBoxState.updateStatus) {
statusBoxState.updateStatus = this.fetchState;
if (!badgeState.updateStatus) {
badgeState.updateStatus = this.fetchState;
}
},
beforeDestroy() {
if (statusBoxState.updateStatus && this.query) {
statusBoxState.updateStatus = null;
if (badgeState.updateStatus && this.query) {
badgeState.updateStatus = null;
}
},
methods: {
@ -97,21 +110,15 @@ export default {
fetchPolicy: fetchPolicies.NO_CACHE,
});
statusBoxState.state = data?.workspace?.issuable?.state;
badgeState.state = data?.workspace?.issuable?.state;
},
},
};
</script>
<template>
<div :class="statusBoxClass">
<gl-icon
v-if="!isMergeRequest"
:name="statusIconName"
class="gl-display-block gl-sm-display-none!"
/>
<span :class="{ 'gl-display-none gl-sm-display-block': !isMergeRequest }">
{{ statusHumanName }}
</span>
</div>
<gl-badge class="issuable-status-badge gl-mr-3" :class="badgeClass" :variant="badgeVariant">
<gl-icon :name="badgeIcon" />
<span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeText }}</span>
</gl-badge>
</template>

View File

@ -49,8 +49,8 @@ export default class Issue {
issueFailMessage = __('Unable to update this issue at this time.'),
) {
if ('id' in data) {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const isClosedBadge = $('.issuable-status-badge-closed');
const isOpenBadge = $('.issuable-status-badge-open');
const projectIssuesCounter = $('.issue_counter');
isClosedBadge.toggleClass('hidden', !isClosed);

View File

@ -1,5 +1,5 @@
<script>
import { GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import {
@ -27,6 +27,7 @@ export default {
WorkspaceType,
components: {
GlIcon,
GlBadge,
GlIntersectionObserver,
titleComponent,
editedComponent,
@ -267,7 +268,10 @@ export default {
: '';
},
statusIcon() {
return this.isClosed ? 'issue-close' : 'issue-open-m';
return this.isClosed ? 'issue-closed' : 'issues';
},
statusVariant() {
return this.isClosed ? 'info' : 'success';
},
statusText() {
return IssuableStatusText[this.issuableStatus];
@ -517,13 +521,9 @@ export default {
<div
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
>
<p
class="issuable-status-box status-box gl-white-space-nowrap gl-my-0"
:class="[isClosed ? 'status-box-issue-closed' : 'status-box-open']"
<gl-badge :icon="statusIcon" :variant="statusVariant" class="gl-mr-2"
><span class="gl-display-none gl-sm-display-block">{{ statusText }}</span></gl-badge
>
<gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
<span class="gl-display-none d-sm-block">{{ statusText }}</span>
</p>
<span v-if="isLocked" data-testid="locked" class="issuable-warning-icon">
<gl-icon name="lock" :aria-label="__('Locked')" />
</span>

View File

@ -6,7 +6,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import createFlash from '~/flash';
import { statusBoxState } from '~/issuable/components/status_box.vue';
import { badgeState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
@ -266,7 +266,7 @@ export default {
const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable;
toggleState()
.then(() => statusBoxState.updateStatus && statusBoxState.updateStatus())
.then(() => badgeState.updateStatus && badgeState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() =>
createFlash({

View File

@ -1,6 +1,6 @@
import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
import { statusBoxState } from '~/issuable/components/status_box.vue';
import { badgeState } from '~/issuable/components/status_box.vue';
import { isInMRPage } from '~/lib/utils/common_utils';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@ -85,8 +85,7 @@ export const getBlockedByIssues = (state) => state.noteableData.blocked_by_issue
export const userCanReply = (state) => Boolean(state.noteableData.current_user.can_create_note);
export const openState = (state) =>
isInMRPage() ? statusBoxState.state : state.noteableData.state;
export const openState = (state) => (isInMRPage() ? badgeState.state : state.noteableData.state);
export const getUserData = (state) => state.userData || {};

View File

@ -20,23 +20,25 @@ export default function initMergeRequestShow() {
initAwardsApp(document.getElementById('js-vue-awards-block'));
const el = document.querySelector('.js-mr-status-box');
const { iid, issuableType, projectPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
new Vue({
el,
name: 'IssuableStatusBoxRoot',
apolloProvider,
provide: {
query: getStateQuery,
projectPath: el.dataset.projectPath,
iid: el.dataset.iid,
iid,
projectPath,
},
render(h) {
return h(StatusBox, {
props: {
initialState: el.dataset.state,
issuableType: 'merge_request',
issuableType,
},
});
},

View File

@ -2,7 +2,7 @@
import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscription.graphql';
@ -21,6 +21,10 @@ export default {
type: String,
required: true,
},
groupIssuesPath: {
type: String,
required: true,
},
},
data() {
return {
@ -85,6 +89,10 @@ export default {
Boolean,
);
},
getIssuesPath(contactId) {
const id = getIdFromGraphQLId(contactId);
return `${this.groupIssuesPath}?crm_contact_id=${id}`;
},
},
};
</script>
@ -110,8 +118,8 @@ export default {
:key="index"
class="gl-pr-2"
>
<span :id="`contact_${index}`" class="gl-font-weight-bold"
>{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</span
<gl-link :id="`contact_${index}`" :href="getIssuesPath(contact.id)"
>{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</gl-link
>
<gl-popover
v-if="shouldShowPopover(contact)"

View File

@ -218,7 +218,7 @@ function mountCrmContactsComponent() {
if (!el) return;
const { issueId } = el.dataset;
const { issueId, groupIssuesPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
@ -231,6 +231,7 @@ function mountCrmContactsComponent() {
createElement('crm-contacts', {
props: {
issueId,
groupIssuesPath,
},
}),
});

View File

@ -1,5 +1,5 @@
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { statusBoxState } from '~/issuable/components/status_box.vue';
import { badgeState } from '~/issuable/components/status_box.vue';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
import {
@ -221,8 +221,8 @@ export default class MergeRequestStore {
}
updateStatusState(state) {
if (this.mergeRequestState !== state && statusBoxState.updateStatus) {
statusBoxState.updateStatus();
if (this.mergeRequestState !== state && badgeState.updateStatus) {
badgeState.updateStatus();
}
}

View File

@ -1,14 +1,23 @@
<script>
import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import {
GlIcon,
GlBadge,
GlButton,
GlTooltipDirective,
GlAvatarLink,
GlAvatarLabeled,
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isExternal } from '~/lib/utils/url_utility';
import { n__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
export default {
components: {
GlIcon,
GlBadge,
GlButton,
GlAvatarLink,
GlAvatarLabeled,
@ -26,6 +35,11 @@ export default {
type: Object,
required: true,
},
issuableState: {
type: String,
required: false,
default: '',
},
statusBadgeClass: {
type: String,
required: false,
@ -36,6 +50,11 @@ export default {
required: false,
default: '',
},
statusIconClass: {
type: String,
required: false,
default: '',
},
blocked: {
type: Boolean,
required: false,
@ -53,6 +72,9 @@ export default {
},
},
computed: {
badgeVariant() {
return this.issuableState === IssuableStates.Opened ? 'success' : 'info';
},
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
},
@ -91,10 +113,15 @@ export default {
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
<div data-testid="status" class="issuable-status-box status-box" :class="statusBadgeClass">
<gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" />
<span class="d-none d-sm-block"><slot name="status-badge"></slot></span>
</div>
<gl-badge
data-testid="status"
class="issuable-status-badge gl-mr-3"
:class="statusBadgeClass"
:variant="badgeVariant"
>
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block"><slot name="status-badge"></slot></span>
</gl-badge>
<div class="issuable-meta gl-display-flex gl-align-items-center d-md-inline-block">
<div v-if="blocked || confidential" class="gl-display-inline-block">
<div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline">

View File

@ -27,6 +27,11 @@ export default {
required: false,
default: '',
},
statusIconClass: {
type: String,
required: false,
default: '',
},
enableEdit: {
type: Boolean,
required: false,
@ -102,8 +107,10 @@ export default {
<template>
<div class="issuable-show-container" data-qa-selector="issuable_show_container">
<issuable-header
:issuable-state="issuable.state"
:status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:blocked="issuable.blocked"
:confidential="issuable.confidential"
:created-at="issuable.createdAt"
@ -122,6 +129,7 @@ export default {
:issuable="issuable"
:status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:enable-edit="enableEdit"
:enable-autocomplete="enableAutocomplete"
:enable-autosave="enableAutosave"

View File

@ -26,6 +26,7 @@
.detail-page-header-body {
position: relative;
display: flex;
align-items: center;
flex: 1 1;
min-width: 0;

View File

@ -69,6 +69,7 @@ module BadgesHelper
icon_only = options[:icon_only]
variant_class = VARIANT_CLASSES[options.fetch(:variant, :muted)]
size_class = SIZE_CLASSES[options.fetch(:size, :md)]
icon_classes = GL_ICON_CLASSES.dup << options.fetch(:icon_classes, nil)
html_options = html_options.merge(
class: [
@ -85,7 +86,6 @@ module BadgesHelper
end
if options[:icon]
icon_classes = GL_ICON_CLASSES.dup
icon_classes << "gl-mr-2" unless icon_only
icon = sprite_icon(options[:icon], css_class: icon_classes.join(' '))

View File

@ -341,14 +341,20 @@ module IssuablesHelper
end
def state_name_with_icon(issuable)
if issuable.is_a?(MergeRequest) && issuable.merged?
[_("Merged"), "git-merge"]
elsif issuable.is_a?(MergeRequest) && issuable.closed?
[_("Closed"), "close"]
elsif issuable.closed?
[_("Closed"), "mobile-issue-close"]
if issuable.is_a?(MergeRequest)
if issuable.open?
[_("Open"), "merge-request-open"]
elsif issuable.merged?
[_("Merged"), "merge"]
else
[_("Closed"), "merge-request-close"]
end
else
[_("Open"), "issue-open-m"]
if issuable.open?
[_("Open"), "issues"]
else
[_("Closed"), "issue-closed"]
end
end
end

View File

@ -35,7 +35,15 @@ module Routing
end
def issue_url(entity, *args)
project_issue_url(entity.project, entity, *args)
if use_work_items_path?(entity)
work_item_url(entity, *args)
else
project_issue_url(entity.project, entity, *args)
end
end
def work_item_url(entity, *args)
project_work_items_url(entity.project, entity.id, *args)
end
def merge_request_url(entity, *args)
@ -77,5 +85,11 @@ module Routing
toggle_subscription_project_merge_request_path(entity.project, entity)
end
end
private
def use_work_items_path?(issue)
issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
end
end
end

View File

@ -4,9 +4,7 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents ::Issue, as: :issue
def issue_path
return url_builder.build(issue, only_path: true) unless use_work_items_path?
project_work_items_path(issue.project, work_items_path: issue.id)
web_path
end
delegator_override :subscribed?
@ -17,18 +15,6 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
def project_emails_disabled?
issue.project.emails_disabled?
end
def web_url
return super unless use_work_items_path?
project_work_items_url(issue.project, work_items_path: issue.id)
end
private
def use_work_items_path?
issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
end
end
IssuePresenter.prepend_mod_with('IssuePresenter')

View File

@ -37,7 +37,7 @@ class IssueBoardEntity < Grape::Entity
end
expose :real_path, if: -> (issue) { issue.project } do |issue|
project_issue_path(issue.project, issue)
Gitlab::UrlBuilder.build(issue, only_path: true)
end
expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|

View File

@ -31,7 +31,7 @@ class IssueEntity < IssuableEntity
end
expose :web_url do |issue|
project_issue_path(issue.project, issue)
Gitlab::UrlBuilder.build(issue, only_path: true)
end
expose :current_user do

View File

@ -18,7 +18,7 @@ class LinkedIssueEntity < Grape::Entity
end
expose :path do |link|
project_issue_path(link.project, link.iid)
Gitlab::UrlBuilder.build(link, only_path: true)
end
expose :relation_path

View File

@ -82,7 +82,13 @@ module Members
if member.request?
approve_request
else
member.save
# Calling #save triggers callbacks even if there is no change on object.
# This previously caused an incident due to the hard to predict
# behaviour caused by the large number of callbacks.
# See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6351
# and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80920#note_911569038
# for details.
member.save if member.changed?
end
end

View File

@ -1,9 +1,8 @@
.detail-page-description.py-2
- if Feature.enabled?(:updated_mr_header, @project)
- state_human_name, _ = state_name_with_icon(@merge_request)
.badge.badge-pill.gl-badge.gl-mr-3.js-mr-status-box{ class: status_box_class(@merge_request), data: { project_path: @merge_request.project.path_with_namespace, iid: @merge_request.iid, state: @merge_request.state } }>
= state_human_name
= merge_request_header(@project, @merge_request)
= render 'shared/issuable/status_box', issuable: @merge_request
.gl-display-inline.gl-vertical-align-top
= merge_request_header(@project, @merge_request)
- else
%h2.title.mb-0{ data: { qa_selector: 'title_content' } }
= markdown_field(@merge_request, :title)

View File

@ -45,7 +45,7 @@
- if issuable_sidebar[:show_crm_contacts]
.block.contact
#js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id] } }
#js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id], group_issues_path: issues_group_path(@project.group) } }
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar, can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid]

View File

@ -1,6 +1,9 @@
- state_human_name, state_icon_name = state_name_with_icon(issuable)
- badge_text = state_name_with_icon(issuable)[0]
- badge_icon = state_name_with_icon(issuable)[1]
- badge_variant = issuable.open? ? :success : issuable.merged? ? :info : :danger
- badge_status_class = issuable.open? ? 'issuable-status-badge-open' : issuable.merged? ? 'issuable-status-badge-merged' : 'issuable-status-badge-closed'
- badge_classes = "js-mr-status-box issuable-status-badge gl-mr-3 #{badge_status_class}"
.issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(issuable), data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, state: issuable.state } }
= sprite_icon(state_icon_name, css_class: 'gl-display-block gl-sm-display-none!')
%span.gl-display-none.gl-sm-display-block
= state_human_name
= gl_badge_tag({ variant: badge_variant, icon: badge_icon, icon_classes: 'gl-mr-0!' }, { class: badge_classes, data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, issuable_type: 'merge_request', state: issuable.state } }) do
%span.gl-display-none.gl-sm-display-block.gl-ml-2
= badge_text

View File

@ -1,17 +1,16 @@
- link = issue_closed_link(@issue, current_user, css_class: 'text-white text-underline')
- badge_classes = 'issuable-status-badge gl-mr-3'
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) }
= sprite_icon('issue-close', css_class: 'gl-display-block gl-sm-display-none!')
.gl-display-none.gl-sm-display-block
= gl_badge_tag({ variant: :info, icon: 'issue-closed', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :closed)} #{badge_classes} issuable-status-badge-closed" }) do
.gl-display-none.gl-sm-display-block.gl-ml-2
= issue_closed_text(issuable, current_user)
- if link
%span.text-white.gl-pl-2.gl-sm-display-none
= "(#{link})"
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) }
= sprite_icon('issue-open-m', css_class: 'gl-display-block gl-sm-display-none!')
%span.gl-display-none.gl-sm-display-block
- if link
%span.text-white.gl-pl-2.gl-sm-display-none
= "(#{link})"
= gl_badge_tag({ variant: :success, icon: 'issues', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :open)} #{badge_classes} issuable-status-badge-open" }) do
%span.gl-display-none.gl-sm-display-block.gl-ml-2
= _('Open')
.issuable-meta

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352123
milestone: '14.8'
type: development
group: group::product planning
default_enabled: false
default_enabled: true

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346082
milestone: '14.3'
type: development
group: group::product planning
default_enabled: false
default_enabled: true

View File

@ -200,10 +200,31 @@ Conversely, a shared runner that executes jobs for public projects could have a
### Monthly reset of CI/CD minutes
On the first day of each calendar month, the accumulated usage of CI/CD minutes is reset to `0`
for all namespaces that use shared runners.
for all namespaces that use shared runners. This means your full quota is available, and
calculations start again from `0`.
For example, if you have a monthly quota of `10,000` CI/CD minutes:
- On **1st April**, you have `10,000` minutes.
- During April, you use only `6,000` of the `10,000` minutes.
- On **1st May**, the accumulated usage of minutes resets to `0`, and you have `10,000` minutes to use again
during May.
Usage data for the previous month is kept to show historical view of the consumption over time.
### Monthly rollover of purchased CI/CD minutes
If you purchase additional CI/CD minutes and don't use the full amount, the remaining amount rolls over to
the next month.
For example:
- On **1st April**, you purchase `5,000` CI/CD minutes.
- During April, you use only `3,000` of the `5,000` minutes.
- On **1st May**, the remaining `2,000` minutes roll over and are added to your monthly quota.
Additional CI/CD minutes are a one-time purchase and do not renew or refresh each month.
## What happens when you exceed the quota
When the quota of CI/CD minutes is used for the current month, GitLab stops

View File

@ -28,10 +28,10 @@ install GitLab:
| Installation method | Description | When to choose |
|----------------------------------------------------------------|-------------|----------------|
| [Linux package](https://docs.gitlab.com/omnibus/installation/) | The official deb/rpm packages (also known as Omnibus GitLab) that contains a bundle of GitLab and the components it depends on, including PostgreSQL, Redis, and Sidekiq. | This method is recommended for getting started. The Linux packages are mature, scalable, and are used today on GitLab.com. If you need additional flexibility and resilience, we recommend deploying GitLab as described in the [reference architecture documentation](../administration/reference_architectures/index.md). |
| [Helm charts](https://docs.gitlab.com/charts/) | The cloud native Helm chart for installing GitLab and all of its components on Kubernetes. | When installing GitLab on Kubernetes, there are some trade-offs that you need to be aware of: <br/>- Administration and troubleshooting requires Kubernetes knowledge.<br/>- It can be more expensive for smaller installations. The default installation requires more resources than a single node Linux package deployment, as most services are deployed in a redundant fashion.<br/><br/> Use this method if your infrastructure is built on Kubernetes and you're familiar with how it works. The methods for management, observability, and some concepts are different than traditional deployments. |
| [Helm charts](https://docs.gitlab.com/charts/) | The cloud native Helm chart for installing GitLab and all of its components on Kubernetes. | When installing GitLab on Kubernetes, it has some trade-offs that you must be aware of: <br/>- Administration and troubleshooting requires Kubernetes knowledge.<br/>- It can be more expensive for smaller installations. The default installation requires more resources than a single node Linux package deployment, as most services are deployed in a redundant fashion.<br/><br/> Use this method if your infrastructure is built on Kubernetes and you're familiar with how it works. The methods for management, observability, and some concepts are different than traditional deployments. |
| [Docker](docker.md) | The GitLab packages, Dockerized. | Use this method if you're familiar with Docker. |
| [Source](installation.md) | Install GitLab and all of its components from scratch. | Use this method if none of the previous methods are available for your platform. Useful for unsupported systems like \*BSD.|
| [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#documentation) | The GitLab Environment Toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Customers are very welcome to trial and evaluate GET today, however be aware of [key limitations](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#missing-features-to-be-aware-of) of the current iteration. For production environments further manual setup will be required based on your specific requirements. |
| [Source](installation.md) | Install GitLab and all of its components from scratch. | Use this method if none of the previous methods are available for your platform. Can be used for unsupported systems like \*BSD.|
| [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#documentation) | The GitLab Environment toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Customers are very welcome to trial and evaluate GET today, however be aware of [key limitations](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#missing-features-to-be-aware-of) of the current iteration. For production environments further manual setup is required based on your specific requirements. |
| [GitLab Operator](https://docs.gitlab.com/operator/) | The GitLab Operator provides an installation and management method for GitLab following the [Kubernetes Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). | Use the GitLab Operator to run GitLab in an [OpenShift](openshift_and_gitlab/index.md) environment. |
## Install GitLab on cloud providers

View File

@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Description
The target website returns AspNet header(s) along with version information of this website. By
The target website returns AspNet headers along with version information of this website. By
exposing these values attackers may attempt to identify if the target software is vulnerable to known
vulnerabilities. Or catalog known sites running particular versions to exploit in the future when a
vulnerability is identified in the particular version.

View File

@ -1220,3 +1220,21 @@ gemnasium-python-dependency_scanning:
before_script:
- pip install setuptools==57.5.0
```
### Dependency Scanning of projects using psycopg2 fails with `pg_config executable not found` error
Scanning a Python project that depends on `psycopg2` can fail with this message:
```plaintext
Error: pg_config executable not found.
```
[psycopg2](https://pypi.org/project/psycopg2/) depends on the `libpq-dev` Debian package,
which is not installed in the `gemnasium-python` Docker image. To work around this error,
install the `libpq-dev` package in a `before_script`:
```yaml
gemnasium-python-dependency_scanning:
before_script:
- apt-get update && apt-get install -y libpq-dev
```

View File

@ -6,12 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Customer relations management (CRM) **(FREE)**
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `customer_relations`.
On GitLab.com, this feature is not available.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
> - In GitLab 14.8 and later, you can [create contacts and organizations only in root groups](https://gitlab.com/gitlab-org/gitlab/-/issues/350634).
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/346082) 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 `customer_relations`.
On GitLab.com, this feature is available.
With customer relations management (CRM) you can create a record of contacts
(individuals) and organizations (companies) and relate them to issues.
@ -118,7 +119,7 @@ organizations using the GraphQL API.
### View issues linked to a contact
To view a contact's issues:
To view a contact's issues, select a contact from the issue sidebar, or:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Customer relations > Contacts**.
@ -166,11 +167,12 @@ API.
## Autocomplete contacts **(FREE SELF)**
> [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.
> - [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 not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `contacts_autocomplete`.
On GitLab.com, this feature is not available.
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.
This feature is not ready for production use.
When you use the `/add_contacts` or `/remove_contacts` quick actions, follow them with `[contact:` and an autocomplete list appears:

View File

@ -446,7 +446,7 @@ The following table lists group permissions available for each role:
### Subgroup permissions
When you add a member to a subgroup, they inherit the membership and
permission level from the parent group(s). This model allows access to
permission level from the parent groups. This model allows access to
nested groups if you have membership in one of its parents.
To learn more, read through the documentation on

View File

@ -226,7 +226,7 @@ on the running pod.
If you are using a self-managed GitLab instance, you need to configure
Amazon credentials. GitLab uses these credentials to assume an Amazon IAM role to create your cluster.
Create an IAM user and ensure it has permissions to assume the role(s) that
Create an IAM user and ensure it has permissions to assume the roles that
your users need to create EKS clusters.
For example, the following policy document allows assuming a role whose name starts with

View File

@ -24,8 +24,8 @@ The following list illustrates the main differences between CVS and Git:
whole, or they fail without any changes. In CVS, commits (and other operations)
are not atomic. If an operation on the repository is interrupted in the middle,
the repository can be left in an inconsistent state.
- **Storage method.** Changes in CVS are per file (changeset), while in Git
a committed file(s) is stored in its entirety (snapshot). That means it's
- **Storage method.** Changes in CVS are per file (changeset), while in Git,
committed files are stored in their entirety (snapshot). This means it is
very easy in Git to revert or undo a whole change.
- **Revision IDs.** The fact that in CVS changes are per files, the revision ID
is depicted by version numbers, for example `1.4` reflects how many times a

View File

@ -541,7 +541,7 @@ This can be due to multiple reasons:
- If no [degradation or error is detected](https://docs.codeclimate.com/docs/maintainability#section-checks),
nothing is displayed.
- The [`artifacts:expire_in`](../../../ci/yaml/index.md#artifactsexpire_in) CI/CD
setting can cause the Code Quality artifact(s) to expire faster than desired.
setting can cause the Code Quality artifacts to expire faster than desired.
- The widgets use the pipeline of the latest commit to the target branch. If commits are made to the default branch that do not run the code quality job, this may cause the merge request widget to have no base report for comparison.
- If you use the [`REPORT_STDOUT` environment variable](https://gitlab.com/gitlab-org/ci-cd/codequality#environment-variables), no report file is generated and nothing displays in the merge request.
- Large `gl-code-quality-report.json` files (esp. >10 MB) are [known to prevent the report from being displayed](https://gitlab.com/gitlab-org/gitlab/-/issues/2737).

View File

@ -203,7 +203,7 @@ These features are associated with merge requests:
- [Fast-forward merge requests](../methods/index.md#fast-forward-merge):
For a linear Git history and a way to accept merge requests without creating merge commits
- [Find the merge request that introduced a change](../versions.md):
When viewing the commit details page, GitLab links to the merge request(s) containing that commit.
When viewing the commit details page, GitLab links to the merge requests containing that commit.
- [Merge requests versions](../versions.md):
Select and compare the different versions of merge request diffs
- [Resolve conflicts](../conflicts.md):

View File

@ -148,7 +148,7 @@ These shortcuts are available when editing a file with the [Web IDE](project/web
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd></kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd></kbd> | Copy line down |
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd></kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd></kbd> | Copy line up [(Linux note)](#linux-shortcuts) |
| <kbd>Command</kbd> + <kbd>U</kbd> | <kbd>Control</kbd> + <kbd>U</kbd> | Cursor undo |
| <kbd>Command</kbd> + <kbd>Backspace<kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Backspace</kbd> | Delete all left |
| <kbd>Command</kbd> + <kbd>Backspace</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Backspace</kbd> | Delete all left |
| <kbd>Control</kbd> + <kbd>K</kbd> | | Delete all right |
| <kbd>Shift</kbd> + <kbd>Command</kbd> + <kbd>K</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>K</kbd> | Delete line |
| | <kbd>Control</kbd> + <kbd>Backspace</kbd> | Delete word |

View File

@ -28,6 +28,8 @@ module Gitlab
compare_url(object, **options)
when Group
instance.group_canonical_url(object, **options)
when WorkItem
instance.work_item_url(object, **options)
when Issue
instance.issue_url(object, **options)
when MergeRequest

View File

@ -5995,6 +5995,9 @@ msgstr ""
msgid "Billing|Free groups on GitLab are limited to %{maxNamespaceSeats} seats"
msgstr ""
msgid "Billing|From June 22, 2022 (GitLab 15.1), free groups will be limited to 5 members"
msgstr ""
msgid "Billing|Group invite"
msgstr ""
@ -6031,6 +6034,9 @@ msgstr ""
msgid "Billing|You are about to remove user %{username} from your subscription. If you continue, the user will be removed from the %{namespace} group and all its subgroups and projects. This action can't be undone."
msgstr ""
msgid "Billing|You can begin moving members in %{namespaceName} now. A member loses access to the group when you turn off %{strongStart}In a seat%{strongEnd}. If over 5 members have %{strongStart}In a seat%{strongEnd} enabled after June 22, 2022, we'll select the 5 members who maintain access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach 5 members. The remaining members will get a status of Over limit and lose access to the group."
msgstr ""
msgid "Bitbucket Server Import"
msgstr ""

View File

@ -4,7 +4,6 @@ plugins {
}
repositories {
jcenter()
maven {
url "<%= gitlab_address_with_port %>/api/v4/projects/<%= package_project.id %>/packages/maven"
name "GitLab"

View File

@ -1,32 +1,27 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create', :requires_admin do
# This test modifies an instance level setting,
# so skipping on live envs to avoid random transient issues
RSpec.describe 'Create', :requires_admin, :skip_live_env do
describe 'push after setting the file size limit via admin/application_settings' do
# Note: The file size limits in this test should be greater than the limits in
# ee/browser_ui/3_create/repository/push_rules_spec to prevent that test from
# triggering the limit set in this test (which can happen on Staging where the
# tests are run in parallel).
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/218620#note_361634705
include Support::API
before(:context) do
@project = Resource::Project.fabricate_via_api! do |p|
let!(:project) do
Resource::Project.fabricate_via_api! do |p|
p.name = 'project-test-push-limit'
p.initialize_with_readme = true
end
@api_client = Runtime::API::Client.as_admin
end
after(:context) do
# need to set the default value after test
# default value for file size limit is empty
set_file_size_limit(nil)
end
it 'push successful when the file size is under the limit', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347758' do
it(
'push successful when the file size is under the limit',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347758'
) do
set_file_size_limit(5)
retry_on_fail do
@ -36,7 +31,10 @@ module QA
end
end
it 'push fails when the file size is above the limit', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347759' do
it(
'push fails when the file size is above the limit',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347759'
) do
set_file_size_limit(2)
retry_on_fail do
@ -46,7 +44,7 @@ module QA
end
def set_file_size_limit(limit)
request = Runtime::API::Request.new(@api_client, '/application/settings')
request = Runtime::API::Request.new(Runtime::API::Client.as_admin, '/application/settings')
response = put request.url, receive_max_input_size: limit
expect(response.code).to eq(200)
@ -56,13 +54,13 @@ module QA
def push_new_file(file_name, wait_for_push: true)
commit_message = 'Adding a new file'
output = Resource::Repository::Push.fabricate! do |p|
p.repository_http_uri = @project.repository_http_location.uri
p.repository_http_uri = project.repository_http_location.uri
p.file_name = file_name
p.file_content = SecureRandom.random_bytes(3000000)
p.commit_message = commit_message
p.new_branch = false
end
@project.wait_for_push commit_message
project.wait_for_push commit_message
output
end
@ -77,10 +75,8 @@ module QA
# under a minute, i.e., in fewer than 6 attempts with a 10 second sleep
# between attempts.
# See https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/30233#note_188616863
def retry_on_fail
Support::Retrier.retry_on_exception(max_attempts: 6, reload_page: nil, sleep_interval: 10) do
yield
end
def retry_on_fail(&block)
Support::Retrier.retry_on_exception(max_attempts: 6, reload_page: false, sleep_interval: 10, &block)
end
end
end

View File

@ -105,7 +105,7 @@ describe('Customer relations contacts root app', () => {
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
expect(issueLink.attributes('href')).toBe('/issues?scope=all&state=opened&crm_contact_id=16');
expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=16');
});
});
});

View File

@ -102,9 +102,7 @@ describe('Customer relations organizations root app', () => {
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
expect(issueLink.attributes('href')).toBe(
'/issues?scope=all&state=opened&crm_organization_id=2',
);
expect(issueLink.attributes('href')).toBe('/issues?crm_organization_id=2');
});
});
});

View File

@ -1,65 +1,53 @@
import { GlSprintf } from '@gitlab/ui';
import { GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusBox from '~/issuable/components/status_box.vue';
let wrapper;
function factory(propsData) {
wrapper = shallowMount(StatusBox, {
propsData,
stubs: { GlSprintf },
provide: { glFeatures: { updatedMrHeader: true } },
});
wrapper = shallowMount(StatusBox, { propsData, stubs: { GlBadge } });
}
const testCases = [
{
name: 'Open',
state: 'opened',
class: 'badge-success',
},
{
name: 'Open',
state: 'locked',
class: 'badge-success',
},
{
name: 'Closed',
state: 'closed',
class: 'badge-danger',
},
{
name: 'Merged',
state: 'merged',
class: 'badge-info',
},
];
describe('Merge request status box component', () => {
const findBadge = () => wrapper.findComponent(GlBadge);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
testCases.forEach((testCase) => {
describe(`when merge request is ${testCase.name}`, () => {
it('renders human readable test', () => {
describe.each`
issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon
${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'}
${'merge_request'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'danger'} | ${'merge-request-close'}
${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'}
${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'}
${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'}
`(
'with issuableType set to "$issuableType" and state set to "$initialState"',
({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => {
beforeEach(() => {
factory({
initialState: testCase.state,
issuableType: 'merge_request',
initialState,
issuableType,
});
expect(wrapper.text()).toContain(testCase.name);
});
it('sets css class', () => {
factory({
initialState: testCase.state,
issuableType: 'merge_request',
});
expect(wrapper.classes()).toContain(testCase.class);
it(`renders badge with text '${badgeText}'`, () => {
expect(findBadge().text()).toBe(badgeText);
});
});
});
it(`sets badge css class as '${badgeClass}'`, () => {
expect(findBadge().classes()).toContain(badgeClass);
});
it(`sets badge variant as '${badgeVariant}`, () => {
expect(findBadge().props('variant')).toBe(badgeVariant);
});
it(`sets badge icon as '${badgeIcon}'`, () => {
expect(findBadge().findComponent(GlIcon).props('name')).toBe(badgeIcon);
});
},
);
});

View File

@ -24,11 +24,11 @@ describe('Issue', () => {
const getIssueCounter = () => document.querySelector('.issue_counter');
const getOpenStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Open/), {
selector: '.status-box-open',
selector: '.issuable-status-badge-open',
});
const getClosedStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Closed/), {
selector: '.status-box-issue-closed',
selector: '.issuable-status-badge-closed',
});
describe.each`

View File

@ -33,7 +33,7 @@ describe('Issue crm contacts component', () => {
[issueCrmContactsSubscription, subscriptionHandler],
]);
wrapper = shallowMountExtended(CrmContacts, {
propsData: { issueId: '123' },
propsData: { issueId: '123', groupIssuesPath: '/groups/flightjs/-/issues' },
apolloProvider: fakeApollo,
});
};
@ -71,8 +71,14 @@ describe('Issue crm contacts component', () => {
await waitForPromises();
expect(wrapper.find('#contact_0').text()).toContain('Someone Important');
expect(wrapper.find('#contact_0').attributes('href')).toBe(
'/groups/flightjs/-/issues?crm_contact_id=1',
);
expect(wrapper.find('#contact_container_0').text()).toContain('si@gitlab.com');
expect(wrapper.find('#contact_1').text()).toContain('Marty McFly');
expect(wrapper.find('#contact_1').attributes('href')).toBe(
'/groups/flightjs/-/issues?crm_contact_id=5',
);
});
it('renders correct results after subscription update', async () => {
@ -83,5 +89,8 @@ describe('Issue crm contacts component', () => {
contact.forEach((property) => {
expect(wrapper.find('#contact_container_0').text()).toContain(property);
});
expect(wrapper.find('#contact_0').attributes('href')).toBe(
'/groups/flightjs/-/issues?crm_contact_id=13',
);
});
});

View File

@ -69,9 +69,11 @@ describe('IssuableHeader', () => {
it('renders issuable status icon and text', () => {
createComponent();
const statusBoxEl = wrapper.findByTestId('status');
const statusIconEl = statusBoxEl.findComponent(GlIcon);
expect(statusBoxEl.exists()).toBe(true);
expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon);
expect(statusIconEl.props('name')).toBe(mockIssuableShowProps.statusIcon);
expect(statusIconEl.attributes('class')).toBe(mockIssuableShowProps.statusIconClass);
expect(statusBoxEl.text()).toContain('Open');
});

View File

@ -49,6 +49,7 @@ describe('IssuableShowRoot', () => {
const {
statusBadgeClass,
statusIcon,
statusIconClass,
enableEdit,
enableAutocomplete,
editFormVisible,
@ -56,7 +57,7 @@ describe('IssuableShowRoot', () => {
descriptionHelpPath,
taskCompletionStatus,
} = mockIssuableShowProps;
const { blocked, confidential, createdAt, author } = mockIssuable;
const { state, blocked, confidential, createdAt, author } = mockIssuable;
it('renders component container element with class `issuable-show-container`', () => {
expect(wrapper.classes()).toContain('issuable-show-container');
@ -67,15 +68,17 @@ describe('IssuableShowRoot', () => {
expect(issuableHeader.exists()).toBe(true);
expect(issuableHeader.props()).toMatchObject({
issuableState: state,
statusBadgeClass,
statusIcon,
statusIconClass,
blocked,
confidential,
createdAt,
author,
taskCompletionStatus,
});
expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open');
expect(issuableHeader.find('.issuable-status-badge').text()).toContain('Open');
expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe(
true,
);

View File

@ -36,8 +36,9 @@ export const mockIssuableShowProps = {
enableTaskList: true,
enableEdit: true,
showFieldTitle: false,
statusBadgeClass: 'status-box-open',
statusIcon: 'issue-open-m',
statusBadgeClass: 'issuable-status-badge-open',
statusIcon: 'issues',
statusIconClass: 'gl-sm-display-none',
taskCompletionStatus: {
completedCount: 0,
count: 5,

View File

@ -89,16 +89,16 @@ RSpec.describe BadgesHelper do
end
describe 'icons' do
let(:spacing_class_regex) { %r{<svg .*class=".*gl-mr-2.*".*>.*</svg>} }
let(:spacing_class_regex) { %r{<svg .*class=".*my-icon-class gl-mr-2".*>.*</svg>} }
describe 'with text' do
subject { helper.gl_badge_tag(label, icon: "question-o") }
subject { helper.gl_badge_tag(label, icon: "question-o", icon_classes: 'my-icon-class') }
it 'renders an icon' do
expect(subject).to match(%r{<svg .*#question-o".*>.*</svg>})
end
it 'adds a spacing class to the icon' do
it 'adds a spacing class and any custom classes to the icon' do
expect(subject).to match(spacing_class_regex)
end
end

View File

@ -464,6 +464,41 @@ RSpec.describe IssuablesHelper do
end
end
describe '#state_name_with_icon' do
let_it_be(:project) { create(:project, :repository) }
context 'for an issue' do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:issue_closed) { create(:issue, :closed, project: project) }
it 'returns the correct state name and icon when issue is open' do
expect(helper.state_name_with_icon(issue)).to match_array([_('Open'), 'issues'])
end
it 'returns the correct state name and icon when issue is closed' do
expect(helper.state_name_with_icon(issue_closed)).to match_array([_('Closed'), 'issue-closed'])
end
end
context 'for a merge request' do
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let_it_be(:merge_request_merged) { create(:merge_request, :merged, source_project: project) }
let_it_be(:merge_request_closed) { create(:merge_request, :closed, source_project: project) }
it 'returns the correct state name and icon when merge request is open' do
expect(helper.state_name_with_icon(merge_request)).to match_array([_('Open'), 'merge-request-open'])
end
it 'returns the correct state name and icon when merge request is merged' do
expect(helper.state_name_with_icon(merge_request_merged)).to match_array([_('Merged'), 'merge'])
end
it 'returns the correct state name and icon when merge request is closed' do
expect(helper.state_name_with_icon(merge_request_closed)).to match_array([_('Closed'), 'merge-request-close'])
end
end
end
describe '#issuable_display_type' do
using RSpec::Parameterized::TableSyntax

View File

@ -5,31 +5,6 @@ require 'spec_helper'
RSpec.describe MergeRequestsHelper do
include ProjectForksHelper
describe '#state_name_with_icon' do
using RSpec::Parameterized::TableSyntax
let(:merge_request) { MergeRequest.new }
where(:state, :expected_name, :expected_icon) do
:merged? | 'Merged' | 'git-merge'
:closed? | 'Closed' | 'close'
:opened? | 'Open' | 'issue-open-m'
end
with_them do
before do
allow(merge_request).to receive(state).and_return(true)
end
it 'returns name and icon' do
name, icon = helper.state_name_with_icon(merge_request)
expect(name).to eq(expected_name)
expect(icon).to eq(expected_icon)
end
end
end
describe '#format_mr_branch_names' do
describe 'within the same project' do
let(:merge_request) { create(:merge_request) }

View File

@ -22,6 +22,8 @@ RSpec.describe Gitlab::UrlBuilder do
:group_board | ->(board) { "/groups/#{board.group.full_path}/-/boards/#{board.id}" }
:commit | ->(commit) { "/#{commit.project.full_path}/-/commit/#{commit.id}" }
:issue | ->(issue) { "/#{issue.project.full_path}/-/issues/#{issue.iid}" }
[:issue, :task] | ->(issue) { "/#{issue.project.full_path}/-/work_items/#{issue.id}" }
:work_item | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.id}" }
:merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" }
:project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" }
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" }
@ -57,7 +59,7 @@ RSpec.describe Gitlab::UrlBuilder do
end
with_them do
let(:object) { build_stubbed(factory) }
let(:object) { build_stubbed(*Array(factory)) }
let(:path) { path_generator.call(object) }
it 'returns the full URL' do
@ -69,6 +71,18 @@ RSpec.describe Gitlab::UrlBuilder do
end
end
context 'when work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'returns an issue path for an issue of type task' do
task = create(:issue, :task)
expect(subject.build(task, only_path: true)).to eq("/#{task.project.full_path}/-/issues/#{task.iid}")
end
end
context 'when passing a compare' do
# NOTE: The Compare requires an actual repository, which isn't available
# with the `build_stubbed` strategy used by the table tests above

View File

@ -24,6 +24,17 @@ RSpec.describe Projects::IssueLinksController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(list_service_response.as_json)
end
context 'when linked issue is a task' do
let(:issue_b) { create :issue, :task, project: project }
it 'returns a work item path for the linked task' do
get namespace_project_issue_links_path(issue_links_params)
expect(json_response.count).to eq(1)
expect(json_response.first).to include('path' => project_work_items_path(issue_b.project, issue_b.id))
end
end
end
describe 'POST /*namespace_id/:project_id/issues/:issue_id/links' do

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe IssueBoardEntity do
include Gitlab::Routing.url_helpers
let_it_be(:project) { create(:project) }
let_it_be(:resource) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@ -40,4 +42,18 @@ RSpec.describe IssueBoardEntity do
expect(subject).to include(labels: array_including(hash_including(:id, :title, :color, :description, :text_color, :priority)))
end
describe 'real_path' do
it 'has an issue path' do
expect(subject[:real_path]).to eq(project_issue_path(project, resource.iid))
end
context 'when issue is of type task' do
let(:resource) { create(:issue, :task, project: project) }
it 'has a work item path' do
expect(subject[:real_path]).to eq(project_work_items_path(project, resource.id))
end
end
end
end

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe IssueEntity do
include Gitlab::Routing.url_helpers
let(:project) { create(:project) }
let(:resource) { create(:issue, project: project) }
let(:user) { create(:user) }
@ -11,6 +13,17 @@ RSpec.describe IssueEntity do
subject { described_class.new(resource, request: request).as_json }
describe 'web_url' do
context 'when issue is of type task' do
let(:resource) { create(:issue, :task, project: project) }
# This was already a path and not a url when the work items change was introduced
it 'has a work item path' do
expect(subject[:web_url]).to eq(project_work_items_path(project, resource.id))
end
end
end
it 'has Issuable attributes' do
expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
:title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe LinkedProjectIssueEntity do
include Gitlab::Routing.url_helpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue_link) { create(:issue_link) }
@ -17,7 +19,25 @@ RSpec.describe LinkedProjectIssueEntity do
issue_link.target.project.add_developer(user)
end
subject(:serialized_entity) { entity.as_json }
describe 'issue_link_type' do
it { expect(entity.as_json).to include(link_type: 'relates_to') }
it { is_expected.to include(link_type: 'relates_to') }
end
describe 'path' do
it 'returns an issue path' do
expect(serialized_entity).to include(path: project_issue_path(related_issue.project, related_issue.iid))
end
context 'when related issue is a task' do
before do
related_issue.update!(issue_type: :task, work_item_type: WorkItems::Type.default_by_type(:task))
end
it 'returns a work items path' do
expect(serialized_entity).to include(path: project_work_items_path(related_issue.project, related_issue.id))
end
end
end
end

View File

@ -3,14 +3,28 @@
require 'spec_helper'
RSpec.describe Members::Groups::CreatorService do
it_behaves_like 'member creation' do
let_it_be(:source, reload: true) { create(:group, :public) }
let_it_be(:member_type) { GroupMember }
end
describe '.access_levels' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
end
end
describe '#execute' do
let_it_be(:source, reload: true) { create(:group, :public) }
let_it_be(:user) { create(:user) }
it_behaves_like 'member creation' do
let_it_be(:member_type) { GroupMember }
end
context 'authorized projects update' do
it 'schedules a single project authorization update job when called multiple times' do
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once
1.upto(3) do
described_class.new(source, user, :maintainer).execute
end
end
end
end
end

View File

@ -3,14 +3,28 @@
require 'spec_helper'
RSpec.describe Members::Projects::CreatorService do
it_behaves_like 'member creation' do
let_it_be(:source, reload: true) { create(:project, :public) }
let_it_be(:member_type) { ProjectMember }
end
describe '.access_levels' do
it 'returns Gitlab::Access.sym_options_with_owner' do
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
end
end
describe '#execute' do
let_it_be(:source, reload: true) { create(:project, :public) }
let_it_be(:user) { create(:user) }
it_behaves_like 'member creation' do
let_it_be(:member_type) { ProjectMember }
end
context 'authorized projects update' do
it 'schedules a single project authorization update job when called multiple times' do
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once
1.upto(3) do
described_class.new(source, user, :maintainer).execute
end
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module TrialStatusWidgetTestHelper
def purchase_href(group)
new_subscriptions_path(namespace_id: group.id, plan_id: 'ultimate-plan-id')
end
end
TrialStatusWidgetTestHelper.prepend_mod

View File

@ -77,312 +77,309 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name|
end
RSpec.shared_examples_for "member creation" do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
describe '#execute' do
it 'returns a Member object', :aggregate_failures do
member = described_class.new(source, user, :maintainer).execute
it 'returns a Member object', :aggregate_failures do
member = described_class.new(source, user, :maintainer).execute
expect(member).to be_a member_type
expect(member).to be_persisted
expect(member).to be_a member_type
expect(member).to be_persisted
end
context 'when adding a project_bot' do
let_it_be(:project_bot) { create(:user, :project_bot) }
before_all do
source.add_owner(user)
end
context 'when adding a project_bot' do
let_it_be(:project_bot) { create(:user, :project_bot) }
before_all do
source.add_owner(user)
context 'when project_bot is already a member' do
before do
source.add_developer(project_bot)
end
context 'when project_bot is already a member' do
before do
source.add_developer(project_bot)
end
it 'does not update the member' do
member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
expect(member.access_level).to eq(Gitlab::Access::DEVELOPER)
expect(member.errors.full_messages).to include(/not authorized to update member/)
end
end
context 'when project_bot is not already a member' do
it 'adds the member' do
member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
end
end
end
context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
it 'sets members.created_by to the given admin current_user' do
member = described_class.new(source, user, :maintainer, current_user: admin).execute
it 'does not update the member' do
member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
expect(source.users.reload).to include(user)
expect(member.created_by).to eq(admin)
expect(member.access_level).to eq(Gitlab::Access::DEVELOPER)
expect(member.errors.full_messages).to include(/not authorized to update member/)
end
end
context 'when admin mode is disabled' do
it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
member = described_class.new(source, user, :maintainer, current_user: admin).execute
context 'when project_bot is not already a member' do
it 'adds the member' do
member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
end
end
end
context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
it 'sets members.created_by to the given admin current_user' do
member = described_class.new(source, user, :maintainer, current_user: admin).execute
expect(member).to be_persisted
expect(source.users.reload).to include(user)
expect(member.created_by).to eq(admin)
end
end
context 'when admin mode is disabled' do
it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
member = described_class.new(source, user, :maintainer, current_user: admin).execute
expect(member).not_to be_persisted
expect(source.users.reload).not_to include(user)
expect(member.errors.full_messages).to include(/not authorized to create member/)
end
end
it 'sets members.expires_at to the given expires_at' do
member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute
expect(member.expires_at).to eq(Date.new(2016, 9, 22))
end
described_class.access_levels.each do |sym_key, int_access_level|
it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do
expect(source.users).not_to include(user)
member = described_class.new(source, user.id, sym_key).execute
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
it "accepts the #{int_access_level} integer as access level", :aggregate_failures do
expect(source.users).not_to include(user)
member = described_class.new(source, user.id, int_access_level).execute
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
end
context 'with no current_user' do
context 'when called with a known user id' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.new(source, user.id, :maintainer).execute
expect(source.users.reload).to include(user)
end
end
context 'when called with an unknown user id' do
it 'does not add the user as a member' do
expect(source.users).not_to include(user)
described_class.new(source, non_existing_record_id, :maintainer).execute
expect(member).not_to be_persisted
expect(source.users.reload).not_to include(user)
expect(member.errors.full_messages).to include(/not authorized to create member/)
end
end
it 'sets members.expires_at to the given expires_at' do
member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute
expect(member.expires_at).to eq(Date.new(2016, 9, 22))
end
described_class.access_levels.each do |sym_key, int_access_level|
it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do
context 'when called with a user object' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
member = described_class.new(source, user.id, sym_key).execute
described_class.new(source, user, :maintainer).execute
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
it "accepts the #{int_access_level} integer as access level", :aggregate_failures do
expect(source.users).not_to include(user)
member = described_class.new(source, user.id, int_access_level).execute
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
end
context 'with no current_user' do
context 'when called with a known user id' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.new(source, user.id, :maintainer).execute
expect(source.users.reload).to include(user)
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
context 'when called with an unknown user id' do
it 'does not add the user as a member' do
expect(source.users).not_to include(user)
described_class.new(source, non_existing_record_id, :maintainer).execute
expect(source.users.reload).not_to include(user)
end
end
context 'when called with a user object' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
it 'adds the requester as a member', :aggregate_failures do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
expect do
described_class.new(source, user, :maintainer).execute
end.to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).to include(user)
end
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'adds the requester as a member', :aggregate_failures do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
expect do
described_class.new(source, user, :maintainer).execute
end.to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).not_to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
end
end
context 'when called with a known user email' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.new(source, user.email, :maintainer).execute
expect(source.users.reload).to include(user)
end
end
context 'when called with an unknown user email' do
it 'creates an invited member' do
expect(source.users).not_to include(user)
described_class.new(source, 'user@example.com', :maintainer).execute
expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
end
context 'when called with an unknown user email starting with a number' do
it 'creates an invited member', :aggregate_failures do
email_starting_with_number = "#{user.id}_email@example.com"
described_class.new(source, email_starting_with_number, :maintainer).execute
expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
expect(source.users.reload).not_to include(user)
end
expect(source.users.reload).not_to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
end
end
context 'when current_user can update member', :enable_admin_mode do
it 'creates the member' do
context 'when called with a known user email' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.new(source, user.email, :maintainer).execute
expect(source.users.reload).to include(user)
end
end
context 'when called with an unknown user email' do
it 'creates an invited member' do
expect(source.users).not_to include(user)
described_class.new(source, 'user@example.com', :maintainer).execute
expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
end
context 'when called with an unknown user email starting with a number' do
it 'creates an invited member', :aggregate_failures do
email_starting_with_number = "#{user.id}_email@example.com"
described_class.new(source, email_starting_with_number, :maintainer).execute
expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
expect(source.users.reload).not_to include(user)
end
end
end
context 'when current_user can update member', :enable_admin_mode do
it 'creates the member' do
expect(source.users).not_to include(user)
described_class.new(source, user, :maintainer, current_user: admin).execute
expect(source.users.reload).to include(user)
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'adds the requester as a member', :aggregate_failures do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.new(source, user, :maintainer, current_user: admin).execute
expect(source.users.reload).to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
end
end
end
context 'when current_user cannot update member' do
it 'does not create the member', :aggregate_failures do
expect(source.users).not_to include(user)
member = described_class.new(source, user, :maintainer, current_user: user).execute
expect(source.users.reload).not_to include(user)
expect(member).not_to be_persisted
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'does not destroy the requester', :aggregate_failures do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
it 'adds the requester as a member', :aggregate_failures do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.new(source, user, :maintainer, current_user: user).execute
described_class.new(source, user, :maintainer, current_user: admin).execute
expect(source.users.reload).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
end
end
end
expect(source.users.reload).to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
end
context 'when member already exists' do
before do
source.add_user(user, :developer)
end
context 'with no current_user' do
it 'updates the member' do
expect(source.users).to include(user)
described_class.new(source, user, :maintainer).execute
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user can update member', :enable_admin_mode do
it 'updates the member' do
expect(source.users).to include(user)
described_class.new(source, user, :maintainer, current_user: admin).execute
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user cannot update member' do
it 'does not create the member', :aggregate_failures do
expect(source.users).not_to include(user)
it 'does not update the member' do
expect(source.users).to include(user)
member = described_class.new(source, user, :maintainer, current_user: user).execute
described_class.new(source, user, :maintainer, current_user: user).execute
expect(source.users.reload).not_to include(user)
expect(member).not_to be_persisted
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'does not destroy the requester', :aggregate_failures do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.new(source, user, :maintainer, current_user: user).execute
expect(source.users.reload).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
end
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
end
context 'when member already exists' do
context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
it 'creates a member_task with the correct attributes', :aggregate_failures do
described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
member = source.members.last
expect(member.tasks_to_be_done).to match_array([:ci, :code])
expect(member.member_task.project).to eq(task_project)
end
context 'with an already existing member' do
before do
source.add_user(user, :developer)
end
context 'with no current_user' do
it 'updates the member' do
expect(source.users).to include(user)
it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
member = source.members.find_by(user_id: user.id)
create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
described_class.new(source, user, :maintainer).execute
expect do
described_class.new(source,
user,
:developer,
tasks_to_be_done: %w(issues),
tasks_project_id: task_project.id).execute
end.not_to change(MemberTask, :count)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user can update member', :enable_admin_mode do
it 'updates the member' do
expect(source.users).to include(user)
described_class.new(source, user, :maintainer, current_user: admin).execute
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user cannot update member' do
it 'does not update the member' do
expect(source.users).to include(user)
described_class.new(source, user, :maintainer, current_user: user).execute
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
end
context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
it 'creates a member_task with the correct attributes', :aggregate_failures do
described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
member = source.members.last
expect(member.tasks_to_be_done).to match_array([:ci, :code])
member.reset
expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project).to eq(task_project)
end
context 'with an already existing member' do
before do
source.add_user(user, :developer)
end
it 'adds tasks to be done if they do not exist', :aggregate_failures do
expect do
described_class.new(source,
user,
:developer,
tasks_to_be_done: %w(issues),
tasks_project_id: task_project.id).execute
end.to change(MemberTask, :count).by(1)
it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
member = source.members.find_by(user_id: user.id)
create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
expect do
described_class.new(source,
user,
:developer,
tasks_to_be_done: %w(issues),
tasks_project_id: task_project.id).execute
end.not_to change(MemberTask, :count)
member.reset
expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project).to eq(task_project)
end
it 'adds tasks to be done if they do not exist', :aggregate_failures do
expect do
described_class.new(source,
user,
:developer,
tasks_to_be_done: %w(issues),
tasks_project_id: task_project.id).execute
end.to change(MemberTask, :count).by(1)
member = source.members.find_by(user_id: user.id)
expect(member.tasks_to_be_done).to match_array([:issues])
expect(member.member_task.project).to eq(task_project)
end
member = source.members.find_by(user_id: user.id)
expect(member.tasks_to_be_done).to match_array([:issues])
expect(member.member_task.project).to eq(task_project)
end
end
end

View File

@ -26,14 +26,14 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Closed (moved)" if an issue has been moved and closed' do
render
expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)')
end
it 'shows "Closed (moved)" if an issue has been moved and discussion is locked' do
allow(issue).to receive(:discussion_locked).and_return(true)
render
expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)')
end
it 'links "moved" to the new issue the original issue was moved to' do
@ -47,7 +47,7 @@ RSpec.describe 'projects/issues/show' do
render
expect(rendered).not_to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
expect(rendered).not_to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)')
end
end
@ -75,7 +75,7 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Closed (duplicated)" if an issue has been duplicated' do
render
expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (duplicated)')
expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (duplicated)')
end
it 'links "duplicated" to the new issue the original issue was duplicated to' do
@ -97,14 +97,14 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Closed" if an issue has not been moved or duplicated' do
render
expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed')
expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed')
end
it 'shows "Closed" if discussion is locked' do
allow(issue).to receive(:discussion_locked).and_return(true)
render
expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed')
expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed')
end
end
@ -117,14 +117,14 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Open" if an issue has been moved' do
render
expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open')
expect(rendered).to have_selector('.issuable-status-badge-open:not(.hidden)', text: 'Open')
end
it 'shows "Open" if discussion is locked' do
allow(issue).to receive(:discussion_locked).and_return(true)
render
expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open')
expect(rendered).to have_selector('.issuable-status-badge-open:not(.hidden)', text: 'Open')
end
end