From d10e03ba6f8d5d425e53931d306a099404a5958b Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 26 Jul 2021 06:10:00 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- Gemfile.lock | 24 +- app/workers/all_queues.yml | 4 +- .../import_pull_request_merged_by_worker.rb | 1 + .../import_pull_request_review_worker.rb | 1 + .../testing_guide/best_practices.md | 13 +- .../compliance/license_compliance/index.md | 3 + doc/user/project/code_owners.md | 105 +++---- .../img/code_owners_mr_widget_v12_4.png | Bin 27875 -> 0 bytes doc/user/project/protected_branches.md | 13 +- qa/qa/resource/issue.rb | 38 ++- qa/qa/resource/merge_request.rb | 7 +- qa/qa/resource/project.rb | 44 +-- .../1_manage/import_large_github_repo_spec.rb | 286 ++++++++++++++++++ qa/qa/support/api.rb | 13 +- .../support/matchers/eventually_matcher.rb | 3 +- .../features/projects/blobs/blob_show_spec.rb | 265 ++++++++++++++++ 16 files changed, 687 insertions(+), 133 deletions(-) delete mode 100644 doc/user/project/img/code_owners_mr_widget_v12_4.png create mode 100644 qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 652f60f93b8..617e1f18caa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,6 @@ GEM remote: https://rubygems.org/ specs: RedCloth (4.3.2) - abstract_type (0.0.7) acme-client (2.0.6) faraday (>= 0.17, < 2.0.0) actioncable (6.1.3.2) @@ -76,9 +75,6 @@ GEM zeitwerk (~> 2.3) acts-as-taggable-on (7.0.0) activerecord (>= 5.0, < 6.2) - adamantium (0.2.0) - ice_nine (~> 0.11.0) - memoizable (~> 0.4.0) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) aes_key_wrap (1.1.0) @@ -205,9 +201,6 @@ GEM colored2 (3.1.2) commonmarker (0.21.0) ruby-enum (~> 0.5) - concord (0.1.5) - adamantium (~> 0.2.0) - equalizer (~> 0.0.9) concurrent-ruby (1.1.9) connection_pool (2.2.2) contracts (0.11.0) @@ -336,7 +329,6 @@ GEM launchy (~> 2.1) mail (~> 2.7) encryptor (3.0.0) - equalizer (0.0.11) erubi (1.9.0) escape_utils (1.2.1) et-orbi (1.2.1) @@ -647,7 +639,6 @@ GEM concurrent-ruby (~> 1.0) i18n_data (0.8.0) icalendar (2.4.1) - ice_nine (0.11.2) invisible_captcha (1.1.0) rails (>= 4.2) ipaddress (0.8.3) @@ -748,8 +739,6 @@ GEM actionpack (>= 2.3) activerecord (>= 2.3) memoist (0.16.2) - memoizable (0.4.2) - thread_safe (~> 0.3, >= 0.3.1) memory_profiler (0.9.14) method_source (1.0.0) mime-types (3.3.1) @@ -935,7 +924,6 @@ GEM coderay parser unparser - procto (0.0.3) prometheus-client-mmap (0.12.0) pry (0.13.1) coderay (~> 1.1) @@ -1086,7 +1074,7 @@ GEM rspec-mocks (3.10.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) - rspec-parameterized (0.4.2) + rspec-parameterized (0.5.0) binding_ninja (>= 0.2.3) parser proc_to_ast @@ -1279,7 +1267,6 @@ GEM eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) thor (1.1.0) - thread_safe (0.3.6) thrift (0.14.0) tilt (2.0.10) timecop (0.9.1) @@ -1332,14 +1319,9 @@ GEM uniform_notifier (1.13.0) unleash (0.1.5) murmurhash3 (~> 0.1.6) - unparser (0.4.7) - abstract_type (~> 0.0.7) - adamantium (~> 0.2.0) - concord (~> 0.1.5) + unparser (0.6.0) diff-lcs (~> 1.3) - equalizer (~> 0.0.9) - parser (>= 2.6.5) - procto (~> 0.0.2) + parser (>= 3.0.0) uri_template (0.7.0) valid_email (0.1.3) activemodel diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 8d08beb56aa..e002039d226 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -903,7 +903,7 @@ :feature_category: :importers :has_external_dependencies: true :urgency: :low - :resource_boundary: :unknown + :resource_boundary: :cpu :weight: 1 :idempotent: :tags: @@ -913,7 +913,7 @@ :feature_category: :importers :has_external_dependencies: true :urgency: :low - :resource_boundary: :unknown + :resource_boundary: :cpu :weight: 1 :idempotent: :tags: diff --git a/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb b/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb index 91dab3470d9..cce179542c7 100644 --- a/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb +++ b/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb @@ -6,6 +6,7 @@ module Gitlab include ObjectImporter tags :exclude_from_kubernetes + worker_resource_boundary :cpu def representation_class Gitlab::GithubImport::Representation::PullRequest diff --git a/app/workers/gitlab/github_import/import_pull_request_review_worker.rb b/app/workers/gitlab/github_import/import_pull_request_review_worker.rb index de10fe40589..8796d6392df 100644 --- a/app/workers/gitlab/github_import/import_pull_request_review_worker.rb +++ b/app/workers/gitlab/github_import/import_pull_request_review_worker.rb @@ -6,6 +6,7 @@ module Gitlab include ObjectImporter tags :exclude_from_kubernetes + worker_resource_boundary :cpu def representation_class Gitlab::GithubImport::Representation::PullRequestReview diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index e153fa9f334..6b6fbe3996e 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -972,11 +972,16 @@ range of inputs, might look like this: describe "#==" do using RSpec::Parameterized::TableSyntax + let(:one) { 1 } + let(:two) { 2 } + where(:a, :b, :result) do - 1 | 1 | true - 1 | 2 | false - true | true | true - true | false | false + 1 | 1 | true + 1 | 2 | false + true | true | true + true | false | false + ref(:one) | ref(:one) | true # let variables must be referenced using `ref` + ref(:one) | ref(:two) | false end with_them do diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 1a43c5ae96f..e39a3f7111b 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -83,6 +83,9 @@ The reported licenses might be incomplete or inaccurate. ## Requirements +WARNING: +License Compliance Scanning does not support run-time installation of compilers and interpreters. + To run a License Compliance scanning job, you need GitLab Runner with the [`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index 2a60c06814b..32ad0db1866 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -8,23 +8,22 @@ type: reference # Code Owners **(PREMIUM)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6916) in GitLab 11.3. -> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4418) in GitLab Premium 11.9. +> - Code Owners for merge request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4418) in GitLab Premium 11.9. > - Moved to GitLab Premium in 13.9. -Code Owners define who owns specific files or paths in a repository. -You can require that Code Owners approve a merge request before it's merged. +Code Owners define who owns specific files or folders in a repository. -Code Owners help you determine who should review or approve merge requests. -If you have a question about a file or feature, Code Owners -can help you find someone who knows the answer. +- The users you define as Code Owners are displayed in the UI when you browse directories. +- You can set your merge requests so they must be approved by Code Owners before merge. +- You can protect a branch and allow only Code Owners to approve changes to the branch. If you don't want to use Code Owners for approvals, you can [configure rules](merge_requests/approvals/rules.md) instead. ## Set up Code Owners -You can specify users or [shared groups](members/share_project_with_groups.md) -that are responsible for specific files and directories in a repository. +You can use Code Owners to specify users or [shared groups](members/share_project_with_groups.md) +that are responsible for specific files and folders in a repository. To set up Code Owners: @@ -38,22 +37,28 @@ To set up Code Owners: 1. In the file, enter text that follows one of these patterns: ```plaintext - # A member as Code Owner of a file - filename @username + # Code Owners for a file + filename @username1 @username2 - # A member as Code Owner of a directory - directory @username + # Code Owners for a directory + foldername @username1 @username2 - # All group members as Code Owners of a file + # All group members as Code Owners for a file filename @groupname - # All group members as Code Owners of a directory - directory @groupname + # All group members as Code Owners for a folder + foldername @groupname ``` -The Code Owners are displayed in the UI by the files or directory they apply to. -These owners apply to this branch only. When you add new files to the repository, -you should update the `CODEOWNERS` file. +The Code Owners are now displayed in the UI. + +Next steps: + +- [Add Code Owners as merge request approvers](merge_requests/approvals/rules.md#code-owners-as-eligible-approvers). +- Set up [Code Owner approval on a protected branch](protected_branches.md#require-code-owner-approval-on-a-protected-branch). + +NOTE: +The Code Owners apply to the current branch only. ## When a file matches multiple `CODEOWNERS` entries @@ -71,42 +76,6 @@ README.md @user1 The user that would show for `README.md` would be `@user2`. -## Approvals by Code Owners - -After you've added Code Owners to a project, you can configure it to -be used for merge request approvals: - -- As [merge request eligible approvers](merge_requests/approvals/rules.md#code-owners-as-eligible-approvers). -- As required approvers for [protected branches](protected_branches.md#require-code-owner-approval-on-a-protected-branch). **(PREMIUM)** - -Developer or higher [permissions](../permissions.md) are required to -approve a merge request. - -After it's set, Code Owners are displayed in merge request widgets: - -![MR widget - Code Owners](img/code_owners_mr_widget_v12_4.png) - -While you can use the `CODEOWNERS` file in addition to Merge Request -[Approval Rules](merge_requests/approvals/rules.md), -you can also use it as the sole driver of merge request approvals -without using [Approval Rules](merge_requests/approvals/rules.md): - -1. Create the file in one of the three locations specified above. -1. Set the code owners as required approvers for - [protected branches](protected_branches.md#require-code-owner-approval-on-a-protected-branch). -1. Use [the syntax of Code Owners files](code_owners.md) - to specify the actual owners and granular permissions. - -Using Code Owners in conjunction with [protected branches](protected_branches.md#require-code-owner-approval-on-a-protected-branch) -prevents any user who is not specified in the `CODEOWNERS` file from pushing -changes for the specified files/paths, except those included in the -**Allowed to push** column. This allows for a more inclusive push strategy, as -administrators don't have to restrict developers from pushing directly to the -protected branch, but can restrict pushing to certain files where a review by -Code Owners is required. - -[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35097) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5, users and groups who are allowed to push to protected branches do not require a merge request to merge their feature branches. Thus, they can skip merge request approval rules, Code Owners included. - ## Groups as Code Owners > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53182) in GitLab 12.1. @@ -154,7 +123,7 @@ file.md @group-x/subgroup-y file.md @group-x @group-x/subgroup-y ``` -### Code Owners Sections **(PREMIUM)** +### Code Owners sections **(PREMIUM)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12137) in GitLab Premium 13.2 behind a feature flag, enabled by default. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42389) in GitLab 13.4. @@ -213,18 +182,18 @@ this example, entries defined under the sections "Documentation" and "DOCUMENTATION" would be combined into one, using the case of the first instance of the section encountered in the file. -When assigned to a section, each code owner rule displayed in merge requests +When assigned to a section, each Code Owner rule displayed in merge requests widget is sorted under a "section" label. In the screenshot below, we can see the rules for "Groups" and "Documentation" sections: ![MR widget - Sectional Code Owners](img/sectional_code_owners_v13.2.png) -#### Optional Code Owners Sections **(PREMIUM)** +#### Optional Code Owners sections **(PREMIUM)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232995) in GitLab Premium 13.8 behind a feature flag, enabled by default. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53227) in GitLab 13.9. -To make a certain section optional, add a code owners section prepended with the +To make a certain section optional, add a Code Owners section prepended with the caret `^` character. Approvals from owners listed in the section are **not** required. For example: ```plaintext @@ -238,13 +207,13 @@ caret `^` character. Approvals from owners listed in the section are **not** req *.go @root ``` -The optional code owners section displays in merge requests under the **Approval Rules** area: +The optional Code Owners section displays in merge requests under the **Approval Rules** area: -![MR widget - Optional Code Owners Sections](img/optional_code_owners_sections_v13_8.png) +![MR widget - Optional Code Owners sections](img/optional_code_owners_sections_v13_8.png) If a section is duplicated in the file, and one of them is marked as optional and the other isn't, the requirement prevails. -For example, the code owners of the "Documentation" section below is still required to approve merge requests: +For example, the Code Owners of the "Documentation" section below is still required to approve merge requests: ```plaintext [Documentation] @@ -260,9 +229,9 @@ For example, the code owners of the "Documentation" section below is still requi *.txt @root ``` -Optional sections in the code owners file are treated as optional only +Optional sections in the `CODEOWNERS` file are treated as optional only when changes are submitted by using merge requests. If a change is submitted directly -to the protected branch, approval from code owners is still required, even if the +to the protected branch, approval from Code Owners is still required, even if the section is marked as optional. We plan to change this behavior in a [future release](https://gitlab.com/gitlab-org/gitlab/-/issues/297638), and allow direct pushes to the protected branch for sections marked as optional. @@ -270,7 +239,7 @@ and allow direct pushes to the protected branch for sections marked as optional. ## Example `CODEOWNERS` file ```plaintext -# This is an example of a code owners file +# This is an example of a CODEOWNERS file # lines starting with a `#` will be ignored. # app/ @commented-rule @@ -291,7 +260,7 @@ and allow direct pushes to the protected branch for sections marked as optional. # Multiple codeowners can be specified, separated by spaces or tabs # In the following case the CODEOWNERS file from the root of the repo -# has 3 code owners (@multiple @code @owners) +# has 3 Code Owners (@multiple @code @owners) CODEOWNERS @multiple @code @owners # Both usernames or email addresses can be used to match @@ -304,11 +273,11 @@ LICENSE @legal this_does_not_match janedoe@gitlab.com # them as owners for a file README @group @group/with-nested/subgroup -# Ending a path in a `/` will specify the code owners for every file +# Ending a path in a `/` will specify the Code Owners for every file # nested in that directory, on any level /docs/ @all-docs -# Ending a path in `/*` will specify code owners for every file in +# Ending a path in `/*` will specify Code Owners for every file in # that directory, but not nested deeper. This will match # `docs/index.md` but not `docs/projects/index.md` /docs/* @root-docs @@ -321,7 +290,7 @@ lib/ @lib-owner # repository /config/ @config-owner -# If the path contains spaces, these need to be escaped like this: +# If the path contains spaces, escape them like this: path\ with\ spaces/ @space-owner # Code Owners section: diff --git a/doc/user/project/img/code_owners_mr_widget_v12_4.png b/doc/user/project/img/code_owners_mr_widget_v12_4.png deleted file mode 100644 index 7f7b15ee017fa9a556e2db9d55875ca0d029ff00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27875 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 diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 4ff651891b2..2e39be57a0e 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -179,8 +179,7 @@ When enabled, members who are can push to this branch can also force push. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13251) in GitLab Premium 12.4. > - [In](https://gitlab.com/gitlab-org/gitlab/-/issues/35097) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5 and later, users and groups who can push to protected branches do not have to use a merge request to merge their feature branches. This means they can skip merge request approval rules. -You can require at least one approval by a [Code Owner](code_owners.md) to a file changed by the -merge request. +For a protected branch, you can require at least one approval by a [Code Owner](code_owners.md). To protect a new branch and enable Code Owner's approval: @@ -201,6 +200,16 @@ When enabled, all merge requests for these branches require approval by a Code Owner per matched rule before they can be merged. Additionally, direct pushes to the protected branch are denied if a rule is matched. +Any user who is not specified in the `CODEOWNERS` file cannot push +changes for the specified files or paths, unless they are specifically allowed to. +You don't have to restrict developers from pushing directly to the +protected branch. Instead, you can restrict pushing to certain files where a review by +Code Owners is required. + +In [GitLab Premium 13.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/35097), users and groups +who are allowed to push to protected branches do not need a merge request to merge their feature branches. +Thus, they can skip merge request approval rules, Code Owners included. + ## Run pipelines on protected branches The permission to merge or push to protected branches defines diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index ffffa0eecda..2144e619c70 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -14,11 +14,11 @@ module QA end end - attribute :id - attribute :iid - attribute :assignee_ids - attribute :labels - attribute :title + attributes :id, + :iid, + :assignee_ids, + :labels, + :title def initialize @assignee_ids = [] @@ -41,13 +41,21 @@ module QA end def api_get_path - "/projects/#{project.id}/issues/#{id}" + "/projects/#{project.id}/issues/#{iid}" end def api_post_path "/projects/#{project.id}/issues" end + def api_put_path + "/projects/#{project.id}/issues/#{iid}" + end + + def api_comments_path + "#{api_get_path}/notes" + end + def api_post_body { assignee_ids: assignee_ids, @@ -59,20 +67,28 @@ module QA end end - def api_put_path - "/projects/#{project.id}/issues/#{iid}" - end - def set_issue_assignees(assignee_ids:) put_body = { assignee_ids: assignee_ids } response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body unless response.code == HTTP_STATUS_OK - raise ResourceUpdateFailedError, "Could not update issue assignees to #{assignee_ids}. Request returned (#{response.code}): `#{response}`." + raise( + ResourceUpdateFailedError, + "Could not update issue assignees to #{assignee_ids}. Request returned (#{response.code}): `#{response}`." + ) end QA::Runtime::Logger.debug("Successfully updated issue assignees to #{assignee_ids}") end + + # Get issue comments + # + # @return [Array] + def comments(auto_paginate: false) + return parse_body(api_get_from(api_comments_path)) unless auto_paginate + + auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url) + end end end end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 8d9de0ea718..419893f0b11 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -160,9 +160,10 @@ module QA # Get MR comments # # @return [Array] - def comments - response = get(Runtime::API::Request.new(api_client, api_comments_path).url) - parse_body(response) + def comments(auto_paginate: false) + return parse_body(api_get_from(api_comments_path)) unless auto_paginate + + auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url) end private diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 70cec0cf747..12ae644cab8 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -32,7 +32,7 @@ module QA end attribute :path_with_namespace do - "#{sandbox_path}#{group.path}/#{name}" if group + "#{group.full_path}/#{name}" end alias_method :full_path, :path_with_namespace @@ -268,14 +268,16 @@ module QA result[:import_status] end - def commits - response = get(request_url(api_commits_path)) - parse_body(response) + def commits(auto_paginate: false) + return parse_body(api_get_from(api_commits_path)) unless auto_paginate + + auto_paginated_response(request_url(api_commits_path, per_page: '100')) end - def merge_requests - response = get(request_url(api_merge_requests_path)) - parse_body(response) + def merge_requests(auto_paginate: false) + return parse_body(api_get_from(api_merge_requests_path)) unless auto_paginate + + auto_paginated_response(request_url(api_merge_requests_path, per_page: '100')) end def merge_request_with_title(title) @@ -299,9 +301,10 @@ module QA parse_body(response) end - def repository_branches - response = get(request_url(api_repository_branches_path)) - parse_body(response) + def repository_branches(auto_paginate: false) + return parse_body(api_get_from(api_repository_branches_path)) unless auto_paginate + + auto_paginated_response(request_url(api_repository_branches_path, per_page: '100')) end def repository_tags @@ -324,19 +327,22 @@ module QA parse_body(response) end - def issues - response = get(request_url(api_issues_path)) - parse_body(response) + def issues(auto_paginate: false) + return parse_body(api_get_from(api_issues_path)) unless auto_paginate + + auto_paginated_response(request_url(api_issues_path, per_page: '100')) end - def labels - response = get(request_url(api_labels_path)) - parse_body(response) + def labels(auto_paginate: false) + return parse_body(api_get_from(api_labels_path)) unless auto_paginate + + auto_paginated_response(request_url(api_labels_path, per_page: '100')) end - def milestones - response = get(request_url(api_milestones_path)) - parse_body(response) + def milestones(auto_paginate: false) + return parse_body(api_get_from(api_milestones_path)) unless auto_paginate + + auto_paginated_response(request_url(api_milestones_path, per_page: '100')) end def wikis diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb new file mode 100644 index 00000000000..22917d0d146 --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +require 'octokit' +require 'parallel' + +# rubocop:disable Rails/Pluck +module QA + # Only executes in custom job/pipeline + RSpec.describe 'Manage', :github, :requires_admin, only: { job: 'large-github-import' } do + describe 'Project import' do + let(:api_client) { Runtime::API::Client.as_admin } + let(:group) do + Resource::Group.fabricate_via_api! do |resource| + resource.api_client = api_client + end + end + + let(:user) do + Resource::User.fabricate_via_api! do |resource| + resource.api_client = api_client + resource.hard_delete_on_api_removal = true + end + end + + let(:differ) { RSpec::Support::Differ.new(color: true) } + let(:github_repo) { 'allure-framework/allure-ruby' } + + let(:github_client) do + Octokit.middleware = Faraday::RackBuilder.new do |builder| + builder.response(:logger, Runtime::Logger.logger, headers: false, bodies: false) + end + + Octokit::Client.new(access_token: Runtime::Env.github_access_token, auto_paginate: true) + end + + let(:gh_branches) { github_client.branches(github_repo).map(&:name) } + let(:gh_commits) { github_client.commits(github_repo).map(&:sha) } + let(:gh_repo) { github_client.repository(github_repo) } + let(:gh_labels) { github_client.labels(github_repo) } + let(:gh_milestones) { github_client.list_milestones(github_repo, state: 'all') } + + let(:gh_all_issues) do + github_client.list_issues(github_repo, state: 'all') + end + + let(:gh_prs) do + gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash| + hash[pr.title] = { + body: pr.body || '', + comments: [*gh_pr_comments[pr.html_url], *gh_issue_comments[pr.html_url]].compact.sort + } + end + end + + let(:gh_issues) do + gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash| + hash[issue.title] = { + body: issue.body || '', + comments: gh_issue_comments[issue.html_url] + } + end + end + + let(:gh_issue_comments) do + github_client.issues_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| + hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key + end + end + + let(:gh_pr_comments) do + github_client.pull_requests_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| + hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key + end + end + + let(:imported_project) do + Resource::ProjectImportedFromGithub.fabricate_via_api! do |project| + project.add_name_uuid = false + project.name = 'imported-project' + project.group = group + project.github_personal_access_token = Runtime::Env.github_access_token + project.github_repository_path = github_repo + project.api_client = api_client + end + end + + before do + group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) + end + + it 'imports large Github repo via api' do + imported_project # import the project + fetch_github_objects # fetch all objects right after import has started + + expect { imported_project.reload!.import_status }.to eventually_eq('finished').within( + duration: 3600, + interval: 30 + ) + + aggregate_failures do + verify_repository_import + verify_merge_requests_import + verify_issues_import + verify_labels_import + verify_milestones_import + end + end + + # Persist all objects from repository being imported + # + # @return [void] + def fetch_github_objects + Runtime::Logger.debug("Fetching objects for github repo: '#{github_repo}'") + + gh_repo + gh_branches + gh_commits + gh_prs + gh_issues + gh_labels + gh_milestones + end + + # Verify repository imported correctly + # + # @return [void] + def verify_repository_import + branches = imported_project.repository_branches(auto_paginate: true).map { |b| b[:name] } + commits = imported_project.commits(auto_paginate: true).map { |c| c[:id] } + + expect(imported_project.description).to eq(gh_repo.description) + # check via include, importer creates more branches + # https://gitlab.com/gitlab-org/gitlab/-/issues/332711 + expect(branches).to include(*gh_branches) + expect(commits).to match_array(gh_commits) + end + + # Verify imported merge requests and mr issues + # + # @return [void] + def verify_merge_requests_import + verify_mrs_or_issues('mrs') + end + + # Verify imported issues and issue comments + # + # @return [void] + def verify_issues_import + verify_mrs_or_issues('issues') + end + + # Verify imported labels + # + # @return [void] + def verify_labels_import + labels = imported_project.labels(auto_paginate: true).map { |label| label.slice(:name, :color) } + actual_labels = gh_labels.map { |label| { name: label.name, color: "##{label.color}" } } + + expect(labels.length).to eq(actual_labels.length) + expect(labels).to match_array(actual_labels) + end + + # Verify milestones import + # + # @return [void] + def verify_milestones_import + milestones = imported_project.milestones(auto_paginate: true).map { |ms| ms.slice(:title, :description) } + actual_milestones = gh_milestones.map { |ms| { title: ms.title, description: ms.description } } + + expect(milestones.length).to eq(actual_milestones.length) + expect(milestones).to match_array(actual_milestones) + end + + private + + # Verify imported mrs or issues + # + # @param [String] type verification object, 'mrs' or 'issues' + # @return [void] + def verify_mrs_or_issues(type) + msg = ->(title) { "expected #{type} with title '#{title}' to have" } + expected = type == 'mrs' ? mrs : gl_issues + actual = type == 'mrs' ? gh_prs : gh_issues + + expect(expected.keys).to match_array(actual.keys) + actual.each do |title, actual_item| + expected_item = expected[title] + + expect(expected_item).to be_truthy, "#{msg.call(title)} been imported" + next unless expected_item + + expect(expected_item[:body]).to( + include(actual_item[:body]), + "#{msg.call(title)} same description. #{diff(expected_item[:body], actual_item[:body])}" + ) + expect(expected_item[:comments].length).to( + eq(actual_item[:comments].length), + "#{msg.call(title)} same amount of comments" + ) + expect(expected_item[:comments]).to match_array(actual_item[:comments]) + end + end + + # Imported project merge requests + # + # @return [Hash] + def mrs + @mrs ||= begin + imported_mrs = imported_project.merge_requests(auto_paginate: true) + # fetch comments in parallel since we need to do it for each mr separately + mrs_hashes = Parallel.map(imported_mrs, in_processes: 5) do |mr| + resource = Resource::MergeRequest.init do |resource| + resource.project = imported_project + resource.iid = mr[:iid] + resource.api_client = api_client + end + + { + title: mr[:title], + body: mr[:description], + comments: resource.comments(auto_paginate: true) + # remove system notes + .reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) } + .map { |c| sanitize(c[:body]) } + } + end + + mrs_hashes.each_with_object({}) do |mr, hash| + hash[mr[:title]] = { + body: mr[:body], + comments: mr[:comments] + } + end + end + end + + # Imported project issues + # + # @return [Hash] + def gl_issues + @gl_issues ||= begin + imported_issues = imported_project.issues(auto_paginate: true) + # fetch comments in parallel since we need to do it for each mr separately + issue_hashes = Parallel.map(imported_issues, in_processes: 5) do |issue| + resource = Resource::Issue.init do |issue_resource| + issue_resource.project = imported_project + issue_resource.iid = issue[:iid] + issue_resource.api_client = api_client + end + + { + title: issue[:title], + body: issue[:description], + comments: resource.comments(auto_paginate: true).map { |c| sanitize(c[:body]) } + } + end + + issue_hashes.each_with_object({}) do |issue, hash| + hash[issue[:title]] = { + body: issue[:body], + comments: issue[:comments] + } + end + end + end + + # Remove added prefixes by importer + # + # @param [String] body + # @return [String] + def sanitize(body) + body.gsub(/\*Created by: \S+\*\n\n/, "") + end + + # Diff of 2 objects + # + # @param [Object] actual + # @param [Object] expected + # @return [String] + def diff(actual, expected) + "diff:\n#{differ.diff(actual, expected)}" + end + end + end +end +# rubocop:enable Rails/Pluck diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index de9da3171b0..1493feeeed7 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -79,11 +79,20 @@ module QA error.response end + def auto_paginated_response(url) + pages = [] + with_paginated_response_body(url) { |response| pages << response } + + pages.flatten + end + def with_paginated_response_body(url) loop do response = get(url) + page, pages = response.headers.values_at(:x_page, :x_total_pages) + api_endpoint = url.match(%r{v4/(\S+)\?})[1] - QA::Runtime::Logger.debug("Fetching page #{response.headers[:x_page]} of #{response.headers[:x_total_pages]}...") + QA::Runtime::Logger.debug("Fetching page (#{page}/#{pages}) for '#{api_endpoint}' ...") unless pages.to_i <= 1 yield parse_body(response) @@ -96,7 +105,7 @@ module QA def pagination_links(response) response.headers[:link].split(',').map do |link| - match = link.match(/\<(?.*)\>\; rel=\"(?\w+)\"/) + match = link.match(/<(?.*)>; rel="(?\w+)"/) break nil unless match { url: match[:url], rel: match[:rel] } diff --git a/qa/spec/support/matchers/eventually_matcher.rb b/qa/spec/support/matchers/eventually_matcher.rb index 3f0afd6fb54..2de6598be4c 100644 --- a/qa/spec/support/matchers/eventually_matcher.rb +++ b/qa/spec/support/matchers/eventually_matcher.rb @@ -24,6 +24,7 @@ module Matchers chain(:within) do |options = {}| @duration = options[:duration] @attempts = options[:attempts] + @interval = options[:interval] end def supports_block_expectations? @@ -55,7 +56,7 @@ module Matchers QA::Support::Retrier.retry_until( max_attempts: @attempts, max_duration: @duration, - sleep_interval: 0.5 + sleep_interval: @interval || 0.5 ) do public_send(expectation_name, actual) rescue RSpec::Expectations::ExpectationNotMetError, QA::Resource::ApiFabricator::ResourceNotFoundError diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 677052eebea..9e4cec200d4 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -13,6 +13,22 @@ RSpec.describe 'File blob', :js do wait_for_requests end + def create_file(file_name, content) + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add #{file_name}", + file_path: file_name, + file_content: <<-SPEC.strip_heredoc + #{content} + SPEC + ).execute + end + context 'Ruby file' do before do visit_blob('files/ruby/popen.rb') @@ -785,6 +801,255 @@ RSpec.describe 'File blob', :js do end end end + + context 'CONTRIBUTING.md' do + before do + file_name = 'CONTRIBUTING.md' + + create_file(file_name, '## Contribution guidelines') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("After you've reviewed these contribution guidelines, you'll be all set to contribute to this project.") + end + end + end + + context 'CHANGELOG.md' do + before do + file_name = 'CHANGELOG.md' + + create_file(file_name, '## Changelog for v1.0.0') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("To find the state of this project's repository at the time of any of these versions, check out the tags.") + end + end + end + + context 'Cargo.toml' do + before do + file_name = 'Cargo.toml' + + create_file(file_name, ' + [package] + name = "hello_world" # the name of the package + version = "0.1.0" # the current version, obeying semver + authors = ["Alice ", "Bob "] + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Cargo.") + end + end + end + + context 'Cartfile' do + before do + file_name = 'Cartfile' + + create_file(file_name, ' + gitlab "Alamofire/Alamofire" == 4.9.0 + gitlab "Alamofire/AlamofireImage" ~> 3.4 + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Carthage.") + end + end + end + + context 'composer.json' do + before do + file_name = 'composer.json' + + create_file(file_name, ' + { + "license": "MIT" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Composer.") + end + end + end + + context 'Gemfile' do + before do + file_name = 'Gemfile' + + create_file(file_name, ' + source "https://rubygems.org" + + # Gems here + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Bundler.") + end + end + end + + context 'Godeps.json' do + before do + file_name = 'Godeps.json' + + create_file(file_name, ' + { + "GoVersion": "go1.6" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using godep.") + end + end + end + + context 'go.mod' do + before do + file_name = 'go.mod' + + create_file(file_name, ' + module example.com/mymodule + + go 1.14 + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Go Modules.") + end + end + end + + context 'package.json' do + before do + file_name = 'package.json' + + create_file(file_name, ' + { + "name": "my-awesome-package", + "version": "1.0.0" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using npm.") + end + end + end + + context 'podfile' do + before do + file_name = 'podfile' + + create_file(file_name, 'platform :ios, "8.0"') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using CocoaPods.") + end + end + end + + context 'test.podspec' do + before do + file_name = 'test.podspec' + + create_file(file_name, ' + Pod::Spec.new do |s| + s.name = "TensorFlowLiteC" + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using CocoaPods.") + end + end + end + + context 'JSON.podspec.json' do + before do + file_name = 'JSON.podspec.json' + + create_file(file_name, ' + { + "name": "JSON" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using CocoaPods.") + end + end + end + + context 'requirements.txt' do + before do + file_name = 'requirements.txt' + + create_file(file_name, 'Project requirements') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using pip.") + end + end + end + + context 'yarn.lock' do + before do + file_name = 'yarn.lock' + + create_file(file_name, ' + # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + # yarn lockfile v1 + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Yarn.") + end + end + end end context 'realtime pipelines' do