Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-22 09:08:45 +00:00
parent 67146ed9ad
commit 3f7e1004e7
77 changed files with 745 additions and 636 deletions

View File

@ -1,5 +0,0 @@
---
# Cop supports --autocorrect.
RSpec/RedundantAround:
Exclude:
- 'spec/spec_helper.rb'

View File

@ -197,6 +197,10 @@ gem 'hamlit', '~> 2.15.0', feature_category: :shared
gem 'carrierwave', '~> 1.3', feature_category: :shared
gem 'mini_magick', '~> 4.12', feature_category: :shared
# PDF generation
gem 'prawn', feature_category: :vulnerability_management
gem 'prawn-svg', feature_category: :vulnerability_management
# for backups
gem 'fog-aws', '~> 3.26', feature_category: :shared
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.

View File

@ -508,6 +508,7 @@
{"name":"parser","version":"3.3.7.1","platform":"ruby","checksum":"7dbe61618025519024ac72402a6677ead02099587a5538e84371b76659e6aca1"},
{"name":"parslet","version":"1.8.2","platform":"ruby","checksum":"08d1ab3721cd3f175bfbee8788b2ddff71f92038f2d69bd65454c22bb9fbd98a"},
{"name":"pastel","version":"0.8.0","platform":"ruby","checksum":"481da9fb7d2f6e6b1a08faf11fa10363172dc40fd47848f096ae21209f805a75"},
{"name":"pdf-core","version":"0.10.0","platform":"ruby","checksum":"0a5d101e2063c01e3f941e1ee47cbb97f1adfc1395b58372f4f65f1300f3ce91"},
{"name":"peek","version":"1.1.0","platform":"ruby","checksum":"d6501ead8cde46d8d8ed0d59eb6f0ba713d0a41c11a2c4a81447b2dce37b3ecc"},
{"name":"pg","version":"1.5.9","platform":"ruby","checksum":"761efbdf73b66516f0c26fcbe6515dc7500c3f0aa1a1b853feae245433c64fdc"},
{"name":"pg","version":"1.5.9","platform":"x64-mingw-ucrt","checksum":"9d9d6a4fcc25251312065b61f94eb56c5266007c0e747606704641d47b92c5eb"},
@ -516,6 +517,8 @@
{"name":"pg_query","version":"6.1.0","platform":"ruby","checksum":"8b005229e209f12c5887c34c60d0eb2a241953b9475b53a9840d24578532481e"},
{"name":"plist","version":"3.7.0","platform":"ruby","checksum":"703ca90a7cb00e8263edd03da2266627f6741d280c910abbbac07c95ffb2f073"},
{"name":"png_quantizator","version":"0.2.1","platform":"ruby","checksum":"6023d4d064125c3a7e02929c95b7320ed6ac0d7341f9e8de0c9ea6576ef3106b"},
{"name":"prawn","version":"2.5.0","platform":"ruby","checksum":"f4e20e3b4f30bf5b9ae37dad15eb421831594553aa930b2391b0fa0a99c43cb6"},
{"name":"prawn-svg","version":"0.37.0","platform":"ruby","checksum":"271bdd032c066777b2e76fe971b570e24cb6f4890d5658588106e8aa5b6e2830"},
{"name":"premailer","version":"1.23.0","platform":"ruby","checksum":"f0d7f6ba299559c96ddf982aa5263f85e5617c86437c8d8ffff120813b2d7efb"},
{"name":"premailer-rails","version":"1.12.0","platform":"ruby","checksum":"c13815d161b9bc7f7d3d81396b0bb0a61a90fa9bd89931548bf4e537c7710400"},
{"name":"prime","version":"0.1.3","platform":"ruby","checksum":"baf031c50d6ce923594913befc8ac86a3251bffb9d6a5e8b03687962054e53e3"},
@ -746,6 +749,7 @@
{"name":"trailblazer-option","version":"0.1.2","platform":"ruby","checksum":"20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3"},
{"name":"train-core","version":"3.10.8","platform":"ruby","checksum":"8493da02015fbe9b11840d22ba879ef18a0aa2633cb0c04eac3f07dd9b87223b"},
{"name":"truncato","version":"0.7.13","platform":"ruby","checksum":"34621943c067eb892389d356d1312822b81b574e8d7dec2b61955fef0e91e380"},
{"name":"ttfunk","version":"1.8.0","platform":"ruby","checksum":"a7cbc7e489cc46e979dde04d34b5b9e4f5c8f1ee5fc6b1a7be39b829919d20ca"},
{"name":"tty-color","version":"0.6.0","platform":"ruby","checksum":"6f9c37ca3a4e2367fb2e6d09722762647d6f455c111f05b59f35730eeb24332a"},
{"name":"tty-command","version":"0.10.1","platform":"ruby","checksum":"0c6c471fcb932d55518734eb4e2e07e9efdd2918713cc39bb7393ba862471192"},
{"name":"tty-cursor","version":"0.7.1","platform":"ruby","checksum":"79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48"},

View File

@ -1448,6 +1448,7 @@ GEM
parslet (1.8.2)
pastel (0.8.0)
tty-color (~> 0.5)
pdf-core (0.10.0)
peek (1.1.0)
railties (>= 4.0.0)
pg (1.5.9)
@ -1455,6 +1456,15 @@ GEM
google-protobuf (>= 3.25.3)
plist (3.7.0)
png_quantizator (0.2.1)
prawn (2.5.0)
matrix (~> 0.4)
pdf-core (~> 0.10.0)
ttfunk (~> 1.8)
prawn-svg (0.37.0)
css_parser (~> 1.6)
matrix (~> 0.4.2)
prawn (>= 0.11.1, < 3)
rexml (>= 3.3.9, < 4)
premailer (1.23.0)
addressable
css_parser (>= 1.12.0)
@ -1896,6 +1906,8 @@ GEM
truncato (0.7.13)
htmlentities (~> 4.3.1)
nokogiri (>= 1.7.0, <= 2.0)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
tty-color (0.6.0)
tty-command (0.10.1)
pastel (~> 0.8)
@ -2277,6 +2289,8 @@ DEPENDENCIES
pg (~> 1.5.6)
pg_query (~> 6.1.0)
png_quantizator (~> 0.2.1)
prawn
prawn-svg
premailer-rails (~> 1.12.0)
prometheus-client-mmap (~> 1.2.8)
pry-byebug

View File

@ -511,6 +511,7 @@
{"name":"parser","version":"3.3.7.1","platform":"ruby","checksum":"7dbe61618025519024ac72402a6677ead02099587a5538e84371b76659e6aca1"},
{"name":"parslet","version":"1.8.2","platform":"ruby","checksum":"08d1ab3721cd3f175bfbee8788b2ddff71f92038f2d69bd65454c22bb9fbd98a"},
{"name":"pastel","version":"0.8.0","platform":"ruby","checksum":"481da9fb7d2f6e6b1a08faf11fa10363172dc40fd47848f096ae21209f805a75"},
{"name":"pdf-core","version":"0.10.0","platform":"ruby","checksum":"0a5d101e2063c01e3f941e1ee47cbb97f1adfc1395b58372f4f65f1300f3ce91"},
{"name":"peek","version":"1.1.0","platform":"ruby","checksum":"d6501ead8cde46d8d8ed0d59eb6f0ba713d0a41c11a2c4a81447b2dce37b3ecc"},
{"name":"pg","version":"1.5.9","platform":"ruby","checksum":"761efbdf73b66516f0c26fcbe6515dc7500c3f0aa1a1b853feae245433c64fdc"},
{"name":"pg","version":"1.5.9","platform":"x64-mingw-ucrt","checksum":"9d9d6a4fcc25251312065b61f94eb56c5266007c0e747606704641d47b92c5eb"},
@ -520,6 +521,8 @@
{"name":"plist","version":"3.7.0","platform":"ruby","checksum":"703ca90a7cb00e8263edd03da2266627f6741d280c910abbbac07c95ffb2f073"},
{"name":"png_quantizator","version":"0.2.1","platform":"ruby","checksum":"6023d4d064125c3a7e02929c95b7320ed6ac0d7341f9e8de0c9ea6576ef3106b"},
{"name":"pp","version":"0.6.2","platform":"ruby","checksum":"947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff"},
{"name":"prawn","version":"2.5.0","platform":"ruby","checksum":"f4e20e3b4f30bf5b9ae37dad15eb421831594553aa930b2391b0fa0a99c43cb6"},
{"name":"prawn-svg","version":"0.37.0","platform":"ruby","checksum":"271bdd032c066777b2e76fe971b570e24cb6f4890d5658588106e8aa5b6e2830"},
{"name":"premailer","version":"1.23.0","platform":"ruby","checksum":"f0d7f6ba299559c96ddf982aa5263f85e5617c86437c8d8ffff120813b2d7efb"},
{"name":"premailer-rails","version":"1.12.0","platform":"ruby","checksum":"c13815d161b9bc7f7d3d81396b0bb0a61a90fa9bd89931548bf4e537c7710400"},
{"name":"prettyprint","version":"0.2.0","platform":"ruby","checksum":"2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193"},
@ -725,8 +728,8 @@
{"name":"state_machines-activemodel","version":"0.8.0","platform":"ruby","checksum":"e932dab190d4be044fb5f9cab01a3ea0b092c5f113d4676c6c0a0d49bf738d2c"},
{"name":"state_machines-activerecord","version":"0.8.0","platform":"ruby","checksum":"072fb701b8ab03de0608297f6c55dc34ed096e556fa8f77e556f3c461c71aab6"},
{"name":"state_machines-rspec","version":"0.6.0","platform":"ruby","checksum":"2ba57a45df57d0c97f79146e2e0f65f519b52e65e182805ef79cb73b1fe5c0bd"},
{"name":"stringio","version":"3.1.6","platform":"java","checksum":"dbdb1ee4e6d75782bbc7e8cc7d84cd05e592df50494f363011cc7cd48153bbf7"},
{"name":"stringio","version":"3.1.6","platform":"ruby","checksum":"292c495d1657adfcdf0a32eecf12a60e6691317a500c3112ad3b2e31068274f5"},
{"name":"stringio","version":"3.1.7","platform":"java","checksum":"167bdd3d60a002ee94bc289cc3259638aaadc36a47b3086a44a694b5dc72a499"},
{"name":"stringio","version":"3.1.7","platform":"ruby","checksum":"5b78b7cb242a315fb4fca61a8255d62ec438f58da2b90be66048546ade4507fa"},
{"name":"strings","version":"0.2.1","platform":"ruby","checksum":"933293b3c95cf85b81eb44b3cf673e3087661ba739bbadfeadf442083158d6fb"},
{"name":"strings-ansi","version":"0.2.0","platform":"ruby","checksum":"90262d760ea4a94cc2ae8d58205277a343409c288cbe7c29416b1826bd511c88"},
{"name":"swd","version":"2.0.3","platform":"ruby","checksum":"4cdbe2a4246c19f093fce22e967ec3ebdd4657d37673672e621bf0c7eb770655"},
@ -759,6 +762,7 @@
{"name":"trailblazer-option","version":"0.1.2","platform":"ruby","checksum":"20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3"},
{"name":"train-core","version":"3.10.8","platform":"ruby","checksum":"8493da02015fbe9b11840d22ba879ef18a0aa2633cb0c04eac3f07dd9b87223b"},
{"name":"truncato","version":"0.7.13","platform":"ruby","checksum":"34621943c067eb892389d356d1312822b81b574e8d7dec2b61955fef0e91e380"},
{"name":"ttfunk","version":"1.8.0","platform":"ruby","checksum":"a7cbc7e489cc46e979dde04d34b5b9e4f5c8f1ee5fc6b1a7be39b829919d20ca"},
{"name":"tty-color","version":"0.6.0","platform":"ruby","checksum":"6f9c37ca3a4e2367fb2e6d09722762647d6f455c111f05b59f35730eeb24332a"},
{"name":"tty-command","version":"0.10.1","platform":"ruby","checksum":"0c6c471fcb932d55518734eb4e2e07e9efdd2918713cc39bb7393ba862471192"},
{"name":"tty-cursor","version":"0.7.1","platform":"ruby","checksum":"79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48"},

View File

@ -1465,6 +1465,7 @@ GEM
parslet (1.8.2)
pastel (0.8.0)
tty-color (~> 0.5)
pdf-core (0.10.0)
peek (1.1.0)
railties (>= 4.0.0)
pg (1.5.9)
@ -1474,6 +1475,15 @@ GEM
png_quantizator (0.2.1)
pp (0.6.2)
prettyprint
prawn (2.5.0)
matrix (~> 0.4)
pdf-core (~> 0.10.0)
ttfunk (~> 1.8)
prawn-svg (0.37.0)
css_parser (~> 1.6)
matrix (~> 0.4.2)
prawn (>= 0.11.1, < 3)
rexml (>= 3.3.9, < 4)
premailer (1.23.0)
addressable
css_parser (>= 1.12.0)
@ -1870,7 +1880,7 @@ GEM
activesupport
rspec (~> 3.3)
state_machines
stringio (3.1.6)
stringio (3.1.7)
strings (0.2.1)
strings-ansi (~> 0.2)
unicode-display_width (>= 1.5, < 3.0)
@ -1930,6 +1940,8 @@ GEM
truncato (0.7.13)
htmlentities (~> 4.3.1)
nokogiri (>= 1.7.0, <= 2.0)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
tty-color (0.6.0)
tty-command (0.10.1)
pastel (~> 0.8)
@ -2311,6 +2323,8 @@ DEPENDENCIES
pg (~> 1.5.6)
pg_query (~> 6.1.0)
png_quantizator (~> 0.2.1)
prawn
prawn-svg
premailer-rails (~> 1.12.0)
prometheus-client-mmap (~> 1.2.8)
pry-byebug

View File

@ -70,7 +70,6 @@ export default {
<gl-link
:href="option.href"
:target="option.target"
:data-test-id="`option-${option.id}`"
@click="option.event && $emit(option.event)"
>{{ option.text }}</gl-link
>

View File

@ -116,7 +116,6 @@ export default {
:selected="isBlameViewer"
category="primary"
variant="default"
data-test-id="blame-toggle"
@click="switchToViewer($options.BLAME_VIEWER)"
>{{ __('Blame') }}</gl-button
>

View File

@ -4,9 +4,8 @@ import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/ci/constants';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { setUrlFragment, visitUrl } from '~/lib/utils/url_utility';
import { __, n__, sprintf, formatNumber } from '~/locale';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PageHeading from '~/vue_shared/components/page_heading.vue';
import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
import { reportToSentry } from '~/ci/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
@ -70,9 +69,6 @@ export default {
pipelineIid: {
default: '',
},
pipelineId: {
default: '',
},
},
apollo: {
pipeline: {
@ -89,6 +85,40 @@ export default {
update(data) {
return data.project.pipeline;
},
result({ data }) {
// we use a manual subscribeToMore call due to issues with
// the skip hook not working correctly for the subscription
if (this.showRealTimePipelineStatus && data?.project?.pipeline?.id) {
this.$apollo.queries.pipeline.subscribeToMore({
document: pipelineCiStatusUpdatedSubscription,
variables: {
pipelineId: data.project.pipeline.id,
},
updateQuery(
previousData,
{
subscriptionData: {
data: { ciPipelineStatusUpdated },
},
},
) {
if (ciPipelineStatusUpdated) {
return {
project: {
...previousData.project,
pipeline: {
...previousData.project.pipeline,
detailedStatus: ciPipelineStatusUpdated.detailedStatus,
},
},
};
}
return previousData;
},
});
}
},
error(error) {
this.reportFailure(LOAD_FAILURE);
reportToSentry(this.$options.name, error);
@ -102,41 +132,6 @@ export default {
this.isRetrying = false;
}
},
subscribeToMore: {
document() {
return pipelineCiStatusUpdatedSubscription;
},
variables() {
return {
pipelineId: convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipelineId),
};
},
skip() {
return !this.showRealTimePipelineStatus;
},
updateQuery(
previousData,
{
subscriptionData: {
data: { ciPipelineStatusUpdated },
},
},
) {
if (ciPipelineStatusUpdated) {
return {
project: {
...previousData.project,
pipeline: {
...previousData.project.pipeline,
detailedStatus: ciPipelineStatusUpdated.detailedStatus,
},
},
};
}
return previousData;
},
},
},
},
data() {

View File

@ -15,7 +15,6 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
const {
fullPath,
pipelineIid,
pipelineId,
pipelinesPath,
identityVerificationPath,
identityVerificationRequired,
@ -36,7 +35,6 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
pipelinesPath,
},
pipelineIid,
pipelineId,
identityVerificationPath,
identityVerificationRequired: parseBoolean(identityVerificationRequired),
mergeTrainsAvailable: parseBoolean(mergeTrainsAvailable),

View File

@ -3,8 +3,12 @@ import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { debounce, throttle } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapState as mapPiniaState, mapActions as mapPiniaActions } from 'pinia';
import {
mapState as mapVuexState,
mapGetters as mapVuexGetters,
mapActions as mapVuexActions,
} from 'vuex';
import { mapState, mapActions } from 'pinia';
import FindingsDrawer from 'ee_component/diffs/components/shared/findings_drawer.vue';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import {
@ -23,12 +27,12 @@ import { BV_HIDE_TOOLTIP, DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/c
import { Mousetrap } from '~/lib/mousetrap';
import { updateHistory, getLocationHash } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import notesEventHub from '~/notes/event_hub';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import getMRCodequalityAndSecurityReports from 'ee_else_ce/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql';
import { useFileBrowser } from '~/diffs/stores/file_browser';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { sortFindingsByFile } from '../utils/sort_findings_by_file';
import {
ALERT_OVERFLOW_HIDDEN,
@ -47,7 +51,6 @@ import {
EVT_DISCUSSIONS_ASSIGNED,
FILE_BROWSER_VISIBLE,
} from '../constants';
import { isCollapsed } from '../utils/diff_file';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
@ -219,12 +222,11 @@ export default {
},
},
computed: {
...mapPiniaState(useLegacyDiffs, {
...mapState(useLegacyDiffs, {
numTotalFiles: 'realSize',
numVisibleFiles: 'size',
}),
...mapState('findingsDrawer', ['activeDrawer']),
...mapPiniaState(useLegacyDiffs, [
...mapState(useLegacyDiffs, [
'isLoading',
'diffViewType',
'commit',
@ -256,9 +258,10 @@ export default {
'flatBlobsList',
'diffFiles',
]),
...mapGetters(['isNotesFetched', 'getNoteableData']),
...mapGetters('findingsDrawer', ['activeDrawer']),
...mapPiniaState(useFileBrowser, ['fileBrowserVisible']),
...mapState(useNotes, ['isNotesFetched', 'getNoteableData']),
...mapState(useFileBrowser, ['fileBrowserVisible']),
...mapVuexState('findingsDrawer', ['activeDrawer']),
...mapVuexGetters('findingsDrawer', ['activeDrawer']),
diffs() {
if (!this.viewDiffsFileByFile) {
return this.diffFiles;
@ -440,8 +443,8 @@ export default {
diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
},
methods: {
...mapActions(['startTaskList']),
...mapPiniaActions(useLegacyDiffs, [
...mapActions(useNotes, ['startTaskList']),
...mapActions(useLegacyDiffs, [
'moveToNeighboringCommit',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
@ -467,8 +470,8 @@ export default {
'reviewFile',
'setFileCollapsedByUser',
]),
...mapActions('findingsDrawer', ['setDrawer']),
...mapPiniaActions(useFileBrowser, ['setFileBrowserVisibility']),
...mapActions(useFileBrowser, ['setFileBrowserVisibility']),
...mapVuexActions('findingsDrawer', ['setDrawer']),
closeDrawer() {
this.setDrawer({});
},

View File

@ -1,7 +1,5 @@
<script>
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import { sprintf } from '~/locale';
import { createAlert } from '~/alert';
@ -16,6 +14,7 @@ import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_d
import NoteForm from '~/notes/components/note_form.vue';
import eventHub from '~/notes/event_hub';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { SAVING_THE_COMMENT_FAILED, SOMETHING_WENT_WRONG } from '../i18n';
import { getDiffMode } from '../store/utils';
@ -70,7 +69,7 @@ export default {
'getCommentFormForDiffFile',
'diffLines',
]),
...mapVuexGetters(['getNoteableData', 'noteableType', 'getUserData']),
...mapState(useNotes, ['getNoteableData', 'noteableType', 'getUserData']),
diffMode() {
return getDiffMode(this.diffFile);
},

View File

@ -1,11 +1,10 @@
<script>
import { GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import { mapState } from 'pinia';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import DiscussionLockedWidget from '~/notes/components/discussion_locked_widget.vue';
import { COMMENT_FORM } from '../../notes/i18n';
import { useNotes } from '~/notes/store/legacy_notes';
import { COMMENT_FORM } from '~/notes/i18n';
import { START_THREAD } from '../i18n';
export default {
@ -31,7 +30,7 @@ export default {
},
},
computed: {
...mapGetters({
...mapState(useNotes, {
currentUser: 'getUserData',
userCanReply: 'userCanReply',
getNoteableData: 'getNoteableData',

View File

@ -1,8 +1,6 @@
<script>
import { GlButton, GlLoadingIcon, GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import { escape } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { IdState } from 'vendor/vue-virtual-scroller';
@ -23,6 +21,7 @@ import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { fileContentsId } from '~/diffs/components/diff_row_utils';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useMrNotes } from '~/mr_notes/store/legacy_mr_notes';
import { useNotes } from '~/notes/store/legacy_notes';
import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE,
@ -136,7 +135,7 @@ export default {
'linkedFile',
]),
...mapState(useMrNotes, ['isLoggedIn']),
...mapVuexGetters(['isNotesFetched', 'getNoteableData', 'noteableType']),
...mapState(useNotes, ['isNotesFetched', 'getNoteableData', 'noteableType']),
autosaveKey() {
if (!this.isLoggedIn) return '';

View File

@ -12,8 +12,6 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import { escape } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import { keysFor, MR_TOGGLE_REVIEW } from '~/behaviors/shortcuts/keybindings';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
@ -26,6 +24,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { createFileUrl, fileContentsId } from '~/diffs/components/diff_row_utils';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
import { DIFF_FILE_HEADER } from '../i18n';
import { collapsedType, isCollapsed } from '../utils/diff_file';
@ -104,7 +103,7 @@ export default {
},
computed: {
...mapState(useLegacyDiffs, ['latestDiff', 'diffHasExpandedDiscussions', 'diffHasDiscussions']),
...mapVuexGetters(['getNoteableData']),
...mapState(useNotes, ['getNoteableData']),
diffContentIDSelector() {
return fileContentsId(this.diffFile);
},

View File

@ -1,7 +1,5 @@
<script>
import { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import { mapState as mapVuexState, mapGetters as mapVuexGetters } from 'vuex';
import { mapState, mapActions } from 'pinia';
import { s__, __, sprintf } from '~/locale';
import { createAlert } from '~/alert';
@ -15,6 +13,7 @@ import NoteForm from '~/notes/components/note_form.vue';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useMrNotes } from '~/mr_notes/store/legacy_mr_notes';
import { useNotes } from '~/notes/store/legacy_notes';
import {
DIFF_NOTE_TYPE,
INLINE_DIFF_LINES_KEY,
@ -77,11 +76,14 @@ export default {
'diffLines',
]),
...mapState(useMrNotes, ['isLoggedIn']),
...mapVuexState({
noteableData: ({ notes }) => notes.noteableData,
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
}),
...mapVuexGetters(['noteableType', 'getNoteableData', 'getNotesDataByProp', 'getUserData']),
...mapState(useNotes, [
'noteableData',
'noteableType',
'getNoteableData',
'getNotesDataByProp',
'getUserData',
'selectedCommentPosition',
]),
author() {
return this.getUserData;
},

View File

@ -1,6 +1,4 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapState as mapVuexState, mapActions as mapVuexActions } from 'vuex';
import { mapState, mapActions } from 'pinia';
import { throttle } from 'lodash';
import { IdState } from 'vendor/vue-virtual-scroller';
@ -10,6 +8,7 @@ import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import { hide } from '~/tooltips';
import { countLinesInBetween } from '~/diffs/utils/diff_file';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { pickDirection } from '../utils/diff_line';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
@ -73,10 +72,7 @@ export default {
'coverageLoaded',
'selectedCommentPosition',
]),
...mapVuexState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
}),
...mapState(useNotes, ['selectedCommentPosition', 'selectedCommentPositionHover']),
diffLinesLength() {
return this.diffLines.length;
},
@ -97,7 +93,7 @@ export default {
this.onDragOverThrottled = throttle((line) => this.onDragOver(line), 100, { leading: true });
},
methods: {
...mapVuexActions(['setSelectedCommentPosition']),
...mapActions(useNotes, ['setSelectedCommentPosition']),
...mapActions(useLegacyDiffs, [
'showCommentForm',
'setHighlightedRow',

View File

@ -1,10 +1,9 @@
<script>
import { GlSprintf, GlEmptyState } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapState } from 'pinia';
import { s__, __ } from '~/locale';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
export default {
i18n: {
@ -27,7 +26,7 @@ export default {
'diffCompareDropdownTargetVersions',
'diffCompareDropdownSourceVersions',
]),
...mapVuexGetters(['getNoteableData']),
...mapState(useNotes, ['getNoteableData']),
selectedSourceVersion() {
return this.diffCompareDropdownSourceVersions.find((x) => x.selected);
},

View File

@ -1,9 +1,9 @@
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { mapActions } from 'pinia';
import { useNotes } from '~/notes/store/legacy_notes';
export default {
methods: {
...mapActions(['toggleDiscussion']),
...mapActions(useNotes, ['toggleDiscussion']),
clickedToggle(discussion) {
this.toggleDiscussion({ discussionId: discussion.id });
},

View File

@ -1,6 +1,4 @@
import { mapState, mapActions } from 'pinia';
// eslint-disable-next-line no-restricted-imports
import { mapState as mapVuexState } from 'vuex';
import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils';
import {
TEXT_DIFF_POSITION_TYPE,
@ -14,13 +12,11 @@ import { formatLineRange } from '~/notes/components/multiline_comment_utils';
import { SAVING_THE_COMMENT_FAILED, SOMETHING_WENT_WRONG } from '~/diffs/i18n';
import { useBatchComments } from '~/batch_comments/store';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
export default {
computed: {
...mapVuexState({
noteableData: (state) => state.notes.noteableData,
notesData: (state) => state.notes.notesData,
}),
...mapState(useNotes, ['noteableData', 'notesData']),
...mapState(useLegacyDiffs, ['getDiffFileByHash', 'commit', 'showWhitespace']),
...mapState(useBatchComments, [
'shouldRenderDraftRowInDiscussion',

View File

@ -93,11 +93,7 @@ export default {
</h4>
<ul class="mb-0">
<li
v-for="(item, index) in $options.i18n.warningListItems"
:key="index"
data-test-id="warning-item"
>
<li v-for="(item, index) in $options.i18n.warningListItems" :key="index">
{{ item }}
</li>
</ul>

View File

@ -1,6 +1,8 @@
<script>
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { logError } from '~/lib/logger';
import { captureException } from '~/sentry/sentry_browser_wrapper';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
@ -56,7 +58,13 @@ export default {
projectPath: this.projectPath,
};
},
error() {
error(error) {
logError(`Unexpected error while fetching projectInfo query`, error);
captureException(error, {
tags: {
vue_component: 'BlobContentViewer',
},
});
this.displayError();
},
update({ project }) {
@ -91,7 +99,13 @@ export default {
this.initHighlightWorker(this.blobInfo, this.isUsingLfs);
this.switchViewer(useSimpleViewer ? SIMPLE_BLOB_VIEWER : RICH_BLOB_VIEWER); // By default, if present, use the rich viewer to render
},
error() {
error(error) {
logError(`Unexpected error while fetching blobInfo query`, error);
captureException(error, {
tags: {
vue_component: 'BlobContentViewer',
},
});
this.displayError();
},
},

View File

@ -61,6 +61,39 @@ export default {
pipeline: pipelines?.length && pipelines[0].node,
};
},
result() {
// we use a manual subscribeToMore call due to issues with
// the skip hook not working correctly for the subscription
if (this.showRealTimePipelineStatus && this.commit?.pipeline?.id) {
this.$apollo.queries.commit.subscribeToMore({
document: pipelineCiStatusUpdatedSubscription,
variables: {
pipelineId: this.commit.pipeline.id,
},
updateQuery(
previousData,
{
subscriptionData: {
data: { ciPipelineStatusUpdated },
},
},
) {
if (ciPipelineStatusUpdated) {
const updatedData = structuredClone(previousData);
const pipeline =
updatedData.project?.repository?.paginatedTree?.nodes[0]?.lastCommit?.pipelines
?.edges[0]?.node || {};
pipeline.detailedStatus = ciPipelineStatusUpdated.detailedStatus;
return updatedData;
}
return previousData;
},
});
}
},
error(error) {
logError(`Unexpected error while fetching projectInfo query`, error);
captureException(error);
@ -68,40 +101,6 @@ export default {
throw error;
},
pollInterval: POLL_INTERVAL,
subscribeToMore: {
document() {
return pipelineCiStatusUpdatedSubscription;
},
variables() {
return {
pipelineId: this.commit?.pipeline?.id,
};
},
skip() {
return !this.showRealTimePipelineStatus || !this.commit?.pipeline?.id;
},
updateQuery(
previousData,
{
subscriptionData: {
data: { ciPipelineStatusUpdated },
},
},
) {
if (ciPipelineStatusUpdated) {
const updatedData = structuredClone(previousData);
const pipeline =
updatedData.project?.repository?.paginatedTree?.nodes[0]?.lastCommit?.pipelines
?.edges[0]?.node || {};
pipeline.detailedStatus = ciPipelineStatusUpdated.detailedStatus;
return updatedData;
}
return previousData;
},
},
},
},
props: {

View File

@ -46,7 +46,7 @@ export default {
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link btn hide-collapsed btn-default btn-sm gl-button btn-default-tertiary gl-float-right gl-ml-auto !gl-text-default"
href="#"
data-test-id="edit-link"
data-testid="edit-link"
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"

View File

@ -123,7 +123,6 @@ export default {
<template #alert-message="{ panelId }">
<gl-popover
v-if="showAlertPopover"
data-test-id="panel-alert-popover"
:aria-describedby="panelId"
triggers="hover focus"
:title="alertPopoverTitle"

View File

@ -356,11 +356,7 @@ export default {
:class="{ truncated: isTruncated, 'has-task-list-item-actions': hasTaskListItemActions }"
@change="toggleCheckboxes"
></div>
<div
v-if="isTruncated"
class="description-more gl-block gl-w-full"
data-test-id="description-read-more"
>
<div v-if="isTruncated" class="description-more gl-block gl-w-full">
<div class="show-all-btn gl-flex gl-w-full gl-items-center gl-justify-center">
<gl-button
ref="show-all-btn"

View File

@ -205,14 +205,6 @@ $comparison-empty-state-height: 62px;
}
}
.diffs.tab-pane {
@include media-breakpoint-up(md) {
// ensure consistent page height when selected file is loading
// https://gitlab.com/gitlab-org/gitlab/-/issues/426250
min-height: 100vh;
}
}
// Wrap MR tabs/buttons so you don't have to scroll on desktop
@include media-breakpoint-down(md) {
.merge-request-tabs-container {

View File

@ -244,6 +244,15 @@
// to the top at different heights, which is a bad-looking defect
$diff-file-header-top: 11px;
.diffs.tab-pane .files {
@include media-breakpoint-up(md) {
// ensure consistent page height when selected file is loading
// https://gitlab.com/gitlab-org/gitlab/-/issues/426250
// also required for file browser to consume all available height
min-height: calc(100vh - (#{$calc-application-header-height} + #{$mr-sticky-header-height} + #{$diff-file-header-top} + #{$content-wrapper-padding}));
}
}
.diff-tree-list {
--file-row-height: 32px;
--file-tree-min-height: 300px;

View File

@ -37,7 +37,6 @@ module Projects
full_path: project.full_path,
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
pipeline_iid: pipeline.iid,
pipeline_id: pipeline.id,
pipelines_path: project_pipelines_path(project)
}
end

View File

@ -34,14 +34,6 @@ module ContainerRegistry
)
end
def self.for_push_exists?(access_level:, repository_path:)
return false if access_level.blank? || repository_path.blank?
where(':access_level < minimum_access_level_for_push', access_level: access_level)
.for_repository_path(repository_path)
.exists?
end
def self.for_action_exists?(action:, access_level:, repository_path:)
return false if [access_level, repository_path].any?(&:blank?)
raise ArgumentError, 'action must be :push or :delete' unless %i[push delete].include?(action)

View File

@ -17,6 +17,15 @@ module Packages
validates :package, :package_reference, :project, presence: true
validates :revision, presence: true, format: { with: ::Gitlab::Regex.conan_revision_regex_v2 }
validates :revision, uniqueness: { scope: [:package_id, :package_reference_id] }, on: %i[create update]
scope :order_by_id_desc, -> { order(id: :desc) }
scope :by_recipe_revision_and_package_reference, ->(recipe_revision, package_reference) do
joins(package_reference: :recipe_revision)
.where(
packages_conan_recipe_revisions: { revision: recipe_revision },
packages_conan_package_references: { reference: package_reference }
)
end
end
end
end

View File

@ -386,13 +386,26 @@ module Auth
repository_project = push_scope_container_registry_path.repository_project
repository_project.container_registry_protection_rules.for_push_exists?(
access_level: repository_project.team.max_member_access(current_user&.id),
protection_rule_for_push_exists?(
current_user: current_user || deploy_token,
project: repository_project,
repository_path: push_scope_container_registry_path.to_s
)
end
end
def protection_rule_for_push_exists?(current_user:, project:, repository_path:)
service_response = ContainerRegistry::Protection::CheckRuleExistenceService.for_push(
current_user: current_user,
project: project,
params: { repository_path: repository_path }
).execute
raise ArgumentError, service_response.message if service_response.error?
service_response[:protection_rule_exists?]
end
# Overridden in EE
def extra_info
{}

View File

@ -12,6 +12,10 @@ module ContainerRegistry
new(params: params.merge(action: :delete), **args)
end
def self.for_push(params:, **args)
new(params: params.merge(action: :push), **args)
end
def initialize(params:, **args)
raise(ArgumentError, 'Invalid param :action') unless params[:action].in?([:push, :delete])

View File

@ -23,6 +23,8 @@ module ContainerRegistry
end
def execute
return service_response_error(message: _('Operation not allowed')) if container_protection_tag_rule.immutable?
unless can?(current_user, :admin_container_image, container_protection_tag_rule.project)
return service_response_error(message: _('Unauthorized to update a protection rule for container image tags'))
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class DropNotNullConstraintFromMergeRequestDiffCommits < Gitlab::Database::Migration[2.2]
milestone '18.0'
# rubocop:disable Migration/ChangeColumnNullOnHighTrafficTable -- We're making them nullable and there is no constraint
def up
change_column_null :merge_request_diff_commits, :sha, true
change_column_null :merge_request_diff_commits, :trailers, true
end
# rubocop:enable Migration/ChangeColumnNullOnHighTrafficTable
def down
# no-op
#
# Setting the columns back to be non-nullable and ensuring that data doesn't
# really have any NULL values for these columns will take time since the
# table is very large.
#
# At this point, we're not setting these columns to have `NULL` values yet.
# That will be done later in https://gitlab.com/gitlab-org/gitlab/-/issues/527240.
end
end

View File

@ -0,0 +1 @@
43653559bec7b7367fec087959d6a01daa921c1918277c8fdb6e38d42d99b107

View File

@ -17213,9 +17213,9 @@ CREATE TABLE merge_request_diff_commits (
committed_date timestamp without time zone,
merge_request_diff_id bigint NOT NULL,
relative_order integer NOT NULL,
sha bytea NOT NULL,
sha bytea,
message text,
trailers jsonb DEFAULT '{}'::jsonb NOT NULL,
trailers jsonb DEFAULT '{}'::jsonb,
commit_author_id bigint,
committer_id bigint
);

View File

@ -21,28 +21,32 @@ title: '`gitlab-sshd`'
{{< /history >}}
`gitlab-sshd` is [a standalone SSH server](https://gitlab.com/gitlab-org/gitlab-shell/-/tree/main/internal/sshd)
written in Go. It is provided as a part of the `gitlab-shell` package. It has a lower memory
use as a OpenSSH alternative, and supports
[group access restriction by IP address](../../user/group/access_and_permissions.md#restrict-group-access-by-ip-address) for applications
running behind the proxy.
written in Go. It is as a lightweight alternative to OpenSSH. It is provided as part of the
`gitlab-shell` package and handles [SSH operations](https://gitlab.com/gitlab-org/gitlab-shell/-/blob/71a7f34a476f778e62f8fe7a453d632d395eaf8f/doc/features.md).
`gitlab-sshd` is a lightweight alternative to OpenSSH for providing
[SSH operations](https://gitlab.com/gitlab-org/gitlab-shell/-/blob/71a7f34a476f778e62f8fe7a453d632d395eaf8f/doc/features.md).
While OpenSSH uses a restricted shell approach, `gitlab-sshd` behaves more like a
modern multi-threaded server application, responding to incoming requests. The major
difference is that OpenSSH uses SSH as a transport protocol while `gitlab-sshd` uses Remote Procedure Calls (RPCs). See [the blog post](https://about.gitlab.com/blog/2022/08/17/why-we-have-implemented-our-own-sshd-solution-on-gitlab-sass/) for more details.
While OpenSSH uses a restricted shell approach, `gitlab-sshd`:
The capabilities of GitLab Shell are not limited to Git operations.
- Functions as a modern multi-threaded server application.
- Uses Remote Procedure Calls (RPCs) instead of the SSH transport protocol.
- Uses less memory than OpenSSH.
- Supports [group access restriction by IP address](../../user/group/access_and_permissions.md#restrict-group-access-by-ip-address)
for applications running behind a proxy.
If you are considering switching from OpenSSH to `gitlab-sshd`, consider these concerns:
For more details about the implementation, see [the blog post](https://about.gitlab.com/blog/2022/08/17/why-we-have-implemented-our-own-sshd-solution-on-gitlab-sass/).
- `gitlab-sshd` supports the PROXY protocol. It can run behind proxy servers that rely
on it, such as HAProxy. The PROXY protocol is not enabled by default, but [it can be enabled](#proxy-protocol-support).
- `gitlab-sshd` does not support SSH certificates. For discussion about adding them,
see [issue 655](https://gitlab.com/gitlab-org/gitlab-shell/-/issues/655).
- `gitlab-sshd` does not support 2FA recovery code regeneration. Attempting to run `2fa_recovery_codes`
results in the following error: `remote: ERROR: Unknown command: 2fa_recovery_codes`.
See [the discussion](https://gitlab.com/gitlab-org/gitlab-shell/-/issues/766#note_1906707753) for more information.
If you are considering switching from OpenSSH to `gitlab-sshd`, consider the following:
- **PROXY protocol**: `gitlab-sshd` supports the PROXY protocol, allowing it to run behind proxy
servers like HAProxy. This feature is not enabled by default but [can be enabled](#proxy-protocol-support).
- **SSH certificates**: `gitlab-sshd` does not support SSH certificates. For more information, see
[issue 655](https://gitlab.com/gitlab-org/gitlab-shell/-/issues/655).
- **2FA recovery codes**: `gitlab-sshd` does not support 2FA recovery code regeneration.
Attempting to run `2fa_recovery_codes` results in the error:
`remote: ERROR: Unknown command: 2fa_recovery_codes`. See
[the discussion](https://gitlab.com/gitlab-org/gitlab-shell/-/issues/766#note_1906707753) for details.
The capabilities of GitLab Shell extend beyond Git operations and can be used for various
SSH-based interactions with GitLab.
## Enable `gitlab-sshd`

View File

@ -14838,7 +14838,7 @@ paths:
operationId: getApiV4ProjectsIdPackagesConanV2ConansSearch
"/api/v4/projects/{id}/packages/conan/v2/conans/{package_name}/{package_version}/{package_username}/{package_channel}/latest":
get:
summary: Get the latest revision
summary: Get the latest recipe revision
description: This feature was introduced in GitLab 17.11
produces:
- application/json
@ -14874,9 +14874,9 @@ paths:
example: stable
responses:
'200':
description: Get the latest revision
description: Get the latest recipe revision
schema:
"$ref": "#/definitions/API_Entities_Packages_Conan_RecipeRevision"
"$ref": "#/definitions/API_Entities_Packages_Conan_Revision"
'400':
description: Bad Request
'401':
@ -14928,7 +14928,7 @@ paths:
'200':
description: Get the list of revisions
schema:
"$ref": "#/definitions/API_Entities_Packages_Conan_RecipeRevision"
"$ref": "#/definitions/API_Entities_Packages_Conan_RecipeRevisions"
'400':
description: Bad Request
'401':
@ -15213,6 +15213,70 @@ paths:
tags:
- conan_packages
operationId: putApiV4ProjectsIdPackagesConanV2ConansPackageNamePackageVersionPackageUsernamePackageChannelRevisionsRecipeRevisionFilesFileNameAuthorize
? "/api/v4/projects/{id}/packages/conan/v2/conans/{package_name}/{package_version}/{package_username}/{package_channel}/revisions/{recipe_revision}/packages/{conan_package_reference}/latest"
: get:
summary: Get the latest package revision
description: This feature was introduced in GitLab 17.11
produces:
- application/json
parameters:
- in: path
name: id
description: The ID or URL-encoded path of the project
type: string
required: true
- in: path
name: package_name
description: Package name
type: string
required: true
example: my-package
- in: path
name: package_version
description: Package version
type: string
required: true
example: '1.0'
- in: path
name: package_username
description: Package username
type: string
required: true
example: my-group+my-project
- in: path
name: package_channel
description: Package channel
type: string
required: true
example: stable
- in: path
name: recipe_revision
description: Recipe revision
type: string
required: true
example: df28fd816be3a119de5ce4d374436b25
- in: path
name: conan_package_reference
description: Package reference
type: string
required: true
example: 5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9
responses:
'200':
description: Get the latest package revision
schema:
"$ref": "#/definitions/API_Entities_Packages_Conan_Revision"
'400':
description: Bad Request
'401':
description: Unauthorized
'403':
description: Forbidden
'404':
description: Not Found
tags:
- conan_packages
operationId: getApiV4ProjectsIdPackagesConanV2ConansPackageNamePackageVersionPackageUsernamePackageChannelRevisionsRecipeRevisionPackagesConanPackageReferenceLatest
? "/api/v4/projects/{id}/packages/conan/v2/conans/{package_name}/{package_version}/{package_username}/{package_channel}/revisions/{recipe_revision}/packages/{conan_package_reference}/revisions/{package_revision}/files/{file_name}"
: put:
summary: Upload package files
@ -51131,18 +51195,31 @@ definitions:
required:
- file
description: Upload package files
API_Entities_Packages_Conan_RecipeRevision:
API_Entities_Packages_Conan_Revision:
type: object
properties:
revision:
type: string
example: 75151329520e7685dcf5da49ded2fec0
description: The revision hash of the Conan recipe
description: The revision hash of the Conan recipe or package
time:
type: string
example: '2024-12-17T09:16:40.334Z'
description: The UTC timestamp when the revision was created
description: API_Entities_Packages_Conan_RecipeRevision model
description: API_Entities_Packages_Conan_Revision model
API_Entities_Packages_Conan_RecipeRevisions:
type: object
properties:
reference:
type: string
example: packageTest/1.2.3@gitlab-org+conan/stable
description: The Conan package reference
revisions:
type: array
items:
"$ref": "#/definitions/API_Entities_Packages_Conan_Revision"
description: List of recipe revisions
description: API_Entities_Packages_Conan_RecipeRevisions model
API_Entities_Packages_Conan_RecipeFilesList:
type: object
properties:

View File

@ -304,6 +304,38 @@ Example response:
}
```
## Get latest package revision
Gets the revision hash and creation date of the latest package revision for a specific recipe revision and package reference.
```plaintext
GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/:package_channel/revisions/:recipe_revision/packages/:conan_package_reference/latest
```
| Attribute | Type | Required | Description |
|---------------------------|--------|----------|-------------|
| `id` | string | yes | The project ID or full project path. |
| `package_name` | string | yes | Name of a package. |
| `package_version` | string | yes | Version of a package. |
| `package_username` | string | yes | Conan username of a package. This attribute is the `+`-separated full path of your project. |
| `package_channel` | string | yes | Channel of a package. |
| `recipe_revision` | string | yes | Revision of the recipe. Does not accept a value of `0`. |
| `conan_package_reference` | string | yes | Reference hash of a Conan package. Conan generates this value. |
```shell
curl --header "Authorization: Bearer <authenticate_token>" \
--url "https://gitlab.example.com/api/v4/projects/9/packages/conan/v2/conans/my-package/1.0/my-group+my-project/stable/revisions/75151329520e7685dcf5da49ded2fec0/packages/103f6067a947f366ef91fc1b7da351c588d1827f/latest"
```
Example response:
```json
{
"revision" : "3bdd2d8c8e76c876ebd1ac0469a4e72c",
"time" : "2024-12-17T09:16:40.334+0000"
}
```
## Upload a package file
Uploads a package file to the package registry.

View File

@ -27,7 +27,7 @@ a more systemic problem you need to investigate.
1. Select **Your work**.
1. Select **Environments**.
![Environments Dashboard with projects.](img/environments_dashboard_v12_5.png)
![Environments Dashboard showing two rows of projects with their deployment environments and pipeline status.](img/environments_dashboard_v12_5.png)
The Environments dashboard displays a paginated list of projects that includes
up to three environments per project.

View File

@ -185,9 +185,10 @@ The following endpoints are available for CI/CD job tokens.
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v1/conans/:package_name/:package_version/:package_username/:package_channel` | Recipe Snapshot |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/export/:file_name` | Download recipe files |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name` | Download package files |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/:package_channel/latest` | Get the latest revision |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/:package_channel/latest` | Get the latest recipe revision |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/:package_channel/revisions/:recipe_revision/files/:file_name` | Download recipe files |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/:package_channel/revisions/:recipe_revision/files` | List recipe files |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/:package_channel/revisions/:recipe_revision/packages/:conan_package_reference/latest` | Get the latest package revision |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username/:package_channel/revisions` | Get the list of revisions |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/generic/:package_name/*package_version/(*path/):file_name` | Download package file |
| Packages: Read | `READ_PACKAGES` | `GET /projects/:id/packages/go/*module_name/@v/:module_version.info` | Version metadata |

View File

@ -114,4 +114,4 @@ These tests are performed:
| Synchronization | Tests whether your subscription: <br>- Has been activated with an activation code and can be synchronized with `customers.gitlab.com`.<br>- Has correct access credentials.<br>- Has been synchronized recently. If it hasn't or the access credentials are missing or expired, you can [manually synchronize](../../subscriptions/self_managed/_index.md#manually-synchronize-subscription-data) your subscription data. |
| System exchange | Tests whether Code Suggestions can be used in your instance. If the system exchange assessment fails, users might not be able to use GitLab Duo features. |
If you are experiencing any issues with the health check, see [GitLab Duo Self-Hosted troubleshooting](../../administration/gitlab_duo_self_hosted/troubleshooting.md#gitlab-duo-health-check-is-not-working).
If you are experiencing any issues with the health check for GitLab Duo Self-Hosted, see the [troubleshooting section](../../administration/gitlab_duo_self_hosted/troubleshooting.md#gitlab-duo-health-check-is-not-working).

View File

@ -26,11 +26,12 @@ which users can make changes to container images in your container repository.
When a container repository is protected, the default behavior enforces these restrictions on the container repository and its images:
| Action | Minimum role |
|------------------------------------------------------------|----------------------|
| Protect a container repository and its container images. | The Maintainer role. |
| Push or create a new image in a container repository. | The role set in the [**Minimum access level for push**](#create-a-container-repository-protection-rule) setting. |
| Push or update an existing image in a container repository. | The role set in the [**Minimum access level for push**](#create-a-container-repository-protection-rule) setting. |
| Action | Minimum role |
|------------------------------------------------------------------------------------------|----------------------|
| Protect a container repository and its container images. | The Maintainer role. |
| Push or create a new image in a container repository. | The role set in the [**Minimum access level for push**](#create-a-container-repository-protection-rule) setting. |
| Push or update an existing image in a container repository. | The role set in the [**Minimum access level for push**](#create-a-container-repository-protection-rule) setting. |
| Push, create, or update an existing image in a container repository with a deploy token. | Not applicable. Deploy tokens can be used with non-protected repositories, but cannot be used to push images to protected container repositories, regardless of their scopes. |
You can use a wildcard (`*`) to protect multiple container repositories with the same container protection rule.
For example, you can protect different container repositories containing temporary container images built during a CI/CD pipeline.

View File

@ -53,9 +53,6 @@ When a branch is protected, the default behavior enforces these restrictions on
1. No one can delete a protected branch using Git commands, however, users with at least Maintainer
role can [delete a protected branch from the UI or API](#delete-a-protected-branch).
You can implement a [merge request approval policy](../../../application_security/policies/merge_request_approval_policies.md#approval_settings)
to prevent protected branches being unprotected or deleted.
### When a branch matches multiple rules
When a branch matches multiple rules, the **most permissive rule** determines the
@ -490,6 +487,10 @@ Protected branches can only be deleted by using GitLab either from the UI or API
This prevents accidentally deleting a branch through local Git commands or
third-party Git clients.
## Merge request approval policies
For security and compliance, you may implement a [merge request approval policy](../../../application_security/policies/merge_request_approval_policies.md#approval_settings) which affects settings otherwise defined in your instance, group, or projects. Policies may affect users ability to unprotect or delete branches, push or force push.
## Related topics
- [Protected branches API](../../../../api/protected_branches.md)

View File

@ -38,9 +38,9 @@ module API
end
namespace 'latest' do
desc 'Get the latest revision' do
desc 'Get the latest recipe revision' do
detail 'This feature was introduced in GitLab 17.11'
success code: 200, model: ::API::Entities::Packages::Conan::RecipeRevision
success code: 200, model: ::API::Entities::Packages::Conan::Revision
failure [
{ code: 400, message: 'Bad Request' },
{ code: 401, message: 'Unauthorized' },
@ -59,13 +59,13 @@ module API
not_found!('Revision') unless revision.present?
present revision, with: ::API::Entities::Packages::Conan::RecipeRevision
present revision, with: ::API::Entities::Packages::Conan::Revision
end
end
namespace 'revisions' do
desc 'Get the list of revisions' do
detail 'This feature was introduced in GitLab 17.11'
success code: 200, model: ::API::Entities::Packages::Conan::RecipeRevision
success code: 200, model: ::API::Entities::Packages::Conan::RecipeRevisions
failure [
{ code: 400, message: 'Bad Request' },
{ code: 401, message: 'Unauthorized' },
@ -191,6 +191,33 @@ module API
documentation: { example: '5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9' }
end
namespace 'packages/:conan_package_reference' do
namespace 'latest' do
desc 'Get the latest package revision' do
detail 'This feature was introduced in GitLab 17.11'
success code: 200, model: ::API::Entities::Packages::Conan::Revision
failure [
{ code: 400, message: 'Bad Request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not Found' }
]
tags %w[conan_packages]
end
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
route_setting :authorization, job_token_policies: :read_packages,
allow_public_access_for_enabled_project_features: :package_registry
get urgency: :low do
not_found!('Package') unless package
revision = package.conan_package_revisions
.by_recipe_revision_and_package_reference(params[:recipe_revision],
params[:conan_package_reference]).order_by_id_desc.first
not_found!('Revision') unless revision.present?
present revision, with: ::API::Entities::Packages::Conan::Revision
end
end
namespace 'revisions' do
params do
requires :package_revision, type: String, regexp: Gitlab::Regex.conan_revision_regex_v2,

View File

@ -14,7 +14,7 @@ module API
}
expose :conan_recipe_revisions, as: :revisions, using:
::API::Entities::Packages::Conan::RecipeRevision, documentation: {
::API::Entities::Packages::Conan::Revision, documentation: {
type: Array,
desc: 'List of recipe revisions',
is_array: true

View File

@ -4,10 +4,10 @@ module API
module Entities
module Packages
module Conan
class RecipeRevision < Grape::Entity
class Revision < Grape::Entity
expose :revision, documentation: {
type: String,
desc: 'The revision hash of the Conan recipe',
desc: 'The revision hash of the Conan recipe or package',
example: '75151329520e7685dcf5da49ded2fec0'
}

View File

@ -98,7 +98,6 @@ spec/frontend/clusters/agents/components/show_spec.js
spec/frontend/clusters/components/new_cluster_spec.js
spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
spec/frontend/clusters_list/components/delete_agent_button_spec.js
spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
spec/frontend/content_editor/components/wrappers/paragraph_spec.js
spec/frontend/custom_emoji/components/list_spec.js
spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@ -106,7 +105,6 @@ spec/frontend/design_management/components/design_overlay_spec.js
spec/frontend/design_management/pages/design/index_spec.js
spec/frontend/design_management/pages/index_spec.js
spec/frontend/diffs/components/diff_line_note_form_spec.js
spec/frontend/diffs/components/image_diff_overlay_spec.js
spec/frontend/editor/components/source_editor_toolbar_spec.js
spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js
spec/frontend/error_tracking/components/error_details_spec.js
@ -167,7 +165,6 @@ spec/frontend/releases/components/asset_links_form_spec.js
spec/frontend/repository/components/table/index_spec.js
spec/frontend/repository/components/table/row_spec.js
spec/frontend/search/sidebar/components/checkbox_filter_spec.js
spec/frontend/search/topbar/components/app_spec.js
spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
spec/frontend/sidebar/components/confidential/confidentiality_dropdown_spec.js

View File

@ -3,7 +3,7 @@
FactoryBot.define do
factory :conan_package_revision, class: 'Packages::Conan::PackageRevision' do
package do
association(:conan_package, without_package_files: true)
association(:conan_package, conan_package_revisions: [], without_package_files: true)
end
project { package.project }

View File

@ -1,7 +1,6 @@
import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { createMockSubscription } from 'mock-apollo-client';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -28,19 +27,21 @@ import {
pipelineCancelMutationResponseFailed,
pipelineDeleteMutationResponseFailed,
mockPipelineStatusUpdatedResponse,
mockPipelineStatusNullResponse,
} from '../mock_data';
Vue.use(VueApollo);
describe('Pipeline header', () => {
let wrapper;
let mockedSubscription;
let apolloProvider;
const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess);
const runningHandler = jest.fn().mockResolvedValue(pipelineHeaderRunning);
const runningHandlerWithDuration = jest.fn().mockResolvedValue(pipelineHeaderRunningWithDuration);
const failedHandler = jest.fn().mockResolvedValue(pipelineHeaderFailed);
const subscriptionHandler = jest.fn().mockResolvedValue(mockPipelineStatusUpdatedResponse);
const subscriptionNullHandler = jest.fn().mockResolvedValue(mockPipelineStatusNullResponse);
const retryMutationHandlerSuccess = jest
.fn()
@ -84,7 +85,10 @@ describe('Pipeline header', () => {
findHeaderActions().vm.$emit(action, id);
};
const defaultHandlers = [[getPipelineDetailsQuery, successHandler]];
const defaultHandlers = [
[getPipelineDetailsQuery, successHandler],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
];
const defaultProvideOptions = {
identityVerificationRequired: false,
@ -95,26 +99,22 @@ describe('Pipeline header', () => {
pipelinesPath: '/namespace/my-project/-/pipelines',
fullProject: '/namespace/my-project',
},
glFeatures: {
ciPipelineStatusRealtime: true,
},
};
const createMockApolloProvider = (handlers) => {
return createMockApollo(handlers);
};
const createComponent = (handlers = defaultHandlers) => {
mockedSubscription = createMockSubscription();
const createComponent = (handlers = defaultHandlers, isRealTime = true) => {
apolloProvider = createMockApolloProvider(handlers);
apolloProvider.defaultClient.setRequestHandler(
pipelineCiStatusUpdatedSubscription,
() => mockedSubscription,
);
wrapper = shallowMountExtended(PipelineHeader, {
provide: defaultProvideOptions,
provide: {
...defaultProvideOptions,
glFeatures: {
ciPipelineStatusRealtime: isRealTime,
},
},
stubs: { GlSprintf },
apolloProvider,
});
@ -176,7 +176,12 @@ describe('Pipeline header', () => {
expect(findBadges().exists()).toBe(true);
});
it('passes pipeline prop to HeaderBadges component', () => {
it('passes pipeline prop to HeaderBadges component', async () => {
await createComponent([
[getPipelineDetailsQuery, successHandler],
[pipelineCiStatusUpdatedSubscription, subscriptionNullHandler],
]);
expect(findBadges().props('pipeline')).toEqual(pipelineHeaderSuccess.data.project.pipeline);
});
@ -204,7 +209,10 @@ describe('Pipeline header', () => {
describe('without pipeline name', () => {
it('displays commit title', async () => {
await createComponent([[getPipelineDetailsQuery, runningHandler]]);
await createComponent([
[getPipelineDetailsQuery, runningHandler],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
const expectedTitle = pipelineHeaderSuccess.data.project.pipeline.commit.title;
@ -232,7 +240,10 @@ describe('Pipeline header', () => {
describe('running pipeline', () => {
beforeEach(() => {
return createComponent([[getPipelineDetailsQuery, runningHandler]]);
return createComponent([
[getPipelineDetailsQuery, runningHandler],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
});
it('does not display finished time ago', () => {
@ -255,7 +266,10 @@ describe('Pipeline header', () => {
describe('running pipeline with duration', () => {
beforeEach(() => {
return createComponent([[getPipelineDetailsQuery, runningHandlerWithDuration]]);
return createComponent([
[getPipelineDetailsQuery, runningHandlerWithDuration],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
});
it('does not display pipeline duration text', () => {
@ -268,6 +282,7 @@ describe('Pipeline header', () => {
await createComponent([
[getPipelineDetailsQuery, failedHandler],
[retryPipelineMutation, retryMutationHandlerSuccess],
[pipelineCiStatusUpdatedSubscription, subscriptionNullHandler],
]);
expect(findHeaderActions().props()).toEqual({
@ -283,6 +298,7 @@ describe('Pipeline header', () => {
return createComponent([
[getPipelineDetailsQuery, failedHandler],
[retryPipelineMutation, retryMutationHandlerSuccess],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
});
@ -308,6 +324,7 @@ describe('Pipeline header', () => {
return createComponent([
[getPipelineDetailsQuery, failedHandler],
[retryPipelineMutation, retryMutationHandlerFailed],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
});
@ -338,6 +355,7 @@ describe('Pipeline header', () => {
await createComponent([
[getPipelineDetailsQuery, runningHandler],
[cancelPipelineMutation, cancelMutationHandlerSuccess],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
clickActionButton('cancelPipeline', pipelineHeaderRunning.data.project.pipeline.id);
@ -359,6 +377,7 @@ describe('Pipeline header', () => {
await createComponent([
[getPipelineDetailsQuery, runningHandler],
[cancelPipelineMutation, cancelMutationHandlerFailed],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
clickActionButton('cancelPipeline', pipelineHeaderRunning.data.project.pipeline.id);
@ -375,6 +394,7 @@ describe('Pipeline header', () => {
await createComponent([
[getPipelineDetailsQuery, successHandler],
[deletePipelineMutation, deleteMutationHandlerSuccess],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
clickActionButton('deletePipeline', pipelineHeaderSuccess.data.project.pipeline.id);
@ -395,6 +415,7 @@ describe('Pipeline header', () => {
await createComponent([
[getPipelineDetailsQuery, successHandler],
[deletePipelineMutation, deleteMutationHandlerFailed],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
clickActionButton('deletePipeline', pipelineHeaderSuccess.data.project.pipeline.id);
@ -408,6 +429,7 @@ describe('Pipeline header', () => {
await createComponent([
[getPipelineDetailsQuery, successHandler],
[deletePipelineMutation, deleteMutationHandlerFailed],
[pipelineCiStatusUpdatedSubscription, subscriptionHandler],
]);
clickActionButton('deletePipeline', pipelineHeaderSuccess.data.project.pipeline.id);
@ -423,34 +445,27 @@ describe('Pipeline header', () => {
});
describe('subscription', () => {
it('updates pipeline status when subscription updates', async () => {
await createComponent([
[getPipelineDetailsQuery, runningHandler],
[cancelPipelineMutation, cancelMutationHandlerFailed],
]);
it('calls subscription with correct variables', async () => {
await createComponent();
const {
data: {
project: {
pipeline: { detailedStatus },
},
project: { pipeline },
},
} = pipelineHeaderRunning;
} = pipelineHeaderSuccess;
expect(findStatus().props('status')).toStrictEqual(detailedStatus);
mockedSubscription.next(mockPipelineStatusUpdatedResponse);
await waitForPromises();
expect(findStatus().props('status')).toStrictEqual({
__typename: 'DetailedStatus',
detailsPath: '/root/simple-ci-project/-/pipelines/1257',
icon: 'status_success',
id: 'success-1255-1255',
text: 'Passed',
expect(subscriptionHandler).toHaveBeenCalledWith({
pipelineId: pipeline.id,
});
});
it('does not call subscription when flag is false', async () => {
const realTime = false;
await createComponent(defaultHandlers, realTime);
expect(subscriptionHandler).not.toHaveBeenCalled();
});
});
});
});

View File

@ -1224,3 +1224,9 @@ export const mockPipelineStatusUpdatedResponse = {
},
},
};
export const mockPipelineStatusNullResponse = {
data: {
ciPipelineStatusUpdated: null,
},
};

View File

@ -1,10 +1,10 @@
import { nextTick } from 'vue';
import { GlLink, GlForm } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
import { stubComponent } from 'helpers/stub_component';
import eventHubFactory from '~/helpers/event_hub_factory';
import waitForPromises from 'helpers/wait_for_promises';
import Audio from '~/content_editor/extensions/audio';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
@ -69,7 +69,7 @@ describe.each`
tiptapEditor,
params: { transaction: createTransactionWithMeta() },
});
await nextTick();
await waitForPromises();
};
const buildWrapperAndDisplayMenu = () => {

View File

@ -12,7 +12,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
import CompareVersions from '~/diffs/components/compare_versions.vue';
@ -21,14 +20,11 @@ import NoChanges from '~/diffs/components/no_changes.vue';
import FindingsDrawer from 'ee_component/diffs/components/shared/findings_drawer.vue';
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import DiffAppControls from '~/diffs/components/diff_app_controls.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import eventHub from '~/diffs/event_hub';
import notesEventHub from '~/notes/event_hub';
import { EVT_DISCUSSIONS_ASSIGNED, FILE_BROWSER_VISIBLE } from '~/diffs/constants';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { Mousetrap } from '~/lib/mousetrap';
@ -51,6 +47,7 @@ import {
MR_PREVIOUS_FILE_IN_DIFF,
MR_TOGGLE_REVIEW,
} from '~/behaviors/shortcuts/keybindings';
import { useNotes } from '~/notes/store/legacy_notes';
import createDiffsStore from '../create_diffs_store';
import diffsMockData from '../mock_data/merge_request_diffs';
@ -132,6 +129,8 @@ describe('diffs/components/app', () => {
store.fetchDiffFilesBatch.mockResolvedValue();
store.assignDiscussionsToDiff.mockResolvedValue();
useNotes();
stubPerformanceWebAPI();
// setup globals (needed for component to mount :/)
window.mrTabs = {

View File

@ -160,7 +160,7 @@ describe('CompareVersions', () => {
expect(link.element.getAttribute('href')).toEqual(PREV_COMMIT_URL);
});
it('triggers the correct Vuex action on click', async () => {
it('triggers the correct action on click', async () => {
const link = getPrevCommitNavElement();
link.trigger('click');
@ -190,7 +190,7 @@ describe('CompareVersions', () => {
expect(link.element.getAttribute('href')).toEqual(NEXT_COMMIT_URL);
});
it('triggers the correct Vuex action on click', async () => {
it('triggers the correct action on click', async () => {
const link = getNextCommitNavElement();
link.trigger('click');

View File

@ -1,9 +1,16 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import DiffCommentCell from '~/diffs/components/diff_comment_cell.vue';
import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
Vue.use(PiniaVuePlugin);
describe('DiffCommentCell', () => {
let pinia;
const createWrapper = (props = {}) => {
const { renderDiscussion, ...otherProps } = props;
const line = {
@ -13,10 +20,15 @@ describe('DiffCommentCell', () => {
const diffFileHash = 'abc';
return shallowMount(DiffCommentCell, {
pinia,
propsData: { line, diffFileHash, ...otherProps },
});
};
beforeEach(() => {
pinia = createTestingPinia();
});
it('renders discussions if line has discussions', () => {
const wrapper = createWrapper({ renderDiscussion: true });

View File

@ -2,8 +2,6 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { createTestingPinia } from '@pinia/testing';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import waitForPromises from 'helpers/wait_for_promises';
import { sprintf } from '~/locale';
@ -24,7 +22,6 @@ import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { getDiffFileMock } from '../mock_data/diff_file';
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
jest.mock('~/alert');
@ -32,28 +29,11 @@ describe('DiffContent', () => {
let wrapper;
let pinia;
const noteableTypeGetterMock = jest.fn();
const getUserDataGetterMock = jest.fn();
const defaultProps = {
diffFile: getDiffFileMock(),
};
const createComponent = ({ props, provide } = {}) => {
const fakeStore = new Vuex.Store({
getters: {
getNoteableData() {
return {
current_user: {
can_create_note: true,
},
};
},
noteableType: noteableTypeGetterMock,
getUserData: getUserDataGetterMock,
},
});
const glFeatures = provide ? { ...provide.glFeatures } : {};
wrapper = shallowMount(DiffContentComponent, {
@ -62,7 +42,6 @@ describe('DiffContent', () => {
...props,
},
pinia,
store: fakeStore,
provide: { glFeatures },
});
};
@ -230,7 +209,7 @@ describe('DiffContent', () => {
y: undefined,
width: undefined,
height: undefined,
noteableType: undefined,
noteableType: 'Issue',
},
});
});

View File

@ -1,24 +1,25 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import DiscussionLockedWidget from '~/notes/components/discussion_locked_widget.vue';
import { START_THREAD } from '~/diffs/i18n';
import { useNotes } from '~/notes/store/legacy_notes';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
describe('DiffDiscussionReply', () => {
let wrapper;
let getters;
let store;
let pinia;
const createComponent = (props = {}, slots = {}) => {
wrapper = shallowMount(DiffDiscussionReply, {
store,
pinia,
propsData: {
...props,
},
@ -28,26 +29,21 @@ describe('DiffDiscussionReply', () => {
});
};
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
useLegacyDiffs();
useNotes();
});
describe('if user is signed in', () => {
beforeEach(() => {
getters = {
userCanReply: () => true,
getNoteableData: () => ({
current_user: {
can_create_note: true,
},
}),
getUserData: () => ({
path: 'test-path',
avatar_url: 'avatar_url',
name: 'John Doe',
id: 1,
}),
useNotes().noteableData.current_user = { can_create_note: true };
useNotes().userData = {
path: 'test-path',
avatar_url: 'avatar_url',
name: 'John Doe',
id: 1,
};
store = new Vuex.Store({
getters,
});
});
it('should render a form if component has form', () => {
@ -84,14 +80,7 @@ describe('DiffDiscussionReply', () => {
`(
'reply button existence is `$showButton` when userCanReply is `$userCanReply`, hasForm is `$hasForm` and renderReplyPlaceholder is `$renderReplyPlaceholder`',
({ userCanReply, hasForm, renderReplyPlaceholder, showButton }) => {
getters = {
...getters,
userCanReply: () => userCanReply,
};
store = new Vuex.Store({
getters,
});
useNotes().noteableData.current_user = { can_create_note: userCanReply };
createComponent({
renderReplyPlaceholder,
@ -103,18 +92,7 @@ describe('DiffDiscussionReply', () => {
);
it('shows the locked discussion widget when the user is not allowed to create notes', () => {
getters = {
...getters,
getNoteableData: () => ({
current_user: {
can_create_note: false,
},
}),
};
store = new Vuex.Store({
getters,
});
useNotes().noteableData.current_user = { can_create_note: false };
createComponent({
renderReplyPlaceholder: false,
@ -127,19 +105,8 @@ describe('DiffDiscussionReply', () => {
describe('if user is signed out', () => {
beforeEach(() => {
getters = {
userCanReply: () => false,
getNoteableData: () => ({
current_user: {
can_create_note: false,
},
}),
getUserData: () => null,
};
store = new Vuex.Store({
getters,
});
useNotes().noteableData.current_user = { can_create_note: false };
useNotes().userData = null;
});
it('renders a signed out widget when user is not logged in', () => {

View File

@ -1,19 +1,10 @@
import Vue, { nextTick } from 'vue';
import { cloneDeep } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { createTestingPinia } from '@pinia/testing';
import { PiniaVuePlugin } from 'pinia';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '~/diffs/constants';
import { reviewFile, setFileForcedOpen } from '~/diffs/store/actions';
import {
SET_DIFF_FILE_VIEWED,
SET_MR_FILE_REVIEWS,
SET_FILE_FORCED_OPEN,
} from '~/diffs/store/mutation_types';
import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
@ -21,12 +12,13 @@ import { sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { globalAccessorPlugin } from '~/pinia/plugins';
import testAction from '../../__helpers__/vuex_action_helper';
import { useNotes } from '~/notes/store/legacy_notes';
import diffDiscussionsMockData from '../mock_data/diff_discussions';
jest.mock('~/lib/utils/common_utils', () => ({
convertObjectPropsToCamelCase: jest.requireActual('~/lib/utils/common_utils')
.convertObjectPropsToCamelCase,
isInMRPage: jest.requireActual('~/lib/utils/common_utils').isInMRPage,
scrollToElement: jest.fn(),
isLoggedIn: () => true,
}));
@ -52,20 +44,11 @@ const createDiffFile = () => ({
},
});
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
describe('DiffFileHeader component', () => {
let wrapper;
let pinia;
let mockStoreConfig;
const defaultMockStoreConfig = {
state: {},
getters: {
getNoteableData: () => ({ current_user: { can_create_note: true } }),
},
};
const getFirstDiffFile = () => useLegacyDiffs().diffFiles[0];
const findHeader = () => wrapper.findComponent({ ref: 'header' });
@ -84,9 +67,6 @@ describe('DiffFileHeader component', () => {
const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']");
const createComponent = ({ props, options = {} } = {}) => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
const store = new Vuex.Store({ ...mockStoreConfig, ...options.store });
wrapper = shallowMountExtended(DiffFileHeader, {
propsData: {
diffFile: getFirstDiffFile(),
@ -95,7 +75,6 @@ describe('DiffFileHeader component', () => {
...props,
},
...options,
store,
pinia,
});
};
@ -103,6 +82,7 @@ describe('DiffFileHeader component', () => {
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
useLegacyDiffs().diffFiles = [createDiffFile()];
useNotes();
});
it.each`
@ -135,18 +115,10 @@ describe('DiffFileHeader component', () => {
createComponent();
findHeader().trigger('click');
return testAction(
setFileForcedOpen,
{ filePath: getFirstDiffFile().file_path, forced: false },
{},
[
{
type: SET_FILE_FORCED_OPEN,
payload: { filePath: getFirstDiffFile().file_path, forced: false },
},
],
[],
);
expect(useLegacyDiffs().setFileForcedOpen).toHaveBeenCalledWith({
filePath: getFirstDiffFile().file_path,
forced: false,
});
});
it('when collapseIcon is clicked emits toggleFile', async () => {
@ -565,19 +537,7 @@ describe('DiffFileHeader component', () => {
expect(document.activeElement.blur).toHaveBeenCalled();
return testAction(
reviewFile,
{ file, reviewed: true },
{},
[
{ type: SET_DIFF_FILE_VIEWED, payload: { id: file.id, seen: true } },
{
type: SET_MR_FILE_REVIEWS,
payload: { [file.file_identifier_hash]: [file.id, `hash:${file.file_hash}`] },
},
],
[],
);
expect(useLegacyDiffs().reviewFile).toHaveBeenCalledWith({ file, reviewed: true });
});
it.each`
@ -682,39 +642,18 @@ describe('DiffFileHeader component', () => {
});
findReviewFileCheckbox().vm.$emit('change', true);
testAction(
setFileForcedOpen,
{ filePath: getFirstDiffFile().file_path, forced: false },
{},
[
{
type: SET_FILE_FORCED_OPEN,
payload: { filePath: getFirstDiffFile().file_path, forced: false },
},
],
[],
);
findReviewFileCheckbox().vm.$emit('change', false);
testAction(
setFileForcedOpen,
{ filePath: getFirstDiffFile().file_path, forced: false },
{},
[
{
type: SET_FILE_FORCED_OPEN,
payload: { filePath: getFirstDiffFile().file_path, forced: false },
},
],
[],
);
expect(useLegacyDiffs().setFileForcedOpen).toHaveBeenCalledWith({
filePath: getFirstDiffFile().file_path,
forced: false,
});
});
});
it('should render the comment on files button', () => {
window.gon = { current_user_id: 1 };
useNotes().noteableData.current_user = { can_create_note: true };
createComponent({
props: {
addMergeRequestButtons: true,

View File

@ -1,7 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { GlSprintf } from '@gitlab/ui';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
@ -26,7 +24,6 @@ import axios from '~/lib/utils/axios_utils';
import { clearDraft } from '~/lib/utils/autosave';
import { scrollToElement, isElementStuck } from '~/lib/utils/common_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import createNotesStore from '~/notes/stores/modules';
import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import notesEventHub from '~/notes/event_hub';
@ -47,7 +44,6 @@ jest.mock('~/notes/mixins/diff_line_note_form', () => ({
},
}));
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent);
@ -70,7 +66,6 @@ const triggerSaveDraftNote = (wrapper, note, parent, error) =>
describe('DiffFile', () => {
let wrapper;
let store;
let pinia;
let axiosMock;
@ -128,26 +123,8 @@ describe('DiffFile', () => {
useLegacyDiffs().diffFiles[index].renderIt = true;
}
function createComponent({
first = false,
last = false,
options = {},
props = {},
getters = {},
} = {}) {
const notes = createNotesStore();
notes.getters = {
...notes.getters,
...getters.notes,
};
store = new Vuex.Store({
...notes,
});
function createComponent({ first = false, last = false, options = {}, props = {} } = {}) {
wrapper = shallowMountExtended(DiffFileComponent, {
store,
pinia,
propsData: {
file: useLegacyDiffs().diffFiles[0],
@ -475,8 +452,6 @@ describe('DiffFile', () => {
describe('toggle', () => {
it('should update store state', () => {
createComponent();
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(() => {});
toggleFile(wrapper);
expect(useLegacyDiffs().setFileCollapsedByUser).toHaveBeenCalledWith({
@ -487,7 +462,6 @@ describe('DiffFile', () => {
describe('scoll-to-top of file after collapse', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(() => {});
isElementStuck.mockReturnValueOnce(true);
});
@ -829,7 +803,7 @@ describe('DiffFile', () => {
noteableData: expect.any(Object),
diffFile: file,
positionType: FILE_DIFF_POSITION_TYPE,
noteableType: store.getters.noteableType,
noteableType: useNotes().noteableType,
},
});
});

View File

@ -6,7 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import { sprintf } from '~/locale';
import { createAlert } from '~/alert';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import store from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import { clearDraft } from '~/lib/utils/autosave';
@ -34,8 +33,6 @@ describe('DiffLineNoteForm', () => {
let diffLines;
const createComponent = ({ props } = {}) => {
wrapper?.destroy();
const propsData = {
diffFileHash: diffFile.file_hash,
diffLines,
@ -46,9 +43,6 @@ describe('DiffLineNoteForm', () => {
};
wrapper = shallowMount(DiffLineNoteForm, {
mocks: {
$store: store,
},
propsData,
pinia,
});
@ -65,13 +59,9 @@ describe('DiffLineNoteForm', () => {
useLegacyDiffs().diffFiles = [diffFile];
useLegacyDiffs().saveDiffDiscussion.mockResolvedValue();
useNotes().userData = { id: 1 };
useNotes().noteableData = noteableDataMock;
useBatchComments().saveDraft.mockResolvedValue();
useMrNotes();
store.reset();
store.state.notes.noteableData = noteableDataMock;
createComponent();
});
@ -169,7 +159,7 @@ describe('DiffLineNoteForm', () => {
describe('saving note', () => {
beforeEach(() => {
store.getters.noteableType = 'merge-request';
useNotes().noteableData.merge_params = {};
});
it('should save original line', async () => {
@ -195,9 +185,9 @@ describe('DiffLineNoteForm', () => {
note: noteBody,
formData: {
noteableData: noteableDataMock,
noteableType: store.getters.noteableType,
noteableType: useNotes().noteableType,
noteTargetLine: diffLines[1],
diffViewType: store.state.diffs.diffViewType,
diffViewType: useLegacyDiffs().diffViewType,
diffFile,
linePosition: '',
lineRange,
@ -211,7 +201,7 @@ describe('DiffLineNoteForm', () => {
it('should save selected line from the store', async () => {
const lineCode = 'test';
store.state.notes.selectedCommentPosition = { start: { line_code: lineCode } };
useNotes().selectedCommentPosition = { start: { line_code: lineCode } };
createComponent();
const noteBody = 'note body';
@ -221,9 +211,9 @@ describe('DiffLineNoteForm', () => {
note: noteBody,
formData: {
noteableData: noteableDataMock,
noteableType: store.getters.noteableType,
noteableType: useNotes().noteableType,
noteTargetLine: diffLines[1],
diffViewType: store.state.diffs.diffViewType,
diffViewType: useLegacyDiffs().diffViewType,
diffFile,
linePosition: '',
lineRange: {

View File

@ -1,8 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { throttle } from 'lodash';
import { PiniaVuePlugin } from 'pinia';
import DiffView from '~/diffs/components/diff_view.vue';
@ -12,7 +10,6 @@ import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { createCustomGetters } from 'helpers/pinia_helpers';
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
jest.mock('lodash/throttle', () => jest.fn((fn) => fn));
@ -24,19 +21,9 @@ describe('DiffView', () => {
const DiffExpansionCell = { template: `<div/>` };
const DiffRow = { template: `<div/>` };
const DiffCommentCell = { template: `<div/>` };
const setSelectedCommentPosition = jest.fn();
const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm;
const createWrapper = ({ props } = {}) => {
const notes = {
actions: { setSelectedCommentPosition },
state: { selectedCommentPosition: null, selectedCommentPositionHover: null },
};
const store = new Vuex.Store({
modules: { notes },
});
const propsData = {
diffFile: { file_hash: '123' },
diffLines: [],
@ -45,7 +32,7 @@ describe('DiffView', () => {
};
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell };
return shallowMount(DiffView, { propsData, pinia, store, stubs });
return shallowMount(DiffView, { propsData, pinia, stubs });
};
beforeEach(() => {
@ -118,14 +105,14 @@ describe('DiffView', () => {
diffRow.$emit('startdragging', { line: { chunk: 0 } });
diffRow.$emit('enterdragging', { chunk: 1 });
expect(setSelectedCommentPosition).not.toHaveBeenCalled();
expect(useNotes().setSelectedCommentPosition).not.toHaveBeenCalled();
});
it.each`
start | end | expectation
${1} | ${2} | ${{ start: { index: 1 }, end: { index: 2 } }}
${2} | ${1} | ${{ start: { index: 1 }, end: { index: 2 } }}
${1} | ${1} | ${{ start: { index: 1 }, end: { index: 1 } }}
${1} | ${2} | ${{ start: { chunk: 1, index: 1 }, end: { chunk: 1, index: 2 } }}
${2} | ${1} | ${{ start: { chunk: 1, index: 1 }, end: { chunk: 1, index: 2 } }}
${1} | ${1} | ${{ start: { chunk: 1, index: 1 }, end: { chunk: 1, index: 1 } }}
`(
'calls `setSelectedCommentPosition` with correct `updatedLineRange`',
({ start, end, expectation }) => {
@ -135,9 +122,7 @@ describe('DiffView', () => {
diffRow.$emit('startdragging', { line: { chunk: 1, index: start } });
diffRow.$emit('enterdragging', { chunk: 1, index: end });
const arg = setSelectedCommentPosition.mock.calls[0][1];
expect(arg).toMatchObject(expectation);
expect(useNotes().setSelectedCommentPosition).toHaveBeenCalledWith(expectation);
},
);
@ -164,7 +149,7 @@ describe('DiffView', () => {
jest.runOnlyPendingTimers();
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(1);
expect(useNotes().setSelectedCommentPosition).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -6,7 +6,7 @@ import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import store from '~/mr_notes/stores';
import { useNotes } from '~/notes/store/legacy_notes';
import { imageDiffDiscussions } from '../mock_data/diff_discussions';
Vue.use(PiniaVuePlugin);
@ -24,7 +24,6 @@ describe('Diffs image diff overlay component', () => {
function createComponent(props = {}) {
wrapper = shallowMount(ImageDiffOverlay, {
store,
pinia,
parentComponent: {
data() {
@ -39,12 +38,17 @@ describe('Diffs image diff overlay component', () => {
...props,
},
});
// Vue 3 doesn't stub parent component's state with data()
if (!wrapper.vm.$parent.width) {
wrapper.vm.$parent.width = dimensions.width;
wrapper.vm.$parent.height = dimensions.height;
}
}
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
useLegacyDiffs();
jest.spyOn(store, 'dispatch').mockResolvedValue();
useNotes();
});
it('renders comment badges', () => {
@ -104,16 +108,15 @@ describe('Diffs image diff overlay component', () => {
it('disables buttons when shouldToggleDiscussion is false', () => {
createComponent({ shouldToggleDiscussion: false });
expect(getAllImageBadges().at(0).attributes('disabled')).toBe('true');
// Vue 3 sets disabled="disabled", Vue 2 disabled="true"
expect(['true', 'disabled']).toContain(getAllImageBadges().at(0).attributes('disabled'));
});
it('dispatches toggleDiscussion when clicking image badge', () => {
createComponent();
getAllImageBadges().at(0).vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
discussionId: '1',
});
expect(useNotes().toggleDiscussion).toHaveBeenCalledWith({ discussionId: '1' });
});
});
@ -130,11 +133,11 @@ describe('Diffs image diff overlay component', () => {
});
it('renders comment form badge', () => {
expect(getAllImageBadges().at(-1).exists()).toBe(true);
expect(getAllImageBadges().at(2).exists()).toBe(true);
});
it('sets comment form badge position', () => {
expect(getAllImageBadges().at(-1).props('position')).toStrictEqual({
expect(getAllImageBadges().at(2).props('position')).toStrictEqual({
left: '10%',
top: '10%',
});

View File

@ -4,9 +4,9 @@ import { shallowMount, mount } from '@vue/test-utils';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import NoChanges from '~/diffs/components/no_changes.vue';
import store from '~/mr_notes/stores';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useNotes } from '~/notes/store/legacy_notes';
import { createMrVersionsMock } from '../mock_data/merge_request_diffs';
jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
@ -21,9 +21,6 @@ describe('Diff no changes empty state', () => {
const createComponent = (mountFn = shallowMount) =>
mountFn(NoChanges, {
mocks: {
$store: store,
},
propsData: {
changesEmptyStateIllustration: '',
},
@ -33,22 +30,16 @@ describe('Diff no changes empty state', () => {
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
useLegacyDiffs().mergeRequestDiffs = createMrVersionsMock();
store.reset();
store.getters.getNoteableData = {
target_branch: TEST_TARGET_BRANCH,
source_branch: TEST_SOURCE_BRANCH,
};
useNotes().noteableData.target_branch = TEST_TARGET_BRANCH;
useNotes().noteableData.source_branch = TEST_SOURCE_BRANCH;
});
const findEmptyState = (wrapper) => wrapper.findComponent(GlEmptyState);
const findMessage = (wrapper) => wrapper.find('[data-testid="no-changes-message"]');
it('prevents XSS', () => {
store.getters.getNoteableData = {
source_branch: '<script>alert("test");</script>',
target_branch: '<script>alert("test");</script>',
};
useNotes().noteableData.source_branch = '<script>alert("test");</script>';
useNotes().noteableData.target_branch = '<script>alert("test");</script>';
const wrapper = createComponent();

View File

@ -34,7 +34,11 @@ describe('Repository last commit component', () => {
const subscriptionHandler = jest.fn().mockResolvedValue(mockPipelineStatusUpdatedResponse);
const createComponent = (data = {}, pipelineSubscriptionHandler = subscriptionHandler) => {
const createComponent = (
data = {},
pipelineSubscriptionHandler = subscriptionHandler,
isRealTime = true,
) => {
const currentPath = 'path';
commitData = createCommitData(data);
@ -50,7 +54,7 @@ describe('Repository last commit component', () => {
propsData: { currentPath, historyUrl: '/history' },
provide: {
glFeatures: {
ciPipelineStatusRealtime: true,
ciPipelineStatusRealtime: isRealTime,
},
},
});
@ -182,6 +186,16 @@ describe('Repository last commit component', () => {
pipelineId: 'gid://gitlab/Ci::Pipeline/167',
});
});
it('does not call the subscription when feature flag is false', async () => {
const realTime = false;
createComponent({}, subscriptionHandler, realTime);
await waitForPromises();
expect(subscriptionHandler).not.toHaveBeenCalled();
});
});
describe('polling', () => {

View File

@ -195,7 +195,7 @@ describe('GlobalSearchTopbar', () => {
it(`calls applyQuery ${called ? '' : 'NOT '}`, async () => {
await nextTick();
findGlSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
findGlSearchBox().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ENTER_KEY }));
expect(actionSpies.applyQuery).toHaveBeenCalledTimes(called ? 1 : 0);
});
});

View File

@ -1,5 +1,5 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import Component from '~/sidebar/components/assignees/assignee_title.vue';
@ -7,7 +7,7 @@ describe('AssigneeTitle component', () => {
let wrapper;
const createComponent = (props) => {
return shallowMount(Component, {
return shallowMountExtended(Component, {
propsData: {
numberOfAssignees: 0,
editable: false,
@ -17,6 +17,8 @@ describe('AssigneeTitle component', () => {
});
};
const findEditLink = () => wrapper.findByTestId('edit-link');
describe('assignee title', () => {
it('renders assignee', () => {
wrapper = createComponent({
@ -41,7 +43,7 @@ describe('AssigneeTitle component', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true });
expect(wrapper.find('[data-test-id="edit-link"]').text()).toEqual('Edit');
expect(findEditLink().text()).toBe('Edit');
});
});
@ -49,7 +51,7 @@ describe('AssigneeTitle component', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true, changing: true });
expect(wrapper.find('[data-test-id="edit-link"]').text()).toEqual('Apply');
expect(findEditLink().text()).toBe('Apply');
});
});

View File

@ -133,7 +133,7 @@ describe('EntitySelect', () => {
});
it("renders the error slot's content", () => {
const selector = 'data-test-id="error-element"';
const selector = 'data-testid="error-element"';
createComponent({
slots: {
error: `<div ${selector} />`,

View File

@ -63,7 +63,6 @@ RSpec.describe Projects::PipelineHelper do
full_path: project.full_path,
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
pipeline_iid: pipeline.iid,
pipeline_id: pipeline.id,
pipelines_path: project_pipelines_path(project)
})
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::API::Entities::Packages::Conan::RecipeRevision, feature_category: :package_registry do
let(:recipe_revision) { build_stubbed(:conan_recipe_revision) }
let(:entity) { described_class.new(recipe_revision) }
subject { entity.as_json }
it 'exposes required attributes' do
is_expected.to eq(
revision: recipe_revision.revision,
time: recipe_revision.created_at
)
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::API::Entities::Packages::Conan::Revision, feature_category: :package_registry do
shared_examples 'exposes revision attributes' do |factory|
let(:revision) { build_stubbed(factory) }
let(:entity) { described_class.new(revision) }
subject { entity.as_json }
it 'exposes required attributes' do
is_expected.to eq(
revision: revision.revision,
time: revision.created_at
)
end
end
it_behaves_like 'exposes revision attributes', :conan_recipe_revision
it_behaves_like 'exposes revision attributes', :conan_package_revision
end

View File

@ -240,100 +240,6 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
end
end
describe '.for_push_exists?' do
subject do
project
.container_registry_protection_rules
.for_push_exists?(
access_level: access_level,
repository_path: repository_path
)
end
context 'when the repository path matches multiple protection rules' do
# The abbreviation `crpr` stands for container registry protection rule
let_it_be(:project_with_crpr) { create(:project) }
let_it_be(:project_without_crpr) { create(:project) }
let_it_be(:protection_rule_for_developer) do
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-stage*",
minimum_access_level_for_push: :maintainer
)
end
let_it_be(:protection_rule_for_maintainer) do
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-prod*",
minimum_access_level_for_push: :owner
)
end
let_it_be(:protection_rule_for_owner) do
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-release*",
minimum_access_level_for_push: :admin
)
end
let_it_be(:protection_rule_overlapping_for_developer) do
create(:container_registry_protection_rule,
project: project_with_crpr,
repository_path_pattern: "#{project_with_crpr.full_path}/my-container-*",
minimum_access_level_for_push: :maintainer
)
end
# rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table syntax
where(:project, :access_level, :repository_path, :for_push_exists) do
ref(:project_with_crpr) | Gitlab::Access::REPORTER | lazy { "#{project_with_crpr.full_path}/my-container-stage-sha-1234" } | true
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | lazy { "#{project_with_crpr.full_path}/my-container-stage-sha-1234" } | true
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | lazy { "#{project_with_crpr.full_path}/my-container-stage-sha-1234" } | false
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | lazy { "#{project_with_crpr.full_path}/my-container-stage-sha-1234" } | false
ref(:project_with_crpr) | Gitlab::Access::OWNER | lazy { "#{project_with_crpr.full_path}/my-container-stage-sha-1234" } | false
ref(:project_with_crpr) | Gitlab::Access::ADMIN | lazy { "#{project_with_crpr.full_path}/my-container-stage-sha-1234" } | false
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | lazy { "#{project_with_crpr.full_path}/my-container-prod-sha-1234" } | true
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | lazy { "#{project_with_crpr.full_path}/my-container-prod-sha-1234" } | true
ref(:project_with_crpr) | Gitlab::Access::OWNER | lazy { "#{project_with_crpr.full_path}/my-container-prod-sha-1234" } | false
ref(:project_with_crpr) | Gitlab::Access::ADMIN | lazy { "#{project_with_crpr.full_path}/my-container-prod-sha-1234" } | false
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | lazy { "#{project_with_crpr.full_path}/my-container-release-v1" } | true
ref(:project_with_crpr) | Gitlab::Access::OWNER | lazy { "#{project_with_crpr.full_path}/my-container-release-v1" } | true
ref(:project_with_crpr) | Gitlab::Access::ADMIN | lazy { "#{project_with_crpr.full_path}/my-container-release-v1" } | false
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | lazy { "#{project_with_crpr.full_path}/my-container-any-suffix" } | true
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | lazy { "#{project_with_crpr.full_path}/my-container-any-suffix" } | false
ref(:project_with_crpr) | Gitlab::Access::OWNER | lazy { "#{project_with_crpr.full_path}/my-container-any-suffix" } | false
# For non-matching repository_path
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | lazy { "#{project_with_crpr.full_path}/non-matching-container" } | false
# For no access level
ref(:project_with_crpr) | Gitlab::Access::NO_ACCESS | lazy { "#{project_with_crpr.full_path}/my-container-prod-sha-1234" } | true
# Edge cases
ref(:project_with_crpr) | 0 | '' | false
ref(:project_with_crpr) | nil | nil | false
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | nil | false
ref(:project_with_crpr) | nil | lazy { "#{project_with_crpr.full_path}/non-matching-container" } | false
# For projects that have no container registry protection rules
ref(:project_without_crpr) | Gitlab::Access::DEVELOPER | lazy { "#{project_without_crpr.full_path}/my-container-prod-sha-1234" } | false
ref(:project_without_crpr) | Gitlab::Access::MAINTAINER | lazy { "#{project_without_crpr.full_path}/my-container-prod-sha-1234" } | false
ref(:project_without_crpr) | Gitlab::Access::OWNER | lazy { "#{project_without_crpr.full_path}/my-container-prod-sha-1234" } | false
end
# rubocop:enable Layout/LineLength
with_them do
it { is_expected.to eq for_push_exists }
end
end
end
describe '.for_action_exists?' do
let_it_be(:project1) { create(:project) }
let_it_be(:project_no_crpr) { create(:project) }

View File

@ -61,4 +61,41 @@ RSpec.describe Packages::Conan::PackageRevision, type: :model, feature_category:
end
end
end
describe 'scopes' do
describe '.order_by_id_desc' do
let_it_be(:revision_1) { create(:conan_package_revision) }
let_it_be(:revision_2) { create(:conan_package_revision) }
subject { described_class.order_by_id_desc }
it { is_expected.to eq([revision_2, revision_1]) }
end
describe '.by_recipe_revision_and_package_reference' do
let_it_be(:package) { create(:conan_package) }
let_it_be(:recipe_revision) { package.conan_recipe_revisions.first }
let_it_be(:package_reference) { package.conan_package_references.first }
let_it_be(:package_revision) { package.conan_package_revisions.first }
let(:revision_value) { recipe_revision.revision }
let(:reference_value) { package_reference.reference }
subject { described_class.by_recipe_revision_and_package_reference(revision_value, reference_value) }
it { is_expected.to contain_exactly(package_revision) }
context 'when recipe revision does not match' do
it 'returns empty relation' do
expect(described_class.by_recipe_revision_and_package_reference('nonexistent', reference_value)).to be_empty
end
end
context 'when package reference does not match' do
it 'returns empty relation' do
expect(described_class.by_recipe_revision_and_package_reference(revision_value, 'nonexistent')).to be_empty
end
end
end
end
end

View File

@ -281,4 +281,52 @@ RSpec.describe API::Conan::V2::ProjectPackages, feature_category: :package_regis
it_behaves_like 'package not found'
it_behaves_like 'project not found by project id'
end
describe 'GET /api/v4/projects/:id/packages/conan/v2/conans/:package_name/:package_version/:package_username' \
'/:package_channel/revisions/:recipe_revision/packages/:conan_package_reference/latest' do
let(:recipe_path) { package.conan_recipe_path }
let(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let(:conan_package_reference) { package.conan_package_references.first.reference }
let(:url_suffix) { "#{recipe_path}/revisions/#{recipe_revision}/packages/#{conan_package_reference}/latest" }
subject(:request) { get api(url), headers: headers }
it 'returns the latest revision' do
request
expect(response).to have_gitlab_http_status(:ok)
package_revision = package.conan_package_revisions.first
expect(json_response['revision']).to eq(package_revision.revision)
expect(json_response['time']).to eq(package_revision.created_at.iso8601(3))
end
shared_examples 'returns 404 when resource does not exist' do
it 'returns 404' do
request
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Revision Not Found')
end
end
context 'when recipe revision does not exist' do
let(:recipe_revision) { OpenSSL::Digest.hexdigest('MD5', 'nonexistent-revision') }
it_behaves_like 'returns 404 when resource does not exist'
end
context 'when package reference does not exist' do
let(:conan_package_reference) { OpenSSL::Digest.hexdigest('SHA1', 'nonexistent-reference') }
it_behaves_like 'returns 404 when resource does not exist'
end
it_behaves_like 'enforcing read_packages job token policy'
it_behaves_like 'accept get request on private project with access to package registry for everyone'
it_behaves_like 'conan FIPS mode'
it_behaves_like 'package not found'
it_behaves_like 'project not found by project id'
end
end

View File

@ -173,4 +173,12 @@ RSpec.describe ContainerRegistry::Protection::UpdateTagRuleService, '#execute',
it_behaves_like 'an erroneous service response',
message: 'GitLab container registry API not supported'
end
context 'when the rule is immutable' do
let_it_be(:container_protection_tag_rule) do
create(:container_registry_protection_tag_rule, :immutable, project: project, tag_name_pattern: 'a')
end
it_behaves_like 'an erroneous service response', message: 'Operation not allowed'
end
end

View File

@ -278,10 +278,6 @@ RSpec.configure do |config|
::Ci::ApplicationRecord.set_open_transactions_baseline
end
config.around do |example|
example.run
end
config.append_after do
ApplicationRecord.reset_open_transactions_baseline
::Ci::ApplicationRecord.reset_open_transactions_baseline

View File

@ -1496,6 +1496,34 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like params[:shared_examples_name]
end
end
context 'for deploy tokens' do
let_it_be(:deploy_token) { create(:deploy_token, write_registry: true, projects: [current_project]) }
let(:current_params) { { scopes: current_params_scopes, deploy_token: deploy_token } }
before do
container_registry_protection_rule.update!(
repository_path_pattern: repository_path_pattern,
minimum_access_level_for_push: minimum_access_level_for_push
)
end
# rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table layout
where(:repository_path_pattern, :minimum_access_level_for_push, :current_params_scopes, :shared_examples_name) do
ref(:container_repository_path) | :maintainer | lazy { ["repository:#{container_repository_path}:push"] } | 'a protected container repository'
ref(:container_repository_path) | :maintainer | lazy { ["repository:#{container_repository_path}:push,pull"] } | 'a protected container repository'
ref(:container_repository_path) | :owner | lazy { ["repository:#{container_repository_path}:push"] } | 'a protected container repository'
ref(:container_repository_path) | :admin | lazy { ["repository:#{container_repository_path}:push"] } | 'a protected container repository'
ref(:container_repository_path_pattern_no_match) | :maintainer | lazy { ["repository:#{container_repository_path}:push"] } | 'a pushable'
ref(:container_repository_path_pattern_no_match) | :admin | lazy { ["repository:#{container_repository_path}:push"] } | 'a pushable'
end
# rubocop:enable Layout/LineLength
with_them do
it_behaves_like params[:shared_examples_name]
end
end
end
context 'with protected tags' do