diff --git a/.eslint_todo/vue-no-unused-properties.mjs b/.eslint_todo/vue-no-unused-properties.mjs
index 2f9eea46947..f8ddb0cfd41 100644
--- a/.eslint_todo/vue-no-unused-properties.mjs
+++ b/.eslint_todo/vue-no-unused-properties.mjs
@@ -313,9 +313,6 @@ export default {
'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/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/epic_item.vue',
'ee/app/assets/javascripts/roadmap/components/epic_item_details.vue',
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index faea698c0c4..67f650fe69e 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -582,6 +582,7 @@ lib/gitlab/checks/**
/doc/administration/application_settings_cache.md @jglassman1
/doc/administration/auditor_users.md @idurham
/doc/administration/auth/ @idurham
+/doc/administration/backup_restore/ @axil
/doc/administration/broadcast_messages.md @sselhorn
/doc/administration/cells.md @emily.sahlani
/doc/administration/cicd/ @lyspin
@@ -658,7 +659,7 @@ lib/gitlab/checks/**
/doc/administration/raketasks/tokens/ @idurham
/doc/administration/raketasks/x509_signatures.md @brendan777
/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/reply_by_email.md @lciutacu
/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/visibility_and_access_controls.md @brendan777
/doc/administration/settings/vscode_extension_marketplace.md @brendan777
-/doc/administration/sidekiq/ @axil @eread
-/doc/administration/sidekiq/sidekiq_memory_killer.md @jglassman1
+/doc/administration/sidekiq/ @axil
/doc/administration/silent_mode/ @axil
/doc/administration/smime_signing_email.md @axil @eread
/doc/administration/snippets/ @brendan777
@@ -953,6 +953,7 @@ lib/gitlab/checks/**
/doc/development/cascading_settings.md @gitlab-org/foundations/engineering
/doc/development/cells/ @OmarQunsulGitlab @bmarjanovic
/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/database/ @OmarQunsulGitlab @bmarjanovic
/doc/development/distributed_tracing.md @gitlab-org/analytics-section/product-analytics/engineers/frontend
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index e15b4120a03..fde30dc5ea1 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -74,6 +74,8 @@ compile-production-assets as-if-foss:
- .qa:rules:test-on-omnibus-ce:follow-up
compile-test-assets:
+ variables:
+ BABEL_ENV: "$BABEL_ENV"
extends:
- .compile-assets-base
- .frontend:rules:compile-test-assets
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 6bac4083910..ddd6e7b3ab1 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -2025,6 +2025,8 @@
variables:
<<: *qa-e2e-test-schedule-variables
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:rules:e2e:test-on-cng:
diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
index 686d01d16b7..594c7481eea 100644
--- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
@@ -56,6 +56,7 @@ workflow:
job: clone-gitlab-repo
variables:
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_DEVELOPMENT_USE_PRECOMPILED_ASSETS: "true"
GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
diff --git a/app/assets/javascripts/blob/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js
index 9c6a5958e1f..c01ebb0d1d1 100644
--- a/app/assets/javascripts/blob/line_highlighter.js
+++ b/app/assets/javascripts/blob/line_highlighter.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */
import { scrollToElement } from '~/lib/utils/common_utils';
+import { updateHash } from '~/blob/state';
// LineHighlighter
//
@@ -174,6 +175,7 @@ LineHighlighter.prototype.setHash = function (firstLineNumber, lastLineNumber) {
hash = `#L${firstLineNumber}`;
}
this._hash = hash;
+ updateHash(hash);
return this.__setLocationHash__(hash);
};
diff --git a/app/assets/javascripts/blob/state.js b/app/assets/javascripts/blob/state.js
index 2f71927f17d..f78cd4508a3 100644
--- a/app/assets/javascripts/blob/state.js
+++ b/app/assets/javascripts/blob/state.js
@@ -4,6 +4,6 @@ export const hashState = Vue.observable({
currentHash: window.location.hash,
});
-export const updateLineNumber = (lineNumber) => {
- hashState.currentHash = lineNumber;
+export const updateHash = (newHash) => {
+ hashState.currentHash = newHash;
};
diff --git a/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue b/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue
index 9dbf5fac7be..fec938c55d3 100644
--- a/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue
+++ b/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue
@@ -6,9 +6,8 @@ import { InternalEvents } from '~/tracking';
import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
import { Mousetrap } from '~/lib/mousetrap';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
-import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
-import { hashState } from '~/blob/state';
-import { getPageParamValue, getPageSearchString } from '~/blob/utils';
+import { hashState, updateHash } from '~/blob/state';
+import { getAbsolutePermalinkPath } from './utils';
Vue.use(GlToast);
@@ -41,26 +40,22 @@ export default {
return shouldDisableShortcuts();
},
absolutePermalinkPath() {
- const baseAbsolutePath = relativePathToAbsolute(this.permalinkPath, getBaseURL());
- 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;
+ return getAbsolutePermalinkPath(this.permalinkPath, hashState.currentHash);
},
},
mounted() {
this.mousetrap = new Mousetrap();
this.mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.triggerCopyPermalink);
+ window.addEventListener('hashchange', this.onHashChange);
},
beforeDestroy() {
this.mousetrap.unbind(keysFor(PROJECT_FILES_GO_TO_PERMALINK));
+ window.removeEventListener('hashchange', this.onHashChange);
},
methods: {
+ onHashChange() {
+ updateHash(window.location.hash || '');
+ },
triggerCopyPermalink() {
const buttonElement = this.$refs.copyPermalinkButton.$el;
buttonElement.click();
diff --git a/app/assets/javascripts/repository/components/header_area/utils.js b/app/assets/javascripts/repository/components/header_area/utils.js
new file mode 100644
index 00000000000..f86a2a9e344
--- /dev/null
+++ b/app/assets/javascripts/repository/components/header_area/utils.js
@@ -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}`;
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
index d047efae83f..b051574f032 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
@@ -6,7 +6,6 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getPageParamValue, getPageSearchString } from '~/blob/utils';
import { addInteractionClass } from '~/code_navigation/utils';
-import { updateLineNumber } from '~/blob/state';
/*
* We only highlight the chunk that is currently visible to the user.
@@ -105,10 +104,6 @@ export default {
this.hasAppeared = true;
this.$emit('appear');
},
- handleOnClick(event) {
- const { lineNumber } = event.target.dataset;
- updateLineNumber(lineNumber);
- },
calculateLineNumber(index) {
return this.startingFrom + index + 1;
},
@@ -148,7 +143,6 @@ export default {
class="file-line-num gl-select-none !gl-shadow-none"
:href="`#L${calculateLineNumber(index)}`"
:data-line-number="calculateLineNumber(index)"
- @click="handleOnClick"
>
{{ calculateLineNumber(index) }}
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index fc5335d2016..2300d690fb9 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -7,10 +7,9 @@
= render 'shared/new_project_item_vue_select'
- 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
- .nav-controls
- = render 'shared/milestones/search_form'
+ = render 'shared/milestones/search_form'
- if @milestones.blank?
= render 'shared/empty_states/milestones_tab', active_tab: params[:state] do
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8c181da4bc9..7bfe03bb0e6 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -41,6 +41,7 @@
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
= dispensable_render_if_exists "shared/silent_mode_banner"
= dispensable_render_if_exists "shared/pipl_compliance_alert"
+ = dispensable_render_if_exists "shared/compromised_password_detection_alert"
= yield :page_level_alert
-# Top bar
diff --git a/babel.config.js b/babel.config.js
index 4c7711dd4d6..d135d2f10be 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,5 +1,7 @@
const coreJSVersion = require('./node_modules/core-js/package.json').version;
+console.debug(`BABEL_ENV inside Babel config is: ${process.env.BABEL_ENV}`);
+
let presets = [
[
'@babel/preset-env',
diff --git a/config/feature_flags/beta/revalidate_gpg_fingerprints.yml b/config/feature_flags/beta/revalidate_gpg_fingerprints.yml
deleted file mode 100644
index d9752597d67..00000000000
--- a/config/feature_flags/beta/revalidate_gpg_fingerprints.yml
+++ /dev/null
@@ -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
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 651e7362ac1..72671ef7048 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -89,6 +89,8 @@ if (WEBPACK_REPORT) {
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 incrementalCompiler = createIncrementalWebpackCompiler(
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 7a4055b9f89..d2bcf6af2b0 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -2068,6 +2068,7 @@ Input type: `AiActionInput`
| `platformOrigin` | [`String`](#string) | Specifies the origin platform of the request. |
| `projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. |
| `resolveVulnerability` | [`AiResolveVulnerabilityInput`](#airesolvevulnerabilityinput) | Input for resolve_vulnerability AI action. |
+| `rootNamespaceId` | [`NamespaceID`](#namespaceid) | Global ID of the top-level namespace the user is acting on. |
| `summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. |
| `summarizeNewMergeRequest` | [`AiSummarizeNewMergeRequestInput`](#aisummarizenewmergerequestinput) | Input for summarize_new_merge_request AI action. |
| `summarizeReview` | [`AiSummarizeReviewInput`](#aisummarizereviewinput) | Input for summarize_review AI action. |
diff --git a/doc/development/duo_workflow/_index.md b/doc/development/duo_workflow/_index.md
index e849e6b92e3..53a45649d96 100644
--- a/doc/development/duo_workflow/_index.md
+++ b/doc/development/duo_workflow/_index.md
@@ -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.
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`
diff --git a/doc/development/testing_guide/end_to_end/troubleshooting.md b/doc/development/testing_guide/end_to_end/troubleshooting.md
index e5402c7b733..5b894badb4f 100644
--- a/doc/development/testing_guide/end_to_end/troubleshooting.md
+++ b/doc/development/testing_guide/end_to_end/troubleshooting.md
@@ -66,3 +66,23 @@ For example, if your IP is `192.168.0.12`:
```shell
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
+```
diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
index 296177aac68..f8c8f0ae71d 100644
--- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
+++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
@@ -9,16 +9,6 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
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|
Gitlab::Gpg.using_tmp_keychain do
Gitlab::Gpg::CurrentKeyChain.add(key.key)
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index 35c2956f230..ff30a606d17 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -42,7 +42,7 @@ namespace :tw do
# CodeOwnerRule.new('Database Operations', ''),
# CodeOwnerRule.new('DataOps', ''),
# CodeOwnerRule.new('Delivery', ''),
- # CodeOwnerRule.new('Durability', ''),
+ CodeOwnerRule.new('Durability', '@axil'),
CodeOwnerRule.new('Duo Chat', '@jglassman1'),
CodeOwnerRule.new('Duo Workflow', '@sselhorn'),
CodeOwnerRule.new('Dynamic Analysis', '@phillipwells'),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6ae362a5691..729dfeb927d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16748,6 +16748,42 @@ msgstr ""
msgid "Components must have a 'name'"
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"
msgstr ""
@@ -27745,9 +27781,6 @@ msgstr ""
msgid "Geo|Resync all"
msgstr ""
-msgid "Geo|Retry count"
-msgstr ""
-
msgid "Geo|Reverify"
msgstr ""
@@ -27793,9 +27826,6 @@ msgstr ""
msgid "Geo|Shards to synchronize"
msgstr ""
-msgid "Geo|Show more"
-msgstr ""
-
msgid "Geo|Site name can't be blank"
msgstr ""
@@ -27829,9 +27859,6 @@ msgstr ""
msgid "Geo|Synchronization"
msgstr ""
-msgid "Geo|Synchronization failed - %{error}"
-msgstr ""
-
msgid "Geo|Synchronization settings"
msgstr ""
@@ -27928,9 +27955,6 @@ msgstr ""
msgid "Geo|Verification concurrency limit"
msgstr ""
-msgid "Geo|Verification failed - %{error}"
-msgstr ""
-
msgid "Geo|Verification information"
msgstr ""
@@ -28195,6 +28219,9 @@ msgstr ""
msgid "GitLab Premium"
msgstr ""
+msgid "GitLab Security"
+msgstr ""
+
msgid "GitLab Shell"
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."
msgstr ""
+msgid "Thank you,"
+msgstr ""
+
msgid "Thanks for signing up to GitLab!"
msgstr ""
diff --git a/qa/qa/specs/spec_helper.rb b/qa/qa/specs/spec_helper.rb
index ae81e3a5734..5f436f9259e 100644
--- a/qa/qa/specs/spec_helper.rb
+++ b/qa/qa/specs/spec_helper.rb
@@ -72,7 +72,7 @@ RSpec.configure do |config|
begin
Capybara.current_session.execute_script("window.__coveragePathsPersistence.reset()")
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
@@ -111,7 +111,7 @@ RSpec.configure do |config|
example.metadata[:coverage_paths] = coverage_paths
front_end_coverage_by_example[example.metadata[:location]] = coverage_paths
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
diff --git a/qa/qa/tools/ci/code_paths_mapping.rb b/qa/qa/tools/ci/code_paths_mapping.rb
index 1496c5a4481..e705e878323 100644
--- a/qa/qa/tools/ci/code_paths_mapping.rb
+++ b/qa/qa/tools/ci/code_paths_mapping.rb
@@ -9,17 +9,24 @@ module QA
include Helpers
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)
- new.export(mapping_files_glob)
+ def self.export(mapping_files_glob, **kwargs)
+ if kwargs.key?(:bucket) || kwargs.key?(:file_name)
+ new.export(mapping_files_glob, **kwargs)
+ else
+ new.export(mapping_files_glob)
+ end
end
# Export code path mappings to GCP
#
# @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]
- 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)
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}")
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"
- upload_to_gcs(file, mapping_data)
+ upload_to_gcs(file, mapping_data, bucket)
end
# Import code path mappings from GCP
#
# @param [String] branch - branch name
# @param [String] run_type - run type
+ # @param [String] bucket - custom bucket name (optional)
+ # @param [String] file_name - custom file name base (optional)
# @return [Hash]
- def import(branch, run_type)
- filename = code_paths_mapping_file("#{branch}/#{run_type}")
+ def import(branch, run_type, bucket: DEFAULT_BUCKET, file_name: DEFAULT_FILE_NAME)
+ prefix = "#{branch}/#{run_type}/#{file_name}"
+
+ filename = code_paths_mapping_file(prefix, bucket)
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])
rescue StandardError => e
logger.error("Failed to download code paths mapping from GCS. Error: #{e}")
@@ -54,8 +65,9 @@ module QA
private
- def upload_to_gcs(file_name, mapping_data)
- client.put_object(BUCKET, file_name, JSON.pretty_generate(mapping_data))
+ def upload_to_gcs(file_name, mapping_data, bucket)
+ client.put_object(bucket, file_name, JSON.pretty_generate(mapping_data))
+ logger.info("Successfully uploaded to bucket: #{bucket}, file: #{file_name}")
rescue StandardError => e
logger.error("Failed to upload code paths mapping to GCS. Error: #{e}")
logger.error("Backtrace: #{e.backtrace}")
@@ -85,8 +97,8 @@ module QA
#
# Get most up to date mapping file based on pipeline type
# @return [String]
- def code_paths_mapping_file(prefix)
- paginated_list(client.list_objects(BUCKET, prefix: prefix)).last&.name
+ def code_paths_mapping_file(prefix, bucket = DEFAULT_BUCKET)
+ paginated_list(client.list_objects(bucket, prefix: prefix)).last&.name
end
# Paginated list of items
@@ -98,7 +110,7 @@ module QA
return list.items if list.next_page_token.nil?
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
end
end
diff --git a/qa/spec/tools/ci/code_paths_mapping_spec.rb b/qa/spec/tools/ci/code_paths_mapping_spec.rb
index 7715ab47abd..dc8f544b14e 100644
--- a/qa/spec/tools/ci/code_paths_mapping_spec.rb
+++ b/qa/spec/tools/ci/code_paths_mapping_spec.rb
@@ -51,6 +51,29 @@ RSpec.describe QA::Tools::Ci::CodePathsMapping do
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
let(:file_paths) { [] }
diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake
index 3e371153be1..f43e4676f6e 100644
--- a/qa/tasks/ci.rake
+++ b/qa/tasks/ci.rake
@@ -103,10 +103,18 @@ namespace :ci do
QA::Tools::Ci::TestMetrics.export(args[:glob])
end
- desc "Export code paths mapping to GCP"
+ desc "Export backend 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])
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
diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index c7a86d6230a..4250a0610ad 100644
--- a/spec/frontend/blob/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -2,8 +2,11 @@
import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LineHighlighter from '~/blob/line_highlighter';
+import { updateHash } from '~/blob/state';
import * as utils from '~/lib/utils/common_utils';
+jest.mock('~/blob/state');
+
describe('LineHighlighter', () => {
const testContext = {};
@@ -283,5 +286,17 @@ describe('LineHighlighter', () => {
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');
+ });
});
});
diff --git a/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js b/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js
index a52f22e543d..7d661f19ac6 100644
--- a/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js
+++ b/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js
@@ -9,7 +9,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { blobControlsDataMock } from 'ee_else_ce_jest/repository/mock_data';
jest.mock('~/behaviors/shortcuts/shortcuts_toggle');
-jest.mock('~/blob/state');
const relativePermalinkPath =
'flightjs/Flight/-/blob/46ca9ebd5a43ec240ee8d64e2bb829169dff744e/bower.json';
diff --git a/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js b/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js
index f548546fa64..19b5032704c 100644
--- a/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js
+++ b/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js
@@ -4,7 +4,7 @@ import PermalinkDropdownItem from '~/repository/components/header_area/permalink
import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
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 { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
@@ -46,6 +46,24 @@ describe('PermalinkDropdownItem', () => {
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', () => {
it('returns absolutePermalinkPath when no line number is set', () => {
expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe(
@@ -54,7 +72,7 @@ describe('PermalinkDropdownItem', () => {
});
it('returns updated path with line number when set', () => {
- hashState.currentHash = 10;
+ hashState.currentHash = '#L10';
createComponent();
expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe(
@@ -158,6 +176,17 @@ describe('PermalinkDropdownItem', () => {
wrapper.destroy();
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', () => {
diff --git a/spec/frontend/repository/components/header_area/utils_spec.js b/spec/frontend/repository/components/header_area/utils_spec.js
new file mode 100644
index 00000000000..b05a2d48036
--- /dev/null
+++ b/spec/frontend/repository/components/header_area/utils_spec.js
@@ -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}`,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index 46bb7216dde..0ee6dccdff4 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -5,11 +5,9 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { addInteractionClass } from '~/code_navigation/utils';
-import { updateLineNumber } from '~/blob/state';
import { CHUNK_1, CHUNK_2 } from '../mock_data';
jest.mock('~/code_navigation/utils');
-jest.mock('~/blob/state');
Vue.use(Vuex);
@@ -103,18 +101,5 @@ describe('Chunk component', () => {
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);
- });
});
});
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index d034644ceeb..6e248cd512e 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -67,23 +67,6 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater, :sidekiq_inline do
)
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
# InvalidGpgSignatureUpdater is called by the after_create hook
create :gpg_key,
@@ -127,23 +110,6 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater, :sidekiq_inline do
)
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
# InvalidGpgSignatureUpdater is called by the after_create hook
create :gpg_key,
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
index 4c71ae611bd..ae4dcd83f82 100644
--- a/spec/support/finder_collection_allowlist.yml
+++ b/spec/support/finder_collection_allowlist.yml
@@ -8,7 +8,8 @@
- 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
- 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)
# For example: