Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									3f29b140ab
								
							
						
					
					
						commit
						4dcdd5bebb
					
				|  | @ -134,7 +134,13 @@ module CommitsHelper | ||||||
|   def conditionally_paginate_diff_files(diffs, paginate:, page:, per:) |   def conditionally_paginate_diff_files(diffs, paginate:, page:, per:) | ||||||
|     if paginate |     if paginate | ||||||
|       diff_files = diffs.diff_files.to_a |       diff_files = diffs.diff_files.to_a | ||||||
|       Gitlab::Utils::BatchLoader.clear_key([:repository_blobs, diffs.project.repository]) |       project = diffs.project | ||||||
|  |       repo = project.repository | ||||||
|  | 
 | ||||||
|  |       # While Feature flag increase_diff_file_performance exists, we clear both | ||||||
|  |       Gitlab::Utils::BatchLoader.clear_key([:repository_blobs, repo, Gitlab::Diff::FileCollection::MergeRequestDiffBase.max_blob_size(project)]) | ||||||
|  |       Gitlab::Utils::BatchLoader.clear_key([:repository_blobs, repo, Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE]) | ||||||
|  |       Gitlab::Utils::BatchLoader.clear_key([:repository_blobs, repo]) | ||||||
| 
 | 
 | ||||||
|       Kaminari.paginate_array(diff_files).page(page).per(per).tap do |diff_files| |       Kaminari.paginate_array(diff_files).page(page).per(per).tap do |diff_files| | ||||||
|         diff_files.each(&:add_blobs_to_batch_loader) |         diff_files.each(&:add_blobs_to_batch_loader) | ||||||
|  |  | ||||||
|  | @ -94,8 +94,11 @@ class Blob < SimpleDelegator | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def self.lazy(repository, commit_id, path, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) |   def self.lazy(repository, commit_id, path, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) | ||||||
|     BatchLoader.for([commit_id, path]).batch(key: [:repository_blobs, repository]) do |items, loader, args| |     key = [:repository_blobs, repository] | ||||||
|       args[:key].last.blobs_at(items, blob_size_limit: blob_size_limit).each do |blob| |     key << blob_size_limit if Feature.enabled?(:increase_diff_file_performance, repository.project) | ||||||
|  | 
 | ||||||
|  |     BatchLoader.for([commit_id, path]).batch(key: key) do |items, loader, args| | ||||||
|  |       args[:key].second.blobs_at(items, blob_size_limit: blob_size_limit).each do |blob| | ||||||
|         loader.call([blob.commit_id, blob.path], blob) if blob |         loader.call([blob.commit_id, blob.path], blob) if blob | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -31,6 +31,16 @@ module Issues | ||||||
|       Issues::RebalancingWorker.perform_async(nil, *issue.project.self_or_root_group_ids) |       Issues::RebalancingWorker.perform_async(nil, *issue.project.self_or_root_group_ids) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     def execute_hooks(issue, action = 'open', old_associations: {}) | ||||||
|  |       issue_data  = Gitlab::Lazy.new { hook_data(issue, action, old_associations: old_associations) } | ||||||
|  |       hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks | ||||||
|  |       issue.namespace.execute_hooks(issue_data, hooks_scope) | ||||||
|  |       issue.namespace.execute_integrations(issue_data, hooks_scope) | ||||||
|  | 
 | ||||||
|  |       execute_incident_hooks(issue, issue_data) if issue.work_item_type&.incident? | ||||||
|  |       execute_group_mention_hooks(issue, issue_data) if action == 'open' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     # overriding this because IssuableBaseService#constructor_container_arg returns { project: value } |     # overriding this because IssuableBaseService#constructor_container_arg returns { project: value } | ||||||
|  | @ -105,16 +115,6 @@ module Issues | ||||||
|         issue, issue.project, current_user, old_assignees) |         issue, issue.project, current_user, old_assignees) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def execute_hooks(issue, action = 'open', old_associations: {}) |  | ||||||
|       issue_data  = Gitlab::Lazy.new { hook_data(issue, action, old_associations: old_associations) } |  | ||||||
|       hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks |  | ||||||
|       issue.namespace.execute_hooks(issue_data, hooks_scope) |  | ||||||
|       issue.namespace.execute_integrations(issue_data, hooks_scope) |  | ||||||
| 
 |  | ||||||
|       execute_incident_hooks(issue, issue_data) if issue.work_item_type&.incident? |  | ||||||
|       execute_group_mention_hooks(issue, issue_data) if action == 'open' |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     # We can remove this code after proposal in |     # We can remove this code after proposal in | ||||||
|     # https://gitlab.com/gitlab-org/gitlab/-/issues/367550#proposal is updated. |     # https://gitlab.com/gitlab-org/gitlab/-/issues/367550#proposal is updated. | ||||||
|     def execute_incident_hooks(issue, issue_data) |     def execute_incident_hooks(issue, issue_data) | ||||||
|  |  | ||||||
|  | @ -37,12 +37,33 @@ module Timelogs | ||||||
|         note: nil |         note: nil | ||||||
|       ) |       ) | ||||||
| 
 | 
 | ||||||
|  |       old_associations = { total_time_spent: issuable.total_time_spent } | ||||||
|  | 
 | ||||||
|       if !timelog.save |       if !timelog.save | ||||||
|         error_in_save(timelog) |         error_in_save(timelog) | ||||||
|       else |       else | ||||||
|         SystemNoteService.created_timelog(issuable, issuable.project, current_user, timelog) |         SystemNoteService.created_timelog(issuable, issuable.project, current_user, timelog) | ||||||
|  | 
 | ||||||
|  |         issuable_base_service.execute_hooks(issuable, 'update', old_associations: old_associations) | ||||||
|  | 
 | ||||||
|         success(timelog) |         success(timelog) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     private | ||||||
|  | 
 | ||||||
|  |     def issuable_base_service | ||||||
|  |       if issuable.is_a?(Issue) | ||||||
|  |         Issues::BaseService.new( | ||||||
|  |           container: issuable.project, | ||||||
|  |           current_user: current_user | ||||||
|  |         ) | ||||||
|  |       else | ||||||
|  |         MergeRequests::BaseService.new( | ||||||
|  |           project: issuable.project, | ||||||
|  |           current_user: current_user | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module WorkItems | ||||||
|  |   module Callbacks | ||||||
|  |     class Notes < Base | ||||||
|  |       def before_update | ||||||
|  |         return unless params.present? && params.key?(:discussion_locked) | ||||||
|  |         return unless has_permission?(:set_work_item_metadata) | ||||||
|  | 
 | ||||||
|  |         work_item.discussion_locked = params[:discussion_locked] | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,8 @@ | ||||||
|  | --- | ||||||
|  | name: increase_diff_file_performance | ||||||
|  | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136674 | ||||||
|  | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432600 | ||||||
|  | milestone: '16.9' | ||||||
|  | type: development | ||||||
|  | group: group::code review | ||||||
|  | default_enabled: false | ||||||
|  | @ -27,6 +27,10 @@ but have several advantages: | ||||||
| Instead of creating your own components, you can also search for published components | Instead of creating your own components, you can also search for published components | ||||||
| that have the functionality you need in the [CI/CD Catalog](#cicd-catalog). | that have the functionality you need in the [CI/CD Catalog](#cicd-catalog). | ||||||
| 
 | 
 | ||||||
|  | <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> | ||||||
|  | For an introduction and hands-on examples, see [Efficient DevSecOps workflows with reusable CI/CD components](https://www.youtube.com/watch?v=-yvfSFKAgbA). | ||||||
|  | <!-- Video published on 2024-01-22. DRI: Developer Relations, https://gitlab.com/groups/gitlab-com/marketing/developer-relations/-/epics/399 --> | ||||||
|  | 
 | ||||||
| ## Component project | ## Component project | ||||||
| 
 | 
 | ||||||
| A component project is a GitLab project with a repository that hosts one or more components. | A component project is a GitLab project with a repository that hosts one or more components. | ||||||
|  | @ -301,7 +305,7 @@ For example: | ||||||
| ```yaml | ```yaml | ||||||
| include: | include: | ||||||
|   # include the component located in the current project from the current SHA |   # include the component located in the current project from the current SHA | ||||||
|   - component: gitlab.com/$CI_PROJECT_PATH/my-component@$CI_COMMIT_SHA |   - component: $CI_SERVER_HOST/$CI_PROJECT_PATH/my-component@$CI_COMMIT_SHA | ||||||
|     inputs: |     inputs: | ||||||
|       stage: build |       stage: build | ||||||
| 
 | 
 | ||||||
|  | @ -315,7 +319,7 @@ ensure-job-added: | ||||||
|   image: badouralix/curl-jq |   image: badouralix/curl-jq | ||||||
|   script: |   script: | ||||||
|     - | |     - | | ||||||
|       route="https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs" |       route="${CI_API_V4_URL}/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs" | ||||||
|       count=`curl --silent --header "PRIVATE-TOKEN: $API_TOKEN" $route | jq 'map(select(.name | contains("component-job"))) | length'` |       count=`curl --silent --header "PRIVATE-TOKEN: $API_TOKEN" $route | jq 'map(select(.name | contains("component-job"))) | length'` | ||||||
|       if [ "$count" != "1" ]; then |       if [ "$count" != "1" ]; then | ||||||
|         exit 1 |         exit 1 | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ NOTE: | ||||||
| Use [this snippet](https://gitlab.com/gitlab-org/gitlab/-/snippets/2554994) for help automating the following section. | Use [this snippet](https://gitlab.com/gitlab-org/gitlab/-/snippets/2554994) for help automating the following section. | ||||||
| 
 | 
 | ||||||
| 1. [Enable Anthropic API features](index.md#configure-anthropic-access). | 1. [Enable Anthropic API features](index.md#configure-anthropic-access). | ||||||
| 1. [Ensure the embedding database is configured](index.md#set-up-the-embedding-database). | 1. [Ensure the embedding database is configured](index.md#embeddings-database). | ||||||
| 1. Ensure that your current branch is up-to-date with `master`. | 1. Ensure that your current branch is up-to-date with `master`. | ||||||
| 1. Enable the feature in Rails console: `Feature.enable(:tanuki_bot_breadcrumbs_entry_point)` | 1. Enable the feature in Rails console: `Feature.enable(:tanuki_bot_breadcrumbs_entry_point)` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -39,6 +39,15 @@ to AI that you think could benefit from being in this list, add it! | ||||||
|   piece of information, which helps to clarify its meaning and implications. |   piece of information, which helps to clarify its meaning and implications. | ||||||
|   For GitLab Duo Chat, context is the attributes of the Issue or Epic being |   For GitLab Duo Chat, context is the attributes of the Issue or Epic being | ||||||
|   referenced in a user question. |   referenced in a user question. | ||||||
|  | - **Embeddings**: In the context of machine learning and large language models, | ||||||
|  |   embeddings refer to a technique used to represent words, phrases, or even | ||||||
|  |   entire documents as dense numerical vectors in a continuous vector space. | ||||||
|  |   At GitLab, [we use Vertex AI's Embeddings API](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129930) | ||||||
|  |   to create a vector representation of GitLab documentation. These | ||||||
|  |   embeddings are stored in the `vertex_gitlab_docs` database table in the | ||||||
|  |   `embeddings` database. The embeddings search is done in Postgres using the | ||||||
|  |   `vector` extension. The vertex embeddings database is updated based on the | ||||||
|  |   latest version of GitLab documentation on daily basis by running `Llm::Embedding::GitlabDocumentation::CreateEmptyEmbeddingsRecordsWorker` as a cronjob.  | ||||||
| - **Golden Questions**: a small subset of the types of questions we think a user | - **Golden Questions**: a small subset of the types of questions we think a user | ||||||
|   should be able to ask GitLab Duo Chat. Used to generate data for Chat evaluation. |   should be able to ask GitLab Duo Chat. Used to generate data for Chat evaluation. | ||||||
|   [Questions for Chat Beta](https://gitlab.com/groups/gitlab-org/-/epics/10550#what-the-user-can-ask). |   [Questions for Chat Beta](https://gitlab.com/groups/gitlab-org/-/epics/10550#what-the-user-can-ask). | ||||||
|  |  | ||||||
|  | @ -77,20 +77,6 @@ RAILS_ENV=development bundle exec rake gitlab:duo:setup['<test-group-name>'] | ||||||
|    1. For Vertex, follow the [instructions below](#configure-gcp-vertex-access). |    1. For Vertex, follow the [instructions below](#configure-gcp-vertex-access). | ||||||
|    1. For Anthropic, create an access request |    1. For Anthropic, create an access request | ||||||
| 
 | 
 | ||||||
| ### Set up the embedding database |  | ||||||
| 
 |  | ||||||
| For features that use the embedding database, additional setup is needed. |  | ||||||
| 
 |  | ||||||
| 1. Enable [`pgvector`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/pgvector.md#enable-pgvector-in-the-gdk) in GDK |  | ||||||
| 1. Enable the embedding database in GDK |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|      gdk config set gitlab.rails.databases.embedding.enabled true |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Run `gdk reconfigure` |  | ||||||
| 1. Run database migrations to create the embedding database |  | ||||||
| 
 |  | ||||||
| ### Configure GCP Vertex access | ### Configure GCP Vertex access | ||||||
| 
 | 
 | ||||||
| In order to obtain a GCP service key for local development, follow the steps below: | In order to obtain a GCP service key for local development, follow the steps below: | ||||||
|  | @ -118,38 +104,71 @@ Gitlab::CurrentSettings.update(vertex_ai_project: PROJECT_ID) | ||||||
| Gitlab::CurrentSettings.update!(anthropic_api_key: <insert API key>) | Gitlab::CurrentSettings.update!(anthropic_api_key: <insert API key>) | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Populating embeddings and using embeddings fixture | ### Embeddings database | ||||||
| 
 | 
 | ||||||
| Embeddings are generated through VertexAI text embeddings endpoint. The sections below explain how to populate | Embeddings are generated through the [VertexAI text embeddings API](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings). The sections | ||||||
| embeddings in the DB or extract embeddings to be used in specs. | below explain how to populate embeddings in the DB or extract embeddings to be | ||||||
|  | used in specs. | ||||||
| 
 | 
 | ||||||
| #### VertexAI embeddings | #### Set up | ||||||
| 
 | 
 | ||||||
| To seed your development database with the embeddings for GitLab Documentation, | 1. Enable [`pgvector`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/pgvector.md#enable-pgvector-in-the-gdk) in GDK | ||||||
| you may use the pre-generated embeddings and a Rake task. | 1. Enable the embedding database in GDK | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |      gdk config set gitlab.rails.databases.embedding.enabled true | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Run `gdk reconfigure` | ||||||
|  | 1. Run database migrations to create the embedding database | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |      RAILS_ENV=development bin/rails db:migrate | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | #### Populate | ||||||
|  | 
 | ||||||
|  | Seed your development database with the embeddings for GitLab Documentation | ||||||
|  | using this Rake task: | ||||||
| 
 | 
 | ||||||
| ```shell | ```shell | ||||||
| RAILS_ENV=development bundle exec rake gitlab:llm:embeddings:vertex:seed | RAILS_ENV=development bundle exec rake gitlab:llm:embeddings:vertex:seed | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| The DBCleaner gem we use clear the database tables before each test runs. | This Rake Task populates the embeddings database with a vectorized | ||||||
| Instead of fully populating the table `vertex_gitlab_docs` where we store VertexAI embeddings for the documentations, | representation of all GitLab Documentation. The file the Rake Task uses as a | ||||||
| we can add a few selected embeddings to the table from a pre-generated fixture. | source is a snapshot of GitLab Documentation at some point in the past and is | ||||||
|  | not updated regularly. As a result, it is helpful to know that this seed task | ||||||
|  | creates embeddings based on GitLab Documentation that is out of date. Slightly | ||||||
|  | outdated documentation embeddings are sufficient for the development | ||||||
|  | environment, which is the use-case for the seed task. | ||||||
| 
 | 
 | ||||||
| For instance, to test that the question "How can I reset my password" is correctly | When writing or updating tests related to embeddings, you may want to update the | ||||||
| retrieving the relevant embeddings and answered, we can extract the top N closet embeddings | embeddings fixture file: | ||||||
| to the question into a fixture and only restore a small number of embeddings quickly. |  | ||||||
| To facilitate an extraction process, a Rake task has been written. |  | ||||||
| You can add or remove the questions needed to be tested in the Rake task and run the task to generate a new fixture. |  | ||||||
| 
 | 
 | ||||||
| ```shell | ```shell | ||||||
| RAILS_ENV=development bundle exec rake gitlab:llm:embeddings:vertex:extract_embeddings | RAILS_ENV=development bundle exec rake gitlab:llm:embeddings:vertex:extract_embeddings | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| #### Using embeddings in specs | #### Use embeddings in specs | ||||||
|  | 
 | ||||||
|  | The `seed` Rake Task populates the development database with embeddings for all GitLab | ||||||
|  | Documentation. The `extract_embeddings` Rake Task populates a fixture file with a subset | ||||||
|  | of embeddings. | ||||||
|  | 
 | ||||||
|  | The set of questions listed in the Rake Task itself determines | ||||||
|  | which embeddings are pulled into the fixture file. For example, one of the | ||||||
|  | questions is "How can I reset my password?" The `extract_embeddings` Task | ||||||
|  | pulls the most relevant embeddings for this question from the development | ||||||
|  | database (which has data from the `seed` Rake Task) and saves those embeddings | ||||||
|  | in `ee/spec/fixtures/vertex_embeddings`. This fixture is used in tests related | ||||||
|  | to embeddings. | ||||||
|  | 
 | ||||||
|  | If you would like to change any of the questions supported in embeddings specs, | ||||||
|  | update and re-run the `extract_embeddings` Rake Task. | ||||||
| 
 | 
 | ||||||
| In the specs where you need to use the embeddings, | In the specs where you need to use the embeddings, | ||||||
| use the RSpec config hook `:ai_embedding_fixtures` on a context. | use the RSpec `:ai_embedding_fixtures` metadata. | ||||||
| 
 | 
 | ||||||
| ```ruby | ```ruby | ||||||
| context 'when asking about how to use GitLab', :ai_embedding_fixtures do | context 'when asking about how to use GitLab', :ai_embedding_fixtures do | ||||||
|  |  | ||||||
|  | @ -32,6 +32,10 @@ For server-side controls and enforcement of best practices, see | ||||||
| 
 | 
 | ||||||
| You can use push options to skip a CI/CD pipeline, or pass CI/CD variables. | You can use push options to skip a CI/CD pipeline, or pass CI/CD variables. | ||||||
| 
 | 
 | ||||||
|  | NOTE: | ||||||
|  | Push options are not available for merge request pipelines. For more information, | ||||||
|  | see [issue 373212](https://gitlab.com/gitlab-org/gitlab/-/issues/373212). | ||||||
|  | 
 | ||||||
| | Push option                    | Description | Example | | | Push option                    | Description | Example | | ||||||
| |--------------------------------|-------------|---------| | |--------------------------------|-------------|---------| | ||||||
| | `ci.skip`                      | Do not create a CI/CD pipeline for the latest push. Skips only branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). This does not skip pipelines for CI/CD integrations, such as Jenkins. | `git push -o ci.skip` | | | `ci.skip`                      | Do not create a CI/CD pipeline for the latest push. Skips only branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). This does not skip pipelines for CI/CD integrations, such as Jenkins. | `git push -o ci.skip` | | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ module Gitlab | ||||||
|     class File |     class File | ||||||
|       include Gitlab::Utils::StrongMemoize |       include Gitlab::Utils::StrongMemoize | ||||||
| 
 | 
 | ||||||
|       attr_reader :diff, :repository, :diff_refs, :fallback_diff_refs, :unique_identifier |       attr_reader :diff, :repository, :diff_refs, :fallback_diff_refs, :unique_identifier, :max_blob_size | ||||||
| 
 | 
 | ||||||
|       delegate :new_file?, :deleted_file?, :renamed_file?, :unidiff, |       delegate :new_file?, :deleted_file?, :renamed_file?, :unidiff, | ||||||
|         :old_path, :new_path, :a_mode, :b_mode, :mode_changed?, |         :old_path, :new_path, :a_mode, :b_mode, :mode_changed?, | ||||||
|  | @ -31,7 +31,8 @@ module Gitlab | ||||||
|         diff_refs: nil, |         diff_refs: nil, | ||||||
|         fallback_diff_refs: nil, |         fallback_diff_refs: nil, | ||||||
|         stats: nil, |         stats: nil, | ||||||
|         unique_identifier: nil) |         unique_identifier: nil, | ||||||
|  |         max_blob_size: nil) | ||||||
| 
 | 
 | ||||||
|         @diff = diff |         @diff = diff | ||||||
|         @stats = stats |         @stats = stats | ||||||
|  | @ -39,6 +40,7 @@ module Gitlab | ||||||
|         @diff_refs = diff_refs |         @diff_refs = diff_refs | ||||||
|         @fallback_diff_refs = fallback_diff_refs |         @fallback_diff_refs = fallback_diff_refs | ||||||
|         @unique_identifier = unique_identifier |         @unique_identifier = unique_identifier | ||||||
|  |         @max_blob_size = max_blob_size | ||||||
|         @unfolded = false |         @unfolded = false | ||||||
| 
 | 
 | ||||||
|         # Ensure items are collected in the the batch |         # Ensure items are collected in the the batch | ||||||
|  | @ -397,8 +399,12 @@ module Gitlab | ||||||
|       def fetch_blob(sha, path) |       def fetch_blob(sha, path) | ||||||
|         return unless sha |         return unless sha | ||||||
| 
 | 
 | ||||||
|  |         if max_blob_size.present? | ||||||
|  |           Blob.lazy(repository, sha, path, blob_size_limit: max_blob_size) | ||||||
|  |         else | ||||||
|           Blob.lazy(repository, sha, path) |           Blob.lazy(repository, sha, path) | ||||||
|         end |         end | ||||||
|  |       end | ||||||
| 
 | 
 | ||||||
|       def total_blob_lines(blob) |       def total_blob_lines(blob) | ||||||
|         @total_lines ||= begin |         @total_lines ||= begin | ||||||
|  |  | ||||||
|  | @ -119,7 +119,8 @@ module Gitlab | ||||||
|                                  repository: project.repository, |                                  repository: project.repository, | ||||||
|                                  diff_refs: diff_refs, |                                  diff_refs: diff_refs, | ||||||
|                                  fallback_diff_refs: fallback_diff_refs, |                                  fallback_diff_refs: fallback_diff_refs, | ||||||
|                                  stats: stats) |                                  stats: stats, | ||||||
|  |                                  max_blob_size: self.class.max_blob_size(project)) | ||||||
| 
 | 
 | ||||||
|           if @use_extra_viewer_as_main && diff_file.has_renderable? |           if @use_extra_viewer_as_main && diff_file.has_renderable? | ||||||
|             diff_file.rendered |             diff_file.rendered | ||||||
|  | @ -131,6 +132,8 @@ module Gitlab | ||||||
|         def sort_diffs(diffs) |         def sort_diffs(diffs) | ||||||
|           Gitlab::Diff::FileCollectionSorter.new(diffs).sort |           Gitlab::Diff::FileCollectionSorter.new(diffs).sort | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         def self.max_blob_size(_) = nil | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -49,6 +49,14 @@ module Gitlab | ||||||
|           diff_stats_cache.clear |           diff_stats_cache.clear | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  |         override :max_blob_size | ||||||
|  |         def self.max_blob_size(project) | ||||||
|  |           return unless Feature.enabled?(:increase_diff_file_performance, project) | ||||||
|  | 
 | ||||||
|  |           [Gitlab::Git::Diff.patch_hard_limit_bytes, | ||||||
|  |             Gitlab.config.extra['maximum_text_highlight_size_kilobytes']].max | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|         private |         private | ||||||
| 
 | 
 | ||||||
|         def highlight_cache |         def highlight_cache | ||||||
|  |  | ||||||
|  | @ -24,4 +24,14 @@ RSpec.describe Gitlab::Diff::FileCollection::Base do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '#raw_diff_files' do | ||||||
|  |     let(:max_blob_size) { 10 } | ||||||
|  | 
 | ||||||
|  |     it 'returns diffs that contain a maximum of max_blob_size of data' do | ||||||
|  |       allow(described_class).to receive(:max_blob_size).and_return(max_blob_size) | ||||||
|  | 
 | ||||||
|  |       expect(described_class.new(diffable, project: merge_request.project).raw_diff_files.all? { |file| file.max_blob_size == max_blob_size }).to be_truthy | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -44,4 +44,43 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBase do | ||||||
|       expect(cache_key).to eq diffable.cache_key |       expect(cache_key).to eq diffable.cache_key | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '.max_blob_size' do | ||||||
|  |     let(:project) { merge_request.project } | ||||||
|  | 
 | ||||||
|  |     context 'when increase_diff_file_performance is enabled' do | ||||||
|  |       before do | ||||||
|  |         allow(Gitlab::Git::Diff).to receive(:patch_hard_limit_bytes).and_return(max_diff) | ||||||
|  |         stub_config(extra: { 'maximum_text_highlight_size_kilobytes' => max_config }) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when Gitlab::Git::Diff.patch_hard_limit_bytes is larger' do | ||||||
|  |         let(:max_diff) { 10 } | ||||||
|  |         let(:max_config) { 1 } | ||||||
|  | 
 | ||||||
|  |         it 'returns the Gitlab::Git::Diff.patch_hard_limit_bytes setting' do | ||||||
|  |           expect(described_class.max_blob_size(project)).to eq(max_diff) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when maximum_text_highlight_size_kilobytes setting is larger' do | ||||||
|  |         let(:max_diff) { 10 } | ||||||
|  |         let(:max_config) { 100 } | ||||||
|  | 
 | ||||||
|  |         it 'returns the maximum_text_highlight_size_kilobytes setting' do | ||||||
|  |           expect(described_class.max_blob_size(project)).to eq(max_config) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when increase_diff_file_performance is disabled' do | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(increase_diff_file_performance: false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'returns nil' do | ||||||
|  |         expect(described_class.max_blob_size(project)).to eq(nil) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -8,7 +8,9 @@ RSpec.describe Gitlab::Diff::File do | ||||||
|   let_it_be(:project) { create(:project, :repository) } |   let_it_be(:project) { create(:project, :repository) } | ||||||
|   let(:commit) { project.commit(sample_commit.id) } |   let(:commit) { project.commit(sample_commit.id) } | ||||||
|   let(:diff) { commit.raw_diffs.first } |   let(:diff) { commit.raw_diffs.first } | ||||||
|   let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } |   let(:diff_file) do | ||||||
|  |     described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
|   def create_file(file_name, content) |   def create_file(file_name, content) | ||||||
|     Files::CreateService.new( |     Files::CreateService.new( | ||||||
|  | @ -285,12 +287,41 @@ RSpec.describe Gitlab::Diff::File do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#old_blob and #new_blob' do |   describe '#old_blob and #new_blob' do | ||||||
|  |     context 'when increase_diff_file_performance is on' do | ||||||
|  |       let(:diff_file) do | ||||||
|  |         described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository, max_blob_size: max_blob_size) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       let(:max_blob_size) { 1000 } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(increase_diff_file_performance: true) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when the blobs are truncated' do | ||||||
|  |         let(:max_blob_size) { 10 } | ||||||
|  | 
 | ||||||
|  |         it 'returns the truncated blobs' do | ||||||
|  |           items = [ | ||||||
|  |             [diff_file.new_content_sha, diff_file.new_path], [diff_file.old_content_sha, diff_file.old_path] | ||||||
|  |           ] | ||||||
|  | 
 | ||||||
|  |           expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: max_blob_size).and_call_original | ||||||
|  | 
 | ||||||
|  |           old_data = diff_file.old_blob.data | ||||||
|  |           data = diff_file.new_blob.data | ||||||
|  | 
 | ||||||
|  |           expect(old_data.size).to eq(max_blob_size) | ||||||
|  |           expect(data.size).to eq(max_blob_size) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       it 'returns blob of base commit and the new commit' do |       it 'returns blob of base commit and the new commit' do | ||||||
|         items = [ |         items = [ | ||||||
|           [diff_file.new_content_sha, diff_file.new_path], [diff_file.old_content_sha, diff_file.old_path] |           [diff_file.new_content_sha, diff_file.new_path], [diff_file.old_content_sha, diff_file.old_path] | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|       expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: 10.megabytes).and_call_original |         expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: max_blob_size).and_call_original | ||||||
| 
 | 
 | ||||||
|         old_data = diff_file.old_blob.data |         old_data = diff_file.old_blob.data | ||||||
|         data = diff_file.new_blob.data |         data = diff_file.new_blob.data | ||||||
|  | @ -300,6 +331,31 @@ RSpec.describe Gitlab::Diff::File do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'when increase_diff_file_performance is off' do | ||||||
|  |       let(:diff_file) do | ||||||
|  |         described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(increase_diff_file_performance: false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'returns blob of base commit and the new commit' do | ||||||
|  |         items = [ | ||||||
|  |           [diff_file.new_content_sha, diff_file.new_path], [diff_file.old_content_sha, diff_file.old_path] | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |         expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE).and_call_original | ||||||
|  | 
 | ||||||
|  |         old_data = diff_file.old_blob.data | ||||||
|  |         data = diff_file.new_blob.data | ||||||
|  | 
 | ||||||
|  |         expect(old_data).to include('raise "System commands must be given as an array of strings"') | ||||||
|  |         expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#diffable?' do |   describe '#diffable?' do | ||||||
|     context 'when attributes exist' do |     context 'when attributes exist' do | ||||||
|       let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } |       let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } | ||||||
|  |  | ||||||
|  | @ -68,31 +68,89 @@ RSpec.describe Blob do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'when increase_diff_file_performance is turned off' do | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(increase_diff_file_performance: false) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       context 'with project' do |       context 'with project' do | ||||||
|       let(:container) { create(:project, :repository) } |         let_it_be(:container) { create(:project, :repository) } | ||||||
|       let(:same_container) { Project.find(container.id) } |         let_it_be(:same_container) { Project.find(container.id) } | ||||||
|       let(:other_container) { create(:project, :repository) } |         let_it_be(:other_container) { create(:project, :repository) } | ||||||
| 
 | 
 | ||||||
|         it_behaves_like '.lazy checks' |         it_behaves_like '.lazy checks' | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'with personal snippet' do |       context 'with personal snippet' do | ||||||
|       let(:container) { create(:personal_snippet, :repository) } |         let_it_be(:container) { create(:personal_snippet, :repository) } | ||||||
|       let(:same_container) { PersonalSnippet.find(container.id) } |         let_it_be(:same_container) { PersonalSnippet.find(container.id) } | ||||||
|       let(:other_container) { create(:personal_snippet, :repository) } |         let_it_be(:other_container) { create(:personal_snippet, :repository) } | ||||||
| 
 | 
 | ||||||
|         it_behaves_like '.lazy checks' |         it_behaves_like '.lazy checks' | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'with project snippet' do |       context 'with project snippet' do | ||||||
|       let(:container) { create(:project_snippet, :repository) } |         let_it_be(:container) { create(:project_snippet, :repository) } | ||||||
|       let(:same_container) { ProjectSnippet.find(container.id) } |         let_it_be(:same_container) { ProjectSnippet.find(container.id) } | ||||||
|       let(:other_container) { create(:project_snippet, :repository) } |         let_it_be(:other_container) { create(:project_snippet, :repository) } | ||||||
| 
 | 
 | ||||||
|         it_behaves_like '.lazy checks' |         it_behaves_like '.lazy checks' | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'when increase_diff_file_performance is turned on' do | ||||||
|  |       context 'with project' do | ||||||
|  |         let_it_be(:container) { create(:project, :repository) } | ||||||
|  |         let_it_be(:same_container) { Project.find(container.id) } | ||||||
|  |         let_it_be(:other_container) { create(:project, :repository) } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like '.lazy checks' | ||||||
|  | 
 | ||||||
|  |         context 'when the blob size limit is different' do | ||||||
|  |           it 'fetches all blobs for the same repository and same blob size limit when one is accessed' do | ||||||
|  |             expect(container.repository).to receive(:blobs_at) | ||||||
|  |               .with([[commit_id, 'CHANGELOG']], blob_size_limit: 10) | ||||||
|  |               .once.and_call_original | ||||||
|  | 
 | ||||||
|  |             expect(same_container.repository).to receive(:blobs_at) | ||||||
|  |               .with([[commit_id, 'CONTRIBUTING.md'], [commit_id, 'README.md']], blob_size_limit: 20) | ||||||
|  |               .once.and_call_original | ||||||
|  | 
 | ||||||
|  |             expect(other_container.repository).not_to receive(:blobs_at) | ||||||
|  | 
 | ||||||
|  |             changelog = described_class.lazy(container.repository, commit_id, 'CHANGELOG', blob_size_limit: 10) | ||||||
|  |             contributing = described_class.lazy(same_container.repository, commit_id, 'CONTRIBUTING.md', | ||||||
|  |               blob_size_limit: 20) | ||||||
|  |             described_class.lazy(same_container.repository, commit_id, 'README.md', | ||||||
|  |               blob_size_limit: 20) | ||||||
|  | 
 | ||||||
|  |             described_class.lazy(other_container.repository, commit_id, 'CHANGELOG', blob_size_limit: 30) | ||||||
|  | 
 | ||||||
|  |             # Access property so the values are loaded | ||||||
|  |             changelog.id | ||||||
|  |             contributing.id | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with personal snippet' do | ||||||
|  |         let_it_be(:container) { create(:personal_snippet, :repository) } | ||||||
|  |         let_it_be(:same_container) { PersonalSnippet.find(container.id) } | ||||||
|  |         let_it_be(:other_container) { create(:personal_snippet, :repository) } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like '.lazy checks' | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with project snippet' do | ||||||
|  |         let_it_be(:container) { create(:project_snippet, :repository) } | ||||||
|  |         let_it_be(:same_container) { ProjectSnippet.find(container.id) } | ||||||
|  |         let_it_be(:other_container) { create(:project_snippet, :repository) } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like '.lazy checks' | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#data' do |   describe '#data' do | ||||||
|     shared_examples '#data checks' do |     shared_examples '#data checks' do | ||||||
|       context 'using a binary blob' do |       context 'using a binary blob' do | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe WorkItems::Widgets::Notes, feature_category: :team_planning do | RSpec.describe WorkItems::Widgets::Notes, feature_category: :team_planning do | ||||||
|   let_it_be(:work_item) { create(:work_item) } |   let_it_be(:work_item) { create(:work_item, :objective, discussion_locked: true) } | ||||||
|   let_it_be(:note) { create(:note, noteable: work_item, project: work_item.project) } |   let_it_be(:note) { create(:note, noteable: work_item, project: work_item.project) } | ||||||
| 
 | 
 | ||||||
|   describe '.type' do |   describe '.type' do | ||||||
|  | @ -17,4 +17,22 @@ RSpec.describe WorkItems::Widgets::Notes, feature_category: :team_planning do | ||||||
|   describe '#notes' do |   describe '#notes' do | ||||||
|     it { expect(described_class.new(work_item).notes).to eq(work_item.notes) } |     it { expect(described_class.new(work_item).notes).to eq(work_item.notes) } | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '.quick_action_params' do | ||||||
|  |     subject { described_class.quick_action_params } | ||||||
|  | 
 | ||||||
|  |     it { is_expected.to include(:discussion_locked) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '.quick_action_commands' do | ||||||
|  |     subject { described_class.quick_action_commands } | ||||||
|  | 
 | ||||||
|  |     it { is_expected.to match_array([:lock, :unlock]) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#discussion_locked' do | ||||||
|  |     subject { described_class.new(work_item).discussion_locked } | ||||||
|  | 
 | ||||||
|  |     it { is_expected.to eq(work_item.discussion_locked) } | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -1479,6 +1479,109 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'with notes widget input' do | ||||||
|  |       let(:discussion_locked) { true } | ||||||
|  |       let(:input) { { 'notesWidget' => { 'discussionLocked' => true } } } | ||||||
|  | 
 | ||||||
|  |       let(:fields) do | ||||||
|  |         <<~FIELDS | ||||||
|  |           workItem { | ||||||
|  |             widgets { | ||||||
|  |               type | ||||||
|  |               ... on WorkItemWidgetNotes { | ||||||
|  |                 discussionLocked | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           errors | ||||||
|  |         FIELDS | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       shared_examples 'work item is not updated' do | ||||||
|  |         it 'ignores the update' do | ||||||
|  |           expect do | ||||||
|  |             post_graphql_mutation(mutation, current_user: current_user) | ||||||
|  |             work_item.reload | ||||||
|  |           end.not_to change(&work_item_change) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'work item is not updated' do | ||||||
|  |         let(:current_user) { guest } | ||||||
|  |         let(:work_item_change) { -> { work_item.discussion_locked } } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when user has permissions to update the work item' do | ||||||
|  |         let(:current_user) { reporter } | ||||||
|  | 
 | ||||||
|  |         it 'updates work item discussion locked attribute on notes widget' do | ||||||
|  |           expect do | ||||||
|  |             post_graphql_mutation(mutation, current_user: current_user) | ||||||
|  |             work_item.reload | ||||||
|  |           end.to change { work_item.discussion_locked }.from(nil).to(true) | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:success) | ||||||
|  |           expect(mutation_response['workItem']['widgets']).to include( | ||||||
|  |             { | ||||||
|  |               'discussionLocked' => true, | ||||||
|  |               'type' => 'NOTES' | ||||||
|  |             } | ||||||
|  |           ) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when using quick action' do | ||||||
|  |           let(:input) { { 'descriptionWidget' => { 'description' => "/lock" } } } | ||||||
|  | 
 | ||||||
|  |           it 'updates work item discussion locked attribute on notes widget' do | ||||||
|  |             expect do | ||||||
|  |               post_graphql_mutation(mutation, current_user: current_user) | ||||||
|  |               work_item.reload | ||||||
|  |             end.to change { work_item.discussion_locked }.from(nil).to(true) | ||||||
|  | 
 | ||||||
|  |             expect(response).to have_gitlab_http_status(:success) | ||||||
|  |             expect(mutation_response['workItem']['widgets']).to include( | ||||||
|  |               { | ||||||
|  |                 'discussionLocked' => true, | ||||||
|  |                 'type' => 'NOTES' | ||||||
|  |               } | ||||||
|  |             ) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           context 'when unlocking discussion' do | ||||||
|  |             let(:input) { { 'descriptionWidget' => { 'description' => "/unlock" } } } | ||||||
|  | 
 | ||||||
|  |             before do | ||||||
|  |               work_item.update!(discussion_locked: true) | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             it 'updates work item discussion locked attribute on notes widget' do | ||||||
|  |               expect do | ||||||
|  |                 post_graphql_mutation(mutation, current_user: current_user) | ||||||
|  |                 work_item.reload | ||||||
|  |               end.to change { work_item.discussion_locked }.from(true).to(false) | ||||||
|  | 
 | ||||||
|  |               expect(response).to have_gitlab_http_status(:success) | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           context 'when the work item type does not support the notes widget' do | ||||||
|  |             let(:input) do | ||||||
|  |               { 'descriptionWidget' => { 'description' => "Updating notes discussion locked.\n/lock" } } | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             before do | ||||||
|  |               WorkItems::Type.default_by_type(:issue).widget_definitions | ||||||
|  |                 .find_by_widget_type(:notes).update!(disabled: true) | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             it_behaves_like 'work item is not updated' do | ||||||
|  |               let(:work_item_change) { -> { work_item.discussion_locked } } | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context 'when unsupported widget input is sent' do |     context 'when unsupported widget input is sent' do | ||||||
|       let_it_be(:work_item) { create(:work_item, :test_case, project: project) } |       let_it_be(:work_item) { create(:work_item, :test_case, project: project) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,66 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe WorkItems::Callbacks::Notes, feature_category: :team_planning do | ||||||
|  |   let_it_be(:guest) { create(:user) } | ||||||
|  |   let_it_be(:reporter) { create(:user) } | ||||||
|  |   let_it_be(:project) { create(:project) } | ||||||
|  |   let_it_be_with_reload(:work_item) do | ||||||
|  |     create(:work_item, project: project, author: guest, discussion_locked: nil) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   let(:current_user) { guest } | ||||||
|  |   let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Notes) } } | ||||||
|  |   let(:discussion_locked) { true } | ||||||
|  |   let(:params) { { discussion_locked: discussion_locked } } | ||||||
|  |   let(:service) { described_class.new(issuable: work_item, current_user: current_user, params: params) } | ||||||
|  | 
 | ||||||
|  |   before_all do | ||||||
|  |     project.add_guest(guest) | ||||||
|  |     project.add_reporter(reporter) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject(:update_discussion_locked) { service.before_update } | ||||||
|  | 
 | ||||||
|  |   describe '#before_update_callback' do | ||||||
|  |     shared_examples 'discussion_locked is unchanged' do | ||||||
|  |       it 'does not change the discussion_locked of the work item' do | ||||||
|  |         expect { update_discussion_locked }.to not_change { work_item.discussion_locked } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when discussion_locked param is not present' do | ||||||
|  |       let(:params) { {} } | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'discussion_locked is unchanged' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when user cannot set work item metadata' do | ||||||
|  |       let(:current_user) { guest } | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'discussion_locked is unchanged' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when user can set work item metadata' do | ||||||
|  |       let(:current_user) { reporter } | ||||||
|  | 
 | ||||||
|  |       it 'sets the discussion_locked for the work item' do | ||||||
|  |         expect { update_discussion_locked }.to change { work_item.discussion_locked }.from(nil).to(true) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when widget does not exist in new type' do | ||||||
|  |         let(:params) { {} } | ||||||
|  | 
 | ||||||
|  |         before do | ||||||
|  |           allow(service).to receive(:new_type_excludes_widget?).and_return(true) | ||||||
|  |           work_item.discussion_locked = true | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it "keeps item's discussion_locked value intact" do | ||||||
|  |           expect { update_discussion_locked }.not_to change { work_item.discussion_locked } | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -9,6 +9,11 @@ RSpec.shared_examples 'issuable supports timelog creation service' do | ||||||
| 
 | 
 | ||||||
|   shared_examples 'success_response' do |   shared_examples 'success_response' do | ||||||
|     it 'sucessfully saves the timelog' do |     it 'sucessfully saves the timelog' do | ||||||
|  |       expect(Projects::TriggeredHooks).to receive(:new).with( | ||||||
|  |         issuable.is_a?(Issue) ? :issue_hooks : :merge_request_hooks, | ||||||
|  |         a_hash_including(changes: a_hash_including(total_time_spent: { previous: 0, current: time_spent })) | ||||||
|  |       ).and_call_original | ||||||
|  | 
 | ||||||
|       is_expected.to be_success |       is_expected.to be_success | ||||||
| 
 | 
 | ||||||
|       timelog = subject.payload[:timelog] |       timelog = subject.payload[:timelog] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue