Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2fa7d2ddf6
commit
d8c06be498
|
@ -1,44 +0,0 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'StageCardListItem',
|
||||
components: {
|
||||
Icon,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{ active: isActive }"
|
||||
class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
|
||||
>
|
||||
<slot></slot>
|
||||
<div v-if="canEdit" class="dropdown">
|
||||
<gl-button
|
||||
:title="__('More actions')"
|
||||
class="more-actions-toggle btn btn-transparent p-0"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<icon class="icon" name="ellipsis_v" />
|
||||
</gl-button>
|
||||
<ul class="more-actions-dropdown dropdown-menu dropdown-open-left">
|
||||
<slot name="dropdown-options"></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,11 +1,6 @@
|
|||
<script>
|
||||
import StageCardListItem from './stage_card_list_item.vue';
|
||||
|
||||
export default {
|
||||
name: 'StageNavItem',
|
||||
components: {
|
||||
StageCardListItem,
|
||||
},
|
||||
props: {
|
||||
isDefaultStage: {
|
||||
type: Boolean,
|
||||
|
@ -40,16 +35,16 @@ export default {
|
|||
hasValue() {
|
||||
return this.value && this.value.length > 0;
|
||||
},
|
||||
editable() {
|
||||
return this.isUserAllowed && this.canEdit;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li @click="$emit('select')">
|
||||
<stage-card-list-item :is-active="isActive" :can-edit="editable">
|
||||
<div
|
||||
:class="{ active: isActive }"
|
||||
class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
|
||||
>
|
||||
<div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }">
|
||||
{{ title }}
|
||||
</div>
|
||||
|
@ -62,27 +57,6 @@ export default {
|
|||
<span class="not-available">{{ __('Not available') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<template v-slot:dropdown-options>
|
||||
<template v-if="isDefaultStage">
|
||||
<li>
|
||||
<button type="button" class="btn-default btn-transparent">
|
||||
{{ __('Hide stage') }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li>
|
||||
<button type="button" class="btn-default btn-transparent">
|
||||
{{ __('Edit stage') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="btn-danger danger">
|
||||
{{ __('Remove stage') }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
</stage-card-list-item>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -38,7 +38,14 @@ export default {
|
|||
path: this.currentPath.replace(/^\//, ''),
|
||||
};
|
||||
},
|
||||
update: data => data.project.repository.tree.lastCommit,
|
||||
update: data => {
|
||||
const pipelines = data.project.repository.tree.lastCommit.pipelines.edges;
|
||||
|
||||
return {
|
||||
...data.project.repository.tree.lastCommit,
|
||||
pipeline: pipelines.length && pipelines[0].node,
|
||||
};
|
||||
},
|
||||
context: {
|
||||
isSingleRequest: true,
|
||||
},
|
||||
|
@ -61,7 +68,7 @@ export default {
|
|||
computed: {
|
||||
statusTitle() {
|
||||
return sprintf(s__('Commits|Commit: %{commitText}'), {
|
||||
commitText: this.commit.latestPipeline.detailedStatus.text,
|
||||
commitText: this.commit.pipeline.detailedStatus.text,
|
||||
});
|
||||
},
|
||||
isLoading() {
|
||||
|
@ -127,14 +134,14 @@ export default {
|
|||
<div v-if="commit.signatureHtml" v-html="commit.signatureHtml"></div>
|
||||
<div class="ci-status-link">
|
||||
<gl-link
|
||||
v-if="commit.latestPipeline"
|
||||
v-if="commit.pipeline"
|
||||
v-gl-tooltip.left
|
||||
:href="commit.latestPipeline.detailedStatus.detailsPath"
|
||||
:href="commit.pipeline.detailedStatus.detailsPath"
|
||||
:title="statusTitle"
|
||||
class="js-commit-pipeline"
|
||||
>
|
||||
<ci-icon
|
||||
:status="commit.latestPipeline.detailedStatus"
|
||||
:status="commit.pipeline.detailedStatus"
|
||||
:size="24"
|
||||
:aria-label="statusTitle"
|
||||
/>
|
||||
|
|
|
@ -34,7 +34,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<article class="file-holder js-hide-on-navigation limited-width-container readme-holder">
|
||||
<article class="file-holder limited-width-container readme-holder">
|
||||
<div class="file-title">
|
||||
<i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
|
||||
<gl-link :href="blob.webUrl">
|
||||
|
|
|
@ -14,13 +14,17 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
|
|||
webUrl
|
||||
}
|
||||
signatureHtml
|
||||
latestPipeline {
|
||||
detailedStatus {
|
||||
detailsPath
|
||||
icon
|
||||
tooltip
|
||||
text
|
||||
group
|
||||
pipelines(ref: $ref, first: 1) {
|
||||
edges {
|
||||
node {
|
||||
detailedStatus {
|
||||
detailsPath
|
||||
icon
|
||||
tooltip
|
||||
text
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,11 @@ const ASCIIDOC_EXTENSIONS = ['adoc', 'ad', 'asciidoc'];
|
|||
const OTHER_EXTENSIONS = ['textile', 'rdoc', 'org', 'creole', 'wiki', 'mediawiki', 'rst'];
|
||||
const EXTENSIONS = [...MARKDOWN_EXTENSIONS, ...ASCIIDOC_EXTENSIONS, ...OTHER_EXTENSIONS];
|
||||
const PLAIN_FILENAMES = ['readme', 'index'];
|
||||
const FILE_REGEXP = new RegExp(`^(${PLAIN_FILENAMES.join('|')})`, 'i');
|
||||
const FILE_REGEXP = new RegExp(
|
||||
`^(${PLAIN_FILENAMES.join('|')})(.(${EXTENSIONS.join('|')}))?$`,
|
||||
'i',
|
||||
);
|
||||
const PLAIN_FILE_REGEXP = new RegExp(`^(${PLAIN_FILENAMES.join('|')})`, 'i');
|
||||
const EXTENSIONS_REGEXP = new RegExp(`.(${EXTENSIONS.join('|')})$`, 'i');
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
|
@ -11,7 +15,7 @@ export const readmeFile = blobs => {
|
|||
const readMeFiles = blobs.filter(f => f.name.search(FILE_REGEXP) !== -1);
|
||||
|
||||
const previewableReadme = readMeFiles.find(f => f.name.search(EXTENSIONS_REGEXP) !== -1);
|
||||
const plainReadme = readMeFiles.find(f => f.name.search(FILE_REGEXP) !== -1);
|
||||
const plainReadme = readMeFiles.find(f => f.name.search(PLAIN_FILE_REGEXP) !== -1);
|
||||
|
||||
return previewableReadme || plainReadme;
|
||||
};
|
||||
|
|
|
@ -148,7 +148,7 @@ module IssuableCollections
|
|||
when 'Issue'
|
||||
common_attributes + [:project, project: :namespace]
|
||||
when 'MergeRequest'
|
||||
common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits]
|
||||
common_attributes + [:target_project, :latest_merge_request_diff, source_project: :route, head_pipeline: :project, target_project: :namespace]
|
||||
end
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
|
|
@ -6,7 +6,7 @@ class Groups::BoardsController < Groups::ApplicationController
|
|||
|
||||
before_action :assign_endpoint_vars
|
||||
before_action do
|
||||
push_frontend_feature_flag(:multi_select_board)
|
||||
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -8,7 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController
|
|||
before_action :authorize_read_board!, only: [:index, :show]
|
||||
before_action :assign_endpoint_vars
|
||||
before_action do
|
||||
push_frontend_feature_flag(:multi_select_board)
|
||||
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -10,6 +10,14 @@ module Resolvers
|
|||
end
|
||||
end
|
||||
|
||||
def self.last
|
||||
@last ||= Class.new(self) do
|
||||
def resolve(**args)
|
||||
super.last
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.resolver_complexity(args, child_complexity:)
|
||||
complexity = 1
|
||||
complexity += 1 if args[:sort]
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
class CommitPipelinesResolver < BaseResolver
|
||||
include ::ResolvesPipelines
|
||||
|
||||
alias_method :commit, :object
|
||||
|
||||
def resolve(**args)
|
||||
resolve_pipelines(commit.project, args.merge!({ sha: commit.sha }))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -29,12 +29,16 @@ module Types
|
|||
field :author, type: Types::UserType, null: true,
|
||||
description: 'Author of the commit'
|
||||
|
||||
field :pipelines, Types::Ci::PipelineType.connection_type,
|
||||
null: true,
|
||||
description: 'Pipelines of the commit ordered latest first',
|
||||
resolver: Resolvers::CommitPipelinesResolver
|
||||
|
||||
field :latest_pipeline,
|
||||
type: Types::Ci::PipelineType,
|
||||
null: true,
|
||||
description: "Latest pipeline of the commit",
|
||||
resolve: -> (obj, ctx, args) do
|
||||
Gitlab::Graphql::Loaders::PipelineForShaLoader.new(obj.project, obj.sha).find_last
|
||||
end
|
||||
deprecation_reason: 'use pipelines',
|
||||
resolver: Resolvers::CommitPipelinesResolver.last
|
||||
end
|
||||
end
|
||||
|
|
|
@ -451,6 +451,14 @@ class Group < Namespace
|
|||
false
|
||||
end
|
||||
|
||||
def export_file_exists?
|
||||
export_file&.file
|
||||
end
|
||||
|
||||
def export_file
|
||||
import_export_upload&.export_file
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_two_factor_requirement
|
||||
|
|
|
@ -212,8 +212,8 @@ class MergeRequest < ApplicationRecord
|
|||
scope :join_project, -> { joins(:target_project) }
|
||||
scope :references_project, -> { references(:target_project) }
|
||||
scope :with_api_entity_associations, -> {
|
||||
preload(:assignees, :author, :unresolved_notes, :labels, :milestone, :timelogs,
|
||||
latest_merge_request_diff: [:merge_request_diff_commits],
|
||||
preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
|
||||
:timelogs, :latest_merge_request_diff,
|
||||
metrics: [:latest_closed_by, :merged_by],
|
||||
target_project: [:route, { namespace: :route }],
|
||||
source_project: [:route, { namespace: :route }])
|
||||
|
@ -396,14 +396,17 @@ class MergeRequest < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def commit_shas
|
||||
if persisted?
|
||||
merge_request_diff.commit_shas
|
||||
elsif compare_commits
|
||||
compare_commits.to_a.reverse.map(&:sha)
|
||||
else
|
||||
Array(diff_head_sha)
|
||||
end
|
||||
def commit_shas(limit: nil)
|
||||
return merge_request_diff.commit_shas(limit: limit) if persisted?
|
||||
|
||||
shas =
|
||||
if compare_commits
|
||||
compare_commits.to_a.reverse.map(&:sha)
|
||||
else
|
||||
Array(diff_head_sha)
|
||||
end
|
||||
|
||||
limit ? shas.take(limit) : shas
|
||||
end
|
||||
|
||||
# Returns true if there are commits that match at least one commit SHA.
|
||||
|
@ -913,7 +916,7 @@ class MergeRequest < ApplicationRecord
|
|||
|
||||
def commit_notes
|
||||
# Fetch comments only from last 100 commits
|
||||
commit_ids = commit_shas.take(100)
|
||||
commit_ids = commit_shas(limit: 100)
|
||||
|
||||
Note
|
||||
.user
|
||||
|
|
|
@ -218,7 +218,7 @@ class MergeRequestDiff < ApplicationRecord
|
|||
end
|
||||
|
||||
def last_commit_sha
|
||||
commit_shas.first
|
||||
commit_shas(limit: 1).first
|
||||
end
|
||||
|
||||
def first_commit
|
||||
|
@ -247,8 +247,8 @@ class MergeRequestDiff < ApplicationRecord
|
|||
project.commit_by(oid: head_commit_sha)
|
||||
end
|
||||
|
||||
def commit_shas
|
||||
merge_request_diff_commits.map(&:sha)
|
||||
def commit_shas(limit: nil)
|
||||
merge_request_diff_commits.limit(limit).pluck(:sha)
|
||||
end
|
||||
|
||||
def commits_by_shas(shas)
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
- if current_user_menu?(:start_trial)
|
||||
%li
|
||||
%a.profile-link{ href: trials_link_url }
|
||||
= s_("CurrentUser|Start a trial")
|
||||
= s_("CurrentUser|Start a Gold trial")
|
||||
= emoji_icon('rocket')
|
||||
- if current_user_menu?(:settings)
|
||||
%li
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update start a trial option in top right drop down to include Gold
|
||||
merge_request: 19971
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update registry.gitlab.com/gitlab-org/security-products/codequality to 12-5-stable
|
||||
merge_request: 20046
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move some project routes under - scope
|
||||
merge_request: 19954
|
||||
author:
|
||||
type: deprecated
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Make User IDs work per scope in Feature Flags
|
||||
merge_request: 19399
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add API endpoint to trigger Group Structure Export
|
||||
merge_request: 19779
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Execute limited request for diff commits instead of preloading
|
||||
merge_request: 19485
|
||||
author:
|
||||
type: performance
|
|
@ -191,6 +191,31 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
get 'proxy/:datasource_id/*proxy_path', to: 'grafana_api#proxy'
|
||||
get :metrics_dashboard, to: 'grafana_api#metrics_dashboard'
|
||||
end
|
||||
|
||||
resource :mattermost, only: [:new, :create]
|
||||
resource :variables, only: [:show, :update]
|
||||
resources :triggers, only: [:index, :create, :edit, :update, :destroy]
|
||||
|
||||
resource :mirror, only: [:show, :update] do
|
||||
member do
|
||||
get :ssh_host_keys, constraints: { format: :json }
|
||||
post :update_now
|
||||
end
|
||||
end
|
||||
|
||||
resource :cycle_analytics, only: [:show]
|
||||
|
||||
namespace :cycle_analytics do
|
||||
scope :events, controller: 'events' do
|
||||
get :issue
|
||||
get :plan
|
||||
get :code
|
||||
get :test
|
||||
get :review
|
||||
get :staging
|
||||
get :production
|
||||
end
|
||||
end
|
||||
end
|
||||
# End of the /-/ scope.
|
||||
|
||||
|
@ -235,8 +260,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
end
|
||||
end
|
||||
|
||||
resource :mattermost, only: [:new, :create]
|
||||
|
||||
namespace :prometheus do
|
||||
resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do
|
||||
get :active_common, on: :collection
|
||||
|
@ -364,17 +387,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
put '/service_desk' => 'service_desk#update', as: :service_desk_refresh
|
||||
end
|
||||
|
||||
resource :variables, only: [:show, :update]
|
||||
|
||||
resources :triggers, only: [:index, :create, :edit, :update, :destroy]
|
||||
|
||||
resource :mirror, only: [:show, :update] do
|
||||
member do
|
||||
get :ssh_host_keys, constraints: { format: :json }
|
||||
post :update_now
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab.ee do
|
||||
resources :push_rules, constraints: { id: /\d+/ }, only: [:update]
|
||||
end
|
||||
|
@ -463,20 +475,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
end
|
||||
end
|
||||
|
||||
resource :cycle_analytics, only: [:show]
|
||||
|
||||
namespace :cycle_analytics do
|
||||
scope :events, controller: 'events' do
|
||||
get :issue
|
||||
get :plan
|
||||
get :code
|
||||
get :test
|
||||
get :review
|
||||
get :staging
|
||||
get :production
|
||||
end
|
||||
end
|
||||
|
||||
namespace :serverless do
|
||||
scope :functions do
|
||||
get '/:environment_id/:id', to: 'functions#show'
|
||||
|
@ -678,7 +676,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
:network, :graphs, :autocomplete_sources,
|
||||
:project_members, :deploy_keys, :deploy_tokens,
|
||||
:labels, :milestones, :services, :boards, :releases,
|
||||
:forks, :group_links, :import, :avatar)
|
||||
:forks, :group_links, :import, :avatar, :mirror,
|
||||
:cycle_analytics, :mattermost, :variables, :triggers)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -139,13 +139,68 @@ type Commit {
|
|||
"""
|
||||
Latest pipeline of the commit
|
||||
"""
|
||||
latestPipeline: Pipeline
|
||||
latestPipeline(
|
||||
"""
|
||||
Filter pipelines by the ref they are run for
|
||||
"""
|
||||
ref: String
|
||||
|
||||
"""
|
||||
Filter pipelines by the sha of the commit they are run for
|
||||
"""
|
||||
sha: String
|
||||
|
||||
"""
|
||||
Filter pipelines by their status
|
||||
"""
|
||||
status: PipelineStatusEnum
|
||||
): Pipeline @deprecated(reason: "use pipelines")
|
||||
|
||||
"""
|
||||
Raw commit message
|
||||
"""
|
||||
message: String
|
||||
|
||||
"""
|
||||
Pipelines of the commit ordered latest first
|
||||
"""
|
||||
pipelines(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
Filter pipelines by the ref they are run for
|
||||
"""
|
||||
ref: String
|
||||
|
||||
"""
|
||||
Filter pipelines by the sha of the commit they are run for
|
||||
"""
|
||||
sha: String
|
||||
|
||||
"""
|
||||
Filter pipelines by their status
|
||||
"""
|
||||
status: PipelineStatusEnum
|
||||
): PipelineConnection
|
||||
|
||||
"""
|
||||
SHA1 ID of the commit
|
||||
"""
|
||||
|
|
|
@ -10210,15 +10210,44 @@
|
|||
"name": "latestPipeline",
|
||||
"description": "Latest pipeline of the commit",
|
||||
"args": [
|
||||
|
||||
{
|
||||
"name": "status",
|
||||
"description": "Filter pipelines by their status",
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "PipelineStatusEnum",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "ref",
|
||||
"description": "Filter pipelines by the ref they are run for",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "sha",
|
||||
"description": "Filter pipelines by the sha of the commit they are run for",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Pipeline",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "use pipelines"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
|
@ -10234,6 +10263,89 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pipelines",
|
||||
"description": "Pipelines of the commit ordered latest first",
|
||||
"args": [
|
||||
{
|
||||
"name": "status",
|
||||
"description": "Filter pipelines by their status",
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "PipelineStatusEnum",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "ref",
|
||||
"description": "Filter pipelines by the ref they are run for",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "sha",
|
||||
"description": "Filter pipelines by the sha of the commit they are run for",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PipelineConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sha",
|
||||
"description": "SHA1 ID of the commit",
|
||||
|
@ -10306,6 +10418,118 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "PipelineConnection",
|
||||
"description": "The connection type for Pipeline.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PipelineEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Pipeline",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "PipelineEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Pipeline",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Pipeline",
|
||||
|
@ -13205,118 +13429,6 @@
|
|||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "PipelineConnection",
|
||||
"description": "The connection type for Pipeline.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PipelineEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Pipeline",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "PipelineEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Pipeline",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "IssueConnection",
|
||||
|
|
|
@ -185,19 +185,40 @@ Container Scanning can be executed on an offline air-gapped GitLab Ultimate inst
|
|||
1. Host the following Docker images on a [local Docker container registry](../../packages/container_registry/index.md):
|
||||
- [arminc/clair-db vulnerabilities database](https://hub.docker.com/r/arminc/clair-db)
|
||||
- [GitLab klar analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/klar)
|
||||
1. [Override the container scanning template](#overriding-the-container-scanning-template) in your `.gitlab-ci.yml` file to refer to the Docker
|
||||
images hosted on your local Docker container registry:
|
||||
1. [Override the container scanning template](#overriding-the-container-scanning-template) in your `.gitlab-ci.yml` file to refer to the Docker images hosted on your local Docker container registry:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: Container-Scanning.gitlab-ci.yml
|
||||
|
||||
container_scanning:
|
||||
image: your.local.registry:5000/gitlab-klar-analyzer
|
||||
image: $CI_REGISTRY/namespace/gitlab-klar-analyzer
|
||||
variables:
|
||||
CLAIR_DB_IMAGE: your.local.registry:5000/clair-vulnerabilities-db
|
||||
CLAIR_DB_IMAGE: $CI_REGISTRY/namespace/clair-vulnerabilities-db
|
||||
```
|
||||
|
||||
It may be worthwhile to set up a [scheduled pipeline](../../project/pipelines/schedules.md) to automatically build a new version of the vulnerabilities database on a preset schedule. You can use the following `.gitlab-yml.ci` as a template:
|
||||
|
||||
```yaml
|
||||
image: docker:stable
|
||||
|
||||
services:
|
||||
- docker:stable-dind
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
build_latest_vulnerabilities:
|
||||
stage: build
|
||||
script:
|
||||
- docker pull arminc/clair-db:latest
|
||||
- docker tag arminc/clair-db:latest $CI_REGISTRY/namespace/clair-vulnerabilities-db
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker push $CI_REGISTRY/namespace/clair-vulnerabilities-db
|
||||
```
|
||||
|
||||
The above template will work for a GitLab Docker registry running on a local installation, however, if you're using a non-GitLab Docker registry, you'll need to change the `$CI_REGISTRY` value and the `docker login` credentials to match the details of your local registry.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### docker: Error response from daemon: failed to copy xattrs
|
||||
|
|
|
@ -313,6 +313,41 @@ The sample function can now be triggered from any HTTP client using a simple `PO
|
|||
|
||||
![function execution](img/function-execution.png)
|
||||
|
||||
### Running functions locally
|
||||
|
||||
Running a function locally is a good way to quickly verify behavior during development.
|
||||
|
||||
Running functions locally requires:
|
||||
|
||||
- Go 1.12 or newer installed.
|
||||
- Docker Engine installed and running.
|
||||
- `gitlabktl` installed using the Go package manager:
|
||||
|
||||
```shell
|
||||
GO111MODULE=on go get gitlab.com/gitlab-org/gitlabktl
|
||||
```
|
||||
|
||||
To run a function locally:
|
||||
|
||||
1. Navigate to the root of your GitLab serverless project.
|
||||
1. Build your function into a Docker image:
|
||||
|
||||
```shell
|
||||
gitlabktl serverless build
|
||||
```
|
||||
|
||||
1. Run your function in Docker:
|
||||
|
||||
```shell
|
||||
docker run -itp 8080:8080 <your_function_name>
|
||||
```
|
||||
|
||||
1. Invoke your function:
|
||||
|
||||
```shell
|
||||
curl http://localhost:8080
|
||||
```
|
||||
|
||||
## Deploying Serverless applications
|
||||
|
||||
> Introduced in GitLab 11.5.
|
||||
|
|
|
@ -113,6 +113,7 @@ module API
|
|||
mount ::API::Files
|
||||
mount ::API::GroupBoards
|
||||
mount ::API::GroupClusters
|
||||
mount ::API::GroupExport
|
||||
mount ::API::GroupLabels
|
||||
mount ::API::GroupMilestones
|
||||
mount ::API::Groups
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class GroupExport < Grape::API
|
||||
before do
|
||||
authorize! :admin_group, user_group
|
||||
end
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a group'
|
||||
end
|
||||
resource :groups, requirements: { id: %r{[^/]+} } do
|
||||
desc 'Download export' do
|
||||
detail 'This feature was introduced in GitLab 12.5.'
|
||||
end
|
||||
get ':id/export/download' do
|
||||
if user_group.export_file_exists?
|
||||
present_carrierwave_file!(user_group.export_file)
|
||||
else
|
||||
render_api_error!('404 Not found or has expired', 404)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Start export' do
|
||||
detail 'This feature was introduced in GitLab 12.5.'
|
||||
end
|
||||
post ':id/export' do
|
||||
GroupExportWorker.perform_async(current_user.id, user_group.id, params)
|
||||
|
||||
accepted!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,7 +18,7 @@ code_quality:
|
|||
--env SOURCE_CODE="$PWD"
|
||||
--volume "$PWD":/code
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
||||
"registry.gitlab.com/gitlab-org/security-products/codequality:12-0-stable" /code
|
||||
"registry.gitlab.com/gitlab-org/security-products/codequality:12-5-stable" /code
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: gl-code-quality-report.json
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Graphql
|
||||
module Loaders
|
||||
class PipelineForShaLoader
|
||||
attr_accessor :project, :sha
|
||||
|
||||
def initialize(project, sha)
|
||||
@project, @sha = project, sha
|
||||
end
|
||||
|
||||
def find_last
|
||||
BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
|
||||
pipelines = args[:key].ci_pipelines.latest_for_shas(shas)
|
||||
|
||||
pipelines.each do |pipeline|
|
||||
loader.call(pipeline.sha, pipeline)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5000,7 +5000,7 @@ msgstr ""
|
|||
msgid "CurrentUser|Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "CurrentUser|Start a trial"
|
||||
msgid "CurrentUser|Start a Gold trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom CI configuration path"
|
||||
|
@ -7306,9 +7306,6 @@ msgstr ""
|
|||
msgid "FeatureFlags|Inactive flag for %{scope}"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|Include additional user IDs"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|Install a %{docs_link_anchored_start}compatible client library%{docs_link_anchored_end} and specify the API URL, application name, and instance ID during the configuration setup. %{docs_link_start}More Information%{docs_link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -9,8 +9,6 @@ describe 'user reads pipeline status', :js do
|
|||
let(:x110_pipeline) { create_pipeline('x1.1.0', 'failed') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(vue_file_list: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
|
||||
project.repository.add_tag(user, 'x1.1.0', 'v1.1.0')
|
||||
|
@ -25,7 +23,7 @@ describe 'user reads pipeline status', :js do
|
|||
visit project_tree_path(project, expected_pipeline.ref)
|
||||
wait_for_requests
|
||||
|
||||
page.within('.blob-commit-info') do
|
||||
page.within('.commit-detail') do
|
||||
expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
|
||||
expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
|
||||
end
|
||||
|
|
|
@ -133,45 +133,19 @@ describe('StageNavItem', () => {
|
|||
hasStageName();
|
||||
});
|
||||
|
||||
it('renders options menu', () => {
|
||||
expect(wrapper.find('.more-actions-toggle').exists()).toBe(true);
|
||||
it('does not render options menu', () => {
|
||||
expect(wrapper.find('.more-actions-toggle').exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('Default stages', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(
|
||||
{ canEdit: true, isUserAllowed: true, isDefaultStage: true },
|
||||
false,
|
||||
);
|
||||
});
|
||||
it('can hide the stage', () => {
|
||||
expect(wrapper.text()).toContain('Hide stage');
|
||||
});
|
||||
it('can not edit the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Edit stage');
|
||||
});
|
||||
it('can not remove the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Remove stage');
|
||||
});
|
||||
it('can not edit the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Edit stage');
|
||||
});
|
||||
it('can not remove the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Remove stage');
|
||||
});
|
||||
|
||||
describe('Custom stages', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(
|
||||
{ canEdit: true, isUserAllowed: true, isDefaultStage: false },
|
||||
false,
|
||||
);
|
||||
});
|
||||
it('can edit the stage', () => {
|
||||
expect(wrapper.text()).toContain('Edit stage');
|
||||
});
|
||||
it('can remove the stage', () => {
|
||||
expect(wrapper.text()).toContain('Remove stage');
|
||||
});
|
||||
|
||||
it('can not hide the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Hide stage');
|
||||
});
|
||||
it('can not hide the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Hide stage');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ function createCommitData(data = {}) {
|
|||
avatarUrl: 'https://test.com',
|
||||
webUrl: 'https://test.com/test',
|
||||
},
|
||||
latestPipeline: {
|
||||
pipeline: {
|
||||
detailedStatus: {
|
||||
detailsPath: 'https://test.com/pipeline',
|
||||
icon: 'failed',
|
||||
|
@ -74,7 +74,7 @@ describe('Repository last commit component', () => {
|
|||
});
|
||||
|
||||
it('hides pipeline components when pipeline does not exist', () => {
|
||||
factory(createCommitData({ latestPipeline: null }));
|
||||
factory(createCommitData({ pipeline: null }));
|
||||
|
||||
expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`Repository file preview component renders file HTML 1`] = `
|
||||
<article
|
||||
class="file-holder js-hide-on-navigation limited-width-container readme-holder"
|
||||
class="file-holder limited-width-container readme-holder"
|
||||
>
|
||||
<div
|
||||
class="file-title"
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('Repository table component', () => {
|
|||
it('renders file preview', () => {
|
||||
factory('/');
|
||||
|
||||
vm.setData({ entries: { blobs: [{ name: 'README.md ' }] } });
|
||||
vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
|
||||
|
||||
expect(vm.find(FilePreview).exists()).toBe(true);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { readmeFile } from '~/repository/utils/readme';
|
||||
|
||||
describe('readmeFile', () => {
|
||||
describe('markdown files', () => {
|
||||
it('returns markdown file', () => {
|
||||
expect(readmeFile([{ name: 'README' }, { name: 'README.md' }])).toEqual({
|
||||
name: 'README.md',
|
||||
});
|
||||
|
||||
expect(readmeFile([{ name: 'README' }, { name: 'index.md' }])).toEqual({
|
||||
name: 'index.md',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('plain files', () => {
|
||||
it('returns plain file', () => {
|
||||
expect(readmeFile([{ name: 'README' }, { name: 'TEST.md' }])).toEqual({
|
||||
name: 'README',
|
||||
});
|
||||
|
||||
expect(readmeFile([{ name: 'readme' }, { name: 'TEST.md' }])).toEqual({
|
||||
name: 'readme',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-previewable file', () => {
|
||||
it('returns undefined', () => {
|
||||
expect(readmeFile([{ name: 'index.js' }, { name: 'TEST.md' }])).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,6 +13,14 @@ describe Resolvers::BaseResolver do
|
|||
end
|
||||
end
|
||||
|
||||
let(:last_resolver) do
|
||||
Class.new(described_class) do
|
||||
def resolve(**args)
|
||||
[1, 2]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.single' do
|
||||
it 'returns a subclass from the resolver' do
|
||||
expect(resolver.single.superclass).to eq(resolver)
|
||||
|
@ -29,6 +37,22 @@ describe Resolvers::BaseResolver do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.last' do
|
||||
it 'returns a subclass from the resolver' do
|
||||
expect(last_resolver.last.superclass).to eq(last_resolver)
|
||||
end
|
||||
|
||||
it 'returns the same subclass every time' do
|
||||
expect(last_resolver.last.object_id).to eq(last_resolver.last.object_id)
|
||||
end
|
||||
|
||||
it 'returns a resolver that gives the last result from the original resolver' do
|
||||
result = resolve(last_resolver.last)
|
||||
|
||||
expect(result).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when field is a connection' do
|
||||
it 'increases complexity based on arguments' do
|
||||
field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1)
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Resolvers::CommitPipelinesResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let(:commit) { create(:commit, project: project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let!(:pipeline) do
|
||||
create(
|
||||
:ci_pipeline,
|
||||
project: project,
|
||||
sha: commit.id,
|
||||
ref: 'master',
|
||||
status: 'success'
|
||||
)
|
||||
end
|
||||
let!(:pipeline2) do
|
||||
create(
|
||||
:ci_pipeline,
|
||||
project: project,
|
||||
sha: commit.id,
|
||||
ref: 'master',
|
||||
status: 'failed'
|
||||
)
|
||||
end
|
||||
let!(:pipeline3) do
|
||||
create(
|
||||
:ci_pipeline,
|
||||
project: project,
|
||||
sha: commit.id,
|
||||
ref: 'my_branch',
|
||||
status: 'failed'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
commit.project.add_developer(current_user)
|
||||
end
|
||||
|
||||
def resolve_pipelines
|
||||
resolve(described_class, obj: commit, ctx: { current_user: current_user }, args: { ref: 'master' })
|
||||
end
|
||||
|
||||
it 'resolves pipelines for commit and ref' do
|
||||
pipelines = resolve_pipelines
|
||||
|
||||
expect(pipelines).to eq([pipeline2, pipeline])
|
||||
end
|
||||
end
|
|
@ -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, :signature_html
|
||||
:author, :web_url, :latest_pipeline, :pipelines, :signature_html
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
|
||||
|
||||
const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables';
|
||||
const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/-/variables';
|
||||
const HIDE_CLASS = 'hide';
|
||||
|
||||
describe('AjaxFormVariableList', () => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::FogbugzImport::Client do
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Graphql::Loaders::PipelineForShaLoader do
|
||||
include GraphqlHelpers
|
||||
|
||||
describe '#find_last' do
|
||||
it 'batch-resolves latest pipeline' do
|
||||
project = create(:project, :repository)
|
||||
pipeline1 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
|
||||
pipeline2 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
|
||||
pipeline3 = create(:ci_pipeline, project: project, ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
|
||||
|
||||
result = batch_sync(max_queries: 1) do
|
||||
[pipeline1.sha, pipeline3.sha].map { |sha| described_class.new(project, sha).find_last }
|
||||
end
|
||||
|
||||
expect(result).to contain_exactly(pipeline2, pipeline3)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -378,6 +378,14 @@ describe MergeRequestDiff do
|
|||
expect(diff_with_commits.commit_shas).not_to be_empty
|
||||
expect(diff_with_commits.commit_shas).to all(match(/\h{40}/))
|
||||
end
|
||||
|
||||
context 'with limit attribute' do
|
||||
it 'returns limited number of shas' do
|
||||
expect(diff_with_commits.commit_shas(limit: 2).size).to eq(2)
|
||||
expect(diff_with_commits.commit_shas(limit: 100).size).to eq(29)
|
||||
expect(diff_with_commits.commit_shas.size).to eq(29)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#compare_with' do
|
||||
|
|
|
@ -1261,13 +1261,49 @@ describe MergeRequest do
|
|||
end
|
||||
|
||||
describe '#commit_shas' do
|
||||
before do
|
||||
allow(subject.merge_request_diff).to receive(:commit_shas)
|
||||
.and_return(['sha1'])
|
||||
context 'persisted merge request' do
|
||||
context 'with a limit' do
|
||||
it 'returns a limited number of commit shas' do
|
||||
expect(subject.commit_shas(limit: 2)).to eq(%w[
|
||||
b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a limit' do
|
||||
it 'returns all commit shas of the merge request diff' do
|
||||
expect(subject.commit_shas.size).to eq(29)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'delegates to merge request diff' do
|
||||
expect(subject.commit_shas).to eq ['sha1']
|
||||
context 'new merge request' do
|
||||
subject { build(:merge_request) }
|
||||
|
||||
context 'compare commits' do
|
||||
before do
|
||||
subject.compare_commits = [
|
||||
double(sha: 'sha1'), double(sha: 'sha2')
|
||||
]
|
||||
end
|
||||
|
||||
context 'without a limit' do
|
||||
it 'returns all shas of compare commits' do
|
||||
expect(subject.commit_shas).to eq(%w[sha2 sha1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a limit' do
|
||||
it 'returns a limited number of shas' do
|
||||
expect(subject.commit_shas(limit: 1)).to eq(['sha2'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns diff_head_sha as an array' do
|
||||
expect(subject.commit_shas).to eq([subject.diff_head_sha])
|
||||
expect(subject.commit_shas(limit: 2)).to eq([subject.diff_head_sha])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe API::GroupExport do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:path) { "/groups/#{group.id}/export" }
|
||||
let(:download_path) { "/groups/#{group.id}/export/download" }
|
||||
|
||||
let(:export_path) { "#{Dir.tmpdir}/group_export_spec" }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Gitlab::ImportExport) do |import_export|
|
||||
expect(import_export).to receive(:storage_path).and_return(export_path)
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(export_path, secure: true)
|
||||
end
|
||||
|
||||
describe 'GET /groups/:group_id/export/download' do
|
||||
let(:upload) { ImportExportUpload.new(group: group) }
|
||||
|
||||
before do
|
||||
stub_uploads_object_storage(ImportExportUploader)
|
||||
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'when export file exists' do
|
||||
before do
|
||||
upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")
|
||||
upload.save!
|
||||
end
|
||||
|
||||
it 'downloads exported group archive' do
|
||||
get api(download_path, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
context 'when export_file.file does not exist' do
|
||||
before do
|
||||
expect_next_instance_of(ImportExportUploader) do |uploader|
|
||||
expect(uploader).to receive(:file).and_return(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns 404' do
|
||||
get api(download_path, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when export file does not exist' do
|
||||
it 'returns 404' do
|
||||
get api(download_path, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /groups/:group_id/export' do
|
||||
context 'when user is a group owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'accepts download' do
|
||||
post api(path, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(202)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not a group owner' do
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'forbids the request' do
|
||||
post api(path, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccessMatchersHelpers
|
||||
USER_ACCESSOR_METHOD_NAME = 'user'
|
||||
|
||||
def provide_user(role, membership = nil)
|
||||
case role
|
||||
when :admin
|
||||
create(:admin)
|
||||
when :auditor
|
||||
create(:user, :auditor)
|
||||
when :user
|
||||
create(:user)
|
||||
when :external
|
||||
create(:user, :external)
|
||||
when :visitor, :anonymous
|
||||
nil
|
||||
when User
|
||||
role
|
||||
when *Gitlab::Access.sym_options_with_owner.keys # owner, maintainer, developer, reporter, guest
|
||||
raise ArgumentError, "cannot provide #{role} when membership reference is blank" unless membership
|
||||
|
||||
provide_user_by_membership(role, membership)
|
||||
else
|
||||
raise ArgumentError, "cannot provide user of an unknown role #{role}"
|
||||
end
|
||||
end
|
||||
|
||||
def provide_user_by_membership(role, membership)
|
||||
if role == :owner && membership.owner
|
||||
membership.owner
|
||||
else
|
||||
create(:user).tap do |user|
|
||||
membership.public_send(:"add_#{role}", user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def raise_if_non_block_expectation!(actual)
|
||||
raise ArgumentError, 'This matcher supports block expectations only.' unless actual.is_a?(Proc)
|
||||
end
|
||||
|
||||
def update_owner(objects, user)
|
||||
return unless objects
|
||||
|
||||
objects.each do |object|
|
||||
if object.respond_to?(:owner)
|
||||
object.update_attribute(:owner, user)
|
||||
elsif object.respond_to?(:user)
|
||||
object.update_attribute(:user, user)
|
||||
else
|
||||
raise ArgumentError, "cannot own this object #{object}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def patch_example_group(user)
|
||||
return if user.nil? # for anonymous users
|
||||
|
||||
# This call is evaluated in context of ExampleGroup instance in which the matcher is called. Overrides the `user`
|
||||
# (or defined by `method_name`) method generated by `let` definition in example group before it's used by `subject`.
|
||||
# This override is per concrete example only because the example group class gets re-created for each example.
|
||||
instance_eval(<<~CODE, __FILE__, __LINE__ + 1)
|
||||
if instance_variable_get(:@__#{USER_ACCESSOR_METHOD_NAME}_patched)
|
||||
raise ArgumentError, 'An access matcher be_allowed_for/be_denied_for can be used only once per example (`it` block)'
|
||||
end
|
||||
instance_variable_set(:@__#{USER_ACCESSOR_METHOD_NAME}_patched, true)
|
||||
|
||||
def #{USER_ACCESSOR_METHOD_NAME}
|
||||
@#{USER_ACCESSOR_METHOD_NAME} ||= User.find(#{user.id})
|
||||
end
|
||||
CODE
|
||||
end
|
||||
|
||||
def prepare_matcher_environment(role, membership, owned_objects)
|
||||
user = provide_user(role, membership)
|
||||
|
||||
if user
|
||||
update_owner(owned_objects, user)
|
||||
patch_example_group(user)
|
||||
end
|
||||
end
|
||||
|
||||
def run_matcher(action, role, membership, owned_objects)
|
||||
raise_if_non_block_expectation!(action)
|
||||
|
||||
prepare_matcher_environment(role, membership, owned_objects)
|
||||
|
||||
if block_given?
|
||||
yield action
|
||||
else
|
||||
action.call
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# AccessMatchersForRequest
|
||||
#
|
||||
# Matchers to test the access permissions for requests specs (most useful for API tests).
|
||||
module AccessMatchersForRequest
|
||||
extend RSpec::Matchers::DSL
|
||||
include AccessMatchersHelpers
|
||||
|
||||
EXPECTED_STATUS_CODES_ALLOWED = [200, 201, 204, 302, 304].freeze
|
||||
EXPECTED_STATUS_CODES_DENIED = [401, 403, 404].freeze
|
||||
|
||||
def description_for(role, type, expected, result)
|
||||
"be #{type} for #{role} role. Expected status code: any of #{expected.join(', ')} Got: #{result}"
|
||||
end
|
||||
|
||||
matcher :be_allowed_for do |role|
|
||||
match do |action|
|
||||
# methods called in this and negated block are being run in context of ExampleGroup
|
||||
# (not matcher) instance so we have to pass data via local vars
|
||||
|
||||
run_matcher(action, role, @membership, @owned_objects)
|
||||
|
||||
EXPECTED_STATUS_CODES_ALLOWED.include?(response.status)
|
||||
end
|
||||
|
||||
match_when_negated do |action|
|
||||
run_matcher(action, role, @membership, @owned_objects)
|
||||
|
||||
EXPECTED_STATUS_CODES_DENIED.include?(response.status)
|
||||
end
|
||||
|
||||
chain :of do |membership|
|
||||
@membership = membership
|
||||
end
|
||||
|
||||
chain :own do |*owned_objects|
|
||||
@owned_objects = owned_objects
|
||||
end
|
||||
|
||||
failure_message do
|
||||
"expected this action to #{description_for(role, 'allowed', EXPECTED_STATUS_CODES_ALLOWED, response.status)}"
|
||||
end
|
||||
|
||||
failure_message_when_negated do
|
||||
"expected this action to #{description_for(role, 'denied', EXPECTED_STATUS_CODES_DENIED, response.status)}"
|
||||
end
|
||||
|
||||
supports_block_expectations
|
||||
end
|
||||
|
||||
RSpec::Matchers.define_negated_matcher :be_denied_for, :be_allowed_for
|
||||
end
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# AccessMatchersGeneric
|
||||
#
|
||||
# Matchers to test the access permissions for service classes or other generic pieces of business logic.
|
||||
module AccessMatchersGeneric
|
||||
extend RSpec::Matchers::DSL
|
||||
include AccessMatchersHelpers
|
||||
|
||||
ERROR_CLASS = Gitlab::Access::AccessDeniedError
|
||||
|
||||
def error_message(error)
|
||||
str = error.class.name
|
||||
str += ": #{error.message}" if error.message != error.class.name
|
||||
str
|
||||
end
|
||||
|
||||
def error_expectation_message(allowed, error)
|
||||
if allowed
|
||||
"Expected to raise nothing but #{error_message(error)} was raised."
|
||||
else
|
||||
"Expected to raise #{ERROR_CLASS} but nothing was raised."
|
||||
end
|
||||
end
|
||||
|
||||
def description_for(role, type, error)
|
||||
allowed = type == 'allowed'
|
||||
"be #{type} for #{role} role. #{error_expectation_message(allowed, error)}"
|
||||
end
|
||||
|
||||
matcher :be_allowed_for do |role|
|
||||
match do |action|
|
||||
# methods called in this and negated block are being run in context of ExampleGroup
|
||||
# (not matcher) instance so we have to pass data via local vars
|
||||
|
||||
run_matcher(action, role, @membership, @owned_objects) do |action|
|
||||
action.call
|
||||
rescue => e
|
||||
@error = e
|
||||
raise unless e.is_a?(ERROR_CLASS)
|
||||
end
|
||||
|
||||
@error.nil?
|
||||
end
|
||||
|
||||
chain :of do |membership|
|
||||
@membership = membership
|
||||
end
|
||||
|
||||
chain :own do |*owned_objects|
|
||||
@owned_objects = owned_objects
|
||||
end
|
||||
|
||||
failure_message do
|
||||
"expected this action to #{description_for(role, 'allowed', @error)}"
|
||||
end
|
||||
|
||||
failure_message_when_negated do
|
||||
"expected this action to #{description_for(role, 'denied', @error)}"
|
||||
end
|
||||
|
||||
supports_block_expectations
|
||||
end
|
||||
|
||||
RSpec::Matchers.define_negated_matcher :be_denied_for, :be_allowed_for
|
||||
end
|
Loading…
Reference in New Issue