Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
67146ed9ad
commit
3f7e1004e7
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
# Cop supports --autocorrect.
|
||||
RSpec/RedundantAround:
|
||||
Exclude:
|
||||
- 'spec/spec_helper.rb'
|
||||
4
Gemfile
4
Gemfile
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
14
Gemfile.lock
14
Gemfile.lock
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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({});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
43653559bec7b7367fec087959d6a01daa921c1918277c8fdb6e38d42d99b107
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ a more systemic problem you need to investigate.
|
|||
1. Select **Your work**.
|
||||
1. Select **Environments**.
|
||||
|
||||

|
||||

|
||||
|
||||
The Environments dashboard displays a paginated list of projects that includes
|
||||
up to three environments per project.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1224,3 +1224,9 @@ export const mockPipelineStatusUpdatedResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockPipelineStatusNullResponse = {
|
||||
data: {
|
||||
ciPipelineStatusUpdated: null,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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%',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue