Commit f28b8d34 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-12-07' into 'master'

CE upstream - 2018-12-07 02:38 UTC

Closes gitlab-ce#54729, gitlab-ce#52607, #2745, and gitlab-ce#54718

See merge request gitlab-org/gitlab-ee!8746
parents 2e3b4da9 b09a0d69
......@@ -628,6 +628,13 @@ entry.
- Check frozen string in style builds. (gfyoung)
## 11.3.12 (2018-12-06)
### Security (1 change)
- Prevent a path traversal attack on global file templates.
## 11.3.11 (2018-11-26)
### Security (33 changes)
......
......@@ -5,7 +5,7 @@ end
gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10'
gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.11'
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# The 2.0.6 version of rack requires monkeypatch to be present in
......@@ -273,6 +273,9 @@ gem 'ace-rails-ap', '~> 4.1.0'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
# Detect mime content type from content
gem 'mimemagic', '~> 0.3.2'
# Faster blank
gem 'fast_blank'
......
......@@ -90,6 +90,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
......@@ -486,7 +487,7 @@ GEM
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mimemagic (0.3.0)
mimemagic (0.3.2)
mini_magick (4.8.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
......@@ -753,8 +754,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-parameterized (0.4.0)
binding_of_caller
rspec-parameterized (0.4.1)
binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
......@@ -924,7 +925,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
unparser (0.2.7)
unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
......@@ -1086,6 +1087,7 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
mimemagic (~> 0.3.2)
mini_magick
minitest (~> 5.7.0)
mysql2 (~> 0.4.10)
......
......@@ -87,6 +87,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
......@@ -483,7 +484,7 @@ GEM
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mimemagic (0.3.0)
mimemagic (0.3.2)
mini_magick (4.8.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
......@@ -647,16 +648,16 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.10)
actionmailer (= 4.2.10)
actionpack (= 4.2.10)
actionview (= 4.2.10)
activejob (= 4.2.10)
activemodel (= 4.2.10)
activerecord (= 4.2.10)
activesupport (= 4.2.10)
rails (4.2.11)
actionmailer (= 4.2.11)
actionpack (= 4.2.11)
actionview (= 4.2.11)
activejob (= 4.2.11)
activemodel (= 4.2.11)
activerecord (= 4.2.11)
activesupport (= 4.2.11)
bundler (>= 1.3.0, < 2.0)
railties (= 4.2.10)
railties (= 4.2.11)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
......@@ -744,8 +745,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-parameterized (0.4.0)
binding_of_caller
rspec-parameterized (0.4.1)
binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
......@@ -918,7 +919,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
unparser (0.2.7)
unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
......@@ -1077,6 +1078,7 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
mimemagic (~> 0.3.2)
mini_magick
minitest (~> 5.7.0)
mysql2 (~> 0.4.10)
......@@ -1121,7 +1123,7 @@ DEPENDENCIES
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
rails (= 4.2.10)
rails (= 4.2.11)
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9)
rainbow (~> 3.0)
......
......@@ -92,20 +92,7 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
<span>Projects</span>
<button
aria-label="Close"
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<icon
name="merge-request-close-m"
data-hidden="true"
class="dropdown-menu-close-icon"
/>
</button>
</div>
<div class="dropdown-title">Projects</div>
<div class="dropdown-input">
<input class="dropdown-input-field" type="search" placeholder="Search projects" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
......
......@@ -90,6 +90,8 @@ export default {
:old-sha="diffFile.diff_refs.base_sha"
:file-hash="diffFile.file_hash"
:project-path="projectPath"
:a-mode="diffFile.a_mode"
:b-mode="diffFile.b_mode"
>
<image-diff-overlay
slot="image-overlay"
......
......@@ -52,7 +52,9 @@ export default {
(!this.file.highlighted_diff_lines &&
!this.isLoadingCollapsedDiff &&
!this.file.too_large &&
this.file.text)
this.file.text &&
!this.file.renamed_file &&
!this.file.mode_changed)
);
},
showLoadingIcon() {
......@@ -143,9 +145,8 @@ export default {
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
>Fork</a
>
Fork
</a>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
......@@ -163,9 +164,9 @@ export default {
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }}
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">
{{ __('Click to expand it.') }}
</a>
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
__('Click to expand it.')
}}</a>
</div>
<div v-if="file.too_large" class="nothing-here-block diff-collapsed js-too-large-diff">
{{ __('This source diff could not be displayed because it is too large.') }}
......
......@@ -324,5 +324,9 @@ export const generateTreeList = files =>
export const getDiffMode = diffFile => {
const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]);
return diffModes[diffModeKey] || diffModes.replaced;
return (
diffModes[diffModeKey] ||
(diffFile.mode_changed && diffModes.mode_changed) ||
diffModes.replaced
);
};
......@@ -26,6 +26,7 @@ export const diffModes = {
new: 'new',
deleted: 'deleted',
renamed: 'renamed',
mode_changed: 'mode_changed',
};
export const rightSidebarViews = {
......
import ServerlessBundle from '~/serverless/serverless_bundle';
document.addEventListener('DOMContentLoaded', () => {
new ServerlessBundle(); // eslint-disable-line no-new
});
<script>
export default {
props: {
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="row empty-state js-empty-state">
<div class="col-12">
<div class="text-content">
<h4 class="state-title text-center">
{{ s__('Serverless|Getting started with serverless') }}
</h4>
<p class="state-description">
{{
s__(`Serverless| In order to start using functions as a service,
you must first install Knative on your Kubernetes cluster.`)
}}
<a :href="helpPath"> {{ __('More information') }} </a>
</p>
<div class="text-center">
<a :href="clustersPath" class="btn btn-success">
{{ s__('Serverless|Install Knative') }}
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
Timeago,
},
props: {
func: {
type: Object,
required: true,
},
},
computed: {
name() {
return this.func.name;
},
url() {
return this.func.url;
},
image() {
return this.func.image;
},
timestamp() {
return this.func.created_at;
},
},
};
</script>
<template>
<div class="gl-responsive-table-row">
<div class="table-section section-20">{{ name }}</div>
<div class="table-section section-50">
<a :href="url">{{ url }}</a>
</div>
<div class="table-section section-20">{{ image }}</div>
<div class="table-section section-10"><timeago :time="timestamp" /></div>
</div>
</template>
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import FunctionRow from './function_row.vue';
import EmptyState from './empty_state.vue';
export default {
components: {
FunctionRow,
EmptyState,
GlSkeletonLoading,
},
props: {
functions: {
type: Array,
required: true,
default: () => [],
},
installed: {
type: Boolean,
required: true,
},
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
loadingData: {
type: Boolean,
required: false,
default: true,
},
hasFunctionData: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
<template>
<section id="serverless-functions">
<div v-if="installed">
<div v-if="hasFunctionData">
<div class="ci-table js-services-list function-element">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-20" role="rowheader">
{{ s__('Serverless|Function') }}
</div>
<div class="table-section section-50" role="rowheader">
{{ s__('Serverless|Domain') }}
</div>
<div class="table-section section-20" role="rowheader">
{{ s__('Serverless|Runtime') }}
</div>
<div class="table-section section-10" role="rowheader">
{{ s__('Serverless|Last Update') }}
</div>
</div>
<template v-if="loadingData">
<div v-for="j in 3" :key="j" class="gl-responsive-table-row">
<gl-skeleton-loading />
</div>
</template>
<template v-else>
<function-row v-for="f in functions" :key="f.name" :func="f" />
</template>
</div>
</div>
<div v-else class="empty-state js-empty-state">
<div class="text-content">
<h4 class="state-title text-center">{{ s__('Serverless|No functions available') }}</h4>
<p class="state-description">
{{
s__(`Serverless|There is currently no function data available from Knative.
This could be for a variety of reasons including:`)
}}
</p>
<ul>
<li>Your repository does not have a corresponding <code>serverless.yml</code> file.</li>
<li>Your <code>gitlab-ci.yml</code> file is not properly configured.</li>
<li>
The functions listed in the <code>serverless.yml</code> file don't match the namespace
of your cluster.
</li>
<li>The deploy job has not finished.</li>
</ul>
<p>
{{
s__(`Serverless|If you believe none of these apply, please check
back later as the function data may be in the process of becoming
available.`)
}}
</p>
<div class="text-center">
<a :href="helpPath" class="btn btn-success">
{{ s__('Serverless|Learn more about Serverless') }}
</a>
</div>
</div>
</div>
</div>
<empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" />
</section>
</template>
<style>
.top-area {
border-bottom: 0;
}
.function-element {
border-bottom: 1px solid #e5e5e5;
border-bottom-color: rgb(229, 229, 229);
border-bottom-style: solid;
border-bottom-width: 1px;
}
</style>
import Vue from 'vue';
export default new Vue();
import Visibility from 'visibilityjs';
import Vue from 'vue';
import { s__ } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import ServerlessStore from './stores/serverless_store';
import GetFunctionsService from './services/get_functions_service';
import Functions from './components/functions.vue';
export default class Serverless {
constructor() {
const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
'.js-serverless-functions-page',
).dataset;
this.service = new GetFunctionsService(statusPath);
this.knativeInstalled = installed !== undefined;
this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath);
this.initServerless();
this.functionLoadCount = 0;
if (statusPath && this.knativeInstalled) {
this.initPolling();
}
}
initServerless() {
const { store } = this;
const el = document.querySelector('#js-serverless-functions');
this.functions = new Vue({
el,
data() {
return {
state: store.state,
};
},
render(createElement) {
return createElement(Functions, {
props: {
functions: this.state.functions,
installed: this.state.installed,
clustersPath: this.state.clustersPath,
helpPath: this.state.helpPath,
loadingData: this.state.loadingData,
hasFunctionData: this.state.hasFunctionData,
},
});
},
});
}
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: data => this.handleSuccess(data),
errorCallback: () => this.handleError(),
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service
.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => this.handleError());
}
Visibility.change(() => {
if (!Visibility.hidden() && !this.destroyed) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
handleSuccess(data) {
if (data.status === 200) {
this.store.updateFunctionsFromServer(data.data);
this.store.updateLoadingState(false);
} else if (data.status === 204) {
/* Time out after 3 attempts to retrieve data */
this.functionLoadCount += 1;
if (this.functionLoadCount === 3) {
this.poll.stop();
this.store.toggleNoFunctionData();
}
}
}
static handleError() {
Flash(s__('Serverless|An error occurred while retrieving serverless components'));
}
destroy() {
this.destroyed = true;
if (this.poll) {
this.poll.stop();
}
this.functions.$destroy();
}
}
import axios from '~/lib/utils/axios_utils';
export default class GetFunctionsService {
constructor(endpoint) {
this.endpoint = endpoint;
}
fetchData() {
return axios.get(this.endpoint);
}
}
export default class ServerlessStore {
constructor(knativeInstalled = false, clustersPath, helpPath) {
this.state = {
functions: [],
hasFunctionData: true,
loadingData: true,
installed: knativeInstalled,
clustersPath,
helpPath,
};
}
updateFunctionsFromServer(functions = []) {
this.state.functions = functions;
}
updateLoadingState(loadingData) {
this.state.loadingData = loadingData;
}
toggleNoFunctionData() {
this.state.hasFunctionData = false;
}
}
<script>
import { diffModes } from '~/ide/constants';
import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils';
import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
import RenamedFile from './viewers/renamed.vue';
import ModeChanged from './viewers/mode_changed.vue';
export default {
props: {
......@@ -30,9 +33,25 @@ export default {
required: false,
default: '',
},
aMode: {
type: String,
required: false,
default: null,
},
bMode: {
type: String,
required: false,
default: null,
},
},
computed: {
viewer() {
if (this.diffMode === diffModes.renamed) {
return RenamedFile;
} else if (this.diffMode === diffModes.mode_changed) {
return ModeChanged;
}
if (!this.newPath) return null;
const previewInfo = viewerInformationForPath(this.newPath);
......@@ -67,8 +86,10 @@ export default {
:new-path="fullNewPath"
:old-path="fullOldPath"
:project-path="projectPath"
:a-mode="aMode"
:b-mode="bMode"
>
<slot slot="image-overlay" name="image-overlay"> </slot>
<slot slot="image-overlay" name="image-overlay"></slot>
</component>
<slot></slot>
</div>
......
<script>
import { sprintf, __ } from '~/locale';
export default {
props: {
aMode: {
type: String,
required: false,
default: null,
},
bMode: {
type: String,
required: false,
default: null,
},
},
computed: {
outputText() {
return sprintf(__('File mode changed from %{a_mode} to %{b_mode}'), {
a_mode: this.aMode,
b_mode: this.bMode,
});
},
},
};
</script>
<template>
<div class="nothing-here-block">{{ outputText }}</div>
</template>
<template>
<div class="nothing-here-block">{{ __('File moved') }}</div>
</template>
......@@ -261,7 +261,7 @@
height: 1px;
margin: 4px -1px;
padding: 0;
background-color: $dropdown-divider-color;
background-color: $dropdown-divider-bg;
}
> .active {
......
......@@ -294,10 +294,10 @@
height: 1px;
margin: #{$grid-size / 2} 0;
padding: 0;
background-color: $dropdown-divider-color;
background-color: $dropdown-divider-bg;
&:hover {
background-color: $dropdown-divider-color;
background-color: $dropdown-divider-bg;
}
}
......@@ -306,7 +306,7 @@
height: 1px;
margin-top: 8px;
margin-bottom: 8px;
background-color: $dropdown-divider-color;
background-color: $dropdown-divider-bg;
}
.dropdown-menu-empty-item a {
......@@ -542,7 +542,7 @@
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
border-bottom: 1px solid $dropdown-divider-color;
border-bottom: 1px solid $dropdown-divider-bg;
overflow: hidden;
}
......@@ -621,7 +621,7 @@
padding: 0 7px;
color: $gl-gray-700;
line-height: 30px;
border: 1px solid $dropdown-divider-color;
border: 1px solid $dropdown-divider-bg;
border-radius: 2px;
outline: 0;
......@@ -656,7 +656,7 @@
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
border-top: 1px solid $dropdown-divider-color;
border-top: 1px solid $dropdown-divider-bg;
}
.dropdown-footer-content {
......
......@@ -341,7 +341,6 @@ $dropdown-max-height: 312px;
$dropdown-vertical-offset: 4px;
$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-shadow-color: rgba(#000, 0.1);
$dropdown-divider-color: rgba(#000, 0.1);
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-fa-color: #c7c7c7;
$dropdown-input-focus-shadow: rgba($blue-300, 0.4);
......
......@@ -20,3 +20,4 @@ $warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
$dropdown-divider-bg: $theme-gray-200;
......@@ -42,7 +42,7 @@
.issue-board-dropdown-content {
margin: 0 8px 10px;
padding-bottom: 10px;
border-bottom: 1px solid $dropdown-divider-color;
border-bottom: 1px solid $dropdown-divider-bg;
> p {
margin: 0;
......
......@@ -10,6 +10,8 @@ module SnippetsActions
def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
workhorse_set_content_type!
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
......
......@@ -38,6 +38,7 @@ module UploadsActions
return render_404 unless uploader
workhorse_set_content_type!
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
......
......@@ -140,15 +140,22 @@ class Projects::JobsController < Projects::ApplicationController
def raw
if trace_artifact_file
workhorse_set_content_type!
send_upload(trace_artifact_file,
send_params: raw_send_params,
redirect_params: raw_redirect_params)
else
build.trace.read do |stream|
if stream.file?
workhorse_set_content_type!
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
send_data stream.raw, type: 'text/plain; charset=utf-8', disposition: 'inline', filename: 'job.log'
# In this case we can't use workhorse_set_content_type! and let
# Workhorse handle the response because the data is streamed directly
# to the user but, because we have the trace content, we can calculate
# the proper content type and disposition here.
raw_data = stream.raw
send_data raw_data, type: 'text/plain; charset=utf-8', disposition: raw_trace_content_disposition(raw_data), filename: 'job.log'
end
end
end
......@@ -201,4 +208,13 @@ class Projects::JobsController < Projects::ApplicationController
def build_path(build)
project_job_path(build.project, build)
end
def raw_trace_content_disposition(raw_data)
mime_type = MimeMagic.by_magic(raw_data)
# if mime_type is nil can also represent 'text/plain'
return 'inline' if mime_type.nil? || mime_type.type == 'text/plain'
'attachment'
end
end
# frozen_string_literal: true
module Projects
module Serverless
class FunctionsController < Projects::ApplicationController
include ProjectUnauthorized
before_action :authorize_read_cluster!
INDEX_PRIMING_INTERVAL = 10_000
INDEX_POLLING_INTERVAL = 30_000
def index
finder = Projects::Serverless::FunctionsFinder.new(project.clusters)
respond_to do |format|
format.json do
functions = finder.execute
if functions.any?
Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL)
render json: Projects::Serverless::ServiceSerializer.new(current_user: @current_user).represent(functions)
else
Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL)
head :no_content
end
end
format.html do
@installed = finder.installed?
render
end
end
end
end
end
end
# frozen_string_literal: true
module Projects
module Serverless
class FunctionsFinder
def initialize(clusters)
@clusters = clusters
end
def execute
knative_services.flatten.compact
end
def installed?
clusters_with_knative_installed.exists?
end
private
def knative_services
clusters_with_knative_installed.preload_knative.map do |cluster|
cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
end
end
def clusters_with_knative_installed
@clusters.with_knative_installed
end
end
end
end
......@@ -140,6 +140,8 @@ module BlobHelper
Gitlab::Sanitizers::SVG.clean(data)
end
# Remove once https://gitlab.com/gitlab-org/gitlab-ce/issues/36103 is closed
# and :workhorse_set_content_type flag is removed
# If we blindly set the 'real' content type when serving a Git blob we
# are enabling XSS attacks. An attacker could upload e.g. a Javascript
# file to a Git repository, trick the browser of a victim into
......@@ -161,6 +163,8 @@ module BlobHelper
end
def content_disposition(blob, inline)
# Remove the following line when https://gitlab.com/gitlab-org/gitlab-ce/issues/36103
# is closed and :workhorse_set_content_type flag is removed
return 'attachment' if blob.extension == 'svg'
inline ? 'inline' : 'attachment'
......
......@@ -313,6 +313,7 @@ module ProjectsHelper
settings: :admin_project,
builds: :read_build,
clusters: :read_cluster,
serverless: :read_cluster,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
......@@ -551,6 +552,7 @@ module ProjectsHelper
%w[
environments
clusters
functions
user
gcp
]
......
......@@ -6,8 +6,13 @@ module WorkhorseHelper
# Send a Git blob through Workhorse
def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
headers['Content-Disposition'] = content_disposition(blob, inline)
headers['Content-Type'] = safe_content_type(blob)
# If enabled, this will override the values set above
workhorse_set_content_type!
render plain: ""
end
......@@ -40,4 +45,8 @@ module WorkhorseHelper
def set_workhorse_internal_api_content_type
headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
end
def workhorse_set_content_type!
headers[Gitlab::Workhorse::DETECT_HEADER] = "true" if Feature.enabled?(:workhorse_set_content_type)
end
end
......@@ -617,13 +617,18 @@ module Ci
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
variables.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
if merge_request? && merge_request
variables.concat(merge_request.predefined_variables)
end
end
end
def queued_duration
......
......@@ -15,6 +15,9 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
include ReactiveCaching
self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
state_machine :status do
before_transition any => [:installed] do |application|
......@@ -29,6 +32,8 @@ module Clusters
validates :hostname, presence: true, hostname: true
scope :for_cluster, -> (cluster) { where(cluster: cluster) }
def chart
'knative/knative'
end
......@@ -55,12 +60,39 @@ module Clusters
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
def client
cluster.kubeclient.knative_client
end
def services
with_reactive_cache do |data|
data[:services]
end
end
def calculate_reactive_cache
{ services: read_services }
end
def ingress_service
cluster.kubeclient.get_service('knative-ingressgateway', 'istio-system')
end
def client
cluster.platform_kubernetes.kubeclient.knative_client
def services_for(ns: namespace)
return unless services
return [] unless ns
services.select do |service|
service.dig('metadata', 'namespace') == ns
end
end
private
def read_services
client.get_services.as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
end
end
......
......@@ -93,6 +93,16 @@ module Clusters
where('NOT EXISTS (?)', subquery)
end
scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.installed) }
scope :preload_knative, -> {
preload(
:kubernetes_namespace,
:platform_kubernetes,
:application_knative
)
}
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters)
hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope
......
......@@ -7,6 +7,7 @@ class Member < ActiveRecord::Base
include Expirable
include Gitlab::Access
include Presentable
include Gitlab::Utils::StrongMemoize
attr_accessor :raw_invite_token
......@@ -22,6 +23,7 @@ class Member < ActiveRecord::Base
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validate :higher_access_level_than_group, unless: :importing?
validates :invite_email,
presence: {
if: :invite?
......@@ -364,6 +366,15 @@ class Member < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
# Find the user's group member with a highest access level
def highest_group_member
strong_memoize(:highest_group_member) do
next unless user_id && source&.ancestors&.any?
GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
end
end
private
def send_invite
......@@ -430,6 +441,14 @@ class Member < ActiveRecord::Base
def notifiable_options
{}
end
def higher_access_level_than_group
if highest_group_member && highest_group_member.access_level >= access_level
error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
end
end
end
Member.prepend(EE::Member)
......@@ -1072,6 +1072,42 @@ class MergeRequest < ActiveRecord::Base
actual_head_pipeline&.has_test_reports?
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s)
variables.append(key: 'CI_MERGE_REQUEST_REF_PATH',
value: ref_path.to_s)
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID',
value: project.id.to_s)
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH',
value: project.full_path)
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL',
value: project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME',
value: target_branch.to_s)
if source_project
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID',
value: source_project.id.to_s)
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH',
value: source_project.full_path)
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL',
value: source_project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME',
value: source_branch.to_s)
end
end
end
# rubocop: disable CodeReuse/ServiceClass
def compare_test_reports
unless has_test_reports?
......
......@@ -575,6 +575,8 @@ class Project < ActiveRecord::Base
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
alias_method :ancestors, :ancestors_upto
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
......
......@@ -7,6 +7,14 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
member.class.access_level_roles
end
def valid_level_roles
return access_level_roles unless member.highest_group_member
access_level_roles.reject do |_name, level|
member.highest_group_member.access_level > level
end
end
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
......
......@@ -46,6 +46,7 @@ class DiffFileEntity < Grape::Entity
expose :deleted_file?, as: :deleted_file
expose :renamed_file?, as: :renamed_file
expose :mode_changed?, as: :mode_changed
expose :old_path
expose :new_path
expose :mode_changed?, as: :mode_changed
......
# frozen_string_literal: true
module Projects
module Serverless
class ServiceEntity < Grape::Entity
include RequestAwareEntity
expose :name do |service|
service.dig('metadata', 'name')
end
expose :namespace do |service|
service.dig('metadata', 'namespace')
end
expose :created_at do |service|
service.dig('metadata', 'creationTimestamp')
end
expose :url do |service|
"http://#{service.dig('status', 'domain')}"
end
expose :description do |service|
service.dig('spec', 'runLatest', 'configuration', 'revisionTemplate', 'metadata', 'annotations', 'Description')
end
expose :image do |service|
service.dig('spec', 'runLatest', 'configuration', 'build', 'template', 'name')
end
end
end
end
# frozen_string_literal: true
module Projects
module Serverless
class ServiceSerializer < BaseSerializer
entity Projects::Serverless::ServiceEntity
end
end
end
......@@ -222,6 +222,12 @@
%span
= _('Environments')
- if project_nav_tab? :serverless
= nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
%span
= _('Serverless')
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
......
......@@ -20,7 +20,7 @@
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
= text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", title: 'A valid repository URL is required'
= text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
= render 'projects/mirrors/instructions'
......
- @no_container = true
- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title 'Serverless'
- page_title 'Serverless'
- status_path = project_serverless_functions_path(@project, format: :json)
- clusters_path = project_clusters_path(@project)
.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } }
%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
.js-serverless-functions-notice
.flash-container
.top-area.adjust
.serverless-functions-table#js-serverless-functions
......@@ -77,7 +77,7 @@
= dropdown_title(_("Change permissions"))
.dropdown-content
%ul
- member.access_level_roles.each do |role, role_id|
- member.valid_level_roles.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
......
---
title: Restrict member access level to be higher than that of any parent group
merge_request: 23226
author:
type: fixed
---
title: Add partial index for ci_builds on project_id and status
merge_request: 23268
author:
type: performance
---
title: Prevent a path traversal attack on global file templates
merge_request:
author:
type: security
---
title: Fix gitlab:web_hook tasks
merge_request: 23635
author:
type: fixed
---
title: Expose merge request pipeline variables
merge_request: 23398
author:
type: changed
---
title: Added feature flag to signal content headers detection by Workhorse
merge_request: 22667
author:
type: added
---
title: Gracefully handle unknown/invalid GPG keys
merge_request: 23492
author:
type: fixed
---
title: Introduce Knative and Serverless Components
merge_request: 23174
author: Chris Baumbauer
type: added
---
title: Use approximate count for big tables for usage statistics.
merge_request:
author:
type: fixed
---
title: Change dropdown divider color to gray-200 (#dfdfdf)
merge_request: 23592
author:
type: changed
---
title: Remove close icon from projects dropdown in issue boards
merge_request: 23567
author:
type: changed
......@@ -595,6 +595,6 @@
- - :approve
- echarts
- :who: Mike Greiling
:why: Apache 2.0 license
:why: https://github.com/apache/incubator-echarts/blob/master/LICENSE
:versions: []
:when: 2018-12-05 22:12:30.550027000 Z
......@@ -39,7 +39,7 @@ Sidekiq.configure_server do |config|
ActiveRecord::Base.clear_all_connections!
end
if Feature.enabled?(:gitlab_sidekiq_reliable_fetcher)
if Feature::FlipperFeature.table_exists? && Feature.enabled?(:gitlab_sidekiq_reliable_fetcher)
Sidekiq::ReliableFetcher.setup_reliable_fetch!(config)
end
......
......@@ -306,6 +306,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
namespace :serverless do
resources :functions, only: [:index]
end
scope '-' do
get 'archive/*id', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+?/ }, to: 'repositories#archive', as: 'archive'
......
require './spec/support/sidekiq'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
User.all.sample(10).each do |user|
source_project = Project.public_only.sample
fork_project = Projects::ForkService.new(source_project, user, namespace: user.namespace).execute
if fork_project.valid?
puts '.'
else
puts 'F'
end
end
end
end
......@@ -8,7 +8,7 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
disable_ddl_transaction!
def up
add_concurrent_index :ci_pipelines, :merge_request_id
add_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
add_concurrent_foreign_key :ci_pipelines, :merge_requests, column: :merge_request_id, on_delete: :cascade
end
......@@ -17,6 +17,6 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
remove_foreign_key :ci_pipelines, :merge_requests
end
remove_concurrent_index :ci_pipelines, :merge_request_id
remove_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddCiBuildsPartialIndexOnProjectIdAndStatus < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(*index_arguments)
end
def down
remove_concurrent_index(*index_arguments)
end
private
def index_arguments
[
:ci_builds,
[:project_id, :status],
{
name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial2',
where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))"
}
]
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveRedundantCiBuildsPartialIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_concurrent_index(*index_arguments)
end
def down
add_concurrent_index(*index_arguments)
end
private
def index_arguments
[
:ci_builds,
[:project_id, :status],
{
name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial',
where: "((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text]))"
}
]
end
end
......@@ -449,6 +449,7 @@ ActiveRecord::Schema.define(version: 20181206121340) do
t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["name"], name: "index_ci_builds_on_name_for_security_products_values", where: "((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text]))", using: :btree
t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree
t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree
t.index ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree
......@@ -581,7 +582,7 @@ ActiveRecord::Schema.define(version: 20181206121340) do
t.integer "iid"
t.integer "merge_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", using: :btree
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)", using: :btree
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
t.index ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
......
......@@ -77,8 +77,11 @@ that builds on this to add some additional niceties, such as allowing
configuration with a single Yaml file for multiple URLs, and uploading of the
profile and log output to S3.
For GitLab.com, you can find the latest results here:
<http://redash.gitlab.com/dashboard/gitlab-profiler-statistics>
For GitLab.com, currently the latest profiling data has been [moved from
Redash to Looker](https://gitlab.com/gitlab-com/Product/issues/5#note_121194467).
We are [currently investigating how to make this data
public](https://gitlab.com/meltano/looker/issues/294).
## Sherlock
......
......@@ -31,11 +31,7 @@ After that, the next pipeline will use the up-to-date
The GitLab test suite is [monitored] for the `master` branch, and any branch
that includes `rspec-profile` in their name.
A [public dashboard] is available for everyone to see. Feel free to look at the
slowest test files and try to improve them.
[monitored]: ../performance.md#rspec-profiling
[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
## CI setup
......
......@@ -38,8 +38,6 @@
## List the webhooks from projects in a given **NAMESPACE**:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:list NAMESPACE=/
sudo gitlab-rake gitlab:web_hook:list NAMESPACE=acme
# source installations
bundle exec rake gitlab:web_hook:list NAMESPACE=/ RAILS_ENV=production
> Note: `/` is the global namespace.
bundle exec rake gitlab:web_hook:list NAMESPACE=acme RAILS_ENV=production
......@@ -167,7 +167,6 @@ Here's a list of what you can't do with subgroups:
- [GitLab Pages](../../project/pages/index.md) are not currently working for
projects hosted under a subgroup. That means that only projects hosted under
the first parent group will work.
- Group level labels don't work in subgroups / sub projects
- It is not possible to share a project with a group that's an ancestor of
the group the project is in. That means you can only share as you walk down
the hierarchy. For example, `group/subgroup01/project` **cannot** be shared
......
......@@ -147,12 +147,14 @@ describe 'Billing plan pages', :feature do
end
context 'on sub-group', :nested_groups do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:group) { create(:group, plan: :bronze_plan) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:subgroup1) { create(:group, parent: group, plan: :silver_plan) }
let!(:subgroup1_member) { create(:group_member, :owner, group: subgroup1, user: user) }
let!(:subgroup1_member) { create(:group_member, :owner, group: subgroup1, user: user2) }
let(:subgroup2) { create(:group, parent: subgroup1) }
let!(:subgroup2_member) { create(:group_member, :owner, group: subgroup2, user: user) }
let!(:subgroup2_member) { create(:group_member, :owner, group: subgroup2, user: user3) }
before do
visit group_billings_path(subgroup2)
......
......@@ -82,7 +82,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the template'
end
get "templates/#{template_type}/:name" do
get "templates/#{template_type}/:name", requirements: { name: /[\w\.-]+/ } do
finder = TemplateFinder.build(template_type, nil, name: declared(params)[:name])
new_template = finder.execute
......
......@@ -40,7 +40,7 @@ module Gitlab
if strategy.enabled?
models_with_missing_counts = models - counts_by_model.keys
break if models_with_missing_counts.empty?
break counts_by_model if models_with_missing_counts.empty?
counts = strategy.new(models_with_missing_counts).count
......
......@@ -20,6 +20,8 @@ module Gitlab
models.each_with_object({}) do |model, data|
data[model] = model.count
end
rescue *CONNECTION_ERRORS
{}
end
def self.enabled?
......
......@@ -44,9 +44,8 @@ module Gitlab
def update_signature!(cached_signature)
using_keychain do |gpg_key|
cached_signature.update!(attributes(gpg_key))
@signature = cached_signature
end
@signature = cached_signature
end
private
......@@ -59,11 +58,15 @@ module Gitlab
# the proper signature.
# NOTE: the invoked method is #fingerprint but it's only returning
# 16 characters (the format used by keyid) instead of 40.
gpg_key = find_gpg_key(verified_signature.fingerprint)
fingerprint = verified_signature&.fingerprint
break unless fingerprint
gpg_key = find_gpg_key(fingerprint)
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
@verified_signature = nil
clear_memoization(:verified_signature)
end
yield gpg_key
......@@ -71,9 +74,16 @@ module Gitlab
end
def verified_signature
@verified_signature ||= GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
strong_memoize(:verified_signature) { gpgme_signature }
end
def gpgme_signature
GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
# Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-ce/issues/54932
break verified_signature
end
rescue GPGME::Error
nil
end
def create_cached_signature!
......@@ -92,7 +102,7 @@ module Gitlab
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
gpg_key_primary_keyid: gpg_key&.keyid || verified_signature&.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
verification_status: verification_status
......@@ -102,7 +112,7 @@ module Gitlab
def verification_status(gpg_key)
return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
return :unverified unless verified_signature.valid?
return :unverified unless verified_signature&.valid?
if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
:verified
......
......@@ -18,6 +18,10 @@ module Gitlab
def find(key)
file_name = "#{key}#{@extension}"
# The key is untrusted input, so ensure we can't be directed outside
# of base_dir
Gitlab::Utils.check_path_traversal!(file_name)
directory = select_directory(file_name)
directory ? File.join(category_directory(directory), file_name) : nil
end
......
......@@ -26,6 +26,11 @@ module Gitlab
def find(key)
file_name = "#{key}#{@extension}"
# The key is untrusted input, so ensure we can't be directed outside
# of base_dir inside the repository
Gitlab::Utils.check_path_traversal!(file_name)
directory = select_directory(file_name)
raise FileNotFoundError if directory.nil?
......
......@@ -2,6 +2,8 @@
module Gitlab
class UsageData
APPROXIMATE_COUNT_MODELS = [Label, MergeRequest, Note, Todo].freeze
class << self
prepend EE::Gitlab::UsageData
......@@ -75,12 +77,9 @@ module Gitlab
issues: count(Issue),
keys: count(Key),
label_lists: count(List.label),
labels: count(Label),
lfs_objects: count(LfsObject),
merge_requests: count(MergeRequest),
milestone_lists: count(List.milestone),
milestones: count(Milestone),
notes: count(Note),
pages_domains: count(PagesDomain),
projects: count(Project),
projects_imported_from_github: count(Project.where(import_type: 'github')),
......@@ -88,10 +87,9 @@ module Gitlab
releases: count(Release),
remote_mirrors: count(RemoteMirror),
snippets: count(Snippet),
todos: count(Todo),
uploads: count(Upload),
web_hooks: count(WebHook)
}.merge(services_usage)
}.merge(services_usage).merge(approximate_counts)
}
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -166,6 +164,16 @@ module Gitlab
fallback
end
# rubocop: enable CodeReuse/ActiveRecord
def approximate_counts
approx_counts = Gitlab::Database::Count.approximate_counts(APPROXIMATE_COUNT_MODELS)
APPROXIMATE_COUNT_MODELS.each_with_object({}) do |model, result|
key = model.name.underscore.pluralize.to_sym
result[key] = approx_counts[model] || -1
end
end
end
end
end
......@@ -4,6 +4,15 @@ module Gitlab
module Utils
extend self
# Ensure that the relative path will not traverse outside the base directory
def check_path_traversal!(path)
raise StandardError.new("Invalid path") if path.start_with?("..#{File::SEPARATOR}") ||
path.include?("#{File::SEPARATOR}..#{File::SEPARATOR}") ||
path.end_with?("#{File::SEPARATOR}..")
path
end
# Run system command without outputting to stdout.
#
# @param cmd [Array<String>]
......
......@@ -13,6 +13,7 @@ module Gitlab
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'.freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
......
......@@ -25,11 +25,22 @@ namespace :gitlab do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
project_ids = projects.pluck(:id)
web_hooks = find_web_hooks(namespace_path)
puts "Removing webhooks with the url '#{web_hook_url}' ... "
count = WebHook.where(url: web_hook_url, project_id: project_ids, type: 'ProjectHook').delete_all
# FIXME: Hook URLs are now encrypted, so there is no way to efficiently
# find them all in SQL. For now, check them in Ruby. If this is too slow,
# we could consider storing a hash of the URL alongside the encrypted
# value to speed up searches
count = 0
web_hooks.find_each do |hook|
next unless hook.url == web_hook_url
hook.destroy!
count += 1
end
puts "#{count} webhooks were removed."
end
......@@ -37,29 +48,37 @@ namespace :gitlab do
task list: :environment do
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
web_hooks = projects.all.map(&:hooks).flatten
web_hooks.each do |hook|
web_hooks = find_web_hooks(namespace_path)
web_hooks.find_each do |hook|
puts "#{hook.project.name.truncate(20).ljust(20)} -> #{hook.url}"
end
puts "\n#{web_hooks.size} webhooks found."
puts "\n#{web_hooks.count} webhooks found."
end
end
def find_projects(namespace_path)
if namespace_path.blank?
Project
elsif namespace_path == '/'
Project.in_namespace(nil)
else
namespace = Namespace.where(path: namespace_path).first
if namespace
Project.in_namespace(namespace.id)
else
namespace = Namespace.find_by_full_path(namespace_path)
unless namespace
puts "Namespace not found: #{namespace_path}".color(:red)
exit 2
end
Project.in_namespace(namespace.id)
end
end
def find_web_hooks(namespace_path)
if namespace_path.blank?
ProjectHook
else
project_ids = find_projects(namespace_path).select(:id)
ProjectHook.where(project_id: project_ids)
end
end
end
......@@ -7684,6 +7684,45 @@ msgstr ""
msgid "Server version"
msgstr ""
msgid "Serverless"
msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
msgstr ""
msgid "Serverless|An error occurred while retrieving serverless components"
msgstr ""
msgid "Serverless|Domain"
msgstr ""
msgid "Serverless|Function"
msgstr ""
msgid "Serverless|Getting started with serverless"
msgstr ""
msgid "Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available."
msgstr ""
msgid "Serverless|Install Knative"
msgstr ""
msgid "Serverless|Last Update"
msgstr ""
msgid "Serverless|Learn more about Serverless"
msgstr ""
msgid "Serverless|No functions available"
msgstr ""
msgid "Serverless|Runtime"
msgstr ""
msgid "Serverless|There is currently no function data available from Knative. This could be for a variety of reasons including:"
msgstr ""
msgid "Service Desk"
msgstr ""
......@@ -10552,6 +10591,9 @@ msgid_plural "replies"
msgstr[0] ""
msgstr[1] ""
msgid "should be higher than %{access} inherited membership from group %{group_name}"
msgstr ""
msgid "source"
msgstr ""
......
......@@ -100,10 +100,6 @@ module QA
find_element(name).set(content)
end
def hover_element(name)
find_element(name).hover
end
def select_element(name, value)
element = find_element(name)
......
......@@ -26,12 +26,37 @@ describe Projects::AvatarsController do
context 'when the avatar is stored in the repository' do
let(:filepath) { 'files/images/logo-white.png' }
it 'sends the avatar' do
subject
context 'when feature flag workhorse_set_content_type is' do
before do
stub_feature_flags(workhorse_set_content_type: flag_value)
end
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/png')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
context 'enabled' do
let(:flag_value) { true }
it 'sends the avatar' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Disposition']).to eq('inline')
expect(response.header['Content-Type']).to eq 'image/png'
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
end
context 'disabled' do
let(:flag_value) { false }
it 'sends the avatar' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/png')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
end
end
end
end
......
......@@ -838,23 +838,48 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context "when job has a trace artifact" do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do
response = subject
context 'when feature flag workhorse_set_content_type is' do
before do
stub_feature_flags(workhorse_set_content_type: flag_value)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.body).to eq(job.job_artifacts_trace.open.read)
context 'enabled' do
let(:flag_value) { true }
it "sets #{Gitlab::Workhorse::DETECT_HEADER} header" do
response = subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.body).to eq(job.job_artifacts_trace.open.read)
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
end
context 'disabled' do
let(:flag_value) { false }
it 'returns a trace' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.body).to eq(job.job_artifacts_trace.open.read)
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to be nil
end
end
end
end
context "when job has a trace file" do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it "send a trace file" do
it 'sends a trace file' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.headers["Content-Disposition"]).to match(/^inline/)
expect(response.body).to eq("BUILD TRACE")
end
end
......@@ -866,12 +891,27 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
job.update_column(:trace, "Sample trace")
end
it "send a trace file" do
it 'sends a trace file' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.body).to eq("Sample trace")
expect(response.headers['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.body).to eq('Sample trace')
end
context 'when trace format is not text/plain' do
before do
job.update_column(:trace, '<html></html>')
end
it 'sets content disposition to attachment' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.headers['Content-Disposition']).to match(/^attachment/)
end
end
end
......
......@@ -14,26 +14,74 @@ describe Projects::RawController do
context 'regular filename' do
let(:filepath) { 'master/README.md' }
it 'delivers ASCII file' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition'])
.to eq('inline')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
context 'when feature flag workhorse_set_content_type is' do
before do
stub_feature_flags(workhorse_set_content_type: flag_value)
subject
end
context 'enabled' do
let(:flag_value) { true }
it 'delivers ASCII file' do
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).to eq('inline')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
context 'disabled' do
let(:flag_value) { false }
it 'delivers ASCII file' do
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).to eq('inline')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
end
end
context 'image header' do
let(:filepath) { 'master/files/images/6049019_460s.jpg' }
it 'sets image content type header' do
subject
context 'when feature flag workhorse_set_content_type is' do
before do
stub_feature_flags(workhorse_set_content_type: flag_value)
end
context 'enabled' do
let(:flag_value) { true }
it 'leaves image content disposition' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
expect(response.header['Content-Disposition']).to eq('inline')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
context 'disabled' do
let(:flag_value) { false }
it 'sets image content type header' do
subject
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
expect(response.header['Content-Disposition']).to eq('inline')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Serverless::FunctionsController do
include KubernetesHelpers
include ReactiveCachingHelpers
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:service) { cluster.platform_kubernetes }
let(:project) { cluster.project}
let(:namespace) do
create(:cluster_kubernetes_namespace,
cluster: cluster,
cluster_project: cluster.cluster_project,
project: cluster.cluster_project.project)
end
before do
project.add_maintainer(user)
sign_in(user)
end
def params(opts = {})
opts.reverse_merge(namespace_id: project.namespace.to_param,
project_id: project.to_param)
end
describe 'GET #index' do
context 'empty cache' do
it 'has no data' do
get :index, params({ format: :json })
expect(response).to have_gitlab_http_status(204)
end
it 'renders an html page' do
get :index, params
expect(response).to have_gitlab_http_status(200)
end
end
end
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do
stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"])
end
it 'has data' do
get :index, params({ format: :json })
expect(response).to have_gitlab_http_status(200)
expect(json_response).to contain_exactly(
a_hash_including(
"name" => project.name,
"url" => "http://#{project.name}.#{namespace.namespace}.example.com"
)
)
end
it 'has data in html' do
get :index, params
expect(response).to have_gitlab_http_status(200)
end
end
end
......@@ -52,24 +52,56 @@ describe Projects::WikisController do
let(:path) { upload_file_to_wiki(project, user, file_name) }
before do
subject
end
subject { get :show, namespace_id: project.namespace, project_id: project, id: path }
context 'when file is an image' do
let(:file_name) { 'dk.png' }
it 'renders the content inline' do
expect(response.headers['Content-Disposition']).to match(/^inline/)
end
context 'when feature flag workhorse_set_content_type is' do
before do
stub_feature_flags(workhorse_set_content_type: flag_value)
subject
end
context 'when file is a svg' do
let(:file_name) { 'unsanitized.svg' }
context 'enabled' do
let(:flag_value) { true }
it 'renders the content as an attachment' do
expect(response.headers['Content-Disposition']).to match(/^attachment/)
it 'delivers the image' do
expect(response.headers['Content-Type']).to eq('image/png')
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
context 'when file is a svg' do
let(:file_name) { 'unsanitized.svg' }
it 'delivers the image' do
expect(response.headers['Content-Type']).to eq('image/svg+xml')
expect(response.headers['Content-Disposition']).to match(/^attachment/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
end
end
context 'disabled' do
let(:flag_value) { false }
it 'renders the content inline' do
expect(response.headers['Content-Type']).to eq('image/png')
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
end
context 'when file is a svg' do
let(:file_name) { 'unsanitized.svg' }
it 'renders the content as an attachment' do
expect(response.headers['Content-Type']).to eq('image/svg+xml')
expect(response.headers['Content-Disposition']).to match(/^attachment/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
end
end
end
end
end
......@@ -77,8 +109,32 @@ describe Projects::WikisController do
context 'when file is a pdf' do
let(:file_name) { 'git-cheat-sheet.pdf' }
it 'sets the content type to application/octet-stream' do
expect(response.headers['Content-Type']).to eq 'application/octet-stream'
context 'when feature flag workhorse_set_content_type is' do
before do
stub_feature_flags(workhorse_set_content_type: flag_value)
subject
end
context 'enabled' do
let(:flag_value) { true }
it 'sets the content type to sets the content response headers' do
expect(response.headers['Content-Type']).to eq 'application/octet-stream'
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
end
context 'disabled' do
let(:flag_value) { false }
it 'sets the content response headers' do
expect(response.headers['Content-Type']).to eq 'application/octet-stream'
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
end
end
end
end
end
......
......@@ -437,7 +437,10 @@ describe SnippetsController do
end
context 'when signed in user is the author' do
let(:flag_value) { false }
before do
stub_feature_flags(workhorse_set_content_type: flag_value)
get :raw, id: personal_snippet.to_param
end
......@@ -451,6 +454,24 @@ describe SnippetsController do
expect(response.header['Content-Disposition']).to match(/inline/)
end
context 'when feature flag workhorse_set_content_type is' do
context 'enabled' do
let(:flag_value) { true }
it "sets #{Gitlab::Workhorse::DETECT_HEADER} header" do
expect(response).to have_gitlab_http_status(200)
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
end
context 'disabled' do
it "does not set #{Gitlab::Workhorse::DETECT_HEADER} header" do
expect(response).to have_gitlab_http_status(200)
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to be nil
end
end
end
end
end
......
......@@ -2,16 +2,19 @@ require 'spec_helper'
describe 'User expands diff', :js do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:merge_request) { create(:merge_request, source_branch: 'expand-collapse-files', source_project: project, target_project: project) }
before do
allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(100.kilobytes)
allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(10.kilobytes)
visit(diffs_project_merge_request_path(project, merge_request))
wait_for_requests
end
it 'allows user to expand diff' do
page.within find('[id="19763941ab80e8c09871c0a425f0560d9053bcb3"]') do
page.within find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do
click_link 'Click to expand it.'
wait_for_requests
......
require 'spec_helper'
describe 'Functions', :js do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
gitlab_sign_in(user)
end
context 'when user does not have a cluster and visits the serverless page' do
before do
visit project_serverless_functions_path(project)
end
it 'sees an empty state' do
expect(page).to have_link('Install Knative')
expect(page).to have_selector('.empty-state')
end
end
context 'when the user does have a cluster and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
before do
visit project_serverless_functions_path(project)
end
it 'sees an empty state' do
expect(page).to have_link('Install Knative')
expect(page).to have_selector('.empty-state')
end
end
context 'when the user has a cluster and knative installed and visits the serverless page' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:project) { knative.cluster.project }
before do
visit project_serverless_functions_path(project)
end
it 'sees an empty listing of serverless functions' do
expect(page).to have_selector('.gl-responsive-table-row')
end
end
end
......@@ -19,7 +19,7 @@ describe GroupMembersFinder, '#execute' do
end
it 'returns members for nested group', :nested_groups do
group.add_maintainer(user2)
group.add_developer(user2)
nested_group.request_access(user4)
member1 = group.add_maintainer(user1)
member3 = nested_group.add_maintainer(user2)
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Serverless::FunctionsFinder do
include KubernetesHelpers
include ReactiveCachingHelpers
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
let(:project) { cluster.project}
let(:namespace) do
create(:cluster_kubernetes_namespace,
cluster: cluster,
cluster_project: cluster.cluster_project,
project: cluster.cluster_project.project)
end
before do
project.add_maintainer(user)
end
describe 'retrieve data from knative' do
it 'does not have knative installed' do
expect(described_class.new(project.clusters).execute).to be_empty
end
context 'has knative installed' do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
it 'there are no functions' do
expect(described_class.new(project.clusters).execute).to be_empty
end
it 'there are functions', :use_clean_rails_memory_store_caching do
stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"])
expect(described_class.new(project.clusters).execute).not_to be_empty
end
end
end
describe 'verify if knative is installed' do
context 'knative is not installed' do
it 'does not have knative installed' do
expect(described_class.new(project.clusters).installed?).to be false
end
end
context 'knative is installed' do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
it 'does have knative installed' do
expect(described_class.new(project.clusters).installed?).to be true
end
end
end
end
......@@ -74,6 +74,32 @@ describe('DiffFile', () => {
});
});
it('should be collapsed for renamed files', done => {
vm.file.renderIt = true;
vm.file.collapsed = false;
vm.file.highlighted_diff_lines = null;
vm.file.renamed_file = true;
vm.$nextTick(() => {
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
done();
});
});
it('should be collapsed for mode changed files', done => {
vm.file.renderIt = true;
vm.file.collapsed = false;
vm.file.highlighted_diff_lines = null;
vm.file.mode_changed = true;
vm.$nextTick(() => {
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
done();
});
});
it('should have loading icon while loading a collapsed diffs', done => {
vm.file.collapsed = true;
vm.isLoadingCollapsedDiff = true;
......
......@@ -559,4 +559,26 @@ describe('DiffsStoreUtils', () => {
]);
});
});
describe('getDiffMode', () => {
it('returns mode when matched in file', () => {
expect(
utils.getDiffMode({
renamed_file: true,
}),
).toBe('renamed');
});
it('returns mode_changed if key has no match', () => {
expect(
utils.getDiffMode({
mode_changed: true,
}),
).toBe('mode_changed');
});
it('defaults to replaced', () => {
expect(utils.getDiffMode({})).toBe('replaced');
});
});
});
......@@ -68,4 +68,30 @@ describe('DiffViewer', () => {
done();
});
});
it('renders renamed component', () => {
createComponent({
diffMode: 'renamed',
newPath: 'test.abc',
newSha: 'ABC',
oldPath: 'testold.abc',
oldSha: 'DEF',
});
expect(vm.$el.textContent).toContain('File moved');
});
it('renders mode changed component', () => {
createComponent({
diffMode: 'mode_changed',
newPath: 'test.abc',
newSha: 'ABC',
oldPath: 'testold.abc',
oldSha: 'DEF',
aMode: '123',
bMode: '321',
});
expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
});
});
import { shallowMount } from '@vue/test-utils';
import ModeChanged from '~/vue_shared/components/diff_viewer/viewers/mode_changed.vue';
describe('Diff viewer mode changed component', () => {
let vm;
beforeEach(() => {
vm = shallowMount(ModeChanged, {
propsData: {
aMode: '123',
bMode: '321',
},
});
});
afterEach(() => {
vm.destroy();
});
it('renders aMode & bMode', () => {
expect(vm.text()).toContain('File mode changed from 123 to 321');
});
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment