Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-11-14 12:06:30 +00:00
parent 2fa7d2ddf6
commit d8c06be498
50 changed files with 1009 additions and 363 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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"
/>

View File

@ -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">

View File

@ -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
}
}
}
}
}

View File

@ -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;
};

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Update start a trial option in top right drop down to include Gold
merge_request: 19971
author:
type: changed

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Move some project routes under - scope
merge_request: 19954
author:
type: deprecated

View File

@ -1,5 +0,0 @@
---
title: Make User IDs work per scope in Feature Flags
merge_request: 19399
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add API endpoint to trigger Group Structure Export
merge_request: 19779
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Execute limited request for diff commits instead of preloading
merge_request: 19485
author:
type: performance

View File

@ -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

View File

@ -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
"""

View File

@ -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",

View File

@ -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

View File

@ -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.

View File

@ -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

34
lib/api/group_export.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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');
});
});
});

View File

@ -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);
});

View File

@ -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"

View File

@ -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);
});

View File

@ -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);
});
});
});

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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', () => {

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::FogbugzImport::Client do

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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