Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2025-05-22 21:07:49 +00:00
parent eda8522f83
commit 6d6df5b256
32 changed files with 336 additions and 132 deletions

View File

@ -313,9 +313,6 @@ export default {
'ee/app/assets/javascripts/projects/components/move_personal_project_to_group_modal.vue', 'ee/app/assets/javascripts/projects/components/move_personal_project_to_group_modal.vue',
'ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue', 'ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue',
'ee/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue', 'ee/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue',
'ee/app/assets/javascripts/requirements/components/export_requirements_modal.vue',
'ee/app/assets/javascripts/requirements/components/import_requirements_modal.vue',
'ee/app/assets/javascripts/requirements/components/requirements_root.vue',
'ee/app/assets/javascripts/roadmap/components/current_day_indicator.vue', 'ee/app/assets/javascripts/roadmap/components/current_day_indicator.vue',
'ee/app/assets/javascripts/roadmap/components/epic_item.vue', 'ee/app/assets/javascripts/roadmap/components/epic_item.vue',
'ee/app/assets/javascripts/roadmap/components/epic_item_details.vue', 'ee/app/assets/javascripts/roadmap/components/epic_item_details.vue',

View File

@ -582,6 +582,7 @@ lib/gitlab/checks/**
/doc/administration/application_settings_cache.md @jglassman1 /doc/administration/application_settings_cache.md @jglassman1
/doc/administration/auditor_users.md @idurham /doc/administration/auditor_users.md @idurham
/doc/administration/auth/ @idurham /doc/administration/auth/ @idurham
/doc/administration/backup_restore/ @axil
/doc/administration/broadcast_messages.md @sselhorn /doc/administration/broadcast_messages.md @sselhorn
/doc/administration/cells.md @emily.sahlani /doc/administration/cells.md @emily.sahlani
/doc/administration/cicd/ @lyspin /doc/administration/cicd/ @lyspin
@ -658,7 +659,7 @@ lib/gitlab/checks/**
/doc/administration/raketasks/tokens/ @idurham /doc/administration/raketasks/tokens/ @idurham
/doc/administration/raketasks/x509_signatures.md @brendan777 /doc/administration/raketasks/x509_signatures.md @brendan777
/doc/administration/read_only_gitlab.md @axil @eread /doc/administration/read_only_gitlab.md @axil @eread
/doc/administration/redis/ @axil @eread /doc/administration/redis/ @axil
/doc/administration/reference_architectures/ @axil @eread /doc/administration/reference_architectures/ @axil @eread
/doc/administration/reply_by_email.md @lciutacu /doc/administration/reply_by_email.md @lciutacu
/doc/administration/reply_by_email_postfix_setup.md @axil @eread /doc/administration/reply_by_email_postfix_setup.md @axil @eread
@ -704,8 +705,7 @@ lib/gitlab/checks/**
/doc/administration/settings/third_party_offers.md @phillipwells /doc/administration/settings/third_party_offers.md @phillipwells
/doc/administration/settings/visibility_and_access_controls.md @brendan777 /doc/administration/settings/visibility_and_access_controls.md @brendan777
/doc/administration/settings/vscode_extension_marketplace.md @brendan777 /doc/administration/settings/vscode_extension_marketplace.md @brendan777
/doc/administration/sidekiq/ @axil @eread /doc/administration/sidekiq/ @axil
/doc/administration/sidekiq/sidekiq_memory_killer.md @jglassman1
/doc/administration/silent_mode/ @axil /doc/administration/silent_mode/ @axil
/doc/administration/smime_signing_email.md @axil @eread /doc/administration/smime_signing_email.md @axil @eread
/doc/administration/snippets/ @brendan777 /doc/administration/snippets/ @brendan777
@ -953,6 +953,7 @@ lib/gitlab/checks/**
/doc/development/cascading_settings.md @gitlab-org/foundations/engineering /doc/development/cascading_settings.md @gitlab-org/foundations/engineering
/doc/development/cells/ @OmarQunsulGitlab @bmarjanovic /doc/development/cells/ @OmarQunsulGitlab @bmarjanovic
/doc/development/cicd/ @gitlab-org/maintainers/cicd-verify /doc/development/cicd/ @gitlab-org/maintainers/cicd-verify
/doc/development/compromised_password_detection.md @gitlab-org/software-supply-chain-security/authentication/approvers
/doc/development/contributing/verify/ @gitlab-org/maintainers/cicd-verify /doc/development/contributing/verify/ @gitlab-org/maintainers/cicd-verify
/doc/development/database/ @OmarQunsulGitlab @bmarjanovic /doc/development/database/ @OmarQunsulGitlab @bmarjanovic
/doc/development/distributed_tracing.md @gitlab-org/analytics-section/product-analytics/engineers/frontend /doc/development/distributed_tracing.md @gitlab-org/analytics-section/product-analytics/engineers/frontend

View File

@ -74,6 +74,8 @@ compile-production-assets as-if-foss:
- .qa:rules:test-on-omnibus-ce:follow-up - .qa:rules:test-on-omnibus-ce:follow-up
compile-test-assets: compile-test-assets:
variables:
BABEL_ENV: "$BABEL_ENV"
extends: extends:
- .compile-assets-base - .compile-assets-base
- .frontend:rules:compile-test-assets - .frontend:rules:compile-test-assets

View File

@ -2025,6 +2025,8 @@
variables: variables:
<<: *qa-e2e-test-schedule-variables <<: *qa-e2e-test-schedule-variables
COVERBAND_ENABLED: "true" COVERBAND_ENABLED: "true"
BABEL_ENV: "istanbul" # This sets the environmental variable enabling `istanbul` plugin in Babel
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/babel.config.js
QA_RUN_IN_PARALLEL: "false" QA_RUN_IN_PARALLEL: "false"
.qa:rules:e2e:test-on-cng: .qa:rules:e2e:test-on-cng:

View File

@ -56,6 +56,7 @@ workflow:
job: clone-gitlab-repo job: clone-gitlab-repo
variables: variables:
COVERBAND_ENABLED: "$COVERBAND_ENABLED" # explicitly define variable so it is passed in to gdk service container COVERBAND_ENABLED: "$COVERBAND_ENABLED" # explicitly define variable so it is passed in to gdk service container
BABEL_ENV: "$BABEL_ENV"
GITLAB_CRON_JOBS_POLL_INTERVAL: "0" GITLAB_CRON_JOBS_POLL_INTERVAL: "0"
GITLAB_DEVELOPMENT_USE_PRECOMPILED_ASSETS: "true" GITLAB_DEVELOPMENT_USE_PRECOMPILED_ASSETS: "true"
GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN

View File

@ -1,6 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */ /* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
import { updateHash } from '~/blob/state';
// LineHighlighter // LineHighlighter
// //
@ -174,6 +175,7 @@ LineHighlighter.prototype.setHash = function (firstLineNumber, lastLineNumber) {
hash = `#L${firstLineNumber}`; hash = `#L${firstLineNumber}`;
} }
this._hash = hash; this._hash = hash;
updateHash(hash);
return this.__setLocationHash__(hash); return this.__setLocationHash__(hash);
}; };

View File

@ -4,6 +4,6 @@ export const hashState = Vue.observable({
currentHash: window.location.hash, currentHash: window.location.hash,
}); });
export const updateLineNumber = (lineNumber) => { export const updateHash = (newHash) => {
hashState.currentHash = lineNumber; hashState.currentHash = newHash;
}; };

View File

@ -6,9 +6,8 @@ import { InternalEvents } from '~/tracking';
import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings'; import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
import { Mousetrap } from '~/lib/mousetrap'; import { Mousetrap } from '~/lib/mousetrap';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle'; import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility'; import { hashState, updateHash } from '~/blob/state';
import { hashState } from '~/blob/state'; import { getAbsolutePermalinkPath } from './utils';
import { getPageParamValue, getPageSearchString } from '~/blob/utils';
Vue.use(GlToast); Vue.use(GlToast);
@ -41,26 +40,22 @@ export default {
return shouldDisableShortcuts(); return shouldDisableShortcuts();
}, },
absolutePermalinkPath() { absolutePermalinkPath() {
const baseAbsolutePath = relativePathToAbsolute(this.permalinkPath, getBaseURL()); return getAbsolutePermalinkPath(this.permalinkPath, hashState.currentHash);
if (hashState.currentHash) {
const page = getPageParamValue(hashState.currentHash);
const searchString = getPageSearchString(baseAbsolutePath, page);
if (Number.isNaN(Number(hashState.currentHash))) {
return `${baseAbsolutePath}${searchString}${hashState.currentHash}`;
}
return `${baseAbsolutePath}${searchString}#L${hashState.currentHash}`;
}
return baseAbsolutePath;
}, },
}, },
mounted() { mounted() {
this.mousetrap = new Mousetrap(); this.mousetrap = new Mousetrap();
this.mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.triggerCopyPermalink); this.mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.triggerCopyPermalink);
window.addEventListener('hashchange', this.onHashChange);
}, },
beforeDestroy() { beforeDestroy() {
this.mousetrap.unbind(keysFor(PROJECT_FILES_GO_TO_PERMALINK)); this.mousetrap.unbind(keysFor(PROJECT_FILES_GO_TO_PERMALINK));
window.removeEventListener('hashchange', this.onHashChange);
}, },
methods: { methods: {
onHashChange() {
updateHash(window.location.hash || '');
},
triggerCopyPermalink() { triggerCopyPermalink() {
const buttonElement = this.$refs.copyPermalinkButton.$el; const buttonElement = this.$refs.copyPermalinkButton.$el;
buttonElement.click(); buttonElement.click();

View File

@ -0,0 +1,25 @@
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
import { getPageParamValue, getPageSearchString } from '~/blob/utils';
/**
* Generates an absolute permalink path with proper handling of URL hash
*
* @param {String} permalinkPath - The relative permalink path
* @param {String} hash - The URL hash (after #)
* @returns {String} - The absolute permalink path with hash handling
*/
export const getAbsolutePermalinkPath = (permalinkPath, inputHash) => {
const baseAbsolutePath = relativePathToAbsolute(permalinkPath, getBaseURL());
const hash = inputHash || '';
const page = getPageParamValue(hash);
const searchString = getPageSearchString(baseAbsolutePath, page);
// Ensure hash starts with # if it doesn't already
let normalizedHash = '';
if (hash.trim()) {
normalizedHash = hash.startsWith('#') ? hash : `#${hash}`;
}
return `${baseAbsolutePath}${searchString}${normalizedHash}`;
};

View File

@ -6,7 +6,6 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html'; import SafeHtml from '~/vue_shared/directives/safe_html';
import { getPageParamValue, getPageSearchString } from '~/blob/utils'; import { getPageParamValue, getPageSearchString } from '~/blob/utils';
import { addInteractionClass } from '~/code_navigation/utils'; import { addInteractionClass } from '~/code_navigation/utils';
import { updateLineNumber } from '~/blob/state';
/* /*
* We only highlight the chunk that is currently visible to the user. * We only highlight the chunk that is currently visible to the user.
@ -105,10 +104,6 @@ export default {
this.hasAppeared = true; this.hasAppeared = true;
this.$emit('appear'); this.$emit('appear');
}, },
handleOnClick(event) {
const { lineNumber } = event.target.dataset;
updateLineNumber(lineNumber);
},
calculateLineNumber(index) { calculateLineNumber(index) {
return this.startingFrom + index + 1; return this.startingFrom + index + 1;
}, },
@ -148,7 +143,6 @@ export default {
class="file-line-num gl-select-none !gl-shadow-none" class="file-line-num gl-select-none !gl-shadow-none"
:href="`#L${calculateLineNumber(index)}`" :href="`#L${calculateLineNumber(index)}`"
:data-line-number="calculateLineNumber(index)" :data-line-number="calculateLineNumber(index)"
@click="handleOnClick"
> >
{{ calculateLineNumber(index) }} {{ calculateLineNumber(index) }}
</a> </a>

View File

@ -7,10 +7,9 @@
= render 'shared/new_project_item_vue_select' = render 'shared/new_project_item_vue_select'
- if @milestone_states.any? { |name, count| count > 0 } - if @milestone_states.any? { |name, count| count > 0 }
.top-area .gl-flex.gl-flex-wrap.gl-items-center.gl-flex-wrap-reverse.gl-border-b
= render 'shared/milestones_filter', counts: @milestone_states = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls = render 'shared/milestones/search_form'
= render 'shared/milestones/search_form'
- if @milestones.blank? - if @milestones.blank?
= render 'shared/empty_states/milestones_tab', active_tab: params[:state] do = render 'shared/empty_states/milestones_tab', active_tab: params[:state] do

View File

@ -41,6 +41,7 @@
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert" = dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
= dispensable_render_if_exists "shared/silent_mode_banner" = dispensable_render_if_exists "shared/silent_mode_banner"
= dispensable_render_if_exists "shared/pipl_compliance_alert" = dispensable_render_if_exists "shared/pipl_compliance_alert"
= dispensable_render_if_exists "shared/compromised_password_detection_alert"
= yield :page_level_alert = yield :page_level_alert
-# Top bar -# Top bar

View File

@ -1,5 +1,7 @@
const coreJSVersion = require('./node_modules/core-js/package.json').version; const coreJSVersion = require('./node_modules/core-js/package.json').version;
console.debug(`BABEL_ENV inside Babel config is: ${process.env.BABEL_ENV}`);
let presets = [ let presets = [
[ [
'@babel/preset-env', '@babel/preset-env',

View File

@ -1,9 +0,0 @@
---
name: revalidate_gpg_fingerprints
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349505
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182738
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/534717
milestone: '17.11'
group: group::source code
type: beta
default_enabled: true

View File

@ -89,6 +89,8 @@ if (WEBPACK_REPORT) {
NO_HASHED_CHUNKS = true; NO_HASHED_CHUNKS = true;
} }
console.debug(`BABEL_ENV inside Webpack is: ${process.env.BABEL_ENV}`);
const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map'; const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';
const incrementalCompiler = createIncrementalWebpackCompiler( const incrementalCompiler = createIncrementalWebpackCompiler(

View File

@ -2068,6 +2068,7 @@ Input type: `AiActionInput`
| <a id="mutationaiactionplatformorigin"></a>`platformOrigin` | [`String`](#string) | Specifies the origin platform of the request. | | <a id="mutationaiactionplatformorigin"></a>`platformOrigin` | [`String`](#string) | Specifies the origin platform of the request. |
| <a id="mutationaiactionprojectid"></a>`projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. | | <a id="mutationaiactionprojectid"></a>`projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. |
| <a id="mutationaiactionresolvevulnerability"></a>`resolveVulnerability` | [`AiResolveVulnerabilityInput`](#airesolvevulnerabilityinput) | Input for resolve_vulnerability AI action. | | <a id="mutationaiactionresolvevulnerability"></a>`resolveVulnerability` | [`AiResolveVulnerabilityInput`](#airesolvevulnerabilityinput) | Input for resolve_vulnerability AI action. |
| <a id="mutationaiactionrootnamespaceid"></a>`rootNamespaceId` | [`NamespaceID`](#namespaceid) | Global ID of the top-level namespace the user is acting on. |
| <a id="mutationaiactionsummarizecomments"></a>`summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. | | <a id="mutationaiactionsummarizecomments"></a>`summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. |
| <a id="mutationaiactionsummarizenewmergerequest"></a>`summarizeNewMergeRequest` | [`AiSummarizeNewMergeRequestInput`](#aisummarizenewmergerequestinput) | Input for summarize_new_merge_request AI action. | | <a id="mutationaiactionsummarizenewmergerequest"></a>`summarizeNewMergeRequest` | [`AiSummarizeNewMergeRequestInput`](#aisummarizenewmergerequestinput) | Input for summarize_new_merge_request AI action. |
| <a id="mutationaiactionsummarizereview"></a>`summarizeReview` | [`AiSummarizeReviewInput`](#aisummarizereviewinput) | Input for summarize_review AI action. | | <a id="mutationaiactionsummarizereview"></a>`summarizeReview` | [`AiSummarizeReviewInput`](#aisummarizereviewinput) | Input for summarize_review AI action. |

View File

@ -37,3 +37,39 @@ There is no need to set up the backend components of Duo Workflow to test change
A local build of the UI is required if you are making Duo Workflow UI changes that you need to view locally. A local build is also required if you want to use a version of the UI that has not been released yet. A local build of the UI is required if you are making Duo Workflow UI changes that you need to view locally. A local build is also required if you want to use a version of the UI that has not been released yet.
Refer to the [GitLab Duo Workflow README](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/main/packages/webview_duo_workflow/README.md) file in the Language Server project to get started with local development of GitLab Duo Workflow UI. Refer to the [GitLab Duo Workflow README](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/main/packages/webview_duo_workflow/README.md) file in the Language Server project to get started with local development of GitLab Duo Workflow UI.
## Development settings
Each of these settings can be turned on in your user settings in VS Code.
### Change view type
Enable the Duo Workflow as a sidepanel instead of fullview. This is going to be the default for public beta.
`"gitlab.featureFlags.duoWorkflowPanel": true,`
### Executor type
Allow to define which Duo Workflow executor is selected. Accepts:
- `shell` - Current default, runs the go binary directly on the user's machine
- `docker` - Runs the go binary inside a Docker container (deprecated)
- `node` - Runs a nodeJs/TypeScript executor directly inside the languge server. Expected to become the default.
`"gitlab.duo.workflow.executor": "node",`
### Workflow graph
Experimental settings that allow Duo Workflow graph to be swapped. Includes:
- `software_development` - default
- `chat` - used by agentic chat
- `search_and_replace` - Used to scan large number of files and replace results with specific instructions
`"gitlab.duo.workflow.graph": "software_development",`
### Tool approval
Allow users to get access to tools that require approval such as running terminal commands.
`"gitlab.duo.workflow.toolApproval": true`

View File

@ -66,3 +66,23 @@ For example, if your IP is `192.168.0.12`:
```shell ```shell
bundle exec bin/qa Test::Instance::All http://192.168.0.12:3000 bundle exec bin/qa Test::Instance::All http://192.168.0.12:3000
``` ```
## Tests sign out when visiting a page
If the tests sign in successfully as a test user, but then unexpectedly sign out, you might be using an
incorrect URL to execute the test. By default, tests use the URL `http://127.0.0.1:3000`, but if a hostname
has been configured for the instance, you must explicitly pass that hostname to the tests. The tests use
the `web_url` returned by the API to go to different pages. They go to the configured hostname, rather than
`http://127.0.0.1:3000`, so the test user appears signed out.
This example runs the tests against `http://127.0.0.1:3000`, and signs out if a hostname has been configured:
```shell
bundle exec rspec qa/specs/features/ee/browser_ui/3_create/repository/code_owners_spec.rb
```
To avoid this, explicitly set `QA_GITLAB_URL` to the configured hostname, for example:
```shell
QA_GITLAB_URL=http://gdk.test:3000 bundle exec rspec qa/specs/features/ee/browser_ui/3_create/repository/code_owners_spec.rb
```

View File

@ -9,16 +9,6 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def run def run
unless Feature.enabled?(:revalidate_gpg_fingerprints, @gpg_key.user)
return CommitSignatures::GpgSignature
.select(:id, :commit_sha, :project_id)
.where('gpg_key_id IS NULL OR verification_status <> ?', CommitSignatures::GpgSignature.verification_statuses[:verified])
.where(gpg_key_primary_keyid: @gpg_key.keyids)
.find_each do |sig|
sig.gpg_commit&.update_signature!(sig)
end
end
[@gpg_key].concat(@gpg_key.subkeys).each do |key| [@gpg_key].concat(@gpg_key.subkeys).each do |key|
Gitlab::Gpg.using_tmp_keychain do Gitlab::Gpg.using_tmp_keychain do
Gitlab::Gpg::CurrentKeyChain.add(key.key) Gitlab::Gpg::CurrentKeyChain.add(key.key)

View File

@ -42,7 +42,7 @@ namespace :tw do
# CodeOwnerRule.new('Database Operations', ''), # CodeOwnerRule.new('Database Operations', ''),
# CodeOwnerRule.new('DataOps', ''), # CodeOwnerRule.new('DataOps', ''),
# CodeOwnerRule.new('Delivery', ''), # CodeOwnerRule.new('Delivery', ''),
# CodeOwnerRule.new('Durability', ''), CodeOwnerRule.new('Durability', '@axil'),
CodeOwnerRule.new('Duo Chat', '@jglassman1'), CodeOwnerRule.new('Duo Chat', '@jglassman1'),
CodeOwnerRule.new('Duo Workflow', '@sselhorn'), CodeOwnerRule.new('Duo Workflow', '@sselhorn'),
CodeOwnerRule.new('Dynamic Analysis', '@phillipwells'), CodeOwnerRule.new('Dynamic Analysis', '@phillipwells'),

View File

@ -16748,6 +16748,42 @@ msgstr ""
msgid "Components must have a 'name'" msgid "Components must have a 'name'"
msgstr "" msgstr ""
msgid "CompromisedPasswordDetection|Change GitLab Password"
msgstr ""
msgid "CompromisedPasswordDetection|Failure to change your password may lead to temporary account access restrictions to prevent potential unauthorized access."
msgstr ""
msgid "CompromisedPasswordDetection|For your security, we also recommend enabling %{mfa_link} if you have not done so already."
msgstr ""
msgid "CompromisedPasswordDetection|GitLab documentation"
msgstr ""
msgid "CompromisedPasswordDetection|Instructions for changing your password can be found in the %{doc_link}."
msgstr ""
msgid "CompromisedPasswordDetection|Please change your password immediately."
msgstr ""
msgid "CompromisedPasswordDetection|Security Alert: Change Your GitLab.com Password"
msgstr ""
msgid "CompromisedPasswordDetection|Security Alert: Change your GitLab password"
msgstr ""
msgid "CompromisedPasswordDetection|The password used for your GitLab.com account %{code_start}%{username}%{code_end} may have been compromised due to a data breach on another service or platform. This does not necessarily mean that your GitLab account was accessed. However, leaving your password unchanged presents a significant security risk to your account."
msgstr ""
msgid "CompromisedPasswordDetection|The password used for your GitLab.com account %{username} may have been compromised due to a data breach on another service or platform. This does not necessarily mean that your GitLab account was accessed. However, leaving your password unchanged presents a significant security risk to your account."
msgstr ""
msgid "CompromisedPasswordDetection|Your GitLab.com account password may be compromised due to a data breach on another service or platform. Please change your password immediately."
msgstr ""
msgid "CompromisedPasswordDetection|two-factor authentication"
msgstr ""
msgid "Compute minutes" msgid "Compute minutes"
msgstr "" msgstr ""
@ -27745,9 +27781,6 @@ msgstr ""
msgid "Geo|Resync all" msgid "Geo|Resync all"
msgstr "" msgstr ""
msgid "Geo|Retry count"
msgstr ""
msgid "Geo|Reverify" msgid "Geo|Reverify"
msgstr "" msgstr ""
@ -27793,9 +27826,6 @@ msgstr ""
msgid "Geo|Shards to synchronize" msgid "Geo|Shards to synchronize"
msgstr "" msgstr ""
msgid "Geo|Show more"
msgstr ""
msgid "Geo|Site name can't be blank" msgid "Geo|Site name can't be blank"
msgstr "" msgstr ""
@ -27829,9 +27859,6 @@ msgstr ""
msgid "Geo|Synchronization" msgid "Geo|Synchronization"
msgstr "" msgstr ""
msgid "Geo|Synchronization failed - %{error}"
msgstr ""
msgid "Geo|Synchronization settings" msgid "Geo|Synchronization settings"
msgstr "" msgstr ""
@ -27928,9 +27955,6 @@ msgstr ""
msgid "Geo|Verification concurrency limit" msgid "Geo|Verification concurrency limit"
msgstr "" msgstr ""
msgid "Geo|Verification failed - %{error}"
msgstr ""
msgid "Geo|Verification information" msgid "Geo|Verification information"
msgstr "" msgstr ""
@ -28195,6 +28219,9 @@ msgstr ""
msgid "GitLab Premium" msgid "GitLab Premium"
msgstr "" msgstr ""
msgid "GitLab Security"
msgstr ""
msgid "GitLab Shell" msgid "GitLab Shell"
msgstr "" msgstr ""
@ -61139,6 +61166,9 @@ msgstr ""
msgid "Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can." msgid "Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can."
msgstr "" msgstr ""
msgid "Thank you,"
msgstr ""
msgid "Thanks for signing up to GitLab!" msgid "Thanks for signing up to GitLab!"
msgstr "" msgstr ""

View File

@ -72,7 +72,7 @@ RSpec.configure do |config|
begin begin
Capybara.current_session.execute_script("window.__coveragePathsPersistence.reset()") Capybara.current_session.execute_script("window.__coveragePathsPersistence.reset()")
rescue StandardError => e rescue StandardError => e
QA::Runtime::Logger.warn("Failed to reset coverage paths: #{e.message}") QA::Runtime::Logger.warn("Failed to reset coverage paths, check if it is an api spec: #{e.message}")
end end
end end
@ -111,7 +111,7 @@ RSpec.configure do |config|
example.metadata[:coverage_paths] = coverage_paths example.metadata[:coverage_paths] = coverage_paths
front_end_coverage_by_example[example.metadata[:location]] = coverage_paths front_end_coverage_by_example[example.metadata[:location]] = coverage_paths
rescue StandardError => e rescue StandardError => e
QA::Runtime::Logger.warn("Failed to collect coverage paths: #{e.message}") QA::Runtime::Logger.warn("Failed to collect coverage paths, check if it is an api spec: #{e.message}")
end end
end end
end end

View File

@ -9,17 +9,24 @@ module QA
include Helpers include Helpers
PROJECT = "gitlab-qa-resources" PROJECT = "gitlab-qa-resources"
BUCKET = "code-path-mappings" DEFAULT_BUCKET = "code-path-mappings"
DEFAULT_FILE_NAME = "test-code-paths-mapping-merged-pipeline"
def self.export(mapping_files_glob) def self.export(mapping_files_glob, **kwargs)
new.export(mapping_files_glob) if kwargs.key?(:bucket) || kwargs.key?(:file_name)
new.export(mapping_files_glob, **kwargs)
else
new.export(mapping_files_glob)
end
end end
# Export code path mappings to GCP # Export code path mappings to GCP
# #
# @param [String] mapping_files_glob - glob pattern for mapping files # @param [String] mapping_files_glob - glob pattern for mapping files
# @param [String] bucket - custom bucket name (optional)
# @param [String] file_name - custom file name (optional)
# @return [void] # @return [void]
def export(mapping_files_glob) def export(mapping_files_glob, bucket: DEFAULT_BUCKET, file_name: DEFAULT_FILE_NAME)
mapping_files = Dir.glob(mapping_files_glob) mapping_files = Dir.glob(mapping_files_glob)
return logger.warn("No files matched pattern, skipping coverage mapping upload") if mapping_files.empty? return logger.warn("No files matched pattern, skipping coverage mapping upload") if mapping_files.empty?
@ -30,21 +37,25 @@ module QA
logger.info("Number of mapping files found: #{mapping_files.size}") logger.info("Number of mapping files found: #{mapping_files.size}")
mapping_data = mapping_files.flat_map { |file| JSON.parse(File.read(file)) }.reduce(:merge!) mapping_data = mapping_files.flat_map { |file| JSON.parse(File.read(file)) }.reduce(:merge!)
file = "#{ENV['CI_COMMIT_REF_SLUG']}/#{ENV['QA_RUN_TYPE']}/test-code-paths-mapping-merged-pipeline-#{\ file = "#{ENV['CI_COMMIT_REF_SLUG']}/#{ENV['QA_RUN_TYPE']}/#{file_name}-#{\
ENV['CI_PIPELINE_ID'] || 'local'}.json" ENV['CI_PIPELINE_ID'] || 'local'}.json"
upload_to_gcs(file, mapping_data) upload_to_gcs(file, mapping_data, bucket)
end end
# Import code path mappings from GCP # Import code path mappings from GCP
# #
# @param [String] branch - branch name # @param [String] branch - branch name
# @param [String] run_type - run type # @param [String] run_type - run type
# @param [String] bucket - custom bucket name (optional)
# @param [String] file_name - custom file name base (optional)
# @return [Hash] # @return [Hash]
def import(branch, run_type) def import(branch, run_type, bucket: DEFAULT_BUCKET, file_name: DEFAULT_FILE_NAME)
filename = code_paths_mapping_file("#{branch}/#{run_type}") prefix = "#{branch}/#{run_type}/#{file_name}"
filename = code_paths_mapping_file(prefix, bucket)
logger.info("The mapping file fetched in import: #{filename}") logger.info("The mapping file fetched in import: #{filename}")
file = client.get_object(BUCKET, filename) file = client.get_object(bucket, filename)
JSON.parse(file[:body]) JSON.parse(file[:body])
rescue StandardError => e rescue StandardError => e
logger.error("Failed to download code paths mapping from GCS. Error: #{e}") logger.error("Failed to download code paths mapping from GCS. Error: #{e}")
@ -54,8 +65,9 @@ module QA
private private
def upload_to_gcs(file_name, mapping_data) def upload_to_gcs(file_name, mapping_data, bucket)
client.put_object(BUCKET, file_name, JSON.pretty_generate(mapping_data)) client.put_object(bucket, file_name, JSON.pretty_generate(mapping_data))
logger.info("Successfully uploaded to bucket: #{bucket}, file: #{file_name}")
rescue StandardError => e rescue StandardError => e
logger.error("Failed to upload code paths mapping to GCS. Error: #{e}") logger.error("Failed to upload code paths mapping to GCS. Error: #{e}")
logger.error("Backtrace: #{e.backtrace}") logger.error("Backtrace: #{e.backtrace}")
@ -85,8 +97,8 @@ module QA
# #
# Get most up to date mapping file based on pipeline type # Get most up to date mapping file based on pipeline type
# @return [String] # @return [String]
def code_paths_mapping_file(prefix) def code_paths_mapping_file(prefix, bucket = DEFAULT_BUCKET)
paginated_list(client.list_objects(BUCKET, prefix: prefix)).last&.name paginated_list(client.list_objects(bucket, prefix: prefix)).last&.name
end end
# Paginated list of items # Paginated list of items
@ -98,7 +110,7 @@ module QA
return list.items if list.next_page_token.nil? return list.items if list.next_page_token.nil?
paginated_list( paginated_list(
client.list_objects(BUCKET, prefix: list.prefixes.first, page_token: list.next_page_token) client.list_objects(list.bucket, prefix: list.prefixes.first, page_token: list.next_page_token)
) + list.items ) + list.items
end end
end end

View File

@ -51,6 +51,29 @@ RSpec.describe QA::Tools::Ci::CodePathsMapping do
end end
end end
context "with bucket and file name prefix passed as arguments" do
let(:new_bucket) { "new_bucket" }
let(:upload_file_name) { "new_name_prefix" }
let(:expected_file_path) { "#{commit_ref}/#{run_type}/#{upload_file_name}-#{ENV['CI_PIPELINE_ID']}.json" }
it "exports mapping json with correct file name prefix" do
expect(logger).to receive(:info).with("Number of mapping files found: #{file_paths.size}")
expect(gcs_client).to receive(:put_object).with(new_bucket, expected_file_path, pretty_generated_mapping_json)
described_class.export(glob, bucket: new_bucket, file_name: upload_file_name)
end
end
context "with bucket and file name prefix not passed as arguments" do
let(:default_bucket) { QA::Tools::Ci::CodePathsMapping::DEFAULT_BUCKET }
let(:default_filename) { QA::Tools::Ci::CodePathsMapping::DEFAULT_FILE_NAME }
let(:expected_file_path) { "#{commit_ref}/#{run_type}/#{default_filename}-#{ENV['CI_PIPELINE_ID']}.json" }
it "exports mapping json with default file name prefix to default bucket" do
expect(gcs_client).to receive(:put_object).with(default_bucket, expected_file_path, pretty_generated_mapping_json)
described_class.export(glob)
end
end
context "with no mapping files present" do context "with no mapping files present" do
let(:file_paths) { [] } let(:file_paths) { [] }

View File

@ -103,10 +103,18 @@ namespace :ci do
QA::Tools::Ci::TestMetrics.export(args[:glob]) QA::Tools::Ci::TestMetrics.export(args[:glob])
end end
desc "Export code paths mapping to GCP" desc "Export backend code paths mapping to GCP"
task :export_code_paths_mapping, [:glob] do |_, args| task :export_code_paths_mapping, [:glob] do |_, args|
raise("Code paths mapping JSON glob pattern is required") unless args[:glob] raise("Code paths mapping JSON glob pattern is required") unless args[:glob]
QA::Tools::Ci::CodePathsMapping.export(args[:glob]) QA::Tools::Ci::CodePathsMapping.export(args[:glob])
end end
desc "Export frontend code paths mapping to GCP"
task :export_code_paths_mapping, [:glob] do |_, args|
raise("Code paths mapping JSON glob pattern is required") unless args[:glob]
QA::Tools::Ci::CodePathsMapping.export(args[:glob], bucket: "code-path-mappings",
file_name: "js-coverage-by-example-merged-pipeline")
end
end end

View File

@ -2,8 +2,11 @@
import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html'; import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LineHighlighter from '~/blob/line_highlighter'; import LineHighlighter from '~/blob/line_highlighter';
import { updateHash } from '~/blob/state';
import * as utils from '~/lib/utils/common_utils'; import * as utils from '~/lib/utils/common_utils';
jest.mock('~/blob/state');
describe('LineHighlighter', () => { describe('LineHighlighter', () => {
const testContext = {}; const testContext = {};
@ -283,5 +286,17 @@ describe('LineHighlighter', () => {
expect(testContext.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15'); expect(testContext.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15');
}); });
it('calls updateHash with the correct hash for a single line', () => {
testContext.subject(5);
expect(updateHash).toHaveBeenCalledWith('#L5');
});
it('calls updateHash with the correct hash for a range', () => {
testContext.subject(5, 15);
expect(updateHash).toHaveBeenCalledWith('#L5-15');
});
}); });
}); });

View File

@ -9,7 +9,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { blobControlsDataMock } from 'ee_else_ce_jest/repository/mock_data'; import { blobControlsDataMock } from 'ee_else_ce_jest/repository/mock_data';
jest.mock('~/behaviors/shortcuts/shortcuts_toggle'); jest.mock('~/behaviors/shortcuts/shortcuts_toggle');
jest.mock('~/blob/state');
const relativePermalinkPath = const relativePermalinkPath =
'flightjs/Flight/-/blob/46ca9ebd5a43ec240ee8d64e2bb829169dff744e/bower.json'; 'flightjs/Flight/-/blob/46ca9ebd5a43ec240ee8d64e2bb829169dff744e/bower.json';

View File

@ -4,7 +4,7 @@ import PermalinkDropdownItem from '~/repository/components/header_area/permalink
import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings'; import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle'; import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { Mousetrap } from '~/lib/mousetrap'; import { Mousetrap } from '~/lib/mousetrap';
import { hashState } from '~/blob/state'; import { hashState, updateHash } from '~/blob/state';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
@ -46,6 +46,24 @@ describe('PermalinkDropdownItem', () => {
expect(findPermalinkLinkDropdown().exists()).toBe(true); expect(findPermalinkLinkDropdown().exists()).toBe(true);
}); });
describe('hash change handling', () => {
it('calls updateHash when hash changes', () => {
window.location.hash = 'L42';
createComponent();
window.dispatchEvent(new Event('hashchange'));
expect(updateHash).toHaveBeenCalledWith('#L42');
});
it('handles empty hash correctly', () => {
window.location.hash = '';
createComponent();
window.dispatchEvent(new Event('hashchange'));
expect(updateHash).toHaveBeenCalledWith('');
});
});
describe('updatedPermalinkPath', () => { describe('updatedPermalinkPath', () => {
it('returns absolutePermalinkPath when no line number is set', () => { it('returns absolutePermalinkPath when no line number is set', () => {
expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe( expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe(
@ -54,7 +72,7 @@ describe('PermalinkDropdownItem', () => {
}); });
it('returns updated path with line number when set', () => { it('returns updated path with line number when set', () => {
hashState.currentHash = 10; hashState.currentHash = '#L10';
createComponent(); createComponent();
expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe( expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe(
@ -158,6 +176,17 @@ describe('PermalinkDropdownItem', () => {
wrapper.destroy(); wrapper.destroy();
expect(unbindSpy).toHaveBeenCalledWith(keysFor(PROJECT_FILES_GO_TO_PERMALINK)); expect(unbindSpy).toHaveBeenCalledWith(keysFor(PROJECT_FILES_GO_TO_PERMALINK));
}); });
it('add and remove event listener for hashChange event', () => {
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
createComponent();
expect(addEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function));
wrapper.destroy();
expect(removeEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function));
});
}); });
it('displays the shortcut key when shortcuts are not disabled', () => { it('displays the shortcut key when shortcuts are not disabled', () => {

View File

@ -0,0 +1,75 @@
import { getAbsolutePermalinkPath } from '~/repository/components/header_area/utils';
import * as urlUtility from '~/lib/utils/url_utility';
import * as blobUtils from '~/blob/utils';
describe('getAbsolutePermalinkPath', () => {
const permalinkPath = '/project/repo/-/blob/main/file.js';
const baseUrl = 'https://gitlab.example.com';
const absolutePath = 'https://gitlab.example.com/project/repo/-/blob/main/file.js';
beforeEach(() => {
jest.spyOn(urlUtility, 'getBaseURL').mockReturnValue(baseUrl);
jest.spyOn(urlUtility, 'relativePathToAbsolute').mockReturnValue(absolutePath);
jest.spyOn(blobUtils, 'getPageParamValue').mockReturnValue(null);
jest.spyOn(blobUtils, 'getPageSearchString').mockReturnValue('');
});
describe('when hash is not provided', () => {
it.each([
['null', null],
['empty string', ''],
['undefined', undefined],
])('returns absolute path when hash is %s', (_, hash) => {
expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe(absolutePath);
});
});
describe('when handling different hash formats', () => {
it.each([
['line number format', '#L6', '#L6'],
['line number range format', '#L10-19', '#L10-19'],
[
'anchor hash',
'#developer-certificate-of-origin--license',
'#developer-certificate-of-origin--license',
],
])('handles %s (%s)', (_, hash, expectedHash) => {
expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe(`${absolutePath}${expectedHash}`);
});
});
describe('when hash normalization is needed', () => {
it.each([
['line number', 'L6', '#L6'],
['line number range', 'L10-19', '#L10-19'],
[
'complex anchor',
'developer-certificate-of-origin--license',
'#developer-certificate-of-origin--license',
],
])('normalizes %s hash by adding # prefix when missing', (_, hash, expectedHash) => {
expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe(`${absolutePath}${expectedHash}`);
});
});
describe('when page parameters are present', () => {
const searchString = '?plain=1';
beforeEach(() => {
blobUtils.getPageParamValue.mockReturnValue('1');
blobUtils.getPageSearchString.mockReturnValue(searchString);
});
it.each([
['with # prefix', '#L6', '#L6'],
['without # prefix', 'L20', '#L20'],
['with empty hash', '', ''],
['with null hash', null, ''],
['with undefined hash', undefined, ''],
])('includes search string when hash is %s', (_, hash, expectedHash) => {
expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe(
`${absolutePath}${searchString}${expectedHash}`,
);
});
});
});

View File

@ -5,11 +5,9 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { addInteractionClass } from '~/code_navigation/utils'; import { addInteractionClass } from '~/code_navigation/utils';
import { updateLineNumber } from '~/blob/state';
import { CHUNK_1, CHUNK_2 } from '../mock_data'; import { CHUNK_1, CHUNK_2 } from '../mock_data';
jest.mock('~/code_navigation/utils'); jest.mock('~/code_navigation/utils');
jest.mock('~/blob/state');
Vue.use(Vuex); Vue.use(Vuex);
@ -103,18 +101,5 @@ describe('Chunk component', () => {
expect(addInteractionClass).toHaveBeenCalledWith({ d: 'test', path: 'index.js' }); expect(addInteractionClass).toHaveBeenCalledWith({ d: 'test', path: 'index.js' });
}); });
it('calls updateLineNumber with the correct line number when a line is clicked', async () => {
createComponent({ ...CHUNK_2, isHighlighted: true });
const lineNumber = '71';
const lineNumberElement = wrapper.find(`[data-line-number="${lineNumber}"]`);
expect(lineNumberElement.exists()).toBe(true);
await lineNumberElement.trigger('click');
expect(updateLineNumber).toHaveBeenCalledWith(lineNumber);
});
}); });
}); });

View File

@ -67,23 +67,6 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater, :sidekiq_inline do
) )
end end
it 'assigns the gpg key even with revalidate_gpg_fingerprints feature flag disabled' do
stub_feature_flags(revalidate_gpg_fingerprints: false)
# InvalidGpgSignatureUpdater is called by the after_create hook
gpg_key = create :gpg_key,
key: GpgHelpers::User1.public_key,
user: user
expect(valid_gpg_signature.reload).to have_attributes(
project: project,
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
verification_status: 'verified'
)
end
it 'does not assign the gpg key when an unrelated gpg key is added' do it 'does not assign the gpg key when an unrelated gpg key is added' do
# InvalidGpgSignatureUpdater is called by the after_create hook # InvalidGpgSignatureUpdater is called by the after_create hook
create :gpg_key, create :gpg_key,
@ -127,23 +110,6 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater, :sidekiq_inline do
) )
end end
it 'does not update the signature when revalidate_gpg_fingerprints feature flag is disabled' do
stub_feature_flags(revalidate_gpg_fingerprints: false)
# InvalidGpgSignatureUpdater is called by the after_create hook
create :gpg_key,
key: GpgHelpers::User1.public_key,
user: user
expect(invalid_gpg_signature.reload).to have_attributes(
project: project,
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.fingerprint,
verification_status: 'unknown_key'
)
end
it 'keeps the signature at being invalid when an unrelated gpg key is added' do it 'keeps the signature at being invalid when an unrelated gpg key is added' do
# InvalidGpgSignatureUpdater is called by the after_create hook # InvalidGpgSignatureUpdater is called by the after_create hook
create :gpg_key, create :gpg_key,

View File

@ -8,7 +8,8 @@
- Groups::EnvironmentScopesFinder # Reason: There is no need to have anything else besides the simple strucutre with the scope name - Groups::EnvironmentScopesFinder # Reason: There is no need to have anything else besides the simple strucutre with the scope name
- Security::RelatedPipelinesFinder # Reason: There is no need to have anything else besides the IDs of pipelines - Security::RelatedPipelinesFinder # Reason: There is no need to have anything else besides the IDs of pipelines
- Llm::ExtraResourceFinder # Reason: The finder does not deal with DB-backend resource for now. - Llm::ExtraResourceFinder # Reason: The finder does not deal with DB-backend resource for now.
- Security::VulnerabilityReadsElasticFinder # Reason: The finder deals with Elasticsearch records and not DB records - Security::VulnerabilityElasticFinder # Reason: The finder deals with Elasticsearch records and not DB records
- Security::VulnerabilityElasticAggregationFinder # Reason: The finder deals with Elasticsearch records and not DB records
# Temporary excludes (aka TODOs) # Temporary excludes (aka TODOs)
# For example: # For example: