Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md14
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile.lock7
-rw-r--r--Gemfile.rails4.lock7
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue15
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue5
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/controllers/concerns/issuable_collections.rb6
-rw-r--r--app/helpers/sorting_helper.rb47
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/pipeline.rb19
-rw-r--r--app/models/concerns/awardable.rb13
-rw-r--r--app/models/concerns/issuable.rb28
-rw-r--r--app/models/member.rb19
-rw-r--r--app/models/merge_request.rb36
-rw-r--r--app/models/project.rb2
-rw-r--r--app/presenters/member_presenter.rb8
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml4
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml12
-rw-r--r--app/views/projects/mirrors/_mirror_repos_form.html.haml2
-rw-r--r--app/views/search/results/_blob.html.haml6
-rw-r--r--app/views/search/results/_wiki_blob.html.haml4
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml16
-rw-r--r--app/views/shared/issuable/_filter.html.haml32
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml20
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--changelogs/unreleased/39849_controller_sorts.yml5
-rw-r--r--changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml5
-rw-r--r--changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml5
-rw-r--r--changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml5
-rw-r--r--changelogs/unreleased/expose-mr-pipeline-variables.yml5
-rw-r--r--changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml5
-rw-r--r--changelogs/unreleased/mg-fix-knative-application-row.yml5
-rw-r--r--changelogs/unreleased/remove-blob-search-limit.yml5
-rw-r--r--changelogs/unreleased/sh-handle-invalid-gpg-sig.yml5
-rw-r--r--changelogs/unreleased/usage-count.yml5
-rw-r--r--changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml5
-rw-r--r--config/gitlab.yml.example16
-rw-r--r--db/fixtures/development/24_forks.rb16
-rw-r--r--db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb4
-rw-r--r--db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb33
-rw-r--r--db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb33
-rw-r--r--db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb11
-rw-r--r--db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb17
-rw-r--r--db/schema.rb7
-rw-r--r--doc/administration/auth/README.md2
-rw-r--r--doc/administration/auth/jwt.md36
-rw-r--r--doc/api/search.md20
-rw-r--r--doc/development/profiling.md7
-rw-r--r--doc/development/testing_guide/ci.md4
-rw-r--r--lib/api/search.rb7
-rw-r--r--lib/gitlab/database/count.rb2
-rw-r--r--lib/gitlab/database/count/exact_count_strategy.rb2
-rw-r--r--lib/gitlab/file_finder.rb57
-rw-r--r--lib/gitlab/gpg/commit.rb24
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/project_search_results.rb43
-rw-r--r--lib/gitlab/search/found_blob.rb162
-rw-r--r--lib/gitlab/search/query.rb6
-rw-r--r--lib/gitlab/search_results.rb36
-rw-r--r--lib/gitlab/usage_data.rb18
-rw-r--r--lib/gitlab/wiki_file_finder.rb6
-rw-r--r--lib/omni_auth/strategies/jwt.rb17
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/page/base.rb20
-rw-r--r--qa/qa/page/project/settings/mirroring_repositories.rb91
-rw-r--r--qa/qa/page/project/settings/repository.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb45
-rw-r--r--qa/qa/support/page/logging.rb20
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb179
-rw-r--r--spec/features/issuables/sorting_list_spec.rb226
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb8
-rw-r--r--spec/features/merge_requests/user_sorts_merge_requests_spec.rb12
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb4
-rw-r--r--spec/finders/group_members_finder_spec.rb2
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json32
-rw-r--r--spec/fixtures/security-reports/master/gl-dependency-scanning-report.json32
-rw-r--r--spec/helpers/sorting_helper_spec.rb43
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js63
-rw-r--r--spec/lib/gitlab/database/count/exact_count_strategy_spec.rb6
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb22
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb131
-rw-r--r--spec/lib/gitlab/search/found_blob_spec.rb138
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb25
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb70
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb44
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb86
-rw-r--r--spec/models/group_spec.rb4
-rw-r--r--spec/models/member_spec.rb23
-rw-r--r--spec/models/members/group_member_spec.rb22
-rw-r--r--spec/models/members/project_member_spec.rb15
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/models/user_spec.rb10
-rw-r--r--spec/presenters/group_member_presenter_spec.rb8
-rw-r--r--spec/presenters/project_member_presenter_spec.rb6
-rw-r--r--spec/requests/api/members_spec.rb31
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb6
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb4
-rw-r--r--spec/support/shared_examples/file_finder.rb13
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb77
106 files changed, 1891 insertions, 638 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d41e5c8642f..d1e324c5518 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.5.3 (2018-12-06)
+
+### Security (1 change)
+
+- Prevent a path traversal attack on global file templates.
+
+
## 11.5.2 (2018-12-03)
### Removed (1 change)
@@ -621,6 +628,13 @@ entry.
- Check frozen string in style builds. (gfyoung)
+## 11.3.12 (2018-12-06)
+
+### Security (1 change)
+
+- Prevent a path traversal attack on global file templates.
+
+
## 11.3.11 (2018-11-26)
### Security (33 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 26aaba0e866..bd8bf882d06 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.2.0
+1.7.0
diff --git a/Gemfile.lock b/Gemfile.lock
index 699d77615aa..f51eaef9357 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -82,6 +82,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
+ binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
@@ -724,8 +725,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
- rspec-parameterized (0.4.0)
- binding_of_caller
+ rspec-parameterized (0.4.1)
+ binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
@@ -895,7 +896,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
- unparser (0.2.7)
+ unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock
index 15e0b782d5b..461550f7ffb 100644
--- a/Gemfile.rails4.lock
+++ b/Gemfile.rails4.lock
@@ -79,6 +79,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
+ binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
@@ -715,8 +716,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
- rspec-parameterized (0.4.0)
- binding_of_caller
+ rspec-parameterized (0.4.1)
+ binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
@@ -889,7 +890,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
- unparser (0.2.7)
+ unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 31651658fe6..d899b7fbd8c 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -92,20 +92,7 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
- <div class="dropdown-title">
- <span>Projects</span>
- <button
- aria-label="Close"
- type="button"
- class="dropdown-title-button dropdown-menu-close"
- >
- <icon
- name="merge-request-close-m"
- data-hidden="true"
- class="dropdown-menu-close-icon"
- />
- </button>
- </div>
+ <div class="dropdown-title">Projects</div>
<div class="dropdown-input">
<input class="dropdown-input-field" type="search" placeholder="Search projects" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 9a96d0fa6d7..665a9c77822 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -296,7 +296,6 @@ export default {
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
<div slot="description" v-html="certManagerDescription"></div>
@@ -396,6 +395,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="knative"
:logo-url="knativeLogo"
:title="applications.knative.title"
@@ -405,7 +405,6 @@ export default {
:request-reason="applications.knative.requestReason"
:install-application-request-params="{ hostname: applications.knative.hostname }"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://github.com/knative/docs"
>
<div slot="description">
@@ -432,7 +431,7 @@ export default {
/>
</div>
</template>
- <template v-else>
+ <template v-else-if="helmInstalled">
<div class="form-group">
<label for="knative-domainname">
{{ s__('ClusterIntegration|Knative Domain Name:') }}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8ea34f5d19d..bb6b6f84849 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -259,6 +259,16 @@ ul.related-merge-requests > li {
display: block;
}
+.issue-sort-dropdown {
+ .btn-group {
+ width: 100%;
+ }
+
+ .reverse-sort-btn {
+ color: $gl-text-color-secondary;
+ }
+}
+
@include media-breakpoint-up(sm) {
.emoji-block .row {
display: flex;
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 0837599977f..9aa98e2ca1f 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -167,12 +167,6 @@ module IssuableCollections
case value
when 'id_asc' then sort_value_oldest_created
when 'id_desc' then sort_value_recently_created
- when 'created_asc' then sort_value_created_date
- when 'created_desc' then sort_value_created_date
- when 'due_date_asc' then sort_value_due_date
- when 'due_date_desc' then sort_value_due_date
- when 'milestone_due_asc' then sort_value_milestone
- when 'milestone_due_desc' then sort_value_milestone
when 'downvotes_asc' then sort_value_popularity
when 'downvotes_desc' then sort_value_popularity
else value
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 74113aee89d..f51b96ba8ce 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -136,6 +136,53 @@ module SortingHelper
link_to item, path, class: sorted_by == item ? 'is-active' : ''
end
+ def issuable_sort_option_overrides
+ {
+ sort_value_oldest_created => sort_value_created_date,
+ sort_value_oldest_updated => sort_value_recently_updated,
+ sort_value_milestone_later => sort_value_milestone
+ }
+ end
+
+ def issuable_reverse_sort_order_hash
+ {
+ sort_value_created_date => sort_value_oldest_created,
+ sort_value_recently_created => sort_value_oldest_created,
+ sort_value_recently_updated => sort_value_oldest_updated,
+ sort_value_milestone => sort_value_milestone_later
+ }.merge(issuable_sort_option_overrides)
+ end
+
+ def issuable_sort_option_title(sort_value)
+ sort_value = issuable_sort_option_overrides[sort_value] || sort_value
+
+ sort_options_hash[sort_value]
+ end
+
+ def issuable_sort_direction_button(sort_value)
+ link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
+ reverse_sort = issuable_reverse_sort_order_hash[sort_value]
+
+ if reverse_sort
+ reverse_url = page_filter_path(sort: reverse_sort)
+ else
+ reverse_url = '#'
+ link_class += ' disabled'
+ end
+
+ link_to(reverse_url, type: 'button', class: link_class, title: 'Sort direction') do
+ icon_suffix =
+ case sort_value
+ when sort_value_milestone, sort_value_due_date, /_asc\z/
+ 'lowest'
+ else
+ 'highest'
+ end
+
+ sprite_icon("sort-#{icon_suffix}", size: 16)
+ end
+ end
+
# Titles.
def sort_title_access_level_asc
s_('SortOptions|Access level, ascending')
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d60861dc95f..d86a6eceb59 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -120,7 +120,7 @@ module Ci
acts_as_taggable
- add_authentication_token_field :token
+ add_authentication_token_field :token, encrypted: true, fallback: true
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 60ff2181a95..d06022a0fb7 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -605,13 +605,18 @@ module Ci
end
def predefined_variables
- Gitlab::Ci::Variables::Collection.new
- .append(key: 'CI_PIPELINE_IID', value: iid.to_s)
- .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
- .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
- .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
- .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
- .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
+ variables.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
+ variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
+ variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
+ variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
+ variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
+
+ if merge_request? && merge_request
+ variables.concat(merge_request.predefined_variables)
+ end
+ end
end
def queued_duration
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 60b7ec2815c..14bc56f0eee 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -43,14 +43,19 @@ module Awardable
end
def order_upvotes_desc
- order_votes_desc(AwardEmoji::UPVOTE_NAME)
+ order_votes(AwardEmoji::UPVOTE_NAME, 'DESC')
+ end
+
+ def order_upvotes_asc
+ order_votes(AwardEmoji::UPVOTE_NAME, 'ASC')
end
def order_downvotes_desc
- order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
+ order_votes(AwardEmoji::DOWNVOTE_NAME, 'DESC')
end
- def order_votes_desc(emoji_name)
+ # Order votes by emoji, optional sort order param `descending` defaults to true
+ def order_votes(emoji_name, direction)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
@@ -62,7 +67,7 @@ module Awardable
)
).join_sources
- joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
+ joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) #{direction}")
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5080fe03cc8..0d363ec68b7 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -145,14 +145,16 @@ module Issuable
def sort_by_attribute(method, excluded_labels: [])
sorted =
case method.to_s
- when 'downvotes_desc' then order_downvotes_desc
- when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
- when 'milestone' then order_milestone_due_asc
- when 'milestone_due_asc' then order_milestone_due_asc
- when 'milestone_due_desc' then order_milestone_due_desc
- when 'popularity' then order_upvotes_desc
- when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
- when 'upvotes_desc' then order_upvotes_desc
+ when 'downvotes_desc' then order_downvotes_desc
+ when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'label_priority_desc' then order_labels_priority('DESC', excluded_labels: excluded_labels)
+ when 'milestone', 'milestone_due_asc' then order_milestone_due_asc
+ when 'milestone_due_desc' then order_milestone_due_desc
+ when 'popularity', 'popularity_desc' then order_upvotes_desc
+ when 'popularity_asc' then order_upvotes_asc
+ when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
+ when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels)
+ when 'upvotes_desc' then order_upvotes_desc
else order_by(method)
end
@@ -160,7 +162,7 @@ module Issuable
sorted.with_order_id_desc
end
- def order_due_date_and_labels_priority(excluded_labels: [])
+ def order_due_date_and_labels_priority(direction = 'ASC', excluded_labels: [])
# The order_ methods also modify the query in other ways:
#
# - For milestones, we add a JOIN.
@@ -177,11 +179,11 @@ module Issuable
order_milestone_due_asc
.order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date])
- .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'),
- Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+ .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, direction),
+ Gitlab::Database.nulls_last_order('highest_priority', direction))
end
- def order_labels_priority(excluded_labels: [], extra_select_columns: [])
+ def order_labels_priority(direction = 'ASC', excluded_labels: [], extra_select_columns: [])
params = {
target_type: name,
target_column: "#{table_name}.id",
@@ -198,7 +200,7 @@ module Issuable
select(select_columns.join(', '))
.group(arel_table[:id])
- .reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+ .reorder(Gitlab::Database.nulls_last_order('highest_priority', direction))
end
def with_label(title, sort = nil)
diff --git a/app/models/member.rb b/app/models/member.rb
index bc8ac14d148..9fc95ea00c3 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -7,6 +7,7 @@ class Member < ActiveRecord::Base
include Expirable
include Gitlab::Access
include Presentable
+ include Gitlab::Utils::StrongMemoize
attr_accessor :raw_invite_token
@@ -22,6 +23,7 @@ class Member < ActiveRecord::Base
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validate :higher_access_level_than_group, unless: :importing?
validates :invite_email,
presence: {
if: :invite?
@@ -364,6 +366,15 @@ class Member < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
+ # Find the user's group member with a highest access level
+ def highest_group_member
+ strong_memoize(:highest_group_member) do
+ next unless user_id && source&.ancestors&.any?
+
+ GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
+ end
+ end
+
private
def send_invite
@@ -430,4 +441,12 @@ class Member < ActiveRecord::Base
def notifiable_options
{}
end
+
+ def higher_access_level_than_group
+ if highest_group_member && highest_group_member.access_level >= access_level
+ error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
+
+ errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
+ end
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f40dff7c1bd..d0811a715bc 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1070,6 +1070,42 @@ class MergeRequest < ActiveRecord::Base
actual_head_pipeline&.has_test_reports?
end
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_REF_PATH',
+ value: ref_path.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID',
+ value: project.id.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH',
+ value: project.full_path)
+
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL',
+ value: project.web_url)
+
+ variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME',
+ value: target_branch.to_s)
+
+ if source_project
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID',
+ value: source_project.id.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH',
+ value: source_project.full_path)
+
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL',
+ value: source_project.web_url)
+
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME',
+ value: source_branch.to_s)
+ end
+ end
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def compare_test_reports
unless has_test_reports?
diff --git a/app/models/project.rb b/app/models/project.rb
index 587bada469e..1adcb73806d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -570,6 +570,8 @@ class Project < ActiveRecord::Base
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
+ alias_method :ancestors, :ancestors_upto
+
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 2497bea4aff..9e9b6973b8e 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -7,6 +7,14 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
member.class.access_level_roles
end
+ def valid_level_roles
+ return access_level_roles unless member.highest_group_member
+
+ access_level_roles.reject do |_name, level|
+ member.highest_group_member.access_level > level
+ end
+ end
+
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index 3effdf934fb..293a2e3ebfe 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -8,14 +8,14 @@
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
= f.select :auth_method,
options_for_select(auth_options, mirror.auth_method),
- {}, { class: "form-control js-mirror-auth-type" }
+ {}, { class: "form-control js-mirror-auth-type qa-authentication-method" }
.form-group
.collapse.js-well-changing-auth
.changing-auth-method= icon('spinner spin lg')
.well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold"
- = f.password_field :password, value: mirror.password, class: 'form-control', autocomplete: 'new-password'
+ = f.password_field :password, value: mirror.password, class: 'form-control qa-password', autocomplete: 'new-password'
- unless is_push
.well-ssh-auth.collapse.js-well-ssh-auth
%p.js-ssh-public-key-present{ class: ('collapse' unless ssh_public_key_present) }
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index dde0fae740b..21b105e6f80 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -1,7 +1,7 @@
- expanded = Rails.env.test?
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
-%section.settings.project-mirror-settings.js-mirror-settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) }
+%section.settings.project-mirror-settings.js-mirror-settings.no-animate.qa-mirroring-repositories-settings#js-push-remote-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Mirroring repositories')
%button.btn.js-settings-toggle
@@ -20,7 +20,7 @@
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
+ = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
= render 'projects/mirrors/instructions'
@@ -32,7 +32,7 @@
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
.panel-footer
- = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit', name: :update_remote_mirror
+ = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
.panel.panel-default
.table-responsive
@@ -50,10 +50,10 @@
= render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index|
- if mirror.enabled
- %tr
- %td= mirror.safe_url
+ %tr.qa-mirrored-repository-row
+ %td.qa-mirror-repository-url= mirror.safe_url
%td= _('Push')
- %td= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
+ %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
%td
- if mirror.last_error.present?
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml
index a2cce83bfab..b49f1d9315e 100644
--- a/app/views/projects/mirrors/_mirror_repos_form.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml
@@ -1,5 +1,5 @@
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
- = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction', disabled: true
+ = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction qa-mirror-direction', disabled: true
= render partial: "projects/mirrors/mirror_repos_push", locals: { f: f }
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index a8d4d4af93a..2a602095845 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,7 +1,7 @@
- project = find_project_for_result_blob(blob)
- return unless project
-- file_name, blob = parse_search_result(blob)
-- blob_link = project_blob_path(project, tree_join(blob.ref, file_name))
+- blob = parse_search_result(blob)
+- blob_link = project_blob_path(project, tree_join(blob.ref, blob.filename))
-= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: file_name, blob_link: blob_link }
+= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: blob.filename, blob_link: blob_link }
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 4346217c230..389e4cc75b9 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,5 +1,5 @@
- project = find_project_for_result_blob(wiki_blob)
-- file_name, wiki_blob = parse_search_result(wiki_blob)
+- wiki_blob = parse_search_result(wiki_blob)
- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
-= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: file_name, blob_link: wiki_blob_link }
+= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link }
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index f32cff18fa8..721a2af8069 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -2,5 +2,5 @@
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') }
= icon("refresh spin")
- else
- = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
+ = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= icon("refresh")
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
deleted file mode 100644
index e4463c1e0d8..00000000000
--- a/app/views/shared/_sort_dropdown.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-- sorted_by = sort_options_hash[@sort]
-- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
-
-.dropdown.inline.prepend-left-10
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
- = sorted_by
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
- %li
- = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by)
- = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
- = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by)
- = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sorted_by)
- = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sorted_by) if viewing_issues
- = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sorted_by)
- = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sorted_by)
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
new file mode 100644
index 00000000000..2ca4657851c
--- /dev/null
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -0,0 +1,32 @@
+.issues-filters
+ .issues-details-filters.row-content-block.second-block
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
+ .issues-other-filters
+ .filter-item.inline
+ - if params[:author_id].present?
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+
+ .filter-item.inline
+ - if params[:assignee_id].present?
+ = hidden_field_tag(:assignee_id, params[:assignee_id])
+ = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+
+ .filter-item.inline.milestone-filter
+ = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
+
+ .filter-item.inline.labels-filter
+ = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
+
+ - unless @no_filters_set
+ .float-right
+ = render 'shared/issuable/sort_dropdown'
+
+ - has_labels = @labels && @labels.any?
+ .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
+ - if has_labels
+ = render 'shared/labels_row', labels: @labels
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 7c5af0b9775..46634693067 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -2,7 +2,6 @@
- board = local_assigns.fetch(:board, nil)
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
-- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
.issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
@@ -142,5 +141,5 @@
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
#js-toggle-focus-btn
- - elsif show_sorting_dropdown
- = render 'shared/sort_dropdown'
+ - elsif type != :boards_modal
+ = render 'shared/issuable/sort_dropdown'
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..c211b9fcaa2
--- /dev/null
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -0,0 +1,20 @@
+- sort_value = @sort
+- sort_title = issuable_sort_option_title(sort_value)
+- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
+
+.dropdown.inline.prepend-left-10.issue-sort-dropdown
+ .btn-group{ role: 'group' }
+ .btn-group{ role: 'group' }
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
+ = sort_title
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
+ %li
+ = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sort_title)
+ = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sort_title)
+ = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sort_title)
+ = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sort_title)
+ = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sort_title) if viewing_issues
+ = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sort_title)
+ = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sort_title)
+ = issuable_sort_direction_button(sort_value)
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index a7fd75d85d7..6b3841ebbc4 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -75,7 +75,7 @@
= dropdown_title(_("Change permissions"))
.dropdown-content
%ul
- - member.access_level_roles.each do |role, role_id|
+ - member.valid_level_roles.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
diff --git a/changelogs/unreleased/39849_controller_sorts.yml b/changelogs/unreleased/39849_controller_sorts.yml
new file mode 100644
index 00000000000..5fad0cb4ede
--- /dev/null
+++ b/changelogs/unreleased/39849_controller_sorts.yml
@@ -0,0 +1,5 @@
+---
+title: Allow sorting issues and MRs in reverse order
+merge_request: 21438
+author:
+type: changed
diff --git a/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml b/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml
new file mode 100644
index 00000000000..96f33a72cc5
--- /dev/null
+++ b/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml
@@ -0,0 +1,5 @@
+---
+title: Restrict member access level to be higher than that of any parent group
+merge_request: 23226
+author:
+type: fixed
diff --git a/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml b/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml
new file mode 100644
index 00000000000..3ef564238c5
--- /dev/null
+++ b/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml
@@ -0,0 +1,5 @@
+---
+title: Support RSA and ECDSA algorithms in Omniauth JWT provider
+merge_request: 23411
+author: Michael Tsyganov
+type: fixed
diff --git a/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml b/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml
new file mode 100644
index 00000000000..4673ba38bae
--- /dev/null
+++ b/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml
@@ -0,0 +1,5 @@
+---
+title: Add partial index for ci_builds on project_id and status
+merge_request: 23268
+author:
+type: performance
diff --git a/changelogs/unreleased/expose-mr-pipeline-variables.yml b/changelogs/unreleased/expose-mr-pipeline-variables.yml
new file mode 100644
index 00000000000..b77b9a69d5c
--- /dev/null
+++ b/changelogs/unreleased/expose-mr-pipeline-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Expose merge request pipeline variables
+merge_request: 23398
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml
new file mode 100644
index 00000000000..04fc88bc3d3
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml
@@ -0,0 +1,5 @@
+---
+title: Encrypt CI/CD builds authentication tokens
+merge_request: 23436
+author:
+type: security
diff --git a/changelogs/unreleased/mg-fix-knative-application-row.yml b/changelogs/unreleased/mg-fix-knative-application-row.yml
new file mode 100644
index 00000000000..95142d380a4
--- /dev/null
+++ b/changelogs/unreleased/mg-fix-knative-application-row.yml
@@ -0,0 +1,5 @@
+---
+title: Hide Knative from group cluster applications until supported
+merge_request: 23577
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove-blob-search-limit.yml b/changelogs/unreleased/remove-blob-search-limit.yml
new file mode 100644
index 00000000000..5bad3a83dbb
--- /dev/null
+++ b/changelogs/unreleased/remove-blob-search-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Remove limit of 100 when searching repository code.
+merge_request: 8671
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml b/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml
new file mode 100644
index 00000000000..185e2547e16
--- /dev/null
+++ b/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml
@@ -0,0 +1,5 @@
+---
+title: Gracefully handle unknown/invalid GPG keys
+merge_request: 23492
+author:
+type: fixed
diff --git a/changelogs/unreleased/usage-count.yml b/changelogs/unreleased/usage-count.yml
new file mode 100644
index 00000000000..efff2615ce4
--- /dev/null
+++ b/changelogs/unreleased/usage-count.yml
@@ -0,0 +1,5 @@
+---
+title: Use approximate count for big tables for usage statistics.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml b/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml
new file mode 100644
index 00000000000..18f7da56edb
--- /dev/null
+++ b/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml
@@ -0,0 +1,5 @@
+---
+title: Remove close icon from projects dropdown in issue boards
+merge_request: 23567
+author:
+type: changed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 58b7c248aaf..1c16b999e55 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -548,15 +548,15 @@ production: &base
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
# - { name: 'jwt',
- # app_secret: 'YOUR_APP_SECRET',
# args: {
- # algorithm: 'HS256',
- # uid_claim: 'email',
- # required_claims: ["name", "email"],
- # info_map: { name: "name", email: "email" },
- # auth_url: 'https://example.com/',
- # valid_within: null,
- # }
+ # secret: 'YOUR_APP_SECRET',
+ # algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
+ # uid_claim: 'email',
+ # required_claims: ['name', 'email'],
+ # info_map: { name: 'name', email: 'email' },
+ # auth_url: 'https://example.com/',
+ # valid_within: 3600 # 1 hour
+ # }
# }
# - { name: 'saml',
# label: 'Our SAML Provider',
diff --git a/db/fixtures/development/24_forks.rb b/db/fixtures/development/24_forks.rb
new file mode 100644
index 00000000000..61e39c871e6
--- /dev/null
+++ b/db/fixtures/development/24_forks.rb
@@ -0,0 +1,16 @@
+require './spec/support/sidekiq'
+
+Sidekiq::Testing.inline! do
+ Gitlab::Seeder.quiet do
+ User.all.sample(10).each do |user|
+ source_project = Project.public_only.sample
+ fork_project = Projects::ForkService.new(source_project, user, namespace: user.namespace).execute
+
+ if fork_project.valid?
+ puts '.'
+ else
+ puts 'F'
+ end
+ end
+ end
+end
diff --git a/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb
index c2b5b239279..03f677a4678 100644
--- a/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb
+++ b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb
@@ -8,7 +8,7 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_concurrent_index :ci_pipelines, :merge_request_id
+ add_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
add_concurrent_foreign_key :ci_pipelines, :merge_requests, column: :merge_request_id, on_delete: :cascade
end
@@ -17,6 +17,6 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
remove_foreign_key :ci_pipelines, :merge_requests
end
- remove_concurrent_index :ci_pipelines, :merge_request_id
+ remove_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
end
end
diff --git a/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb b/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb
new file mode 100644
index 00000000000..5b47a279438
--- /dev/null
+++ b/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCiBuildsPartialIndexOnProjectIdAndStatus < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(*index_arguments)
+ end
+
+ def down
+ remove_concurrent_index(*index_arguments)
+ end
+
+ private
+
+ def index_arguments
+ [
+ :ci_builds,
+ [:project_id, :status],
+ {
+ name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial2',
+ where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))"
+ }
+ ]
+ end
+end
diff --git a/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb b/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb
new file mode 100644
index 00000000000..a0a02e81323
--- /dev/null
+++ b/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveRedundantCiBuildsPartialIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index(*index_arguments)
+ end
+
+ def down
+ add_concurrent_index(*index_arguments)
+ end
+
+ private
+
+ def index_arguments
+ [
+ :ci_builds,
+ [:project_id, :status],
+ {
+ name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial',
+ where: "((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text]))"
+ }
+ ]
+ end
+end
diff --git a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
new file mode 100644
index 00000000000..11b98203793
--- /dev/null
+++ b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddTokenEncryptedToCiBuilds < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :token_encrypted, :string
+ end
+end
diff --git a/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb
new file mode 100644
index 00000000000..f90aca008e5
--- /dev/null
+++ b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCiBuildsTokenEncrypted < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, :token_encrypted, unique: true, where: 'token_encrypted IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :ci_builds, :token_encrypted
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 65a69c2850c..5bc7c7c71fc 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20181126153547) do
+ActiveRecord::Schema.define(version: 20181129104944) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -345,6 +345,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.boolean "protected"
t.integer "failure_reason"
t.datetime_with_timezone "scheduled_at"
+ t.string "token_encrypted"
t.index ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
t.index ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -353,6 +354,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
+ t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree
t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree
t.index ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree
@@ -360,6 +362,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
t.index ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
t.index ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
+ t.index ["token_encrypted"], name: "index_ci_builds_on_token_encrypted", unique: true, where: "(token_encrypted IS NOT NULL)", using: :btree
t.index ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
t.index ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
end
@@ -476,7 +479,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.integer "iid"
t.integer "merge_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
- t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", using: :btree
+ t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)", using: :btree
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
t.index ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 373d4239f71..54be7b616cc 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -10,7 +10,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
- Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID
+ Bitbucket, Facebook, Shibboleth, Crowd, Azure, Authentiq ID, and JWT
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md
index 8b00f52ffc1..497298503ad 100644
--- a/doc/administration/auth/jwt.md
+++ b/doc/administration/auth/jwt.md
@@ -26,15 +26,15 @@ JWT will provide you with a secret key for you to use.
```ruby
gitlab_rails['omniauth_providers'] = [
{ name: 'jwt',
- app_secret: 'YOUR_APP_SECRET',
args: {
- algorithm: 'HS256',
- uid_claim: 'email',
- required_claims: ["name", "email"],
- info_maps: { name: "name", email: "email" },
- auth_url: 'https://example.com/',
- valid_within: nil,
- }
+ secret: 'YOUR_APP_SECRET',
+ algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
+ uid_claim: 'email',
+ required_claims: ['name', 'email'],
+ info_maps: { name: 'name', email: 'email' },
+ auth_url: 'https://example.com/',
+ valid_within: 3600 # 1 hour
+ }
}
]
```
@@ -43,15 +43,15 @@ JWT will provide you with a secret key for you to use.
```
- { name: 'jwt',
- app_secret: 'YOUR_APP_SECRET',
args: {
- algorithm: 'HS256',
- uid_claim: 'email',
- required_claims: ["name", "email"],
- info_map: { name: "name", email: "email" },
- auth_url: 'https://example.com/',
- valid_within: null,
- }
+ secret: 'YOUR_APP_SECRET',
+ algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
+ uid_claim: 'email',
+ required_claims: ['name', 'email'],
+ info_map: { name: 'name', email: 'email' },
+ auth_url: 'https://example.com/',
+ valid_within: 3600 # 1 hour
+ }
}
```
@@ -60,7 +60,7 @@ JWT will provide you with a secret key for you to use.
1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL.
1. Save the configuration file.
-1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a JWT icon below the regular sign in form.
@@ -68,5 +68,5 @@ Click the icon to begin the authentication process. JWT will ask the user to
sign in and authorize the GitLab application. If everything goes well, the user
will be redirected to GitLab and will be signed in.
-[reconfigure GitLab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/api/search.md b/doc/api/search.md
index 9716f682ace..a9369930003 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -722,6 +722,17 @@ Example response:
### Scope: wiki_blobs
+Wiki blobs searches are performed on both filenames and contents. Search
+results:
+
+- Found in filenames are displayed before results found in contents.
+- May contain multiple matches for the same blob because the search string
+ might be found in both the filename and content, and matches of the different
+types are displayed separately.
+- May contain multiple matches for the same blob because the search string
+ might be found if the search string appears multiple times in the content.
+
+
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=wiki_blobs&search=bye
```
@@ -783,6 +794,15 @@ Filters are available for this scope:
to use a filter simply include it in your query like so: `a query filename:some_name*`.
+Blobs searches are performed on both filenames and contents. Search results:
+
+- Found in filenames are displayed before results found in contents.
+- May contain multiple matches for the same blob because the search string
+ might be found in both the filename and content, and matches of the different
+types are displayed separately.
+- May contain multiple matches for the same blob because the search string
+ might be found if the search string appears multiple times in the content.
+
You may use wildcards (`*`) to use glob matching.
```bash
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 0ca8bb67a77..0b0c6dfc8cf 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -77,8 +77,11 @@ that builds on this to add some additional niceties, such as allowing
configuration with a single Yaml file for multiple URLs, and uploading of the
profile and log output to S3.
-For GitLab.com, you can find the latest results here:
-<http://redash.gitlab.com/dashboard/gitlab-profiler-statistics>
+For GitLab.com, currently the latest profiling data has been [moved from
+Redash to Looker](https://gitlab.com/gitlab-com/Product/issues/5#note_121194467).
+We are [currently investigating how to make this data
+public](https://gitlab.com/meltano/looker/issues/294).
+
## Sherlock
diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md
index 8d9706a9501..d685cacf9ea 100644
--- a/doc/development/testing_guide/ci.md
+++ b/doc/development/testing_guide/ci.md
@@ -31,11 +31,7 @@ After that, the next pipeline will use the up-to-date
The GitLab test suite is [monitored] for the `master` branch, and any branch
that includes `rspec-profile` in their name.
-A [public dashboard] is available for everyone to see. Feel free to look at the
-slowest test files and try to improve them.
-
[monitored]: ../performance.md#rspec-profiling
-[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
## CI setup
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 5900e1cccc2..f5db692afe5 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -35,12 +35,7 @@ module API
end
def process_results(results)
- case params[:scope]
- when 'blobs', 'wiki_blobs'
- paginate(results).map { |blob| blob[1] }
- else
- paginate(results)
- end
+ paginate(results)
end
def snippets?
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
index c996d786909..f3d37ccd72a 100644
--- a/lib/gitlab/database/count.rb
+++ b/lib/gitlab/database/count.rb
@@ -40,7 +40,7 @@ module Gitlab
if strategy.enabled?
models_with_missing_counts = models - counts_by_model.keys
- break if models_with_missing_counts.empty?
+ break counts_by_model if models_with_missing_counts.empty?
counts = strategy.new(models_with_missing_counts).count
diff --git a/lib/gitlab/database/count/exact_count_strategy.rb b/lib/gitlab/database/count/exact_count_strategy.rb
index 0276fe2b54f..fa6951eda22 100644
--- a/lib/gitlab/database/count/exact_count_strategy.rb
+++ b/lib/gitlab/database/count/exact_count_strategy.rb
@@ -20,6 +20,8 @@ module Gitlab
models.each_with_object({}) do |model, data|
data[model] = model.count
end
+ rescue *CONNECTION_ERRORS
+ {}
end
def self.enabled?
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index b4db3f93c9c..3958814208c 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -4,8 +4,6 @@
# the result is joined and sorted by file name
module Gitlab
class FileFinder
- BATCH_SIZE = 100
-
attr_reader :project, :ref
delegate :repository, to: :project
@@ -16,60 +14,35 @@ module Gitlab
end
def find(query)
- query = Gitlab::Search::Query.new(query) do
- filter :filename, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}$/i }
- filter :path, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}/i }
- filter :extension, matcher: ->(filter, blob) { blob.filename =~ /\.#{filter[:regex_value]}$/i }
+ query = Gitlab::Search::Query.new(query, encode_binary: true) do
+ filter :filename, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}$/i }
+ filter :path, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}/i }
+ filter :extension, matcher: ->(filter, blob) { blob.binary_filename =~ /\.#{filter[:regex_value]}$/i }
end
- by_content = find_by_content(query.term)
-
- already_found = Set.new(by_content.map(&:filename))
- by_filename = find_by_filename(query.term, except: already_found)
+ files = find_by_filename(query.term) + find_by_content(query.term)
- files = (by_content + by_filename)
- .sort_by(&:filename)
+ files = query.filter_results(files) if query.filters.any?
- query.filter_results(files).map { |blob| [blob.filename, blob] }
+ files
end
private
def find_by_content(query)
- results = repository.search_files_by_content(query, ref).first(BATCH_SIZE)
- results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result, project) }
- end
-
- def find_by_filename(query, except: [])
- filenames = search_filenames(query, except)
-
- blobs(filenames).map do |blob|
- Gitlab::SearchResults::FoundBlob.new(
- id: blob.id,
- filename: blob.path,
- basename: File.basename(blob.path, File.extname(blob.path)),
- ref: ref,
- startline: 1,
- data: blob.data,
- project: project
- )
+ repository.search_files_by_content(query, ref).map do |result|
+ Gitlab::Search::FoundBlob.new(content_match: result, project: project, ref: ref, repository: repository)
end
end
- def search_filenames(query, except)
- filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE)
-
- filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
-
- filenames
- end
-
- def blob_refs(filenames)
- filenames.map { |filename| [ref, filename] }
+ def find_by_filename(query)
+ search_filenames(query).map do |filename|
+ Gitlab::Search::FoundBlob.new(blob_filename: filename, project: project, ref: ref, repository: repository)
+ end
end
- def blobs(filenames)
- Gitlab::Git::Blob.batch(repository, blob_refs(filenames), blob_size_limit: 1024)
+ def search_filenames(query)
+ repository.search_files_by_name(query, ref)
end
end
end
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 31bab20b044..4fbb87385c3 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -44,9 +44,8 @@ module Gitlab
def update_signature!(cached_signature)
using_keychain do |gpg_key|
cached_signature.update!(attributes(gpg_key))
+ @signature = cached_signature
end
-
- @signature = cached_signature
end
private
@@ -59,11 +58,15 @@ module Gitlab
# the proper signature.
# NOTE: the invoked method is #fingerprint but it's only returning
# 16 characters (the format used by keyid) instead of 40.
- gpg_key = find_gpg_key(verified_signature.fingerprint)
+ fingerprint = verified_signature&.fingerprint
+
+ break unless fingerprint
+
+ gpg_key = find_gpg_key(fingerprint)
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
- @verified_signature = nil
+ clear_memoization(:verified_signature)
end
yield gpg_key
@@ -71,9 +74,16 @@ module Gitlab
end
def verified_signature
- @verified_signature ||= GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
+ strong_memoize(:verified_signature) { gpgme_signature }
+ end
+
+ def gpgme_signature
+ GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
+ # Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-ce/issues/54932
break verified_signature
end
+ rescue GPGME::Error
+ nil
end
def create_cached_signature!
@@ -92,7 +102,7 @@ module Gitlab
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
- gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
+ gpg_key_primary_keyid: gpg_key&.keyid || verified_signature&.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
verification_status: verification_status
@@ -102,7 +112,7 @@ module Gitlab
def verification_status(gpg_key)
return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
- return :unverified unless verified_signature.valid?
+ return :unverified unless verified_signature&.valid?
if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
:verified
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 93065879ec6..7cdea9d1ce4 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -142,6 +142,7 @@ excluded_attributes:
statuses:
- :trace
- :token
+ - :token_encrypted
- :when
- :artifacts_file
- :artifacts_metadata
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 04df881bf03..a68f8801c2a 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -17,9 +17,9 @@ module Gitlab
when 'notes'
notes.page(page).per(per_page)
when 'blobs'
- Kaminari.paginate_array(blobs).page(page).per(per_page)
+ paginated_blobs(blobs, page)
when 'wiki_blobs'
- Kaminari.paginate_array(wiki_blobs).page(page).per(per_page)
+ paginated_blobs(wiki_blobs, page)
when 'commits'
Kaminari.paginate_array(commits).page(page).per(per_page)
else
@@ -55,37 +55,6 @@ module Gitlab
@commits_count ||= commits.count
end
- def self.parse_search_result(result, project = nil)
- ref = nil
- filename = nil
- basename = nil
-
- data = []
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/)&.tap do |matches|
- ref = matches[:ref]
- filename = matches[:filename]
- startline = matches[:startline]
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- end
-
- data << line.sub(prefix.to_s, '')
- end
-
- FoundBlob.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data.join,
- project: project
- )
- end
-
def single_commit_result?
return false if commits_count != 1
@@ -97,6 +66,14 @@ module Gitlab
private
+ def paginated_blobs(blobs, page)
+ results = Kaminari.paginate_array(blobs).page(page).per(per_page)
+
+ Gitlab::Search::FoundBlob.preload_blobs(results)
+
+ results
+ end
+
def blobs
return [] unless Ability.allowed?(@current_user, :download_code, @project)
diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb
new file mode 100644
index 00000000000..a62ab1521a7
--- /dev/null
+++ b/lib/gitlab/search/found_blob.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Search
+ class FoundBlob
+ include EncodingHelper
+ include Presentable
+ include BlobLanguageFromGitAttributes
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :project, :content_match, :blob_filename
+
+ FILENAME_REGEXP = /\A(?<ref>[^:]*):(?<filename>[^\x00]*)\x00/.freeze
+ CONTENT_REGEXP = /^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze
+
+ def self.preload_blobs(blobs)
+ to_fetch = blobs.select { |blob| blob.is_a?(self) && blob.blob_filename }
+
+ to_fetch.each { |blob| blob.fetch_blob }
+ end
+
+ def initialize(opts = {})
+ @id = opts.fetch(:id, nil)
+ @binary_filename = opts.fetch(:filename, nil)
+ @binary_basename = opts.fetch(:basename, nil)
+ @ref = opts.fetch(:ref, nil)
+ @startline = opts.fetch(:startline, nil)
+ @binary_data = opts.fetch(:data, nil)
+ @per_page = opts.fetch(:per_page, 20)
+ @project = opts.fetch(:project, nil)
+ # Some caller does not have project object (e.g. elastic search),
+ # yet they can trigger many calls in one go,
+ # causing duplicated queries.
+ # Allow those to just pass project_id instead.
+ @project_id = opts.fetch(:project_id, nil)
+ @content_match = opts.fetch(:content_match, nil)
+ @blob_filename = opts.fetch(:blob_filename, nil)
+ @repository = opts.fetch(:repository, nil)
+ end
+
+ def id
+ @id ||= parsed_content[:id]
+ end
+
+ def ref
+ @ref ||= parsed_content[:ref]
+ end
+
+ def startline
+ @startline ||= parsed_content[:startline]
+ end
+
+ # binary_filename is used for running filters on all matches,
+ # for grepped results (which use content_match), we get
+ # filename from the beginning of the grepped result which is faster
+ # then parsing whole snippet
+ def binary_filename
+ @binary_filename ||= content_match ? search_result_filename : parsed_content[:binary_filename]
+ end
+
+ def filename
+ @filename ||= encode_utf8(@binary_filename || parsed_content[:binary_filename])
+ end
+
+ def basename
+ @basename ||= encode_utf8(@binary_basename || parsed_content[:binary_basename])
+ end
+
+ def data
+ @data ||= encode_utf8(@binary_data || parsed_content[:binary_data])
+ end
+
+ def path
+ filename
+ end
+
+ def project_id
+ @project_id || @project&.id
+ end
+
+ def present
+ super(presenter_class: BlobPresenter)
+ end
+
+ def fetch_blob
+ path = [ref, blob_filename]
+ missing_blob = { binary_filename: blob_filename }
+
+ BatchLoader.for(path).batch(default_value: missing_blob) do |refs, loader|
+ Gitlab::Git::Blob.batch(repository, refs, blob_size_limit: 1024).each do |blob|
+ # if the blob couldn't be fetched for some reason,
+ # show at least the blob filename
+ data = {
+ id: blob.id,
+ binary_filename: blob.path,
+ binary_basename: File.basename(blob.path, File.extname(blob.path)),
+ ref: ref,
+ startline: 1,
+ binary_data: blob.data,
+ project: project
+ }
+
+ loader.call([ref, blob.path], data)
+ end
+ end
+ end
+
+ private
+
+ def search_result_filename
+ content_match.match(FILENAME_REGEXP) { |matches| matches[:filename] }
+ end
+
+ def parsed_content
+ strong_memoize(:parsed_content) do
+ if content_match
+ parse_search_result
+ elsif blob_filename
+ fetch_blob
+ else
+ {}
+ end
+ end
+ end
+
+ def parse_search_result
+ ref = nil
+ filename = nil
+ basename = nil
+
+ data = []
+ startline = 0
+
+ content_match.each_line.each_with_index do |line, index|
+ prefix ||= line.match(CONTENT_REGEXP)&.tap do |matches|
+ ref = matches[:ref]
+ filename = matches[:filename]
+ startline = matches[:startline]
+ startline = startline.to_i - index
+ extname = Regexp.escape(File.extname(filename))
+ basename = filename.sub(/#{extname}$/, '')
+ end
+
+ data << line.sub(prefix.to_s, '')
+ end
+
+ {
+ binary_filename: filename,
+ binary_basename: basename,
+ ref: ref,
+ startline: startline,
+ binary_data: data.join,
+ project: project
+ }
+ end
+
+ def repository
+ @repository ||= project.repository
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search/query.rb b/lib/gitlab/search/query.rb
index 7f69083a492..ba0e16607a6 100644
--- a/lib/gitlab/search/query.rb
+++ b/lib/gitlab/search/query.rb
@@ -3,6 +3,8 @@
module Gitlab
module Search
class Query < SimpleDelegator
+ include EncodingHelper
+
def initialize(query, filter_opts = {}, &block)
@raw_query = query.dup
@filters = []
@@ -50,7 +52,9 @@ module Gitlab
end
def parse_filter(filter, input)
- filter[:parser].call(input)
+ result = filter[:parser].call(input)
+
+ @filter_options[:encode_binary] ? encode_binary(result) : result
end
end
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 458737f31eb..491148ec1a6 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -2,42 +2,6 @@
module Gitlab
class SearchResults
- class FoundBlob
- include EncodingHelper
- include Presentable
- include BlobLanguageFromGitAttributes
-
- attr_reader :id, :filename, :basename, :ref, :startline, :data, :project
-
- def initialize(opts = {})
- @id = opts.fetch(:id, nil)
- @filename = encode_utf8(opts.fetch(:filename, nil))
- @basename = encode_utf8(opts.fetch(:basename, nil))
- @ref = opts.fetch(:ref, nil)
- @startline = opts.fetch(:startline, nil)
- @data = encode_utf8(opts.fetch(:data, nil))
- @per_page = opts.fetch(:per_page, 20)
- @project = opts.fetch(:project, nil)
- # Some caller does not have project object (e.g. elastic search),
- # yet they can trigger many calls in one go,
- # causing duplicated queries.
- # Allow those to just pass project_id instead.
- @project_id = opts.fetch(:project_id, nil)
- end
-
- def path
- filename
- end
-
- def project_id
- @project_id || @project&.id
- end
-
- def present
- super(presenter_class: BlobPresenter)
- end
- end
-
attr_reader :current_user, :query, :per_page
# Limit search results by passed projects
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index bfcc8efdc96..008e9cd1d24 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -2,6 +2,8 @@
module Gitlab
class UsageData
+ APPROXIMATE_COUNT_MODELS = [Label, MergeRequest, Note, Todo].freeze
+
class << self
def data(force_refresh: false)
Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) { uncached_data }
@@ -73,12 +75,9 @@ module Gitlab
issues: count(Issue),
keys: count(Key),
label_lists: count(List.label),
- labels: count(Label),
lfs_objects: count(LfsObject),
- merge_requests: count(MergeRequest),
milestone_lists: count(List.milestone),
milestones: count(Milestone),
- notes: count(Note),
pages_domains: count(PagesDomain),
projects: count(Project),
projects_imported_from_github: count(Project.where(import_type: 'github')),
@@ -86,10 +85,9 @@ module Gitlab
releases: count(Release),
remote_mirrors: count(RemoteMirror),
snippets: count(Snippet),
- todos: count(Todo),
uploads: count(Upload),
web_hooks: count(WebHook)
- }.merge(services_usage)
+ }.merge(services_usage).merge(approximate_counts)
}
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -164,6 +162,16 @@ module Gitlab
fallback
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def approximate_counts
+ approx_counts = Gitlab::Database::Count.approximate_counts(APPROXIMATE_COUNT_MODELS)
+
+ APPROXIMATE_COUNT_MODELS.each_with_object({}) do |model, result|
+ key = model.name.underscore.pluralize.to_sym
+
+ result[key] = approx_counts[model] || -1
+ end
+ end
end
end
end
diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb
index a00cd65594c..5303b3582ab 100644
--- a/lib/gitlab/wiki_file_finder.rb
+++ b/lib/gitlab/wiki_file_finder.rb
@@ -2,6 +2,8 @@
module Gitlab
class WikiFileFinder < FileFinder
+ BATCH_SIZE = 100
+
attr_reader :repository
def initialize(project, ref)
@@ -12,13 +14,11 @@ module Gitlab
private
- def search_filenames(query, except)
+ def search_filenames(query)
safe_query = Regexp.escape(query.tr(' ', '-'))
safe_query = Regexp.new(safe_query, Regexp::IGNORECASE)
filenames = repository.ls_files(ref)
- filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
-
filenames.grep(safe_query).first(BATCH_SIZE)
end
end
diff --git a/lib/omni_auth/strategies/jwt.rb b/lib/omni_auth/strategies/jwt.rb
index a792903fde7..2f3d477a591 100644
--- a/lib/omni_auth/strategies/jwt.rb
+++ b/lib/omni_auth/strategies/jwt.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'omniauth'
+require 'openssl'
require 'jwt'
module OmniAuth
@@ -37,7 +38,19 @@ module OmniAuth
end
def decoded
- @decoded ||= ::JWT.decode(request.params['jwt'], options.secret, options.algorithm).first
+ secret =
+ case options.algorithm
+ when *%w[RS256 RS384 RS512]
+ OpenSSL::PKey::RSA.new(options.secret).public_key
+ when *%w[ES256 ES384 ES512]
+ OpenSSL::PKey::EC.new(options.secret).tap { |key| key.private_key = nil }
+ when *%w(HS256 HS384 HS512)
+ options.secret
+ else
+ raise NotImplementedError, "Unsupported algorithm: #{options.algorithm}"
+ end
+
+ @decoded ||= ::JWT.decode(request.params['jwt'], secret, true, { algorithm: options.algorithm }).first
(options.required_claims || []).each do |field|
raise ClaimInvalid, "Missing required '#{field}' claim" unless @decoded.key?(field.to_s)
@@ -45,7 +58,7 @@ module OmniAuth
raise ClaimInvalid, "Missing required 'iat' claim" if options.valid_within && !@decoded["iat"]
- if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within
+ if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within.to_i
raise ClaimInvalid, "'iat' timestamp claim is too skewed from present"
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f023a9be3eb..23ee90ff0dd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7932,6 +7932,9 @@ msgid_plural "replies"
msgstr[0] ""
msgstr[1] ""
+msgid "should be higher than %{access} inherited membership from group %{group_name}"
+msgstr ""
+
msgid "source"
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index aa0b78b37e8..10a50f5cc72 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -184,6 +184,7 @@ module QA
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
autoload :Members, 'qa/page/project/settings/members'
+ autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories'
end
module Issue
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 91e229c4c8c..f4bba3c9560 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -15,7 +15,7 @@ module QA
def_delegators :evaluator, :view, :views
def refresh
- visit current_url
+ page.refresh
end
def wait(max: 60, time: 0.1, reload: true)
@@ -80,8 +80,8 @@ module QA
page.evaluate_script('xhr.status') == 200
end
- def find_element(name)
- find(element_selector_css(name))
+ def find_element(name, wait: Capybara.default_max_wait_time)
+ find(element_selector_css(name), wait: wait)
end
def all_elements(name)
@@ -100,6 +100,14 @@ module QA
find_element(name).set(content)
end
+ def select_element(name, value)
+ element = find_element(name)
+
+ return if element.text.downcase.to_s == value.to_s
+
+ element.select value.to_s.capitalize
+ end
+
def has_element?(name)
has_css?(element_selector_css(name))
end
@@ -110,6 +118,12 @@ module QA
end
end
+ def within_element_by_index(name, index)
+ page.within all_elements(name)[index] do
+ yield
+ end
+ end
+
def scroll_to_element(name, *args)
scroll_to(element_selector_css(name), *args)
end
diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb
new file mode 100644
index 00000000000..a73be7dfeda
--- /dev/null
+++ b/qa/qa/page/project/settings/mirroring_repositories.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class MirroringRepositories < Page::Base
+ view 'app/views/projects/mirrors/_authentication_method.html.haml' do
+ element :authentication_method
+ element :password
+ end
+
+ view 'app/views/projects/mirrors/_mirror_repos.html.haml' do
+ element :mirror_repository_url_input
+ element :mirror_repository_button
+ element :mirror_repository_url
+ element :mirror_last_update_at
+ element :mirrored_repository_row
+ end
+
+ view 'app/views/projects/mirrors/_mirror_repos_form.html.haml' do
+ element :mirror_direction
+ end
+
+ view 'app/views/shared/_remote_mirror_update_button.html.haml' do
+ element :update_now_button
+ end
+
+ def repository_url=(value)
+ fill_element :mirror_repository_url_input, value
+ end
+
+ def password=(value)
+ fill_element :password, value
+ end
+
+ def mirror_direction=(value)
+ raise ArgumentError, "Mirror direction must be :push or :pull" unless [:push, :pull].include? value
+
+ select_element(:mirror_direction, value)
+ end
+
+ def authentication_method=(value)
+ raise ArgumentError, "Authentication method must be :password or :none" unless [:password, :none].include? value
+
+ select_element(:authentication_method, value)
+ end
+
+ def mirror_repository
+ click_element :mirror_repository_button
+ end
+
+ def update(url)
+ row_index = find_repository_row_index url
+
+ within_element_by_index(:mirrored_repository_row, row_index) do
+ click_element :update_now_button
+ end
+
+ # Wait a few seconds for the sync to occur and then refresh the page
+ # so that 'last update' shows 'just now' or a period in seconds
+ sleep 5
+ refresh
+
+ wait(time: 1) do
+ within_element_by_index(:mirrored_repository_row, row_index) do
+ last_update = find_element(:mirror_last_update_at, wait: 0)
+ last_update.has_text?('just now') || last_update.has_text?('seconds')
+ end
+ end
+
+ # Fail early if the page still shows that there has been no update
+ within_element_by_index(:mirrored_repository_row, row_index) do
+ find_element(:mirror_last_update_at, wait: 0).assert_no_text('Never')
+ end
+ end
+
+ private
+
+ def find_repository_row_index(target_url)
+ all_elements(:mirror_repository_url).index do |url|
+ # The url might be a sanitized url but the target_url won't be so
+ # we compare just the paths instead of the full url
+ URI.parse(url.text).path == target_url.path
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 53ebe28970b..ac0b87aca5e 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -13,6 +13,10 @@ module QA
element :protected_branches_settings
end
+ view 'app/views/projects/mirrors/_mirror_repos.html.haml' do
+ element :mirroring_repositories_settings
+ end
+
def expand_deploy_keys(&block)
expand_section(:deploy_keys_settings) do
DeployKeys.perform(&block)
@@ -30,6 +34,12 @@ module QA
DeployTokens.perform(&block)
end
end
+
+ def expand_mirroring_repositories(&block)
+ expand_section(:mirroring_repositories_settings) do
+ MirroringRepositories.perform(&block)
+ end
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
new file mode 100644
index 00000000000..2d0e281ab59
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push mirror a repository over HTTP' do
+ it 'configures and syncs a (push) mirrored repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ target_project = Resource::Project.fabricate! do |project|
+ project.name = 'push-mirror-target-project'
+ end
+ target_project_uri = target_project.repository_http_location.uri
+ target_project_uri.user = Runtime::User.username
+
+ source_project_push = Resource::Repository::ProjectPush.fabricate! do |push|
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
+ end
+ source_project_push.project.visit!
+
+ Page::Project::Show.perform(&:wait_for_push)
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+ Page::Project::Settings::Repository.perform do |settings|
+ settings.expand_mirroring_repositories do |mirror_settings|
+ # Configure the source project to push to the target project
+ mirror_settings.repository_url = target_project_uri
+ mirror_settings.mirror_direction = :push
+ mirror_settings.authentication_method = :password
+ mirror_settings.password = Runtime::User.password
+ mirror_settings.mirror_repository
+ mirror_settings.update target_project_uri
+ end
+ end
+
+ # Check that the target project has the commit from the source
+ target_project.visit!
+ expect(page).to have_content('README.md')
+ expect(page).to have_content('This is a test project')
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index cf5cd3a79f8..43bc16d8c9a 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -37,8 +37,8 @@ module QA
exists
end
- def find_element(name)
- log("finding :#{name}")
+ def find_element(name, wait: Capybara.default_max_wait_time)
+ log("finding :#{name} (wait: #{wait})")
element = super
@@ -71,6 +71,12 @@ module QA
super
end
+ def select_element(name, value)
+ log(%Q(selecting "#{value}" in :#{name}))
+
+ super
+ end
+
def has_element?(name)
found = super
@@ -89,6 +95,16 @@ module QA
element
end
+ def within_element_by_index(name, index)
+ log("within elements :#{name} at index #{index}")
+
+ element = super
+
+ log("end within elements :#{name} at index #{index}")
+
+ element
+ end
+
private
def log(msg)
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
deleted file mode 100644
index caee7a67aec..00000000000
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ /dev/null
@@ -1,179 +0,0 @@
-require 'spec_helper'
-
-describe 'Projects > Issuables > Default sort order' do
- let(:project) { create(:project, :public) }
-
- let(:first_created_issuable) { issuables.order_created_asc.first }
- let(:last_created_issuable) { issuables.order_created_desc.first }
-
- let(:first_updated_issuable) { issuables.order_updated_asc.first }
- let(:last_updated_issuable) { issuables.order_updated_desc.first }
-
- context 'for merge requests' do
- include MergeRequestHelpers
-
- let!(:issuables) do
- timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
- { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
- { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
-
- timestamps.each_with_index do |ts, i|
- create issuable_type, { title: "#{issuable_type}_#{i}",
- source_branch: "#{issuable_type}_#{i}",
- source_project: project }.merge(ts)
- end
-
- MergeRequest.all
- end
-
- context 'in the "merge requests" tab', :js do
- let(:issuable_type) { :merge_request }
-
- it 'is "last created"' do
- visit_merge_requests project
-
- expect(first_merge_request).to include(last_created_issuable.title)
- expect(last_merge_request).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "merge requests / open" tab', :js do
- let(:issuable_type) { :merge_request }
-
- it 'is "created date"' do
- visit_merge_requests_with_state(project, 'open')
-
- expect(selected_sort_order).to eq('created date')
- expect(first_merge_request).to include(last_created_issuable.title)
- expect(last_merge_request).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "merge requests / merged" tab', :js do
- let(:issuable_type) { :merged_merge_request }
-
- it 'is "last updated"' do
- visit_merge_requests_with_state(project, 'merged')
-
- expect(find('.issues-other-filters')).to have_content('Last updated')
- expect(first_merge_request).to include(last_updated_issuable.title)
- expect(last_merge_request).to include(first_updated_issuable.title)
- end
- end
-
- context 'in the "merge requests / closed" tab', :js do
- let(:issuable_type) { :closed_merge_request }
-
- it 'is "last updated"' do
- visit_merge_requests_with_state(project, 'closed')
-
- expect(find('.issues-other-filters')).to have_content('Last updated')
- expect(first_merge_request).to include(last_updated_issuable.title)
- expect(last_merge_request).to include(first_updated_issuable.title)
- end
- end
-
- context 'in the "merge requests / all" tab', :js do
- let(:issuable_type) { :merge_request }
-
- it 'is "created date"' do
- visit_merge_requests_with_state(project, 'all')
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_merge_request).to include(last_created_issuable.title)
- expect(last_merge_request).to include(first_created_issuable.title)
- end
- end
- end
-
- context 'for issues' do
- include IssueHelpers
-
- let!(:issuables) do
- timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
- { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
- { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
-
- timestamps.each_with_index do |ts, i|
- create issuable_type, { title: "#{issuable_type}_#{i}",
- project: project }.merge(ts)
- end
-
- Issue.all
- end
-
- context 'in the "issues" tab', :js do
- let(:issuable_type) { :issue }
-
- it 'is "created date"' do
- visit_issues project
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "issues / open" tab', :js do
- let(:issuable_type) { :issue }
-
- it 'is "created date"' do
- visit_issues_with_state(project, 'open')
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "issues / closed" tab', :js do
- let(:issuable_type) { :closed_issue }
-
- it 'is "last updated"' do
- visit_issues_with_state(project, 'closed')
-
- expect(find('.issues-other-filters')).to have_content('Last updated')
- expect(first_issue).to include(last_updated_issuable.title)
- expect(last_issue).to include(first_updated_issuable.title)
- end
- end
-
- context 'in the "issues / all" tab', :js do
- let(:issuable_type) { :issue }
-
- it 'is "created date"' do
- visit_issues_with_state(project, 'all')
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
-
- context 'when the sort in the URL is id_desc' do
- let(:issuable_type) { :issue }
-
- before do
- visit_issues(project, sort: 'id_desc')
- end
-
- it 'shows the sort order as created date' do
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
- end
-
- def selected_sort_order
- find('.filter-dropdown-container .dropdown button').text.downcase
- end
-
- def visit_merge_requests_with_state(project, state)
- visit_merge_requests project, state: state
- end
-
- def visit_issues_with_state(project, state)
- visit_issues project, state: state
- end
-end
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
new file mode 100644
index 00000000000..0601dd47c03
--- /dev/null
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -0,0 +1,226 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'Sort Issuable List' do
+ let(:project) { create(:project, :public) }
+
+ let(:first_created_issuable) { issuables.order_created_asc.first }
+ let(:last_created_issuable) { issuables.order_created_desc.first }
+
+ let(:first_updated_issuable) { issuables.order_updated_asc.first }
+ let(:last_updated_issuable) { issuables.order_updated_desc.first }
+
+ context 'for merge requests' do
+ include MergeRequestHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ source_branch: "#{issuable_type}_#{i}",
+ source_project: project }.merge(ts)
+ end
+
+ MergeRequest.all
+ end
+
+ context 'default sort order' do
+ context 'in the "merge requests" tab', :js do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests project
+
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / open" tab', :js do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "created date"' do
+ visit_merge_requests_with_state(project, 'open')
+
+ expect(selected_sort_order).to eq('created date')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / merged" tab', :js do
+ let(:issuable_type) { :merged_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'merged')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / closed" tab', :js do
+ let(:issuable_type) { :closed_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / all" tab', :js do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "created date"' do
+ visit_merge_requests_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'custom sorting' do
+ let(:issuable_type) { :merge_request }
+
+ it 'supports sorting in asc and desc order' do
+ visit_merge_requests_with_state(project, 'open')
+
+ page.within('.issues-other-filters') do
+ click_button('Created date')
+ click_link('Last updated')
+ end
+
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+
+ find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click
+
+ expect(first_merge_request).to include(first_updated_issuable.title)
+ expect(last_merge_request).to include(last_updated_issuable.title)
+ end
+ end
+ end
+ end
+
+ context 'for issues' do
+ include IssueHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ project: project }.merge(ts)
+ end
+
+ Issue.all
+ end
+
+ context 'default sort order' do
+ context 'in the "issues" tab', :js do
+ let(:issuable_type) { :issue }
+
+ it 'is "created date"' do
+ visit_issues project
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / open" tab', :js do
+ let(:issuable_type) { :issue }
+
+ it 'is "created date"' do
+ visit_issues_with_state(project, 'open')
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / closed" tab', :js do
+ let(:issuable_type) { :closed_issue }
+
+ it 'is "last updated"' do
+ visit_issues_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_issue).to include(last_updated_issuable.title)
+ expect(last_issue).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "issues / all" tab', :js do
+ let(:issuable_type) { :issue }
+
+ it 'is "created date"' do
+ visit_issues_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'when the sort in the URL is id_desc' do
+ let(:issuable_type) { :issue }
+
+ before do
+ visit_issues(project, sort: 'id_desc')
+ end
+
+ it 'shows the sort order as created date' do
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+ end
+
+ context 'custom sorting' do
+ let(:issuable_type) { :issue }
+
+ it 'supports sorting in asc and desc order' do
+ visit_issues_with_state(project, 'open')
+
+ page.within('.issues-other-filters') do
+ click_button('Created date')
+ click_link('Last updated')
+ end
+
+ expect(first_issue).to include(last_updated_issuable.title)
+ expect(last_issue).to include(first_updated_issuable.title)
+
+ find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click
+
+ expect(first_issue).to include(first_updated_issuable.title)
+ expect(last_issue).to include(last_updated_issuable.title)
+ end
+ end
+ end
+
+ def selected_sort_order
+ find('.filter-dropdown-container .dropdown button').text.downcase
+ end
+
+ def visit_merge_requests_with_state(project, state)
+ visit_merge_requests project, state: state
+ end
+
+ def visit_issues_with_state(project, state)
+ visit_issues project, state: state
+ end
+end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 4d9b8262f21..a29380a180e 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -430,7 +430,7 @@ describe 'Filter issues', :js do
expect_issues_list_count(2)
- sort_toggle = find('.filter-dropdown-container .dropdown-menu-toggle')
+ sort_toggle = find('.filter-dropdown-container .dropdown')
sort_toggle.click
find('.filter-dropdown-container .dropdown-menu li a', text: 'Created date').click
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 3bc93933183..eebd2d57cca 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -20,9 +20,9 @@ describe "User sorts issues" do
end
it 'keeps the sort option' do
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
@@ -40,9 +40,9 @@ describe "User sorts issues" do
end
it "sorts by popularity" do
- find(".filter-dropdown-container button.dropdown-menu-toggle").click
+ find('.filter-dropdown-container .dropdown').click
- page.within(".content ul.dropdown-menu.dropdown-menu-right li") do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link("Popularity")
end
diff --git a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
index 61e8f1c4662..fa887110c13 100644
--- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
@@ -19,9 +19,9 @@ describe 'User sorts merge requests' do
end
it 'keeps the sort option' do
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
@@ -49,9 +49,9 @@ describe 'User sorts merge requests' do
it 'separates remember sorting with issues' do
create(:issue, project: project)
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
@@ -70,9 +70,9 @@ describe 'User sorts merge requests' do
end
it 'sorts by popularity' do
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Popularity')
end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index b778c72bc76..25417cf4955 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -32,7 +32,7 @@ describe 'Issue prioritization' do
visit project_issues_path(project, sort: 'label_priority')
# Ensure we are indicating that issues are sorted by priority
- expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority')
+ expect(page).to have_selector('.dropdown', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
@@ -70,7 +70,7 @@ describe 'Issue prioritization' do
sign_in user
visit project_issues_path(project, sort: 'label_priority')
- expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority')
+ expect(page).to have_selector('.dropdown', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index f545da3aee4..8975ea0f063 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -19,7 +19,7 @@ describe GroupMembersFinder, '#execute' do
end
it 'returns members for nested group', :nested_groups do
- group.add_maintainer(user2)
+ group.add_developer(user2)
nested_group.request_access(user4)
member1 = group.add_maintainer(user1)
member3 = nested_group.add_maintainer(user2)
diff --git a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
index 314f04107eb..ce66f562175 100644
--- a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
+++ b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
@@ -11,7 +11,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/pom.xml"
+ "file": "app/pom.xml",
+ "dependency": {
+ "package": {
+ "name": "io.netty/netty"
+ },
+ "version": "3.9.1.Final"
+ }
},
"identifiers": [
{
@@ -55,7 +61,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/requirements.txt"
+ "file": "app/requirements.txt",
+ "dependency": {
+ "package": {
+ "name": "Django"
+ },
+ "version": "1.11.3"
+ }
},
"identifiers": [
{
@@ -93,7 +105,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "rails/Gemfile.lock"
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
},
"identifiers": [
{
@@ -131,7 +149,13 @@
"name": "bundler-audit"
},
"location": {
- "file": "sast-sample-rails/Gemfile.lock"
+ "file": "sast-sample-rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "ffi"
+ },
+ "version": "1.9.18"
+ }
},
"identifiers": [
{
diff --git a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
index 314f04107eb..ce66f562175 100644
--- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
+++ b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
@@ -11,7 +11,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/pom.xml"
+ "file": "app/pom.xml",
+ "dependency": {
+ "package": {
+ "name": "io.netty/netty"
+ },
+ "version": "3.9.1.Final"
+ }
},
"identifiers": [
{
@@ -55,7 +61,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/requirements.txt"
+ "file": "app/requirements.txt",
+ "dependency": {
+ "package": {
+ "name": "Django"
+ },
+ "version": "1.11.3"
+ }
},
"identifiers": [
{
@@ -93,7 +105,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "rails/Gemfile.lock"
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
},
"identifiers": [
{
@@ -131,7 +149,13 @@
"name": "bundler-audit"
},
"location": {
- "file": "sast-sample-rails/Gemfile.lock"
+ "file": "sast-sample-rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "ffi"
+ },
+ "version": "1.9.18"
+ }
},
"identifiers": [
{
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
new file mode 100644
index 00000000000..cba0d93e144
--- /dev/null
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe SortingHelper do
+ include ApplicationHelper
+ include IconsHelper
+
+ describe '#issuable_sort_option_title' do
+ it 'returns correct title for issuable_sort_option_overrides key' do
+ expect(issuable_sort_option_title('created_asc')).to eq('Created date')
+ end
+
+ it 'returns correct title for a valid sort value' do
+ expect(issuable_sort_option_title('priority')).to eq('Priority')
+ end
+
+ it 'returns nil for invalid sort value' do
+ expect(issuable_sort_option_title('invalid_key')).to eq(nil)
+ end
+ end
+
+ describe '#issuable_sort_direction_button' do
+ before do
+ allow(self).to receive(:request).and_return(double(path: 'http://test.com', query_parameters: {}))
+ end
+
+ it 'returns icon with sort-highest when sort is created_date' do
+ expect(issuable_sort_direction_button('created_date')).to include('sort-highest')
+ end
+
+ it 'returns icon with sort-lowest when sort is asc' do
+ expect(issuable_sort_direction_button('created_asc')).to include('sort-lowest')
+ end
+
+ it 'returns icon with sort-lowest when sorting by milestone' do
+ expect(issuable_sort_direction_button('milestone')).to include('sort-lowest')
+ end
+
+ it 'returns icon with sort-lowest when sorting by due_date' do
+ expect(issuable_sort_direction_button('due_date')).to include('sort-lowest')
+ end
+ end
+end
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index 928bf70f3a2..e46edec9abb 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
+import { CLUSTER_TYPE } from '~/clusters/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Applications', () => {
@@ -14,9 +15,10 @@ describe('Applications', () => {
vm.$destroy();
});
- describe('', () => {
+ describe('Project cluster applications', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
+ type: CLUSTER_TYPE.PROJECT,
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
@@ -30,31 +32,76 @@ describe('Applications', () => {
});
it('renders a row for Helm Tiller', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
});
it('renders a row for Ingress', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
});
it('renders a row for Cert-Manager', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
});
it('renders a row for Prometheus', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
it('renders a row for GitLab Runner', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
});
it('renders a row for Jupyter', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null);
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
+ });
+ });
+
+ describe('Group cluster applications', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ type: CLUSTER_TYPE.GROUP,
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ cert_manager: { title: 'Cert-Manager' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub' },
+ knative: { title: 'Knative' },
+ },
+ });
+ });
+
+ it('renders a row for Helm Tiller', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
+ });
+
+ it('renders a row for Ingress', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
+ });
+
+ it('renders a row for Cert-Manager', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
+ });
+
+ it('renders a row for Prometheus', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull();
+ });
+
+ it('renders a row for GitLab Runner', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeNull();
+ });
+
+ it('renders a row for Jupyter', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).toBeNull();
+ });
+
+ it('renders a row for Knative', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull();
});
});
diff --git a/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
index f518bb3dc3e..3991c737a26 100644
--- a/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
@@ -16,6 +16,12 @@ describe Gitlab::Database::Count::ExactCountStrategy do
expect(subject).to eq({ Project => 3, Identity => 1 })
end
+
+ it 'returns default value if count times out' do
+ allow(models.first).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
+
+ expect(subject).to eq({})
+ end
end
describe '.enabled?' do
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 8c6d673391b..8229f0eb794 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -26,6 +26,28 @@ describe Gitlab::Gpg::Commit do
end
end
+ context 'invalid signature' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
+
+ let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
+
+ before do
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
+ .with(Gitlab::Git::Repository, commit_sha)
+ .and_return(
+ [
+ # Corrupt the key
+ GpgHelpers::User1.signed_commit_signature.tr('=', 'a'),
+ GpgHelpers::User1.signed_commit_base_data
+ ]
+ )
+ end
+
+ it 'returns nil' do
+ expect(described_class.new(commit).signature).to be_nil
+ end
+ end
+
context 'known key' do
context 'user matches the key uid' do
context 'user email matches the email committer' do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 4a0dc3686ec..6831274d37c 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -54,11 +54,18 @@ describe Gitlab::ProjectSearchResults do
end
it 'finds by name' do
- expect(results.map(&:first)).to include(expected_file_by_name)
+ expect(results.map(&:filename)).to include(expected_file_by_name)
+ end
+
+ it "loads all blobs for filename matches in single batch" do
+ expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original
+
+ expected = project.repository.search_files_by_name(query, 'master')
+ expect(results.map(&:filename)).to include(*expected)
end
it 'finds by content' do
- blob = results.select { |result| result.first == expected_file_by_content }.flatten.last
+ blob = results.select { |result| result.filename == expected_file_by_content }.flatten.last
expect(blob.filename).to eq(expected_file_by_content)
end
@@ -122,126 +129,6 @@ describe Gitlab::ProjectSearchResults do
let(:blob_type) { 'blobs' }
let(:entity) { project }
end
-
- describe 'parsing results' do
- let(:results) { project.repository.search_files_by_content('feature', 'master') }
- let(:search_result) { results.first }
-
- subject { described_class.parse_search_result(search_result) }
-
- it "returns a valid FoundBlob" do
- is_expected.to be_an Gitlab::SearchResults::FoundBlob
- expect(subject.id).to be_nil
- expect(subject.path).to eq('CHANGELOG')
- expect(subject.filename).to eq('CHANGELOG')
- expect(subject.basename).to eq('CHANGELOG')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(188)
- expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
- end
-
- context 'when the matching filename contains a colon' do
- let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" }
-
- it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/project::function1.yaml')
- expect(subject.basename).to eq('testdata/project::function1')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("---\n")
- end
- end
-
- context 'when the matching content contains a number surrounded by colons' do
- let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" }
-
- it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/foo.txt')
- expect(subject.basename).to eq('testdata/foo')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq('blah:9:blah')
- end
- end
-
- context 'when the matching content contains multiple null bytes' do
- let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" }
-
- it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/foo.txt')
- expect(subject.basename).to eq('testdata/foo')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("blah\x001\x00foo")
- end
- end
-
- context 'when the search result ends with an empty line' do
- let(:results) { project.repository.search_files_by_content('Role models', 'master') }
-
- it 'returns a valid FoundBlob that ends with an empty line' do
- expect(subject.filename).to eq('files/markdown/ruby-style-guide.md')
- expect(subject.basename).to eq('files/markdown/ruby-style-guide')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n")
- end
- end
-
- context 'when the search returns non-ASCII data' do
- context 'with UTF-8' do
- let(:results) { project.repository.search_files_by_content('файл', 'master') }
-
- it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/russian.rb')
- expect(subject.basename).to eq('encoding/russian')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("Хороший файл\n")
- end
- end
-
- context 'with UTF-8 in the filename' do
- let(:results) { project.repository.search_files_by_content('webhook', 'master') }
-
- it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/テスト.txt')
- expect(subject.basename).to eq('encoding/テスト')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(3)
- expect(subject.data).to include('WebHookの確認')
- end
- end
-
- context 'with ISO-8859-1' do
- let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) }
-
- it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/iso8859.txt')
- expect(subject.basename).to eq('encoding/iso8859')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("Äü\n\nfoo\n")
- end
- end
- end
-
- context "when filename has extension" do
- let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
-
- it { expect(subject.path).to eq('CONTRIBUTE.md') }
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
- it { expect(subject.basename).to eq('CONTRIBUTE') }
- end
-
- context "when file under directory" do
- let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" }
-
- it { expect(subject.path).to eq('a/b/c.md') }
- it { expect(subject.filename).to eq('a/b/c.md') }
- it { expect(subject.basename).to eq('a/b/c') }
- end
- end
end
describe 'wiki search' do
diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb
new file mode 100644
index 00000000000..74157e5c67c
--- /dev/null
+++ b/spec/lib/gitlab/search/found_blob_spec.rb
@@ -0,0 +1,138 @@
+# coding: utf-8
+
+require 'spec_helper'
+
+describe Gitlab::Search::FoundBlob do
+ describe 'parsing results' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:results) { project.repository.search_files_by_content('feature', 'master') }
+ let(:search_result) { results.first }
+
+ subject { described_class.new(content_match: search_result, project: project) }
+
+ it "returns a valid FoundBlob" do
+ is_expected.to be_an described_class
+ expect(subject.id).to be_nil
+ expect(subject.path).to eq('CHANGELOG')
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.basename).to eq('CHANGELOG')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(188)
+ expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
+ end
+
+ it "doesn't parses content if not needed" do
+ expect(subject).not_to receive(:parse_search_result)
+ expect(subject.project_id).to eq(project.id)
+ expect(subject.binary_filename).to eq('CHANGELOG')
+ end
+
+ it "parses content only once when needed" do
+ expect(subject).to receive(:parse_search_result).once.and_call_original
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.startline).to eq(188)
+ end
+
+ context 'when the matching filename contains a colon' do
+ let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/project::function1.yaml')
+ expect(subject.basename).to eq('testdata/project::function1')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("---\n")
+ end
+ end
+
+ context 'when the matching content contains a number surrounded by colons' do
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.basename).to eq('testdata/foo')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq('blah:9:blah')
+ end
+ end
+
+ context 'when the matching content contains multiple null bytes' do
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.basename).to eq('testdata/foo')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("blah\x001\x00foo")
+ end
+ end
+
+ context 'when the search result ends with an empty line' do
+ let(:results) { project.repository.search_files_by_content('Role models', 'master') }
+
+ it 'returns a valid FoundBlob that ends with an empty line' do
+ expect(subject.filename).to eq('files/markdown/ruby-style-guide.md')
+ expect(subject.basename).to eq('files/markdown/ruby-style-guide')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n")
+ end
+ end
+
+ context 'when the search returns non-ASCII data' do
+ context 'with UTF-8' do
+ let(:results) { project.repository.search_files_by_content('файл', 'master') }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/russian.rb')
+ expect(subject.basename).to eq('encoding/russian')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("Хороший файл\n")
+ end
+ end
+
+ context 'with UTF-8 in the filename' do
+ let(:results) { project.repository.search_files_by_content('webhook', 'master') }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/テスト.txt')
+ expect(subject.basename).to eq('encoding/テスト')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(3)
+ expect(subject.data).to include('WebHookの確認')
+ end
+ end
+
+ context 'with ISO-8859-1' do
+ let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/iso8859.txt')
+ expect(subject.basename).to eq('encoding/iso8859')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("Äü\n\nfoo\n")
+ end
+ end
+ end
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.path).to eq('CONTRIBUTE.md') }
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" }
+
+ it { expect(subject.path).to eq('a/b/c.md') }
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index e2de612ff46..deb19fe1a4b 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -213,4 +213,29 @@ describe Gitlab::UsageData do
expect(described_class.count(relation, fallback: 15)).to eq(15)
end
end
+
+ describe '#approximate_counts' do
+ it 'gets approximate counts for selected models' do
+ create(:label)
+
+ expect(Gitlab::Database::Count).to receive(:approximate_counts)
+ .with(described_class::APPROXIMATE_COUNT_MODELS).once.and_call_original
+
+ counts = described_class.approximate_counts.values
+
+ expect(counts.count).to eq(described_class::APPROXIMATE_COUNT_MODELS.count)
+ expect(counts.any? { |count| count < 0 }).to be_falsey
+ end
+
+ it 'returns default values if counts can not be retrieved' do
+ described_class::APPROXIMATE_COUNT_MODELS.map do |model|
+ model.name.underscore.pluralize.to_sym
+ end
+
+ expect(Gitlab::Database::Count).to receive(:approximate_counts)
+ .and_return({})
+
+ expect(described_class.approximate_counts.values.uniq).to eq([-1])
+ end
+ end
end
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index 88d6d0b559a..c2e2db27362 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -4,12 +4,10 @@ describe OmniAuth::Strategies::Jwt do
include Rack::Test::Methods
include DeviseHelpers
- context '.decoded' do
- let(:strategy) { described_class.new({}) }
+ context '#decoded' do
+ subject { described_class.new({}) }
let(:timestamp) { Time.now.to_i }
let(:jwt_config) { Devise.omniauth_configs[:jwt] }
- let(:key) { JWT.encode(claims, jwt_config.strategy.secret) }
-
let(:claims) do
{
id: 123,
@@ -18,19 +16,55 @@ describe OmniAuth::Strategies::Jwt do
iat: timestamp
}
end
+ let(:algorithm) { 'HS256' }
+ let(:secret) { jwt_config.strategy.secret }
+ let(:private_key) { secret }
+ let(:payload) { JWT.encode(claims, private_key, algorithm) }
before do
- allow_any_instance_of(OmniAuth::Strategy).to receive(:options).and_return(jwt_config.strategy)
- allow_any_instance_of(Rack::Request).to receive(:params).and_return({ 'jwt' => key })
+ subject.options[:secret] = secret
+ subject.options[:algorithm] = algorithm
+
+ expect_next_instance_of(Rack::Request) do |rack_request|
+ expect(rack_request).to receive(:params).and_return('jwt' => payload)
+ end
end
- it 'decodes the user information' do
- result = strategy.decoded
+ ECDSA_NAMED_CURVES = {
+ 'ES256' => 'prime256v1',
+ 'ES384' => 'secp384r1',
+ 'ES512' => 'secp521r1'
+ }.freeze
- expect(result["id"]).to eq(123)
- expect(result["name"]).to eq("user_example")
- expect(result["email"]).to eq("user@example.com")
- expect(result["iat"]).to eq(timestamp)
+ {
+ OpenSSL::PKey::RSA => %w[RS256 RS384 RS512],
+ OpenSSL::PKey::EC => %w[ES256 ES384 ES512],
+ String => %w[HS256 HS384 HS512]
+ }.each do |private_key_class, algorithms|
+ algorithms.each do |algorithm|
+ context "when the #{algorithm} algorithm is used" do
+ let(:algorithm) { algorithm }
+ let(:secret) do
+ if private_key_class == OpenSSL::PKey::RSA
+ private_key_class.generate(2048)
+ .to_pem
+ elsif private_key_class == OpenSSL::PKey::EC
+ private_key_class.new(ECDSA_NAMED_CURVES[algorithm])
+ .tap { |key| key.generate_key! }
+ .to_pem
+ else
+ private_key_class.new(jwt_config.strategy.secret)
+ end
+ end
+ let(:private_key) { private_key_class ? private_key_class.new(secret) : secret }
+
+ it 'decodes the user information' do
+ result = subject.decoded
+
+ expect(result).to eq(claims.stringify_keys)
+ end
+ end
+ end
end
context 'required claims is missing' do
@@ -43,7 +77,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -57,11 +91,12 @@ describe OmniAuth::Strategies::Jwt do
end
before do
- jwt_config.strategy.valid_within = Time.now.to_i
+ # Omniauth config values are always strings!
+ subject.options[:valid_within] = 2.days.to_s
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -76,11 +111,12 @@ describe OmniAuth::Strategies::Jwt do
end
before do
- jwt_config.strategy.valid_within = 2.seconds
+ # Omniauth config values are always strings!
+ subject.options[:valid_within] = 2.seconds.to_s
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 4cdcae5f670..89f78f629d4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1925,7 +1925,7 @@ describe Ci::Build do
context 'when token is empty' do
before do
- build.token = nil
+ build.update_columns(token: nil, token_encrypted: nil)
end
it { is_expected.to be_nil}
@@ -2141,7 +2141,7 @@ describe Ci::Build do
end
before do
- build.token = 'my-token'
+ build.set_token('my-token')
build.yaml_variables = []
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ba9540c84d4..b67c6a4cffa 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -350,6 +350,50 @@ describe Ci::Pipeline, :mailer do
CI_COMMIT_TITLE
CI_COMMIT_DESCRIPTION]
end
+
+ context 'when source is merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ it 'exposes merge request pipeline variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
+ 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
+ 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
+ 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s)
+ end
+
+ context 'when source project does not exist' do
+ before do
+ merge_request.update_column(:source_project_id, nil)
+ end
+
+ it 'does not expose source project related variables' do
+ expect(subject.to_hash.keys).not_to include(
+ %w[CI_MERGE_REQUEST_SOURCE_PROJECT_ID
+ CI_MERGE_REQUEST_SOURCE_PROJECT_PATH
+ CI_MERGE_REQUEST_SOURCE_PROJECT_URL
+ CI_MERGE_REQUEST_SOURCE_BRANCH_NAME])
+ end
+ end
+ end
end
describe '#protected_ref?' do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 0cdf430e9ab..55d83bc3a6b 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -351,3 +351,89 @@ describe PersonalAccessToken, 'TokenAuthenticatable' do
end
end
end
+
+describe Ci::Build, 'TokenAuthenticatable' do
+ let(:token_field) { :token }
+ let(:build) { FactoryBot.build(:ci_build) }
+
+ it_behaves_like 'TokenAuthenticatable'
+
+ describe 'generating new token' do
+ context 'token is not generated yet' do
+ describe 'token field accessor' do
+ it 'makes it possible to access token' do
+ expect(build.token).to be_nil
+
+ build.save!
+
+ expect(build.token).to be_present
+ end
+ end
+
+ describe "ensure_token" do
+ subject { build.ensure_token }
+
+ it { is_expected.to be_a String }
+ it { is_expected.not_to be_blank }
+
+ it 'does not persist token' do
+ expect(build).not_to be_persisted
+ end
+ end
+
+ describe 'ensure_token!' do
+ it 'persists a new token' do
+ expect(build.ensure_token!).to eq build.reload.token
+ expect(build).to be_persisted
+ end
+
+ it 'persists new token as an encrypted string' do
+ build.ensure_token!
+
+ encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token)
+
+ expect(build.read_attribute('token_encrypted')).to eq encrypted
+ end
+
+ it 'does not persist a token in a clear text' do
+ build.ensure_token!
+
+ expect(build.read_attribute('token')).to be_nil
+ end
+ end
+ end
+
+ describe '#reset_token!' do
+ it 'persists a new token' do
+ build.save!
+
+ build.token.yield_self do |previous_token|
+ build.reset_token!
+
+ expect(build.token).not_to eq previous_token
+ expect(build.token).to be_a String
+ end
+ end
+ end
+ end
+
+ describe 'setting a new token' do
+ subject { build.set_token('0123456789') }
+
+ it 'returns the token' do
+ expect(subject).to eq '0123456789'
+ end
+
+ it 'writes a new encrypted token' do
+ expect(build.read_attribute('token_encrypted')).to be_nil
+ expect(subject).to eq '0123456789'
+ expect(build.read_attribute('token_encrypted')).to be_present
+ end
+
+ it 'does not write a new cleartext token' do
+ expect(build.read_attribute('token')).to be_nil
+ expect(subject).to eq '0123456789'
+ expect(build.read_attribute('token')).to be_nil
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0c3a49cd0f2..87aa5a46c21 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -76,7 +76,7 @@ describe Group do
before do
group.add_developer(user)
- sub_group.add_developer(user)
+ sub_group.add_maintainer(user)
end
it 'also gets notification settings from parent groups' do
@@ -498,7 +498,7 @@ describe Group do
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
- deep_nested_group.add_developer(user_a)
+ deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fca1b1f90d9..188beac1582 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -53,6 +53,29 @@ describe Member do
expect(member).to be_valid
end
end
+
+ context "when a child member inherits its access level" do
+ let(:user) { create(:user) }
+ let(:member) { create(:group_member, :developer, user: user) }
+ let(:child_group) { create(:group, parent: member.group) }
+ let(:child_member) { build(:group_member, group: child_group, user: user) }
+
+ it "requires a higher level" do
+ child_member.access_level = GroupMember::REPORTER
+
+ child_member.validate
+
+ expect(child_member).not_to be_valid
+ end
+
+ it "is valid with a higher level" do
+ child_member.access_level = GroupMember::MAINTAINER
+
+ child_member.validate
+
+ expect(child_member).to be_valid
+ end
+ end
end
describe 'Scopes & finders' do
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 97959ed4304..a3451c67bd8 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -50,4 +50,26 @@ describe GroupMember do
group_member.destroy
end
end
+
+ context 'access levels', :nested_groups do
+ context 'with parent group' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:entity) { create(:group, parent: parent_entity) }
+ end
+ end
+
+ context 'with parent group and a sub subgroup' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:subgroup) { create(:group, parent: parent_entity) }
+ let(:entity) { create(:group, parent: subgroup) }
+ end
+
+ context 'when only the subgroup has the member' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:parent_entity) { create(:group, parent: create(:group)) }
+ let(:entity) { create(:group, parent: parent_entity) }
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 334d4f95f53..097b1bb30dc 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -124,4 +124,19 @@ describe ProjectMember do
end
it_behaves_like 'members notifications', :project
+
+ context 'access levels' do
+ context 'with parent group' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:entity) { create(:project, group: parent_entity) }
+ end
+ end
+
+ context 'with parent group and a subgroup', :nested_groups do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:subgroup) { create(:group, parent: parent_entity) }
+ let(:entity) { create(:project, group: subgroup) }
+ end
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 6ee19c0ddf4..96561dab1c9 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -538,7 +538,7 @@ describe Namespace do
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
- deep_nested_group.add_developer(user_a)
+ deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e5490e0a156..6cb27246f06 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2325,11 +2325,11 @@ describe User do
context 'user is member of all groups' do
before do
- group.add_owner(user)
- nested_group_1.add_owner(user)
- nested_group_1_1.add_owner(user)
- nested_group_2.add_owner(user)
- nested_group_2_1.add_owner(user)
+ group.add_reporter(user)
+ nested_group_1.add_developer(user)
+ nested_group_1_1.add_maintainer(user)
+ nested_group_2.add_developer(user)
+ nested_group_2_1.add_maintainer(user)
end
it 'returns all groups' do
diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb
index c00e41725d9..bb66523a83d 100644
--- a/spec/presenters/group_member_presenter_spec.rb
+++ b/spec/presenters/group_member_presenter_spec.rb
@@ -135,4 +135,12 @@ describe GroupMemberPresenter do
end
end
end
+
+ it_behaves_like '#valid_level_roles', :group do
+ let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Owner' => 50, 'Reporter' => 20 } }
+
+ before do
+ entity.parent = group
+ end
+ end
end
diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb
index 83db5c56cdf..73ef113a1c5 100644
--- a/spec/presenters/project_member_presenter_spec.rb
+++ b/spec/presenters/project_member_presenter_spec.rb
@@ -135,4 +135,10 @@ describe ProjectMemberPresenter do
end
end
end
+
+ it_behaves_like '#valid_level_roles', :project do
+ before do
+ entity.group = group
+ end
+ end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 93e1c3a2294..bb32d581176 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -224,6 +224,37 @@ describe API::Members do
end
end
+ context 'access levels' do
+ it 'does not create the member if group level is higher', :nested_groups do
+ parent = create(:group)
+
+ group.update(parent: parent)
+ project.update(group: group)
+ parent.add_developer(stranger)
+
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ user_id: stranger.id, access_level: Member::REPORTER
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
+ end
+
+ it 'creates the member if group level is lower', :nested_groups do
+ parent = create(:group)
+
+ group.update(parent: parent)
+ project.update(group: group)
+ parent.add_developer(stranger)
+
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ user_id: stranger.id, access_level: Member::MAINTAINER
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['access_level']).to eq(Member::MAINTAINER)
+ end
+ end
+
it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: maintainer.id, access_level: Member::MAINTAINER
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 62b6a3ce42e..e40db55cd20 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1906,7 +1906,7 @@ describe API::Projects do
let(:group) { create(:group) }
let(:group2) do
group = create(:group, name: 'group2_name')
- group.add_owner(user2)
+ group.add_maintainer(user2)
group
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index e779675744c..87185891470 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -20,9 +20,9 @@ describe Ci::RetryBuildService do
CLONE_ACCESSORS = described_class::CLONE_ACCESSORS
REJECT_ACCESSORS =
- %i[id status user token coverage trace runner artifacts_expire_at
- artifacts_file artifacts_metadata artifacts_size created_at
- updated_at started_at finished_at queued_at erased_by
+ %i[id status user token token_encrypted coverage trace runner
+ artifacts_expire_at artifacts_file artifacts_metadata artifacts_size
+ created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index a1ae428586e..003ecb251fe 100644
--- a/spec/support/helpers/features/sorting_helpers.rb
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -13,9 +13,9 @@ module Spec
module Features
module SortingHelpers
def sort_by(value)
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link(value)
end
end
diff --git a/spec/support/shared_examples/file_finder.rb b/spec/support/shared_examples/file_finder.rb
index ef144bdf61c..0dc351b5149 100644
--- a/spec/support/shared_examples/file_finder.rb
+++ b/spec/support/shared_examples/file_finder.rb
@@ -3,18 +3,19 @@ shared_examples 'file finder' do
let(:search_results) { subject.find(query) }
it 'finds by name' do
- filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_name }
- expect(filename).to eq(expected_file_by_name)
- expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ blob = search_results.find { |blob| blob.filename == expected_file_by_name }
+
+ expect(blob.filename).to eq(expected_file_by_name)
+ expect(blob).to be_a(Gitlab::Search::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
end
it 'finds by content' do
- filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_content }
+ blob = search_results.find { |blob| blob.filename == expected_file_by_content }
- expect(filename).to eq(expected_file_by_content)
- expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ expect(blob.filename).to eq(expected_file_by_content)
+ expect(blob).to be_a(Gitlab::Search::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
end
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
new file mode 100644
index 00000000000..77376496854
--- /dev/null
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+shared_examples_for 'inherited access level as a member of entity' do
+ let(:parent_entity) { create(:group) }
+ let(:user) { create(:user) }
+ let(:member) { entity.is_a?(Group) ? entity.group_member(user) : entity.project_member(user) }
+
+ context 'with root parent_entity developer member' do
+ before do
+ parent_entity.add_developer(user)
+ end
+
+ it 'is allowed to be a maintainer of the entity' do
+ entity.add_maintainer(user)
+
+ expect(member.access_level).to eq(Gitlab::Access::MAINTAINER)
+ end
+
+ it 'is not allowed to be a reporter of the entity' do
+ entity.add_reporter(user)
+
+ expect(member).to be_nil
+ end
+
+ it 'is allowed to change to be a developer of the entity' do
+ entity.add_maintainer(user)
+
+ expect { member.update(access_level: Gitlab::Access::DEVELOPER) }
+ .to change { member.access_level }.to(Gitlab::Access::DEVELOPER)
+ end
+
+ it 'is not allowed to change to be a guest of the entity' do
+ entity.add_maintainer(user)
+
+ expect { member.update(access_level: Gitlab::Access::GUEST) }
+ .not_to change { member.reload.access_level }
+ end
+
+ it "shows an error if the member can't be updated" do
+ entity.add_maintainer(user)
+
+ member.update(access_level: Gitlab::Access::REPORTER)
+
+ expect(member.errors.full_messages).to eq(["Access level should be higher than Developer inherited membership from group #{parent_entity.name}"])
+ end
+
+ it 'allows changing the level from a non existing member' do
+ non_member_user = create(:user)
+
+ entity.add_maintainer(non_member_user)
+
+ non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user)
+
+ expect { non_member.update(access_level: Gitlab::Access::GUEST) }
+ .to change { non_member.reload.access_level }
+ end
+ end
+end
+
+shared_examples_for '#valid_level_roles' do |entity_name|
+ let(:member_user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:entity) { create(entity_name) }
+ let(:entity_member) { create("#{entity_name}_member", :developer, source: entity, user: member_user) }
+ let(:presenter) { described_class.new(entity_member, current_user: member_user) }
+ let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Reporter' => 20 } }
+
+ it 'returns all roles when no parent member is present' do
+ expect(presenter.valid_level_roles).to eq(entity_member.class.access_level_roles)
+ end
+
+ it 'returns higher roles when a parent member is present' do
+ group.add_reporter(member_user)
+
+ expect(presenter.valid_level_roles).to eq(expected_roles)
+ end
+end