diff options
98 files changed, 1249 insertions, 1110 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de0606589f..fda536ae157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,6 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. -## 12.4.3 - -- No changes. - ## 12.4.2 ### Fixed (10 changes) diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 334c162954e..32491dfbcb6 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -168,7 +168,7 @@ export default { } const recentBoardsPromise = new Promise((resolve, reject) => - gl.boardService + boardsStore .recentBoards() .then(resolve) .catch(err => { @@ -184,7 +184,7 @@ export default { }), ); - Promise.all([gl.boardService.allBoards(), recentBoardsPromise]) + Promise.all([boardsStore.allBoards(), recentBoardsPromise]) .then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data]) .then(([allBoardsJson, recentBoardsJson]) => { this.loading = false; diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 6a32cef79bc..66ce1ab5659 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, one-var, no-else-return */ +/* eslint-disable func-names, no-else-return */ import $ from 'jquery'; import Api from './api'; @@ -7,7 +7,7 @@ import { s__ } from './locale'; const projectSelect = () => { $('.ajax-project-select').each(function(i, select) { - var placeholder; + let placeholder; const simpleFilter = $(select).data('simpleFilter') || false; const isInstantiated = $(select).data('select2'); this.groupId = $(select).data('groupId'); @@ -31,20 +31,17 @@ const projectSelect = () => { placeholder, minimumInputLength: 0, query: query => { - var finalCallback, projectsCallback; - finalCallback = function(projects) { - var data; - data = { + let projectsCallback; + const finalCallback = function(projects) { + const data = { results: projects, }; return query.callback(data); }; if (this.includeGroups) { projectsCallback = function(projects) { - var groupsCallback; - groupsCallback = function(groups) { - var data; - data = groups.concat(projects); + const groupsCallback = function(groups) { + const data = groups.concat(projects); return finalCallback(data); }; return Api.groups(query.term, {}, groupsCallback); diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index 26c1f5813f5..70678b0db37 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -1,5 +1,6 @@ <script> import { GlTooltipDirective, GlLink, GlButton, GlLoadingIcon } from '@gitlab/ui'; +import defaultAvatarUrl from 'images/no_avatar.png'; import { sprintf, s__ } from '~/locale'; import Icon from '../../vue_shared/components/icon.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -83,6 +84,7 @@ export default { this.showDescription = !this.showDescription; }, }, + defaultAvatarUrl, }; </script> @@ -97,6 +99,9 @@ export default { :img-size="40" class="avatar-cell" /> + <span v-else class="avatar-cell user-avatar-link"> + <img :src="$options.defaultAvatarUrl" width="40" height="40" class="avatar s40" /> + </span> <div class="commit-detail flex-list"> <div class="commit-content qa-commit-content"> <gl-link :href="commit.webUrl" class="commit-row-message item-title"> @@ -119,6 +124,9 @@ export default { > {{ commit.author.name }} </gl-link> + <template v-else> + {{ commit.authorName }} + </template> {{ s__('LastCommit|authored') }} <timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" /> </div> @@ -132,9 +140,8 @@ export default { </div> <div class="commit-actions flex-row"> <div v-if="commit.signatureHtml" v-html="commit.signatureHtml"></div> - <div class="ci-status-link"> + <div v-if="commit.pipeline" class="ci-status-link"> <gl-link - v-if="commit.pipeline" v-gl-tooltip.left :href="commit.pipeline.detailedStatus.detailsPath" :title="statusTitle" diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index ac20549acb8..8f2e9264bca 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -75,6 +75,7 @@ export default { v-for="entry in val" :id="entry.id" :key="`${entry.flatPath}-${entry.id}`" + :sha="entry.sha" :project-path="projectPath" :current-path="path" :name="entry.name" diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 52f861fbb61..cf0457a2abf 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -37,6 +37,10 @@ export default { type: String, required: true, }, + sha: { + type: String, + required: true, + }, projectPath: { type: String, required: true, @@ -98,7 +102,7 @@ export default { return this.path.replace(new RegExp(`^${this.currentPath}/`), ''); }, shortSha() { - return this.id.slice(0, 8); + return this.sha.slice(0, 8); }, hasLockLabel() { return this.commit && this.commit.lockLabel; diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index 87310278b9e..5bf30e625a0 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -26,9 +26,12 @@ export function fetchLogsTree(client, path, offset, resolver = null) { const { ref } = client.readQuery({ query: getRef }); fetchpromise = axios - .get(`${gon.gitlab_url}/${projectPath}/refs/${ref}/logs_tree/${path.replace(/^\//, '')}`, { - params: { format: 'json', offset }, - }) + .get( + `${gon.relative_url_root}/${projectPath}/refs/${ref}/logs_tree/${path.replace(/^\//, '')}`, + { + params: { format: 'json', offset }, + }, + ) .then(({ data, headers }) => { const headerLogsOffset = headers['more-logs-offset']; const { commits } = client.readQuery({ query: getCommits }); diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql index c4814f8e63a..2aaf5066b4a 100644 --- a/app/assets/javascripts/repository/queries/getFiles.query.graphql +++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql @@ -2,6 +2,7 @@ fragment TreeEntry on Entry { id + sha name flatPath type diff --git a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql index 4bb959a8001..9be025afe39 100644 --- a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql +++ b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql @@ -8,6 +8,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { description webUrl authoredDate + authorName author { name avatarUrl diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 341c9534763..611001df32f 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -218,7 +218,7 @@ export default { display: inline-block; flex: 1; max-width: inherit; - height: 18px; + height: 19px; line-height: 16px; text-overflow: ellipsis; white-space: nowrap; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index a669e004d3a..2d826064569 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -357,12 +357,18 @@ } } - .filter-dropdown-container > div { - margin: 0; + .filter-dropdown-container { + > div { + margin: 0; - > .btn { - margin: 0 0 10px; - width: 100%; + > .btn { + margin: 0 0 10px; + width: 100%; + } + } + + .board-labels-toggle-wrapper { + margin-bottom: $gl-input-padding; } } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 45d0579052a..d26979bc174 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -497,7 +497,7 @@ .add-issues-footer-to-list { padding-left: $gl-vert-padding; padding-right: $gl-vert-padding; - line-height: 34px; + line-height: $input-height; } .issue-card-selected { @@ -551,9 +551,5 @@ * Make the wrapper the same height as a button so it aligns properly when the * filtered-search-box input element increases in size on Linux smaller breakpoints */ - height: 34px; - - @include media-breakpoint-down(sm) { - margin-bottom: 10px; - } + height: $input-height; } diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb index dcf4a2802c7..87f84ec576f 100644 --- a/app/graphql/types/commit_type.rb +++ b/app/graphql/types/commit_type.rb @@ -24,6 +24,8 @@ module Types description: 'Web URL of the commit' field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true, description: 'Rendered HTML of the commit signature' + field :author_name, type: GraphQL::STRING_TYPE, null: true, + description: 'Commit authors name' # models/commit lazy loads the author by email field :author, type: Types::UserType, null: true, diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index f71dd114d1f..48ff5819286 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -8,6 +8,7 @@ module Types value 'DUE_DATE_ASC', 'Due date by ascending order', value: 'due_date_asc' value 'DUE_DATE_DESC', 'Due date by descending order', value: 'due_date_desc' + value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: 'relative_position_asc' end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/tree/entry_type.rb b/app/graphql/types/tree/entry_type.rb index 10c2ad8815e..87a3eced896 100644 --- a/app/graphql/types/tree/entry_type.rb +++ b/app/graphql/types/tree/entry_type.rb @@ -5,6 +5,7 @@ module Types include Types::BaseInterface field :id, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions + field :sha, GraphQL::STRING_TYPE, null: false, description: "Last commit sha for entry", method: :id field :name, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions field :type, Tree::TypeEnum, null: false # rubocop:disable Graphql/Descriptions field :path, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4804364d404..59a2c09bd28 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -898,12 +898,6 @@ module Ci value.with_indifferent_access end end - - def build_attributes_from_config - return {} unless pipeline.config_processor - - pipeline.config_processor.build_attributes(name) - end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f9840e13e03..f730b949ee9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -551,23 +551,6 @@ module Ci end end - def stage_seeds - return [] unless config_processor - - strong_memoize(:stage_seeds) do - seeds = config_processor.stages_attributes.inject([]) do |previous_stages, attributes| - seed = Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes, previous_stages) - previous_stages + [seed] - end - - seeds.select(&:included?) - end - end - - def seeds_size - stage_seeds.sum(&:size) - end - def has_kubernetes_active? project.deployment_platform&.active? end @@ -587,62 +570,14 @@ module Ci end end - def set_config_source - if ci_yaml_from_repo - self.config_source = :repository_source - elsif implied_ci_yaml_file - self.config_source = :auto_devops_source - end - end - - ## - # TODO, setting yaml_errors should be moved to the pipeline creation chain. - # - def config_processor - return unless ci_yaml_file - return @config_processor if defined?(@config_processor) - - @config_processor ||= begin - ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha, user: user }) - rescue Gitlab::Ci::YamlProcessor::ValidationError => e - self.yaml_errors = e.message - nil - rescue => ex - self.yaml_errors = "Undefined error (#{Labkit::Correlation::CorrelationId.current_id})" - - Gitlab::Sentry.track_acceptable_exception(ex, extra: { - project_id: project.id, - sha: sha, - ci_yaml_file: ci_yaml_file_path - }) - nil - end - end - - def ci_yaml_file_path + # TODO: this logic is duplicate with Pipeline::Chain::Config::Content + # we should persist this is `ci_pipelines.config_path` + def config_path return unless repository_source? || unknown_source? project.ci_config_path.presence || '.gitlab-ci.yml' end - def ci_yaml_file - return @ci_yaml_file if defined?(@ci_yaml_file) - - @ci_yaml_file = - if auto_devops_source? - implied_ci_yaml_file - else - ci_yaml_from_repo - end - - if @ci_yaml_file - @ci_yaml_file - else - self.yaml_errors = "Failed to load CI/CD config file for #{sha}" - nil - end - end - def has_yaml_errors? yaml_errors.present? end @@ -711,7 +646,7 @@ module Ci def predefined_variables 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_CONFIG_PATH', value: config_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) @@ -906,24 +841,6 @@ module Ci private - def ci_yaml_from_repo - return unless project - return unless sha - return unless ci_yaml_file_path - - project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path) - rescue GRPC::NotFound, GRPC::Internal - nil - end - - def implied_ci_yaml_file - return unless project - - if project.auto_devops_enabled? - Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content - end - end - def pipeline_data Gitlab::DataBuilder::Pipeline.build(self) end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index ce98a3ad3d6..9c2b0372d54 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -57,18 +57,20 @@ module Storage # Move the namespace directory in all storages used by member projects repository_storages(legacy_only: true).each do |repository_storage| # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage, full_path_before_last_save) + Gitlab::GitalyClient::NamespaceService.allow do + gitlab_shell.add_namespace(repository_storage, full_path_before_last_save) - # Ensure new directory exists before moving it (if there's a parent) - gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent + # Ensure new directory exists before moving it (if there's a parent) + gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent - unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path) + unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path) - Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger + Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger - # if we cannot move namespace directory we should rollback - # db changes in order to prevent out of sync between db and fs - raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') + end end end end @@ -95,13 +97,15 @@ module Storage # We will remove it later async new_path = "#{full_path}+#{id}+deleted" - if gitlab_shell.mv_namespace(repository_storage, full_path, new_path) - Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}") + Gitlab::GitalyClient::NamespaceService.allow do + if gitlab_shell.mv_namespace(repository_storage, full_path, new_path) + Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}") - # Remove namespace directory async with delay so - # GitLab has time to remove all projects first - run_after_commit do - GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path) + # Remove namespace directory async with delay so + # GitLab has time to remove all projects first + run_after_commit do + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path) + end end end end diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index a495d34c07c..d089a004d3d 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ProjectCiCdSetting < ApplicationRecord + # TODO: remove once GitLab 12.7 is released + # https://gitlab.com/gitlab-org/gitlab/issues/36651 + self.ignored_columns += %i[merge_trains_enabled] belongs_to :project, inverse_of: :ci_cd_settings # The version of the schema that first introduced this model/table. diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 6d370f6241c..81018398d5d 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -21,7 +21,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def statistics_anchors(show_auto_devops_callout:) [ - license_anchor_data, commits_anchor_data, branches_anchor_data, tags_anchor_data, @@ -32,6 +31,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def statistics_buttons(show_auto_devops_callout:) [ readme_anchor_data, + license_anchor_data, changelog_anchor_data, contribution_guide_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), @@ -41,15 +41,14 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def empty_repo_statistics_anchors - [ - license_anchor_data - ].compact.select { |item| item.is_link } + [] end def empty_repo_statistics_buttons [ new_file_anchor_data, readme_anchor_data, + license_anchor_data, changelog_anchor_data, contribution_guide_anchor_data, gitlab_ci_anchor_data @@ -227,17 +226,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated icon = statistic_icon('scale') if repository.license_blob.present? - AnchorData.new(true, - icon + content_tag(:strong, license_short_name, class: 'project-stat-value'), - license_path) + AnchorData.new(false, + icon + content_tag(:span, license_short_name, class: 'project-stat-value'), + license_path, + 'default') else if current_user && can_current_user_push_to_default_branch? - AnchorData.new(true, - content_tag(:span, icon + _('Add license'), class: 'add-license-link d-flex'), + AnchorData.new(false, + content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'), add_license_path) else - AnchorData.new(true, - icon + content_tag(:strong, _('No license. All rights reserved'), class: 'project-stat-value'), + AnchorData.new(false, + icon + content_tag(:span, _('No license. All rights reserved'), class: 'project-stat-value'), nil) end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 74c188c39d5..5778a48bce6 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -7,12 +7,14 @@ module Ci CreateError = Class.new(StandardError) SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build, - Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs, Gitlab::Ci::Pipeline::Chain::Validate::Abilities, Gitlab::Ci::Pipeline::Chain::Validate::Repository, - Gitlab::Ci::Pipeline::Chain::Validate::Config, + Gitlab::Ci::Pipeline::Chain::Config::Content, + Gitlab::Ci::Pipeline::Chain::Config::Process, + Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs, Gitlab::Ci::Pipeline::Chain::Skip, Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules, + Gitlab::Ci::Pipeline::Chain::Seed, Gitlab::Ci::Pipeline::Chain::Limit::Size, Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Create, diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index f9408184cb6..4d8cba5168d 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -39,10 +39,6 @@ %th = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - - elsif pipeline.project.builds_enabled? && !pipeline.ci_yaml_file - .bs-callout.bs-callout-warning - = _("%{gitlab_ci_yml} not found in this commit") % { gitlab_ci_yml: ".gitlab-ci.yml" } - - if @pipeline.failed_builds.present? #js-tab-failures.build-failures.tab-pane.build-page %table.table.responsive-table.ci-table.responsive-table-sm-rounded diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index e6335284541..d341520e4a2 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -5,169 +5,170 @@ - user_can_admin_list = board && can?(current_user, :admin_list, board.resource_parent) .issues-filters{ class: ("w-100" if type == :boards_modal) } - .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal } - - if type == :boards - = render "shared/boards/switcher", board: board - = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do - - if params[:search].present? - = hidden_field_tag :search, params[:search] - - if @can_bulk_update - .check-all-holder.d-none.d-sm-block.hidden - = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left" - .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row - .filtered-search-box - - if type != :boards_modal && type != :boards - = dropdown_tag(_('Recent searches'), - options: { wrapper_class: "filtered-search-history-dropdown-wrapper", - toggle_class: "filtered-search-history-dropdown-toggle-button", - dropdown_class: "filtered-search-history-dropdown", - content_class: "filtered-search-history-dropdown-content" }) do - .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } } - .filtered-search-box-input-container.droplab-dropdown - .scroll-container - %ul.tokens-container.list-unstyled - %li.input-token - %input.form-control.filtered-search{ search_filter_input_options(type) } - #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { action: 'submit' } } - %button.btn.btn-link{ type: 'button' } - = sprite_icon('search') - %span - = _('Press Enter or click to search') - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link{ type: 'button' } - -# Encapsulate static class name `{{icon}}` inside #{} to bypass - -# haml lint's ClassAttributeWithStaticValue - %svg - %use{ 'xlink:href': "#{'{{icon}}'}" } - %span.js-filter-hint - {{hint}} - %span.js-filter-tag.dropdown-light-content - {{tag}} - #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu - - if current_user + .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-lg-row{ class: block_css_class, "v-pre" => type == :boards_modal } + .d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0 + - if type == :boards + = render "shared/boards/switcher", board: board + = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + - if @can_bulk_update + .check-all-holder.d-none.d-sm-block.hidden + = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left" + .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row + .filtered-search-box + - if type != :boards_modal && type != :boards + = dropdown_tag(_('Recent searches'), + options: { wrapper_class: "filtered-search-history-dropdown-wrapper", + toggle_class: "filtered-search-history-dropdown-toggle-button", + dropdown_class: "filtered-search-history-dropdown", + content_class: "filtered-search-history-dropdown-content" }) do + .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } } + .filtered-search-box-input-container.droplab-dropdown + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ search_filter_input_options(type) } + #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } - = render 'shared/issuable/user_dropdown_item', - user: current_user - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - = render 'shared/issuable/user_dropdown_item', - user: User.new(username: '{{username}}', name: '{{name}}'), - avatar: { lazy: true, url: '{{avatar_url}}' } - #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore + %li.filter-dropdown-item{ data: { action: 'submit' } } + %button.btn.btn-link{ type: 'button' } + = sprite_icon('search') + %span + = _('Press Enter or click to search') + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link{ type: 'button' } + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %svg + %use{ 'xlink:href': "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu - if current_user + %ul{ data: { dropdown: true } } + = render 'shared/issuable/user_dropdown_item', + user: current_user + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + = render 'shared/issuable/user_dropdown_item', + user: User.new(username: '{{username}}', name: '{{name}}'), + avatar: { lazy: true, url: '{{avatar_url}}' } + #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + - if current_user + = render 'shared/issuable/user_dropdown_item', + user: current_user + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } = render 'shared/issuable/user_dropdown_item', - user: current_user - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - = render 'shared/issuable/user_dropdown_item', - user: User.new(username: '{{username}}', name: '{{name}}'), - avatar: { lazy: true, url: '{{avatar_url}}' } - = render_if_exists 'shared/issuable/approver_dropdown' - #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.btn.btn-link{ type: 'button' } - = _('Any') - %li.filter-dropdown-item{ data: { value: 'Upcoming' } } - %button.btn.btn-link{ type: 'button' } - = _('Upcoming') - %li.filter-dropdown-item{ data: { value: 'Started' } } - %button.btn.btn-link{ type: 'button' } - = _('Started') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link.js-data-value{ type: 'button' } - {{title}} - #js-dropdown-release.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link.js-data-value{ type: 'button' } - {{title}} - #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link{ type: 'button' } - %span.dropdown-label-box{ style: 'background: {{color}}' } - %span.label-title.js-data-value + user: User.new(username: '{{username}}', name: '{{name}}'), + avatar: { lazy: true, url: '{{avatar_url}}' } + = render_if_exists 'shared/issuable/approver_dropdown' + #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.filter-dropdown-item{ data: { value: 'Upcoming' } } + %button.btn.btn-link{ type: 'button' } + = _('Upcoming') + %li.filter-dropdown-item{ data: { value: 'Started' } } + %button.btn.btn-link{ type: 'button' } + = _('Started') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value{ type: 'button' } + {{title}} + #js-dropdown-release.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value{ type: 'button' } + {{title}} + #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link{ type: 'button' } + %span.dropdown-label-box{ style: 'background: {{color}}' } + %span.label-title.js-data-value + {{title}} + #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link{ type: 'button' } + %gl-emoji + %span.js-data-value.prepend-left-10 + {{name}} + #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('No') + #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('No') + #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value.monospace {{title}} - #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu - %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'None' } } - %button.btn.btn-link{ type: 'button' } - = _('None') - %li.filter-dropdown-item{ data: { value: 'Any' } } - %button.btn.btn-link{ type: 'button' } - = _('Any') - %li.divider.droplab-item-ignore - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link{ type: 'button' } - %gl-emoji - %span.js-data-value.prepend-left-10 - {{name}} - #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } - %button.btn.btn-link{ type: 'button' } - = _('Yes') - %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } - %button.btn.btn-link{ type: 'button' } - = _('No') - #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } - %button.btn.btn-link{ type: 'button' } - = _('Yes') - %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } - %button.btn.btn-link{ type: 'button' } - = _('No') - #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link.js-data-value.monospace - {{title}} - = render_if_exists 'shared/issuable/filter_weight', type: type + = render_if_exists 'shared/issuable/filter_weight', type: type - %button.clear-search.hidden{ type: 'button' } - = icon('times') - #js-board-labels-toggle - .filter-dropdown-container.d-flex.flex-column.flex-md-row - - if type == :boards - .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } } - - if user_can_admin_list - = render 'shared/issuable/board_create_list_dropdown', board: board - - if @project - #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } - #js-toggle-focus-btn - - elsif is_not_boards_modal_or_productivity_analytics - = render 'shared/issuable/sort_dropdown' + %button.clear-search.hidden{ type: 'button' } + = icon('times') + .filter-dropdown-container.d-flex.flex-column.flex-md-row + #js-board-labels-toggle + - if type == :boards + .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } } + - if user_can_admin_list + = render 'shared/issuable/board_create_list_dropdown', board: board + - if @project + #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } + #js-toggle-focus-btn + - elsif is_not_boards_modal_or_productivity_analytics + = render 'shared/issuable/sort_dropdown' diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index 5dcf901e041..57e64570c09 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -8,6 +8,8 @@ class GitlabShellWorker latency_sensitive_worker! def perform(action, *arg) - gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend + Gitlab::GitalyClient::NamespaceService.allow do + gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend + end end end diff --git a/changelogs/unreleased/28302-move-add-license-button.yml b/changelogs/unreleased/28302-move-add-license-button.yml new file mode 100644 index 00000000000..d9a5c15990f --- /dev/null +++ b/changelogs/unreleased/28302-move-add-license-button.yml @@ -0,0 +1,5 @@ +--- +title: Move add license button to project buttons +merge_request: 19370 +author: +type: changed diff --git a/changelogs/unreleased/29713-graphql-add-issue-relative-position-sort-2.yml b/changelogs/unreleased/29713-graphql-add-issue-relative-position-sort-2.yml new file mode 100644 index 00000000000..38a02a027de --- /dev/null +++ b/changelogs/unreleased/29713-graphql-add-issue-relative-position-sort-2.yml @@ -0,0 +1,5 @@ +--- +title: 'Graphql query for issues can now be sorted by relative_position' +merge_request: 19713 +author: +type: added diff --git a/changelogs/unreleased/Update-boards_selector-vue-to-use-boardsStore.yml b/changelogs/unreleased/Update-boards_selector-vue-to-use-boardsStore.yml new file mode 100644 index 00000000000..95234bd9069 --- /dev/null +++ b/changelogs/unreleased/Update-boards_selector-vue-to-use-boardsStore.yml @@ -0,0 +1,5 @@ +--- +title: remove all references of BoardService in boards_selector.vue +merge_request: 20147 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/remove_var_from_project_select_js.yml b/changelogs/unreleased/remove_var_from_project_select_js.yml new file mode 100644 index 00000000000..736f8adc5da --- /dev/null +++ b/changelogs/unreleased/remove_var_from_project_select_js.yml @@ -0,0 +1,5 @@ +--- +title: Remove var from project_select.js +merge_request: 20091 +author: Lee Tickett +type: other diff --git a/changelogs/unreleased/sh-add-exception-backtrace-production-log.yml b/changelogs/unreleased/sh-add-exception-backtrace-production-log.yml new file mode 100644 index 00000000000..05e17892611 --- /dev/null +++ b/changelogs/unreleased/sh-add-exception-backtrace-production-log.yml @@ -0,0 +1,5 @@ +--- +title: Add backtrace to production_json.log +merge_request: 20122 +author: +type: changed diff --git a/changelogs/unreleased/sh-support-project-template-id-in-api.yml b/changelogs/unreleased/sh-support-project-template-id-in-api.yml new file mode 100644 index 00000000000..5087c6c711a --- /dev/null +++ b/changelogs/unreleased/sh-support-project-template-id-in-api.yml @@ -0,0 +1,5 @@ +--- +title: Support template_project_id parameter in project creation API +merge_request: 20258 +author: +type: added diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index d5d4c589884..769ef2af0e7 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -10,6 +10,11 @@ unless Sidekiq.server? # unmaintained gem that monkey patches `Time` config.lograge.formatter = Lograge::Formatters::Json.new config.lograge.logger = ActiveSupport::Logger.new(filename) + config.lograge.before_format = lambda do |data, payload| + data.delete(:error) + data + end + # Add request parameters to log output config.lograge.custom_options = lambda do |event| params = event.payload[:params] @@ -36,6 +41,20 @@ unless Sidekiq.server? payload[:cpu_s] = cpu_s end + # https://github.com/roidrage/lograge#logging-errors--exceptions + exception = event.payload[:exception_object] + + if exception + payload[:exception] = { + class: exception.class.name, + message: exception.message + } + + if exception.backtrace + payload[:exception][:backtrace] = Gitlab::Profiler.clean_backtrace(exception.backtrace) + end + end + payload end end diff --git a/doc/administration/logs.md b/doc/administration/logs.md index dae0dae8395..aa10cdd220c 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -42,6 +42,48 @@ User clone/fetch activity using http transport appears in this log as `action: g In addition, the log contains the IP address from which the request originated (`remote_ip`) as well as the user's ID (`user_id`), and username (`username`). +NOTE: **Note:** Starting with GitLab 12.5, if an error occurs, an +`exception` field is included with `class`, `message`, and +`backtrace`. Previous versions included an `error` field instead of +`exception.class` and `exception.message`. For example: + +```json +{ + "method": "GET", + "path": "/admin", + "format": "html", + "controller": "Admin::DashboardController", + "action": "index", + "status": 500, + "duration": 2584.11, + "view": 0, + "db": 9.21, + "time": "2019-11-14T13:12:46.156Z", + "params": [], + "remote_ip": "127.0.0.1", + "user_id": 1, + "username": "root", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0", + "queue_duration": 274.35, + "correlation_id": "KjDVUhNvvV3", + "cpu_s": 2.837645135999999, + "exception": { + "class": "NameError", + "message": "undefined local variable or method `adsf' for #<Admin::DashboardController:0x00007ff3c9648588>", + "backtrace": [ + "app/controllers/admin/dashboard_controller.rb:11:in `index'", + "ee/app/controllers/ee/admin/dashboard_controller.rb:14:in `index'", + "ee/lib/gitlab/ip_address_state.rb:10:in `with'", + "ee/app/controllers/ee/application_controller.rb:43:in `set_current_ip_address'", + "lib/gitlab/session.rb:11:in `with_session'", + "app/controllers/application_controller.rb:450:in `set_session_storage'", + "app/controllers/application_controller.rb:444:in `set_locale'", + "ee/lib/gitlab/jira/middleware.rb:19:in `call'" + ] + } +} +``` + ## `production.log` This file lives in `/var/log/gitlab/gitlab-rails/production.log` for diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 8a4e8166dde..a357c93b020 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -76,6 +76,11 @@ type Blob implements Entry { lfsOid: String name: String! path: String! + + """ + Last commit sha for entry + """ + sha: String! type: EntryType! webUrl: String } @@ -122,6 +127,11 @@ type Commit { author: User """ + Commit authors name + """ + authorName: String + + """ Timestamp of when the commit was authored """ authoredDate: Time @@ -1100,6 +1110,11 @@ interface Entry { id: ID! name: String! path: String! + + """ + Last commit sha for entry + """ + sha: String! type: EntryType! } @@ -2518,6 +2533,11 @@ enum IssueSort { DUE_DATE_DESC """ + Relative position by ascending order + """ + RELATIVE_POSITION_ASC + + """ Created at ascending order """ created_asc @@ -4767,6 +4787,11 @@ type Submodule implements Entry { id: ID! name: String! path: String! + + """ + Last commit sha for entry + """ + sha: String! treeUrl: String type: EntryType! webUrl: String @@ -5113,6 +5138,11 @@ type TreeEntry implements Entry { id: ID! name: String! path: String! + + """ + Last commit sha for entry + """ + sha: String! type: EntryType! webUrl: String } diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index bf0cb2ca6f2..fea67f28d69 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -10228,6 +10228,20 @@ "deprecationReason": null }, { + "name": "authorName", + "description": "Commit authors name", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "authoredDate", "description": "Timestamp of when the commit was authored", "args": [ @@ -11333,6 +11347,24 @@ "deprecationReason": null }, { + "name": "sha", + "description": "Last commit sha for entry", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "type", "description": null, "args": [ @@ -11454,6 +11486,24 @@ "deprecationReason": null }, { + "name": "sha", + "description": "Last commit sha for entry", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "type", "description": null, "args": [ @@ -11712,6 +11762,24 @@ "deprecationReason": null }, { + "name": "sha", + "description": "Last commit sha for entry", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "treeUrl", "description": null, "args": [ @@ -11973,6 +12041,24 @@ "deprecationReason": null }, { + "name": "sha", + "description": "Last commit sha for entry", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "type", "description": null, "args": [ @@ -13680,6 +13766,12 @@ "description": "Due date by descending order", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "RELATIVE_POSITION_ASC", + "description": "Relative position by ascending order", + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8432e9a43c3..151e43f4cff 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -36,6 +36,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | Name | Type | Description | | --- | ---- | ---------- | | `id` | ID! | | +| `sha` | String! | Last commit sha for entry | | `name` | String! | | | `type` | EntryType! | | | `path` | String! | | @@ -55,6 +56,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `authoredDate` | Time | Timestamp of when the commit was authored | | `webUrl` | String! | Web URL of the commit | | `signatureHtml` | String | Rendered HTML of the commit signature | +| `authorName` | String | Commit authors name | | `author` | User | Author of the commit | | `latestPipeline` | Pipeline | Latest pipeline of the commit | @@ -738,6 +740,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | Name | Type | Description | | --- | ---- | ---------- | | `id` | ID! | | +| `sha` | String! | Last commit sha for entry | | `name` | String! | | | `type` | EntryType! | | | `path` | String! | | @@ -794,6 +797,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | Name | Type | Description | | --- | ---- | ---------- | | `id` | ID! | | +| `sha` | String! | Last commit sha for entry | | `name` | String! | | | `type` | EntryType! | | | `path` | String! | | diff --git a/doc/api/projects.md b/doc/api/projects.md index 2ec412d0f56..222ab729810 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -948,6 +948,7 @@ POST /projects | `mirror_trigger_builds` | boolean | no | **(STARTER)** Pull mirroring triggers builds | | `initialize_with_readme` | boolean | no | `false` by default | | `template_name` | string | no | When used without `use_custom_template`, name of a [built-in project template](../gitlab-basics/create-project.md#built-in-templates). When used with `use_custom_template`, name of a custom project template | +| `template_project_id` | integer | no | **(PREMIUM)** When used with `use_custom_template`, project ID of a custom project template. This is preferable to using `template_name` since `template_name` may be ambiguous. | | `use_custom_template` | boolean | no | **(PREMIUM)** Use either custom [instance](../user/admin_area/custom_project_templates.md) or [group](../user/group/custom_project_templates.md) (with `group_with_project_templates_id`) project template | | `group_with_project_templates_id` | integer | no | **(PREMIUM)** For group-level custom templates, specifies ID of group from which all the custom project templates are sourced. Leave empty for instance-level templates. Requires `use_custom_template` to be true | diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index fe4f6d7bec8..894a613ec2d 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -19,6 +19,14 @@ To save duplicated clients getting created in different apps, we have a [default client][default-client] that should be used. This setups the Apollo client with the correct URL and also sets the CSRF headers. +Default client accepts two parameters: `resolvers` and `config`. + +- `resolvers` parameter is created to accept an object of resolvers for [local state management](#local-state-with-apollo) queries and mutations +- `config` parameter takes an object of configuration settings: + - `cacheConfig` field accepts an optional object of settings to [customize Apollo cache](https://github.com/apollographql/apollo-client/tree/master/packages/apollo-cache-inmemory#configuration) + - `baseUrl` allows us to pass a URL for GraphQL endpoint different from our main endpoint (i.e.`${gon.relative_url_root}/api/graphql`) + - `assumeImmutableResults` (set to `false` by default) - this setting, when set to `true`, will assume that every single operation on updating Apollo Cache is immutable. It also sets `freezeResults` to `true`, so any attempt on mutating Apollo Cache will throw a console warning in development environment. Please ensure you're following the immutability pattern on cache update operations before setting this option to `true`. + ## GraphQL Queries To save query compilation at runtime, webpack can directly import `.graphql` diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 3d10f41d2e0..669def2b63c 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -71,7 +71,8 @@ module API optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' optional :import_url, type: String, desc: 'URL from which the project is imported' optional :template_name, type: String, desc: "Name of template from which to create project" - mutually_exclusive :import_url, :template_name + optional :template_project_id, type: Integer, desc: "Project ID of template from which to create project" + mutually_exclusive :import_url, :template_name, :template_project_id end def load_projects diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb index bab1c73e2f1..aabdf7ce47d 100644 --- a/lib/gitlab/ci/pipeline/chain/base.rb +++ b/lib/gitlab/ci/pipeline/chain/base.rb @@ -5,7 +5,7 @@ module Gitlab module Pipeline module Chain class Base - attr_reader :pipeline, :command + attr_reader :pipeline, :command, :config delegate :project, :current_user, to: :command diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index 899df81ea5c..9662209f88e 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -22,8 +22,6 @@ module Gitlab external_pull_request: @command.external_pull_request, variables_attributes: Array(@command.variables_attributes) ) - - @pipeline.set_config_source end def break? diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 58f89a6be5e..c2df419cca0 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -10,7 +10,9 @@ module Gitlab :trigger_request, :schedule, :merge_request, :external_pull_request, :ignore_skip_ci, :save_incompleted, :seeds_block, :variables_attributes, :push_options, - :chat_data, :allow_mirror_update + :chat_data, :allow_mirror_update, + # These attributes are set by Chains during processing: + :config_content, :config_processor, :stage_seeds ) do include Gitlab::Utils::StrongMemoize diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb new file mode 100644 index 00000000000..a8cd99b8e92 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content < Chain::Base + include Chain::Helpers + + def perform! + return if @command.config_content + + if content = content_from_repo + @command.config_content = content + @pipeline.config_source = :repository_source + # TODO: we should persist ci_config_path + # @pipeline.config_path = ci_config_path + elsif content = content_from_auto_devops + @command.config_content = content + @pipeline.config_source = :auto_devops_source + end + + unless @command.config_content + return error("Missing #{ci_config_path} file") + end + end + + def break? + @pipeline.errors.any? || @pipeline.persisted? + end + + private + + def content_from_repo + return unless project + return unless @pipeline.sha + return unless ci_config_path + + project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path) + rescue GRPC::NotFound, GRPC::Internal + nil + end + + def content_from_auto_devops + return unless project&.auto_devops_enabled? + + Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content + end + + def ci_config_path + project.ci_config_path.presence || '.gitlab-ci.yml' + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb new file mode 100644 index 00000000000..731b0fdb286 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/process.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Process < Chain::Base + include Chain::Helpers + + def perform! + raise ArgumentError, 'missing config content' unless @command.config_content + + @command.config_processor = ::Gitlab::Ci::YamlProcessor.new( + @command.config_content, { + project: project, + sha: @pipeline.sha, + user: current_user + } + ) + rescue Gitlab::Ci::YamlProcessor::ValidationError => ex + error(ex.message, config_error: true) + rescue => ex + Gitlab::Sentry.track_acceptable_exception(ex, extra: { + project_id: project.id, + sha: @pipeline.sha + }) + + error("Undefined error (#{Labkit::Correlation::CorrelationId.current_id})", + config_error: true) + end + + def break? + @pipeline.errors.any? || @pipeline.persisted? + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb index 5b46a43b725..0ee9485eebc 100644 --- a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb +++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb @@ -41,7 +41,7 @@ module Gitlab end def workflow_config - @pipeline.config_processor.workflow_attributes || {} + @command.config_processor.workflow_attributes || {} end end end diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index 13eca5a9d28..3a40c7b167c 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -10,29 +10,12 @@ module Gitlab PopulateError = Class.new(StandardError) def perform! - # Allocate next IID. This operation must be outside of transactions of pipeline creations. - pipeline.ensure_project_iid! - - # Protect the pipeline. This is assigned in Populate instead of - # Build to prevent erroring out on ambiguous refs. - pipeline.protected = @command.protected_ref? - - ## - # Populate pipeline with block argument of CreatePipelineService#execute. - # - @command.seeds_block&.call(pipeline) - - ## - # Gather all runtime build/stage errors - # - if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence - return error(seeds_errors.join("\n"), config_error: true) - end + raise ArgumentError, 'missing stage seeds' unless @command.stage_seeds ## # Populate pipeline with all stages, and stages with builds. # - pipeline.stages = pipeline.stage_seeds.map(&:to_resource) + pipeline.stages = @command.stage_seeds.map(&:to_resource) if pipeline.stages.none? return error('No stages / jobs for this pipeline.') diff --git a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb index 1e09b417311..9267c72efa4 100644 --- a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb +++ b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb @@ -6,11 +6,13 @@ module Gitlab module Chain class RemoveUnwantedChatJobs < Chain::Base def perform! - return unless pipeline.config_processor && pipeline.chat? + raise ArgumentError, 'missing config processor' unless @command.config_processor + + return unless pipeline.chat? # When scheduling a chat pipeline we only want to run the build # that matches the chat command. - pipeline.config_processor.jobs.select! do |name, _| + @command.config_processor.jobs.select! do |name, _| name.to_s == command.chat_data[:command].to_s end end diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb new file mode 100644 index 00000000000..2e177cfec7e --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/seed.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + class Seed < Chain::Base + include Chain::Helpers + include Gitlab::Utils::StrongMemoize + + def perform! + raise ArgumentError, 'missing config processor' unless @command.config_processor + + # Allocate next IID. This operation must be outside of transactions of pipeline creations. + pipeline.ensure_project_iid! + + # Protect the pipeline. This is assigned in Populate instead of + # Build to prevent erroring out on ambiguous refs. + pipeline.protected = @command.protected_ref? + + ## + # Populate pipeline with block argument of CreatePipelineService#execute. + # + @command.seeds_block&.call(pipeline) + + ## + # Gather all runtime build/stage errors + # + if stage_seeds_errors + return error(stage_seeds_errors.join("\n"), config_error: true) + end + + @command.stage_seeds = stage_seeds + end + + def break? + pipeline.errors.any? + end + + private + + def stage_seeds_errors + stage_seeds.flat_map(&:errors).compact.presence + end + + def stage_seeds + strong_memoize(:stage_seeds) do + seeds = stages_attributes.inject([]) do |previous_stages, attributes| + seed = Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, previous_stages) + previous_stages + [seed] + end + + seeds.select(&:included?) + end + end + + def stages_attributes + @command.config_processor.stages_attributes + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb deleted file mode 100644 index 28c38cc3d18..00000000000 --- a/lib/gitlab/ci/pipeline/chain/validate/config.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Pipeline - module Chain - module Validate - class Config < Chain::Base - include Chain::Helpers - - def perform! - unless @pipeline.config_processor - unless @pipeline.ci_yaml_file - return error("Missing #{@pipeline.ci_yaml_file_path} file") - end - - if @command.save_incompleted && @pipeline.has_yaml_errors? - @pipeline.drop!(:config_error) - end - - error(@pipeline.yaml_errors) - end - end - - def break? - @pipeline.errors.any? || @pipeline.persisted? - end - end - end - end - end - end -end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb index 3e8a9b89998..cea25967801 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb @@ -66,11 +66,13 @@ module Gitlab def move_repositories(namespace, old_full_path, new_full_path) repo_shards_for_namespace(namespace).each do |repository_storage| # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage, old_full_path) + Gitlab::GitalyClient::NamespaceService.allow do + gitlab_shell.add_namespace(repository_storage, old_full_path) - unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path) - message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}" - Rails.logger.error message # rubocop:disable Gitlab/RailsLogger + unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path) + message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}" + Rails.logger.error message # rubocop:disable Gitlab/RailsLogger + end end end end diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb index f95833eed01..dbcebec3aa2 100644 --- a/lib/gitlab/gitaly_client/namespace_service.rb +++ b/lib/gitlab/gitaly_client/namespace_service.rb @@ -3,7 +3,22 @@ module Gitlab module GitalyClient class NamespaceService + extend Gitlab::TemporarilyAllow + + NamespaceServiceAccessError = Class.new(StandardError) + ALLOW_KEY = :allow_namespace + + def self.allow + temporarily_allow(ALLOW_KEY) { yield } + end + + def self.denied? + !temporarily_allowed?(ALLOW_KEY) + end + def initialize(storage) + raise NamespaceServiceAccessError if self.class.denied? + @storage = storage end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 82bd6053144..f1b14d78292 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -261,9 +261,6 @@ msgstr "" msgid "%{from} to %{to}" msgstr "" -msgid "%{gitlab_ci_yml} not found in this commit" -msgstr "" - msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects." msgstr "" @@ -908,6 +905,9 @@ msgstr "" msgid "Add Kubernetes cluster" msgstr "" +msgid "Add LICENSE" +msgstr "" + msgid "Add README" msgstr "" @@ -11947,6 +11947,9 @@ msgstr "" msgid "Owner" msgstr "" +msgid "Package deleted successfully" +msgstr "" + msgid "Package information" msgstr "" @@ -11965,10 +11968,10 @@ msgstr "" msgid "PackageRegistry|Copy yarn setup command" msgstr "" -msgid "PackageRegistry|Delete Package" +msgid "PackageRegistry|Delete Package Version" msgstr "" -msgid "PackageRegistry|Delete Package Version" +msgid "PackageRegistry|Delete package" msgstr "" msgid "PackageRegistry|Installation" diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb index 232c8743de7..e581edcb7c7 100644 --- a/qa/qa/vendor/github/page/login.rb +++ b/qa/qa/vendor/github/page/login.rb @@ -12,7 +12,7 @@ module QA fill_in 'password', with: QA::Runtime::Env.github_password click_on 'Sign in' - Support::Retrier.retry_until(exit_on_failure: true) do + Support::Retrier.retry_until(exit_on_failure: true, sleep_interval: 35) do otp = OnePassword::CLI.new.otp fill_in 'otp', with: otp diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 94bcabd3ca4..f538df89fd3 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -157,39 +157,6 @@ describe 'Commits' do end end end - - describe '.gitlab-ci.yml not found warning' do - before do - project.add_reporter(user) - end - - context 'ci builds enabled' do - it 'does not show warning' do - visit pipeline_path(pipeline) - - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end - - it 'shows warning' do - stub_ci_pipeline_yaml_file(nil) - - visit pipeline_path(pipeline) - - expect(page).to have_content '.gitlab-ci.yml not found in this commit' - end - end - - context 'ci builds disabled' do - it 'does not show warning' do - stub_ci_builds_disabled - stub_ci_pipeline_yaml_file(nil) - - visit pipeline_path(pipeline) - - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end - end - end end context 'viewing commits for a branch' do diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb index b4531f5da4e..b7813c8ba30 100644 --- a/spec/features/issuables/sorting_list_spec.rb +++ b/spec/features/issuables/sorting_list_spec.rb @@ -57,7 +57,7 @@ describe 'Sort Issuable List' do it 'is "last updated"' do visit_merge_requests_with_state(project, 'merged') - expect(find('.issues-other-filters')).to have_content('Last updated') + expect(find('.filter-dropdown-container')).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 @@ -69,7 +69,7 @@ describe 'Sort Issuable List' do it 'is "last updated"' do visit_merge_requests_with_state(project, 'closed') - expect(find('.issues-other-filters')).to have_content('Last updated') + expect(find('.filter-dropdown-container')).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 @@ -81,7 +81,7 @@ describe 'Sort Issuable List' do it 'is "created date"' do visit_merge_requests_with_state(project, 'all') - expect(find('.issues-other-filters')).to have_content('Created date') + expect(find('.filter-dropdown-container')).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 @@ -94,7 +94,7 @@ describe 'Sort Issuable List' do it 'supports sorting in asc and desc order' do visit_merge_requests_with_state(project, 'open') - page.within('.issues-other-filters') do + page.within('.filter-dropdown-container') do click_button('Created date') click_link('Last updated') end @@ -102,7 +102,7 @@ describe 'Sort Issuable List' do 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 .rspec-reverse-sort').click + find('.filter-dropdown-container .rspec-reverse-sort').click expect(first_merge_request).to include(first_updated_issuable.title) expect(last_merge_request).to include(last_updated_issuable.title) @@ -133,7 +133,7 @@ describe 'Sort Issuable List' do it 'is "created date"' do visit_issues project - expect(find('.issues-other-filters')).to have_content('Created date') + expect(find('.filter-dropdown-container')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end @@ -145,7 +145,7 @@ describe 'Sort Issuable List' do it 'is "created date"' do visit_issues_with_state(project, 'open') - expect(find('.issues-other-filters')).to have_content('Created date') + expect(find('.filter-dropdown-container')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end @@ -157,7 +157,7 @@ describe 'Sort Issuable List' do it 'is "last updated"' do visit_issues_with_state(project, 'closed') - expect(find('.issues-other-filters')).to have_content('Last updated') + expect(find('.filter-dropdown-container')).to have_content('Last updated') expect(first_issue).to include(last_updated_issuable.title) expect(last_issue).to include(first_updated_issuable.title) end @@ -169,7 +169,7 @@ describe 'Sort Issuable List' do it 'is "created date"' do visit_issues_with_state(project, 'all') - expect(find('.issues-other-filters')).to have_content('Created date') + expect(find('.filter-dropdown-container')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end @@ -183,7 +183,7 @@ describe 'Sort Issuable List' do end it 'shows the sort order as created date' do - expect(find('.issues-other-filters')).to have_content('Created date') + expect(find('.filter-dropdown-container')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end @@ -196,7 +196,7 @@ describe 'Sort Issuable List' do it 'supports sorting in asc and desc order' do visit_issues_with_state(project, 'open') - page.within('.issues-other-filters') do + page.within('.filter-dropdown-container') do click_button('Created date') click_link('Last updated') end @@ -204,7 +204,7 @@ describe 'Sort Issuable List' do 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 .rspec-reverse-sort').click + find('.filter-dropdown-container .rspec-reverse-sort').click expect(first_issue).to include(first_updated_issuable.title) expect(last_issue).to include(last_updated_issuable.title) diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb index 1cff7f2c385..b22715a44f0 100644 --- a/spec/features/projects/commits/user_browses_commits_spec.rb +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -56,10 +56,6 @@ describe 'User browses commits' do project.enable_ci create(:ci_build, pipeline: pipeline) - - allow_next_instance_of(Ci::Pipeline) do |instance| - allow(instance).to receive(:ci_yaml_file).and_return('') - end end it 'renders commit ci info' do diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb index 0e43f2fd26b..622764487d8 100644 --- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb +++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb @@ -7,13 +7,11 @@ describe 'Projects > Files > User views files page' do let(:user) { project.owner } before do - stub_feature_flags(vue_file_list: false) - sign_in user visit project_tree_path(project, project.repository.root_ref) end - it 'user sees folders and submodules sorted together, followed by files' do + it 'user sees folders and submodules sorted together, followed by files', :js do rows = all('td.tree-item-file-name').map(&:text) tree = project.repository.tree diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 943c6e0e959..9fccb3441d6 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -7,7 +7,6 @@ describe 'Projects > Files > Project owner creates a license file', :js do let(:project_maintainer) { project.owner } before do - stub_feature_flags(vue_file_list: false) project.repository.delete_file(project_maintainer, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') sign_in(project_maintainer) @@ -39,7 +38,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do end it 'project maintainer creates a license file from the "Add license" link' do - click_link 'Add license' + click_link 'Add LICENSE' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 9f63b312146..ad6c565c8f9 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -12,7 +12,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file it 'project maintainer creates a license file from a template' do visit project_path(project) - click_on 'Add license' + click_on 'Add LICENSE' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb index 0b3f905b5de..10672bbec68 100644 --- a/spec/features/projects/files/user_browses_files_spec.rb +++ b/spec/features/projects/files/user_browses_files_spec.rb @@ -13,23 +13,22 @@ describe "User browses files" do let(:user) { project.owner } before do - stub_feature_flags(vue_file_list: false) sign_in(user) end - it "shows last commit for current directory" do + it "shows last commit for current directory", :js do visit(tree_path_root_ref) click_link("files") last_commit = project.repository.last_commit_for_path(project.default_branch, "files") - page.within(".blob-commit-info") do + page.within(".commit-detail") do expect(page).to have_content(last_commit.short_id).and have_content(last_commit.author_name) end end - context "when browsing the master branch" do + context "when browsing the master branch", :js do before do visit(tree_path_root_ref) end @@ -124,8 +123,7 @@ describe "User browses files" do expect(current_path).to eq(project_tree_path(project, "markdown/doc/raketasks")) expect(page).to have_content("backup_restore.md").and have_content("maintenance.md") - click_link("shop") - click_link("Maintenance") + click_link("maintenance.md") expect(current_path).to eq(project_blob_path(project, "markdown/doc/raketasks/maintenance.md")) expect(page).to have_content("bundle exec rake gitlab:env:info RAILS_ENV=production") @@ -144,7 +142,7 @@ describe "User browses files" do # rubocop:disable Lint/Void # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`. - find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown/d") + find("a", text: "..")["href"] == project_tree_url(project, "markdown/d") # rubocop:enable Lint/Void page.within(".tree-table") do @@ -168,7 +166,7 @@ describe "User browses files" do end end - context "when browsing a specific ref" do + context "when browsing a specific ref", :js do let(:ref) { project_tree_path(project, "6d39438") } before do @@ -180,7 +178,7 @@ describe "User browses files" do expect(page).to have_content(".gitignore").and have_content("LICENSE") end - it "shows files from a repository with apostroph in its name", :js do + it "shows files from a repository with apostroph in its name" do first(".js-project-refs-dropdown").click page.within(".project-refs-form") do @@ -191,10 +189,10 @@ describe "User browses files" do visit(project_tree_path(project, "'test'")) - expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...") + expect(page).not_to have_selector(".tree-commit .animation-container") end - it "shows the code with a leading dot in the directory", :js do + it "shows the code with a leading dot in the directory" do first(".js-project-refs-dropdown").click page.within(".project-refs-form") do @@ -203,7 +201,7 @@ describe "User browses files" do visit(project_tree_path(project, "fix/.testdir")) - expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...") + expect(page).not_to have_selector(".tree-commit .animation-container") end it "does not show the permalink link" do @@ -221,7 +219,7 @@ describe "User browses files" do click_link(".gitignore") end - it "shows a file content", :js do + it "shows a file content" do expect(page).to have_content("*.rbc") end diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb index 08ebeed2cdd..618290416bd 100644 --- a/spec/features/projects/files/user_browses_lfs_files_spec.rb +++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb @@ -7,8 +7,6 @@ describe 'Projects > Files > User browses LFS files' do let(:user) { project.owner } before do - stub_feature_flags(vue_file_list: false) - sign_in(user) end diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb index f828ee63cd7..b8765066217 100644 --- a/spec/features/projects/files/user_creates_directory_spec.rb +++ b/spec/features/projects/files/user_creates_directory_spec.rb @@ -13,8 +13,6 @@ describe 'Projects > Files > User creates a directory', :js do let(:user) { create(:user) } before do - stub_feature_flags(vue_file_list: false) - project.add_developer(user) sign_in(user) visit project_tree_path(project, 'master') diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb index 23663aeaef0..eb9a4d8cb09 100644 --- a/spec/features/projects/files/user_creates_files_spec.rb +++ b/spec/features/projects/files/user_creates_files_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Projects > Files > User creates files' do +describe 'Projects > Files > User creates files', :js do let(:fork_message) do "You're not allowed to make changes to this project directly. "\ "A fork of this project has been created that you can make changes in, so you can submit a merge request." @@ -14,7 +14,6 @@ describe 'Projects > Files > User creates files' do let(:user) { create(:user) } before do - stub_feature_flags(vue_file_list: false) stub_feature_flags(web_ide_default: false) project.add_maintainer(user) @@ -68,8 +67,7 @@ describe 'Projects > Files > User creates files' do file_name = find('#file_name') file_name.set options[:file_name] || 'README.md' - file_content = find('#file-content', visible: false) - file_content.set options[:file_content] || 'Some content' + find('.ace_text-input', visible: false).send_keys.native.send_keys options[:file_content] || 'Some content' click_button 'Commit changes' end @@ -89,7 +87,7 @@ describe 'Projects > Files > User creates files' do expect(page).to have_content 'Path cannot include directory traversal' end - it 'creates and commit a new file', :js do + it 'creates and commit a new file' do find('#editor') execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:file_name, with: 'not_a_file.md') @@ -105,7 +103,7 @@ describe 'Projects > Files > User creates files' do expect(page).to have_content('*.rbca') end - it 'creates and commit a new file with new lines at the end of file', :js do + it 'creates and commit a new file with new lines at the end of file' do find('#editor') execute_script('ace.edit("editor").setValue("Sample\n\n\n")') fill_in(:file_name, with: 'not_a_file.md') @@ -122,7 +120,7 @@ describe 'Projects > Files > User creates files' do expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n") end - it 'creates and commit a new file with a directory name', :js do + it 'creates and commit a new file with a directory name' do fill_in(:file_name, with: 'foo/bar/baz.txt') expect(page).to have_selector('.file-editor') @@ -139,7 +137,7 @@ describe 'Projects > Files > User creates files' do expect(page).to have_content('*.rbca') end - it 'creates and commit a new file specifying a new branch', :js do + it 'creates and commit a new file specifying a new branch' do expect(page).to have_selector('.file-editor') find('#editor') @@ -174,7 +172,7 @@ describe 'Projects > Files > User creates files' do expect(page).to have_content(message) end - it 'creates and commit new file in forked project', :js do + it 'creates and commit new file in forked project' do expect(page).to have_selector('.file-editor') find('#editor') diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb index 570813ce085..0f543e47631 100644 --- a/spec/features/projects/files/user_deletes_files_spec.rb +++ b/spec/features/projects/files/user_deletes_files_spec.rb @@ -14,8 +14,6 @@ describe 'Projects > Files > User deletes files', :js do let(:user) { create(:user) } before do - stub_feature_flags(vue_file_list: false) - sign_in(user) end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index c0312f5bb62..374a7fb7936 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -12,7 +12,6 @@ describe 'Projects > Files > User edits files', :js do before do stub_feature_flags(web_ide_default: false) - stub_feature_flags(vue_file_list: false) sign_in(user) end diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb index bdef40690a2..4c54bbdcd67 100644 --- a/spec/features/projects/files/user_replaces_files_spec.rb +++ b/spec/features/projects/files/user_replaces_files_spec.rb @@ -16,8 +16,6 @@ describe 'Projects > Files > User replaces files', :js do let(:user) { create(:user) } before do - stub_feature_flags(vue_file_list: false) - sign_in(user) end diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb index 284f891731c..35a3835ff12 100644 --- a/spec/features/projects/files/user_uploads_files_spec.rb +++ b/spec/features/projects/files/user_uploads_files_spec.rb @@ -16,8 +16,6 @@ describe 'Projects > Files > User uploads files' do let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) } before do - stub_feature_flags(vue_file_list: false) - project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb index bbb3a066ed5..ff133b58f89 100644 --- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb +++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb @@ -2,12 +2,11 @@ require 'spec_helper' -describe 'Projects > Show > Collaboration links' do +describe 'Projects > Show > Collaboration links', :js do let(:project) { create(:project, :repository) } let(:user) { create(:user) } before do - stub_feature_flags(vue_file_list: false) project.add_developer(user) sign_in(user) end @@ -17,15 +16,21 @@ describe 'Projects > Show > Collaboration links' do # The navigation bar page.within('.header-new') do + find('.qa-new-menu-toggle').click + aggregate_failures 'dropdown links in the navigation bar' do expect(page).to have_link('New issue') expect(page).to have_link('New merge request') expect(page).to have_link('New snippet', href: new_project_snippet_path(project)) end + + find('.qa-new-menu-toggle').click end # The dropdown above the tree page.within('.repo-breadcrumb') do + find('.qa-add-to-tree').click + aggregate_failures 'dropdown links above the repo tree' do expect(page).to have_link('New file') expect(page).to have_link('Upload file') @@ -45,23 +50,19 @@ describe 'Projects > Show > Collaboration links' do visit project_path(project) page.within('.header-new') do + find('.qa-new-menu-toggle').click + aggregate_failures 'dropdown links' do expect(page).not_to have_link('New issue') expect(page).not_to have_link('New merge request') expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project)) end - end - page.within('.repo-breadcrumb') do - aggregate_failures 'dropdown links' do - expect(page).not_to have_link('New file') - expect(page).not_to have_link('Upload file') - expect(page).not_to have_link('New directory') - expect(page).not_to have_link('New branch') - expect(page).not_to have_link('New tag') - end + find('.qa-new-menu-toggle').click end + expect(page).not_to have_selector('.qa-add-to-tree') + expect(page).not_to have_link('Web IDE') end end diff --git a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb index fdc238d55cf..cf1a679102c 100644 --- a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb +++ b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb @@ -5,10 +5,6 @@ require 'spec_helper' describe 'Projects > Show > User sees last commit CI status' do set(:project) { create(:project, :repository, :public) } - before do - stub_feature_flags(vue_file_list: false) - end - it 'shows the project README', :js do project.enable_ci pipeline = create(:ci_pipeline, project: project, sha: project.commit.sha, ref: 'master') @@ -16,9 +12,9 @@ describe 'Projects > Show > User sees last commit CI status' do visit project_path(project) - page.within '.blob-commit-info' do + page.within '.commit-detail' do expect(page).to have_content(project.commit.sha[0..6]) - expect(page).to have_link('Pipeline: skipped') + expect(page).to have_selector('[aria-label="Commit: skipped"]') end end end diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb index c136d7607fd..41c3c6b5770 100644 --- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb +++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb @@ -59,8 +59,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do end it '"Add license" button linked to new file populated for a license' do - page.within('.project-stats') do - expect(page).to have_link('Add license', href: presenter.add_license_path) + page.within('.project-buttons') do + expect(page).to have_link('Add LICENSE', href: presenter.add_license_path) end end end @@ -175,7 +175,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do expect(project.repository.license_blob).not_to be_nil page.within('.project-buttons') do - expect(page).not_to have_link('Add license') + expect(page).not_to have_link('Add LICENSE') end end diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb index ca616be341d..180ffac4d4d 100644 --- a/spec/features/projects/tree/tree_show_spec.rb +++ b/spec/features/projects/tree/tree_show_spec.rb @@ -10,7 +10,6 @@ describe 'Projects tree', :js do let(:test_sha) { '7975be0116940bf2ad4321f79d02a55c5f7779aa' } before do - stub_feature_flags(vue_file_list: false) project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index df71a4f3f70..90e48f3c230 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -6,10 +6,6 @@ describe 'Project' do include ProjectForksHelper include MobileHelpers - before do - stub_feature_flags(vue_file_list: false) - end - describe 'creating from template' do let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } @@ -272,7 +268,7 @@ describe 'Project' do end end - describe 'tree view (default view is set to Files)' do + describe 'tree view (default view is set to Files)', :js do let(:user) { create(:user, project_view: 'files') } let(:project) { create(:forked_project_with_submodules) } @@ -285,19 +281,19 @@ describe 'Project' do it 'has working links to files' do click_link('PROCESS.md') - expect(page.status_code).to eq(200) + expect(page).to have_selector('.file-holder') end it 'has working links to directories' do click_link('encoding') - expect(page.status_code).to eq(200) + expect(page).to have_selector('.breadcrumb-item', text: 'encoding') end it 'has working links to submodules' do click_link('645f6c4c') - expect(page.status_code).to eq(200) + expect(page).to have_selector('.qa-branches-select', text: '645f6c4c82fd3f5e06f67134450a570b795e55a6') end context 'for signed commit on default branch', :js do diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index 9e2634657a0..f56bd055224 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -173,13 +173,5 @@ describe 'GPG signed commits' do context 'with vue tree view enabled' do it_behaves_like 'a commit with a signature' end - - context 'with vue tree view disabled' do - before do - stub_feature_flags(vue_file_list: false) - end - - it_behaves_like 'a commit with a signature' - end end end diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js index 1e2f3501c8c..41450becabb 100644 --- a/spec/frontend/repository/components/table/index_spec.js +++ b/spec/frontend/repository/components/table/index_spec.js @@ -9,6 +9,7 @@ let $apollo; const MOCK_BLOBS = [ { id: '123abc', + sha: '123abc', flatPath: 'blob', name: 'blob.md', type: 'blob', @@ -16,6 +17,7 @@ const MOCK_BLOBS = [ }, { id: '124abc', + sha: '124abc', flatPath: 'blob2', name: 'blob2.md', type: 'blob', diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 04a5e0778a1..aa0b9385f1a 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -41,6 +41,7 @@ describe('Repository table row component', () => { it('renders table row', () => { factory({ id: '1', + sha: '123', path: 'test', type: 'file', currentPath: '/', @@ -57,6 +58,7 @@ describe('Repository table row component', () => { `('renders a $componentName for type $type', ({ type, component }) => { factory({ id: '1', + sha: '123', path: 'test', type, currentPath: '/', @@ -73,6 +75,7 @@ describe('Repository table row component', () => { `('pushes new router if type $type is tree', ({ type, pushes }) => { factory({ id: '1', + sha: '123', path: 'test', type, currentPath: '/', @@ -95,6 +98,7 @@ describe('Repository table row component', () => { `('calls visitUrl if $type is not tree', ({ type, pushes }) => { factory({ id: '1', + sha: '123', path: 'test', type, currentPath: '/', @@ -112,6 +116,7 @@ describe('Repository table row component', () => { it('renders commit ID for submodule', () => { factory({ id: '1', + sha: '123', path: 'test', type: 'commit', currentPath: '/', @@ -123,6 +128,7 @@ describe('Repository table row component', () => { it('renders link with href', () => { factory({ id: '1', + sha: '123', path: 'test', type: 'blob', url: 'https://test.com', @@ -135,6 +141,7 @@ describe('Repository table row component', () => { it('renders LFS badge', () => { factory({ id: '1', + sha: '123', path: 'test', type: 'commit', currentPath: '/', @@ -147,6 +154,7 @@ describe('Repository table row component', () => { it('renders commit and web links with href for submodule', () => { factory({ id: '1', + sha: '123', path: 'test', type: 'commit', url: 'https://test.com', @@ -161,6 +169,7 @@ describe('Repository table row component', () => { it('renders lock icon', () => { factory({ id: '1', + sha: '123', path: 'test', type: 'tree', currentPath: '/', diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js index ad42f8b2ffc..9199c726680 100644 --- a/spec/frontend/repository/log_tree_spec.js +++ b/spec/frontend/repository/log_tree_spec.js @@ -41,7 +41,7 @@ describe('fetchLogsTree', () => { jest.spyOn(axios, 'get'); - global.gon = { gitlab_url: 'https://test.com' }; + global.gon = { relative_url_root: '' }; client = { readQuery: () => ({ @@ -64,10 +64,9 @@ describe('fetchLogsTree', () => { it('calls axios get', () => fetchLogsTree(client, '', '0', resolver).then(() => { - expect(axios.get).toHaveBeenCalledWith( - 'https://test.com/gitlab-org/gitlab-foss/refs/master/logs_tree/', - { params: { format: 'json', offset: '0' } }, - ); + expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/refs/master/logs_tree/', { + params: { format: 'json', offset: '0' }, + }); })); it('calls axios get once', () => diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 3ac698e0e72..bf9106643eb 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -99,6 +99,19 @@ describe Resolvers::IssuesResolver do expect(resolve_issues(sort: :due_date_desc)).to eq [due_issue1, due_issue3, due_issue4, due_issue2] end end + + context 'when sorting by relative position' do + let(:project) { create(:project) } + + let!(:relative_issue1) { create(:issue, project: project, relative_position: 2000) } + let!(:relative_issue2) { create(:issue, project: project, relative_position: nil) } + let!(:relative_issue3) { create(:issue, project: project, relative_position: 1000) } + let!(:relative_issue4) { create(:issue, project: project, relative_position: nil) } + + it 'sorts issues ascending' do + expect(resolve_issues(sort: :relative_position_asc)).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2] + end + end end it 'returns issues user can see' do diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb index ee9af886e60..1c3b46ecfde 100644 --- a/spec/graphql/types/commit_type_spec.rb +++ b/spec/graphql/types/commit_type_spec.rb @@ -10,7 +10,7 @@ describe GitlabSchema.types['Commit'] do it 'contains attributes related to commit' do expect(described_class).to have_graphql_fields( :id, :sha, :title, :description, :message, :authored_date, - :author, :web_url, :latest_pipeline, :pipelines, :signature_html + :author_name, :author, :web_url, :latest_pipeline, :pipelines, :signature_html ) end end diff --git a/spec/graphql/types/issue_sort_enum_spec.rb b/spec/graphql/types/issue_sort_enum_spec.rb index da7126f3d84..1b6aa6d6069 100644 --- a/spec/graphql/types/issue_sort_enum_spec.rb +++ b/spec/graphql/types/issue_sort_enum_spec.rb @@ -8,6 +8,6 @@ describe GitlabSchema.types['IssueSort'] do it_behaves_like 'common sort values' it 'exposes all the existing issue sort values' do - expect(described_class.values.keys).to include(*%w[DUE_DATE_ASC DUE_DATE_DESC]) + expect(described_class.values.keys).to include(*%w[DUE_DATE_ASC DUE_DATE_DESC RELATIVE_POSITION_ASC]) end end diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb index 22c11aff90a..516c862b9c6 100644 --- a/spec/graphql/types/tree/blob_type_spec.rb +++ b/spec/graphql/types/tree/blob_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' describe Types::Tree::BlobType do it { expect(described_class.graphql_name).to eq('Blob') } - it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url, :lfs_oid) } + it { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid) } end diff --git a/spec/graphql/types/tree/submodule_type_spec.rb b/spec/graphql/types/tree/submodule_type_spec.rb index 768eccba68c..81f7ad825a1 100644 --- a/spec/graphql/types/tree/submodule_type_spec.rb +++ b/spec/graphql/types/tree/submodule_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' describe Types::Tree::SubmoduleType do it { expect(described_class.graphql_name).to eq('Submodule') } - it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url, :tree_url) } + it { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :tree_url) } end diff --git a/spec/graphql/types/tree/tree_entry_type_spec.rb b/spec/graphql/types/tree/tree_entry_type_spec.rb index ea1b6426034..228a4be0949 100644 --- a/spec/graphql/types/tree/tree_entry_type_spec.rb +++ b/spec/graphql/types/tree/tree_entry_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' describe Types::Tree::TreeEntryType do it { expect(described_class.graphql_name).to eq('TreeEntry') } - it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) } + it { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url) } end diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb index c2c1960eeab..9267231390d 100644 --- a/spec/initializers/lograge_spec.rb +++ b/spec/initializers/lograge_spec.rb @@ -68,4 +68,52 @@ describe 'lograge', type: :request do subject end end + + context 'with a log subscriber' do + let(:subscriber) { Lograge::RequestLogSubscriber.new } + + let(:event) do + ActiveSupport::Notifications::Event.new( + 'process_action.action_controller', + Time.now, + Time.now, + 2, + status: 200, + controller: 'HomeController', + action: 'index', + format: 'application/json', + method: 'GET', + path: '/home?foo=bar', + params: {}, + db_runtime: 0.02, + view_runtime: 0.01 + ) + end + + let(:log_output) { StringIO.new } + let(:logger) do + Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } + end + + describe 'with an exception' do + let(:exception) { RuntimeError.new('bad request') } + let(:backtrace) { caller } + + before do + allow(exception).to receive(:backtrace).and_return(backtrace) + event.payload[:exception_object] = exception + Lograge.logger = logger + end + + it 'adds exception data to log' do + subscriber.process_action(event) + + log_data = JSON.parse(log_output.string) + + expect(log_data['exception']['class']).to eq('RuntimeError') + expect(log_data['exception']['message']).to eq('bad request') + expect(log_data['exception']['backtrace']).to eq(Gitlab::Profiler.clean_backtrace(backtrace)) + end + end + end end diff --git a/spec/javascripts/boards/components/boards_selector_spec.js b/spec/javascripts/boards/components/boards_selector_spec.js index 473cc0612ea..d1f36a0a652 100644 --- a/spec/javascripts/boards/components/boards_selector_spec.js +++ b/spec/javascripts/boards/components/boards_selector_spec.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import BoardService from '~/boards/services/board_service'; import BoardsSelector from '~/boards/components/boards_selector.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { TEST_HOST } from 'spec/test_constants'; @@ -37,7 +36,6 @@ describe('BoardsSelector', () => { bulkUpdatePath: '', boardId: '', }); - window.gl.boardService = new BoardService(); allBoardsResponse = Promise.resolve({ data: boards, @@ -46,8 +44,8 @@ describe('BoardsSelector', () => { data: recentBoards, }); - spyOn(BoardService.prototype, 'allBoards').and.returnValue(allBoardsResponse); - spyOn(BoardService.prototype, 'recentBoards').and.returnValue(recentBoardsResponse); + spyOn(boardsStore, 'allBoards').and.returnValue(allBoardsResponse); + spyOn(boardsStore, 'recentBoards').and.returnValue(recentBoardsResponse); const Component = Vue.extend(BoardsSelector); vm = mountComponent( @@ -94,7 +92,6 @@ describe('BoardsSelector', () => { afterEach(() => { vm.$destroy(); - window.gl.boardService = undefined; }); describe('filtering', () => { diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index ba4f841cf43..a631cd2777b 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -11,6 +11,7 @@ describe Gitlab::Ci::Pipeline::Chain::Build do [{ key: 'first', secret_value: 'world' }, { key: 'second', secret_value: 'second_world' }] end + let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( source: :push, @@ -51,12 +52,6 @@ describe Gitlab::Ci::Pipeline::Chain::Build do .to eq variables_attributes.map(&:with_indifferent_access) end - it 'sets a valid config source' do - step.perform! - - expect(pipeline.repository_source?).to be true - end - it 'returns a valid pipeline' do step.perform! diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 75160a93ba7..52e9432dc92 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -18,19 +18,32 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do seeds_block: nil) end + let(:dependencies) do + [ + Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command), + Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command), + Gitlab::Ci::Pipeline::Chain::Seed.new(pipeline, command) + ] + end + let(:step) { described_class.new(pipeline, command) } let(:config) do { rspec: { script: 'rspec' } } end + def run_chain + dependencies.map(&:perform!) + step.perform! + end + before do stub_ci_pipeline_yaml_file(YAML.dump(config)) end context 'when pipeline doesn not have seeds block' do before do - step.perform! + run_chain end it 'does not persist the pipeline' do @@ -66,7 +79,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end before do - step.perform! + run_chain end it 'breaks the chain' do @@ -84,16 +97,16 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end describe 'pipeline protect' do - subject { step.perform! } - context 'when ref is protected' do before do allow(project).to receive(:protected_for?).with('master').and_return(true) allow(project).to receive(:protected_for?).with('refs/heads/master').and_return(true) + + dependencies.map(&:perform!) end it 'does not protect the pipeline' do - subject + run_chain expect(pipeline.protected).to eq(true) end @@ -101,7 +114,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do context 'when ref is not protected' do it 'does not protect the pipeline' do - subject + run_chain expect(pipeline.protected).to eq(false) end @@ -114,7 +127,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end before do - step.perform! + run_chain end it 'breaks the chain' do @@ -146,7 +159,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end it 'populates pipeline with resources described in the seeds block' do - step.perform! + run_chain expect(pipeline).not_to be_persisted expect(pipeline.variables).not_to be_empty @@ -156,7 +169,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end it 'has pipeline iid' do - step.perform! + run_chain expect(pipeline.iid).to be > 0 end @@ -168,7 +181,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end it 'wastes pipeline iid' do - expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved) + expect { run_chain }.to raise_error(ActiveRecord::RecordNotSaved) last_iid = InternalId.ci_pipelines .where(project_id: project.id) @@ -183,14 +196,14 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do let(:pipeline) { create(:ci_pipeline, project: project) } it 'raises error' do - expect { step.perform! }.to raise_error(described_class::PopulateError) + expect { run_chain }.to raise_error(described_class::PopulateError) end end context 'when variables policy is specified' do shared_examples_for 'a correct pipeline' do it 'populates pipeline according to used policies' do - step.perform! + run_chain expect(pipeline.stages.size).to eq 1 expect(pipeline.stages.first.statuses.size).to eq 1 diff --git a/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb index af0f4d68150..92eadf5548c 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb @@ -2,36 +2,38 @@ require 'spec_helper' -describe Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs do - let(:project) { create(:project, :repository) } +describe ::Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs do + let(:project) { create(:project) } let(:pipeline) do build(:ci_pipeline, project: project) end let(:command) do - double(:command, project: project, chat_data: { command: 'echo' }) - end - - before do - stub_ci_pipeline_yaml_file(YAML.dump(rspec: { script: 'rspec' })) + double(:command, + config_processor: double(:processor, + jobs: { echo: double(:job_echo), rspec: double(:job_rspec) }), + project: project, + chat_data: { command: 'echo' }) end describe '#perform!' do - it 'removes unwanted jobs for chat pipelines' do - allow(pipeline).to receive(:chat?).and_return(true) + subject { described_class.new(pipeline, command).perform! } - pipeline.config_processor.jobs[:echo] = double(:job) + it 'removes unwanted jobs for chat pipelines' do + expect(pipeline).to receive(:chat?).and_return(true) - described_class.new(pipeline, command).perform! + subject - expect(pipeline.config_processor.jobs.keys).to eq([:echo]) + expect(command.config_processor.jobs.keys).to eq([:echo]) end - end - it 'does not remove any jobs for non-chat pipelines' do - described_class.new(pipeline, command).perform! + it 'does not remove any jobs for non chat-pipelines' do + expect(pipeline).to receive(:chat?).and_return(false) + + subject - expect(pipeline.config_processor.jobs.keys).to eq([:rspec]) + expect(command.config_processor.jobs.keys).to eq([:echo, :rspec]) + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb new file mode 100644 index 00000000000..aa54f19b26c --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Seed do + let(:project) { create(:project, :repository) } + let(:user) { create(:user, developer_projects: [project]) } + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + origin_ref: 'master', + seeds_block: nil) + end + + def run_chain(pipeline, command) + [ + Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command), + Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command) + ].map(&:perform!) + + described_class.new(pipeline, command).perform! + end + + let(:pipeline) { build(:ci_pipeline, project: project) } + + describe '#perform!' do + before do + stub_ci_pipeline_yaml_file(YAML.dump(config)) + run_chain(pipeline, command) + end + + let(:config) do + { rspec: { script: 'rake' } } + end + + it 'allocates next IID' do + expect(pipeline.iid).to be_present + end + + it 'sets the seeds in the command object' do + expect(command.stage_seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Base) + expect(command.stage_seeds.count).to eq 1 + end + + context 'when no ref policy is specified' do + let(:config) do + { + production: { stage: 'deploy', script: 'cap prod' }, + rspec: { stage: 'test', script: 'rspec' }, + spinach: { stage: 'test', script: 'spinach' } + } + end + + it 'correctly fabricates a stage seeds object' do + seeds = command.stage_seeds + expect(seeds.size).to eq 2 + expect(seeds.first.attributes[:name]).to eq 'test' + expect(seeds.second.attributes[:name]).to eq 'deploy' + expect(seeds.dig(0, 0, :name)).to eq 'rspec' + expect(seeds.dig(0, 1, :name)).to eq 'spinach' + expect(seeds.dig(1, 0, :name)).to eq 'production' + end + end + + context 'when refs policy is specified' do + let(:pipeline) do + build(:ci_pipeline, project: project, ref: 'feature', tag: true) + end + + let(:config) do + { + production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, + spinach: { stage: 'test', script: 'spinach', only: ['tags'] } + } + end + + it 'returns stage seeds only assigned to master' do + seeds = command.stage_seeds + + expect(seeds.size).to eq 1 + expect(seeds.first.attributes[:name]).to eq 'test' + expect(seeds.dig(0, 0, :name)).to eq 'spinach' + end + end + + context 'when source policy is specified' do + let(:pipeline) { create(:ci_pipeline, source: :schedule) } + + let(:config) do + { + production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, + spinach: { stage: 'test', script: 'spinach', only: ['schedules'] } + } + end + + it 'returns stage seeds only assigned to schedules' do + seeds = command.stage_seeds + + expect(seeds.size).to eq 1 + expect(seeds.first.attributes[:name]).to eq 'test' + expect(seeds.dig(0, 0, :name)).to eq 'spinach' + end + end + + context 'when kubernetes policy is specified' do + let(:config) do + { + spinach: { stage: 'test', script: 'spinach' }, + production: { + stage: 'deploy', + script: 'cap', + only: { kubernetes: 'active' } + } + } + end + + context 'when kubernetes is active' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + let(:pipeline) { build(:ci_pipeline, project: project) } + + it 'returns seeds for kubernetes dependent job' do + seeds = command.stage_seeds + + expect(seeds.size).to eq 2 + expect(seeds.dig(0, 0, :name)).to eq 'spinach' + expect(seeds.dig(1, 0, :name)).to eq 'production' + end + end + end + + context 'when kubernetes is not active' do + it 'does not return seeds for kubernetes dependent job' do + seeds = command.stage_seeds + + expect(seeds.size).to eq 1 + expect(seeds.dig(0, 0, :name)).to eq 'spinach' + end + end + end + + context 'when variables policy is specified' do + let(:config) do + { + unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } }, + feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } + } + end + + it 'returns stage seeds only when variables expression is truthy' do + seeds = command.stage_seeds + + expect(seeds.size).to eq 1 + expect(seeds.dig(0, 0, :name)).to eq 'unit' + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb deleted file mode 100644 index ed3ce6760a3..00000000000 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb +++ /dev/null @@ -1,165 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::Ci::Pipeline::Chain::Validate::Config do - set(:project) { create(:project, :repository) } - set(:user) { create(:user) } - - let(:command) do - Gitlab::Ci::Pipeline::Chain::Command.new( - project: project, - current_user: user, - save_incompleted: true) - end - - let(:pipeline) do - build(:ci_pipeline, project: project) - end - - let!(:step) { described_class.new(pipeline, command) } - - subject { step.perform! } - - context 'when pipeline has no YAML configuration' do - let(:pipeline) do - build_stubbed(:ci_pipeline, project: project) - end - - it 'appends errors about missing configuration' do - subject - - expect(pipeline.errors.to_a) - .to include 'Missing .gitlab-ci.yml file' - end - - it 'breaks the chain' do - subject - - expect(step.break?).to be true - end - end - - context 'when YAML configuration contains errors' do - before do - stub_ci_pipeline_yaml_file('invalid YAML') - subject - end - - it 'appends errors about YAML errors' do - expect(pipeline.errors.to_a) - .to include 'Invalid configuration format' - end - - it 'breaks the chain' do - expect(step.break?).to be true - end - - context 'when saving incomplete pipeline is allowed' do - let(:command) do - double('command', project: project, - current_user: user, - save_incompleted: true) - end - - it 'fails the pipeline' do - subject - - expect(pipeline.reload).to be_failed - end - - it 'sets a config error failure reason' do - subject - - expect(pipeline.reload.config_error?).to eq true - end - end - - context 'when saving incomplete pipeline is not allowed' do - let(:command) do - double('command', project: project, - current_user: user, - save_incompleted: false) - end - - it 'does not drop pipeline' do - subject - - expect(pipeline).not_to be_failed - expect(pipeline).not_to be_persisted - end - end - end - - context 'when pipeline contains configuration validation errors' do - before do - stub_ci_pipeline_yaml_file(YAML.dump({ - rspec: { - before_script: 10, - script: 'ls -al' - } - })) - - subject - end - - it 'appends configuration validation errors to pipeline errors' do - expect(pipeline.errors.to_a) - .to include "jobs:rspec:before_script config should be an array containing strings and arrays of strings" - end - - it 'breaks the chain' do - expect(step.break?).to be true - end - end - - context 'when pipeline is correct and complete' do - before do - stub_ci_pipeline_yaml_file(YAML.dump({ - rspec: { - script: 'rspec' - } - })) - subject - end - - it 'does not invalidate the pipeline' do - expect(pipeline).to be_valid - end - - it 'does not break the chain' do - expect(step.break?).to be false - end - end - - context 'when pipeline source is merge request' do - before do - stub_ci_pipeline_yaml_file(YAML.dump(config)) - subject - end - - let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } - - let(:merge_request_pipeline) do - build(:ci_pipeline, source: :merge_request_event, project: project) - end - - let(:chain) { described_class.new(merge_request_pipeline, command).tap(&:perform!) } - - context "when config contains 'merge_requests' keyword" do - let(:config) { { rspec: { script: 'echo', only: ['merge_requests'] } } } - - it 'does not break the chain' do - expect(chain).not_to be_break - end - end - - context "when config contains 'merge_request' keyword" do - let(:config) { { rspec: { script: 'echo', only: ['merge_request'] } } } - - it 'does not break the chain' do - expect(chain).not_to be_break - end - end - end -end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 79ad1c0b43f..eefc548a4d9 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -401,7 +401,7 @@ describe Gitlab::Shell do describe '#add_namespace' do it 'creates a namespace' do - subject.add_namespace(storage, "mepmep") + Gitlab::GitalyClient::NamespaceService.allow { subject.add_namespace(storage, "mepmep") } expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(true) end @@ -425,8 +425,10 @@ describe Gitlab::Shell do describe '#remove' do it 'removes the namespace' do - subject.add_namespace(storage, "mepmep") - subject.rm_namespace(storage, "mepmep") + Gitlab::GitalyClient::NamespaceService.allow do + subject.add_namespace(storage, "mepmep") + subject.rm_namespace(storage, "mepmep") + end expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(false) end @@ -434,8 +436,10 @@ describe Gitlab::Shell do describe '#mv_namespace' do it 'renames the namespace' do - subject.add_namespace(storage, "mepmep") - subject.mv_namespace(storage, "mepmep", "2mep") + Gitlab::GitalyClient::NamespaceService.allow do + subject.add_namespace(storage, "mepmep") + subject.mv_namespace(storage, "mepmep", "2mep") + end expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(false) expect(TestEnv.storage_dir_exists?(storage, "2mep")).to be(true) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 04c8fb8c5f7..24fa3b9b1ea 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2221,7 +2221,7 @@ describe Ci::Build do { key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false }, { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false }, { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false }, - { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true, masked: false }, + { key: 'CI_CONFIG_PATH', value: pipeline.config_path, public: true, masked: false }, { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false }, { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true, masked: false }, { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true, masked: false }, @@ -2667,11 +2667,17 @@ describe Ci::Build do it { is_expected.to include(deployment_variable) } end + context 'when project has default CI config path' do + let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: '.gitlab-ci.yml', public: true, masked: false } } + + it { is_expected.to include(ci_config_path) } + end + context 'when project has custom CI config path' do let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true, masked: false } } before do - project.update(ci_config_path: 'custom') + expect_any_instance_of(Project).to receive(:ci_config_path) { 'custom' } end it { is_expected.to include(ci_config_path) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 1242cf05a59..d24cf3d2115 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -979,149 +979,6 @@ describe Ci::Pipeline, :mailer do end describe 'pipeline stages' do - describe '#stage_seeds' do - before do - stub_ci_pipeline_yaml_file(YAML.dump(config)) - end - - let(:pipeline) { build(:ci_pipeline) } - let(:config) { { rspec: { script: 'rake' } } } - - it 'returns preseeded stage seeds object' do - expect(pipeline.stage_seeds) - .to all(be_a Gitlab::Ci::Pipeline::Seed::Base) - expect(pipeline.stage_seeds.count).to eq 1 - end - - context 'when no refs policy is specified' do - let(:config) do - { production: { stage: 'deploy', script: 'cap prod' }, - rspec: { stage: 'test', script: 'rspec' }, - spinach: { stage: 'test', script: 'spinach' } } - end - - it 'correctly fabricates a stage seeds object' do - seeds = pipeline.stage_seeds - - expect(seeds.size).to eq 2 - expect(seeds.first.attributes[:name]).to eq 'test' - expect(seeds.second.attributes[:name]).to eq 'deploy' - expect(seeds.dig(0, 0, :name)).to eq 'rspec' - expect(seeds.dig(0, 1, :name)).to eq 'spinach' - expect(seeds.dig(1, 0, :name)).to eq 'production' - end - end - - context 'when refs policy is specified' do - let(:pipeline) do - build(:ci_pipeline, ref: 'feature', tag: true) - end - - let(:config) do - { production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, - spinach: { stage: 'test', script: 'spinach', only: ['tags'] } } - end - - it 'returns stage seeds only assigned to master to master' do - seeds = pipeline.stage_seeds - - expect(seeds.size).to eq 1 - expect(seeds.first.attributes[:name]).to eq 'test' - expect(seeds.dig(0, 0, :name)).to eq 'spinach' - end - end - - context 'when source policy is specified' do - let(:pipeline) { build(:ci_pipeline, source: :schedule) } - - let(:config) do - { production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, - spinach: { stage: 'test', script: 'spinach', only: ['schedules'] } } - end - - it 'returns stage seeds only assigned to schedules' do - seeds = pipeline.stage_seeds - - expect(seeds.size).to eq 1 - expect(seeds.first.attributes[:name]).to eq 'test' - expect(seeds.dig(0, 0, :name)).to eq 'spinach' - end - end - - context 'when kubernetes policy is specified' do - let(:config) do - { - spinach: { stage: 'test', script: 'spinach' }, - production: { - stage: 'deploy', - script: 'cap', - only: { kubernetes: 'active' } - } - } - end - - context 'when kubernetes is active' do - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - let(:pipeline) { build(:ci_pipeline, project: project) } - - it 'returns seeds for kubernetes dependent job' do - seeds = pipeline.stage_seeds - - expect(seeds.size).to eq 2 - expect(seeds.dig(0, 0, :name)).to eq 'spinach' - expect(seeds.dig(1, 0, :name)).to eq 'production' - end - end - end - - context 'when kubernetes is not active' do - it 'does not return seeds for kubernetes dependent job' do - seeds = pipeline.stage_seeds - - expect(seeds.size).to eq 1 - expect(seeds.dig(0, 0, :name)).to eq 'spinach' - end - end - end - - context 'when variables policy is specified' do - let(:config) do - { unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } }, - feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } } - end - - it 'returns stage seeds only when variables expression is truthy' do - seeds = pipeline.stage_seeds - - expect(seeds.size).to eq 1 - expect(seeds.dig(0, 0, :name)).to eq 'unit' - end - end - end - - describe '#seeds_size' do - before do - stub_ci_pipeline_yaml_file(YAML.dump(config)) - end - - context 'when refs policy is specified' do - let(:config) do - { production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, - spinach: { stage: 'test', script: 'spinach', only: ['tags'] } } - end - - let(:pipeline) do - build(:ci_pipeline, ref: 'feature', tag: true) - end - - it 'returns real seeds size' do - expect(pipeline.seeds_size).to eq 1 - end - end - end - describe 'legacy stages' do before do create(:commit_status, pipeline: pipeline, @@ -2194,161 +2051,6 @@ describe Ci::Pipeline, :mailer do end end - describe '#ci_yaml_file_path' do - subject { pipeline.ci_yaml_file_path } - - %i[unknown_source repository_source].each do |source| - context source.to_s do - before do - pipeline.config_source = described_class.config_sources.fetch(source) - end - - it 'returns the path from project' do - allow(pipeline.project).to receive(:ci_config_path) { 'custom/path' } - - is_expected.to eq('custom/path') - end - - it 'returns default when custom path is nil' do - allow(pipeline.project).to receive(:ci_config_path) { nil } - - is_expected.to eq('.gitlab-ci.yml') - end - - it 'returns default when custom path is empty' do - allow(pipeline.project).to receive(:ci_config_path) { '' } - - is_expected.to eq('.gitlab-ci.yml') - end - end - end - - context 'when pipeline is for auto-devops' do - before do - pipeline.config_source = 'auto_devops_source' - end - - it 'does not return config file' do - is_expected.to be_nil - end - end - end - - describe '#set_config_source' do - context 'when pipelines does not contain needed data and auto devops is disabled' do - before do - stub_application_setting(auto_devops_enabled: false) - end - - it 'defines source to be unknown' do - pipeline.set_config_source - - expect(pipeline).to be_unknown_source - end - end - - context 'when pipeline contains all needed data' do - let(:pipeline) do - create(:ci_pipeline, project: project, - sha: '1234', - ref: 'master', - source: :push) - end - - context 'when the repository has a config file' do - before do - allow(project.repository).to receive(:gitlab_ci_yml_for) - .and_return('config') - end - - it 'defines source to be from repository' do - pipeline.set_config_source - - expect(pipeline).to be_repository_source - end - - context 'when loading an object' do - let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) } - - it 'does not redefine the source' do - # force to overwrite the source - pipeline.unknown_source! - - expect(new_pipeline).to be_unknown_source - end - end - end - - context 'when the repository does not have a config file' do - let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } - - context 'auto devops enabled' do - before do - allow(project).to receive(:ci_config_path) { 'custom' } - end - - it 'defines source to be auto devops' do - pipeline.set_config_source - - expect(pipeline).to be_auto_devops_source - end - end - end - end - end - - describe '#ci_yaml_file' do - let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } - - context 'the source is unknown' do - before do - pipeline.unknown_source! - end - - it 'returns the configuration if found' do - allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) - .and_return('config') - - expect(pipeline.ci_yaml_file).to be_a(String) - expect(pipeline.ci_yaml_file).not_to eq(implied_yml) - expect(pipeline.yaml_errors).to be_nil - end - - it 'sets yaml errors if not found' do - expect(pipeline.ci_yaml_file).to be_nil - expect(pipeline.yaml_errors) - .to start_with('Failed to load CI/CD config file') - end - end - - context 'the source is the repository' do - before do - pipeline.repository_source! - end - - it 'returns the configuration if found' do - allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) - .and_return('config') - - expect(pipeline.ci_yaml_file).to be_a(String) - expect(pipeline.ci_yaml_file).not_to eq(implied_yml) - expect(pipeline.yaml_errors).to be_nil - end - end - - context 'when the source is auto_devops_source' do - before do - stub_application_setting(auto_devops_enabled: true) - pipeline.auto_devops_source! - end - - it 'finds the implied config' do - expect(pipeline.ci_yaml_file).to eq(implied_yml) - expect(pipeline.yaml_errors).to be_nil - end - end - end - describe '#update_status' do context 'when pipeline is empty' do it 'updates does not change pipeline status' do @@ -2894,49 +2596,19 @@ describe Ci::Pipeline, :mailer do end describe '#has_yaml_errors?' do - before do - stub_ci_pipeline_yaml_file(YAML.dump(config)) - end - - let(:pipeline) { create(:ci_pipeline) } - - context 'when pipeline has errors' do - let(:config) { { rspec: nil } } - - it 'contains yaml errors' do - pipeline.config_processor - - expect(pipeline).to have_yaml_errors - expect(pipeline.yaml_errors).to include('contains unknown keys') + context 'when yaml_errors is set' do + before do + pipeline.yaml_errors = 'File not found' end - end - - context 'when pipeline has undefined error' do - let(:config) { double(:config) } - - it 'contains yaml errors' do - expect(::Gitlab::Ci::YamlProcessor).to receive(:new) - .and_raise(RuntimeError, 'undefined failure') - - expect(Gitlab::Sentry).to receive(:track_acceptable_exception) - .with(be_a(RuntimeError), anything) - .and_call_original - - pipeline.config_processor + it 'returns true if yaml_errors is set' do expect(pipeline).to have_yaml_errors - expect(pipeline.yaml_errors).to include('Undefined error') + expect(pipeline.yaml_errors).to include('File not foun') end end - context 'when pipeline does not have errors' do - let(:config) do - { rspec: { script: 'rake test' } } - end - - it 'does not contain yaml errors' do - expect(pipeline).not_to have_yaml_errors - end + it 'returns false if yaml_errors is not set' do + expect(pipeline).not_to have_yaml_errors end end diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 87345270100..ce095d2225f 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -312,8 +312,8 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:license_blob).and_return(nil) - expect(presenter.license_anchor_data).to have_attributes(is_link: true, - label: a_string_including('Add license'), + expect(presenter.license_anchor_data).to have_attributes(is_link: false, + label: a_string_including('Add LICENSE'), link: presenter.add_license_path) end end @@ -322,7 +322,7 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:license_blob).and_return(double(name: 'foo')) - expect(presenter.license_anchor_data).to have_attributes(is_link: true, + expect(presenter.license_anchor_data).to have_attributes(is_link: false, label: a_string_including(presenter.license_short_name), link: presenter.license_path) end @@ -420,6 +420,7 @@ describe ProjectPresenter do it 'orders the items correctly' do allow(project.repository).to receive(:readme).and_return(double(name: 'readme')) + allow(project.repository).to receive(:license_blob).and_return(nil) allow(project.repository).to receive(:changelog).and_return(nil) allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo')) allow(presenter).to receive(:filename_path).and_return('fake/path') @@ -433,25 +434,54 @@ describe ProjectPresenter do end end - describe '#empty_repo_statistics_buttons' do - let(:project) { create(:project, :repository) } + describe '#repo_statistics_buttons' do let(:presenter) { described_class.new(project, current_user: user) } - subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons } before do - project.add_developer(user) allow(project).to receive(:auto_devops_enabled?).and_return(false) end - it 'orders the items correctly in an empty project' do - expect(empty_repo_statistics_buttons.map(&:label)).to start_with( - a_string_including('New'), - a_string_including('README'), - a_string_including('CHANGELOG'), - a_string_including('CONTRIBUTING'), - a_string_including('CI/CD') - ) + context 'empty repo' do + let(:project) { create(:project, :stubbed_repository)} + + context 'for a guest user' do + it 'orders the items correctly' do + expect(empty_repo_statistics_buttons.map(&:label)).to start_with( + a_string_including('No license') + ) + end + end + + context 'for a developer' do + before do + project.add_developer(user) + end + + it 'orders the items correctly' do + expect(empty_repo_statistics_buttons.map(&:label)).to start_with( + a_string_including('New'), + a_string_including('README'), + a_string_including('LICENSE'), + a_string_including('CHANGELOG'), + a_string_including('CONTRIBUTING'), + a_string_including('CI/CD') + ) + end + end + end + + context 'initialized repo' do + let(:project) { create(:project, :repository) } + + it 'orders the items correctly' do + expect(empty_repo_statistics_buttons.map(&:label)).to start_with( + a_string_including('README'), + a_string_including('License'), + a_string_including('CHANGELOG'), + a_string_including('CONTRIBUTING') + ) + end end end end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index c98f39ffee8..4ce7a3912a3 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -200,6 +200,52 @@ describe 'getting an issue list for a project' do end end end + + context 'when sorting by relative position' do + let(:sort_project) { create(:project, :public) } + + let!(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } + let!(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } + let!(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } + let!(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) } + let!(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } + + let(:params) { 'sort: RELATIVE_POSITION_ASC' } + + def query(issue_params = params) + graphql_query_for( + 'project', + { 'fullPath' => sort_project.full_path }, + "issues(#{issue_params}) { pageInfo { endCursor} edges { node { iid dueDate } } }" + ) + end + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + context 'when ascending' do + it 'sorts issues' do + expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] + end + + context 'when paginating' do + let(:params) { 'sort: RELATIVE_POSITION_ASC, first: 2' } + + it 'sorts issues' do + expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid] + + cursored_query = query("sort: RELATIVE_POSITION_ASC, after: \"#{end_cursor}\"") + post_graphql(cursored_query, current_user: current_user) + response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] + + expect(grab_iids(response_data)).to eq [relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] + end + end + end + end end def grab_iids(data = issues_data) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index ddfe42129c0..c96c80b6998 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1043,14 +1043,12 @@ describe API::MergeRequests do describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:ci_yaml_file) - .and_return(YAML.dump({ - rspec: { - script: 'ls', - only: ['merge_requests'] - } - })) + stub_ci_pipeline_yaml_file(YAML.dump({ + rspec: { + script: 'ls', + only: ['merge_requests'] + } + })) end let(:project) do diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index a2169a015ee..de0f4841215 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -65,6 +65,7 @@ describe Ci::CreatePipelineService do expect(pipeline.iid).not_to be_nil expect(pipeline.repository_source?).to be true expect(pipeline.builds.first).to be_kind_of(Ci::Build) + expect(pipeline.yaml_errors).not_to be_present end it 'increments the prometheus counter' do @@ -474,6 +475,66 @@ describe Ci::CreatePipelineService do end end + context 'config evaluation' do + context 'when config is in a file in repository' do + before do + content = YAML.dump(rspec: { script: 'echo' }) + stub_ci_pipeline_yaml_file(content) + end + + it 'pull it from the repository' do + pipeline = execute_service + expect(pipeline).to be_repository_source + expect(pipeline.builds.map(&:name)).to eq ['rspec'] + end + end + + context 'when config is from Auto-DevOps' do + before do + stub_ci_pipeline_yaml_file(nil) + allow_any_instance_of(Project).to receive(:auto_devops_enabled?).and_return(true) + end + + it 'pull it from Auto-DevOps' do + pipeline = execute_service + expect(pipeline).to be_auto_devops_source + expect(pipeline.builds.map(&:name)).to eq %w[test code_quality build] + end + end + + context 'when config is not found' do + before do + stub_ci_pipeline_yaml_file(nil) + end + + it 'attaches errors to the pipeline' do + pipeline = execute_service + + expect(pipeline.errors.full_messages).to eq ['Missing .gitlab-ci.yml file'] + expect(pipeline).not_to be_persisted + end + end + + context 'when an unexpected error is raised' do + before do + expect(Gitlab::Ci::YamlProcessor).to receive(:new) + .and_raise(RuntimeError, 'undefined failure') + end + + it 'saves error in pipeline' do + pipeline = execute_service + + expect(pipeline.yaml_errors).to include('Undefined error') + end + + it 'logs error' do + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + execute_service + end + end + end + context 'when yaml is invalid' do let(:ci_yaml) { 'invalid: file: fiile' } let(:message) { 'Message' } @@ -539,6 +600,25 @@ describe Ci::CreatePipelineService do end end + context 'when an unexpected error is raised' do + before do + expect(Gitlab::Ci::YamlProcessor).to receive(:new) + .and_raise(RuntimeError, 'undefined failure') + end + + it 'saves error in pipeline' do + pipeline = execute_service + + expect(pipeline.yaml_errors).to include('Undefined error') + end + + it 'logs error' do + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + execute_service + end + end + context 'when commit contains a [ci skip] directive' do let(:message) { "some message[ci skip]" } diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index e3dde888277..fe343da7838 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -18,8 +18,13 @@ module StubGitlabCalls stub_ci_pipeline_yaml_file(gitlab_ci_yaml) end - def stub_ci_pipeline_yaml_file(ci_yaml) - allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml } + def stub_ci_pipeline_yaml_file(ci_yaml_content) + allow_any_instance_of(Repository).to receive(:gitlab_ci_yml_for).and_return(ci_yaml_content) + + # Ensure we don't hit auto-devops when config not found in repository + unless ci_yaml_content + allow_any_instance_of(Project).to receive(:auto_devops_enabled?).and_return(false) + end end def stub_pipeline_modified_paths(pipeline, modified_paths) diff --git a/spec/views/projects/show.html.haml_spec.rb b/spec/views/projects/show.html.haml_spec.rb index 820772b592f..4f5f0f0285c 100644 --- a/spec/views/projects/show.html.haml_spec.rb +++ b/spec/views/projects/show.html.haml_spec.rb @@ -18,6 +18,14 @@ describe 'projects/show' do end context 'commit signatures' do + context 'with vue tree view enabled' do + it 'are not rendered via js-signature-container' do + render + + expect(rendered).not_to have_css('.js-signature-container') + end + end + context 'with vue tree view disabled' do before do stub_feature_flags(vue_file_list: false) @@ -29,13 +37,5 @@ describe 'projects/show' do expect(rendered).to have_css('.js-signature-container') end end - - context 'with vue tree view enabled' do - it 'are not rendered via js-signature-container' do - render - - expect(rendered).not_to have_css('.js-signature-container') - end - end end end diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb index 4307d1b49c9..8c6b229247d 100644 --- a/spec/views/projects/tree/show.html.haml_spec.rb +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -13,8 +13,6 @@ describe 'projects/tree/show' do let(:tree) { repository.tree(commit.id, path) } before do - stub_feature_flags(vue_file_list: false) - assign(:project, project) assign(:repository, repository) assign(:lfs_blob_ids, []) @@ -39,12 +37,15 @@ describe 'projects/tree/show' do render expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref) - expect(rendered).to have_css('.readme-holder') end end context 'commit signatures' do context 'with vue tree view disabled' do + before do + stub_feature_flags(vue_file_list: false) + end + it 'rendered via js-signature-container' do render @@ -53,10 +54,6 @@ describe 'projects/tree/show' do end context 'with vue tree view enabled' do - before do - stub_feature_flags(vue_file_list: true) - end - it 'are not rendered via js-signature-container' do render |