Commit bb259b76 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into projects-a-refactor-ee

parents 2578d7d7 0fa15a51
...@@ -351,7 +351,7 @@ group :development, :test do ...@@ -351,7 +351,7 @@ group :development, :test do
gem 'rubocop', '~> 0.52.0' gem 'rubocop', '~> 0.52.0'
gem 'rubocop-rspec', '~> 1.20.1' gem 'rubocop-rspec', '~> 1.20.1'
gem 'scss_lint', '~> 0.54.0', require: false gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false gem 'flay', '~> 2.8.0', require: false
......
...@@ -724,6 +724,9 @@ GEM ...@@ -724,6 +724,9 @@ GEM
rake rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.3.0) rake (12.3.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2) rbnacl (4.0.2)
...@@ -838,7 +841,11 @@ GEM ...@@ -838,7 +841,11 @@ GEM
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.22) sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.0.6) sass-rails (5.0.6)
railties (>= 4.0.0, < 6) railties (>= 4.0.0, < 6)
sass (~> 3.1) sass (~> 3.1)
...@@ -848,9 +855,9 @@ GEM ...@@ -848,9 +855,9 @@ GEM
sawyer (0.8.1) sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6) addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0) faraday (~> 0.8, < 1.0)
scss_lint (0.54.0) scss_lint (0.56.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.4.20) sass (~> 3.5.3)
securecompare (1.0.0) securecompare (1.0.0)
seed-fu (2.3.6) seed-fu (2.3.6)
activerecord (>= 3.1) activerecord (>= 3.1)
...@@ -1200,7 +1207,7 @@ DEPENDENCIES ...@@ -1200,7 +1207,7 @@ DEPENDENCIES
rugged (~> 0.26.0) rugged (~> 0.26.0)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0) scss_lint (~> 0.56.0)
seed-fu (= 2.3.6) seed-fu (= 2.3.6)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5) selenium-webdriver (~> 3.5)
......
...@@ -56,7 +56,6 @@ import GfmAutoComplete from './gfm_auto_complete'; ...@@ -56,7 +56,6 @@ import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob'; import ShortcutsBlob from './shortcuts_blob';
import SigninTabsMemoizer from './signin_tabs_memoizer'; import SigninTabsMemoizer from './signin_tabs_memoizer';
import Star from './star'; import Star from './star';
import Todos from './todos';
import TreeView from './tree'; import TreeView from './tree';
import UsagePing from './usage_ping'; import UsagePing from './usage_ping';
import UsernameValidator from './username_validator'; import UsernameValidator from './username_validator';
...@@ -122,6 +121,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line ...@@ -122,6 +121,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
} }
const fail = () => Flash('Error loading dynamic module'); const fail = () => Flash('Error loading dynamic module');
const callDefault = m => m.default();
path = page.split(':'); path = page.split(':');
shortcut_handler = null; shortcut_handler = null;
...@@ -239,7 +239,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line ...@@ -239,7 +239,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
projectSelect(); projectSelect();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); import('./pages/dashboard/todos/index').then(callDefault).catch(fail);
break; break;
case 'dashboard:projects:index': case 'dashboard:projects:index':
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
...@@ -612,7 +612,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line ...@@ -612,7 +612,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
new CILintEditor(); new CILintEditor();
break; break;
case 'users:show': case 'users:show':
import('./pages/users/show').then(m => m.default()).catch(fail); import('./pages/users/show').then(callDefault).catch(fail);
break; break;
case 'admin:conversational_development_index:show': case 'admin:conversational_development_index:show':
new UserCallout(); new UserCallout();
......
...@@ -57,12 +57,12 @@ class GfmAutoComplete { ...@@ -57,12 +57,12 @@ class GfmAutoComplete {
displayTpl(value) { displayTpl(value) {
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template; if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
let tpl = '<li>/${name}'; let tpl = '<li><span class="name">/${name}</span>';
if (value.aliases.length > 0) { if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>';
} }
if (value.params.length > 0) { if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>'; tpl += ' <small class="params"><%- params.join(" ") %></small>';
} }
if (value.description !== '') { if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>'; tpl += '<small class="description"><i><%- description %></i></small>';
......
...@@ -141,7 +141,8 @@ export default { ...@@ -141,7 +141,8 @@ export default {
<div <div
v-if="group.description" v-if="group.description"
class="description"> class="description">
{{group.description}} <span v-html="group.description">
</span>
</div> </div>
</div> </div>
<group-folder <group-folder
......
...@@ -71,7 +71,7 @@ export default class GroupsStore { ...@@ -71,7 +71,7 @@ export default class GroupsStore {
id: rawGroupItem.id, id: rawGroupItem.id,
name: rawGroupItem.name, name: rawGroupItem.name,
fullName: rawGroupItem.full_name, fullName: rawGroupItem.full_name,
description: rawGroupItem.description, description: rawGroupItem.markdown_description,
visibility: rawGroupItem.visibility, visibility: rawGroupItem.visibility,
avatarUrl: rawGroupItem.avatar_url, avatarUrl: rawGroupItem.avatar_url,
relativePath: rawGroupItem.relative_path, relativePath: rawGroupItem.relative_path,
......
...@@ -69,8 +69,8 @@ ...@@ -69,8 +69,8 @@
currentFlagPosition: 0, currentFlagPosition: 0,
showFlag: false, showFlag: false,
showFlagContent: false, showFlagContent: false,
showDeployInfo: true,
timeSeries: [], timeSeries: [],
realPixelRatio: 1,
}; };
}, },
...@@ -87,10 +87,7 @@ ...@@ -87,10 +87,7 @@
}, },
innerViewBox() { innerViewBox() {
if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
}
return '0 0 0 0';
}, },
axisTransform() { axisTransform() {
...@@ -102,6 +99,10 @@ ...@@ -102,6 +99,10 @@
paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`, paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
}; };
}, },
deploymentFlagData() {
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
},
}, },
methods: { methods: {
...@@ -122,6 +123,10 @@ ...@@ -122,6 +123,10 @@
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight; this.baseGraphHeight = this.graphHeight;
this.baseGraphWidth = this.graphWidth; this.baseGraphWidth = this.graphWidth;
// pixel offsets inside the svg and outside are not 1:1
this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth);
this.renderAxesPaths(); this.renderAxesPaths();
this.formatDeployments(); this.formatDeployments();
}, },
...@@ -261,6 +266,11 @@ ...@@ -261,6 +266,11 @@
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
/> />
<graph-deployment
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<rect <rect
class="prometheus-graph-overlay" class="prometheus-graph-overlay"
:width="(graphWidth - 70)" :width="(graphWidth - 70)"
...@@ -269,24 +279,21 @@ ...@@ -269,24 +279,21 @@
ref="graphOverlay" ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)"> @mousemove="handleMouseOverGraph($event)">
</rect> </rect>
<graph-deployment </svg>
:show-deploy-info="showDeployInfo" </svg>
:deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<graph-flag <graph-flag
v-if="showFlag" :real-pixel-ratio="realPixelRatio"
:current-x-coordinate="currentXCoordinate" :current-x-coordinate="currentXCoordinate"
:current-data="currentData" :current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
/> />
</svg>
</svg>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
import Icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: { props: {
showDeployInfo: {
type: Boolean,
required: true,
},
deploymentData: { deploymentData: {
type: Array, type: Array,
required: true, required: true,
...@@ -20,14 +13,6 @@ ...@@ -20,14 +13,6 @@
type: Number, type: Number,
required: true, required: true,
}, },
graphWidth: {
type: Number,
required: true,
},
},
components: {
Icon,
}, },
computed: { computed: {
...@@ -37,52 +22,17 @@ ...@@ -37,52 +22,17 @@
}, },
methods: { methods: {
refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 8);
},
formatTime(deploymentTime) {
return timeFormat(deploymentTime);
},
formatDate(deploymentTime) {
return dateFormatWithName(deploymentTime);
},
nameDeploymentClass(deployment) {
return `deploy-info-${deployment.id}`;
},
transformDeploymentGroup(deployment) { transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
positionFlag(deployment) {
let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 225)) {
xPosition = -142;
}
return xPosition;
},
svgContainerHeight(tag) {
let svgHeight = 80;
if (!tag) {
svgHeight -= 20;
}
return svgHeight;
}, },
}, },
}; };
</script> </script>
<template> <template>
<g <g class="deploy-info">
class="deploy-info"
v-if="showDeployInfo">
<g <g
v-for="(deployment, index) in deploymentData" v-for="(deployment, index) in deploymentData"
:key="index" :key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)"> :transform="transformDeploymentGroup(deployment)">
<rect <rect
x="0" x="0"
...@@ -99,81 +49,6 @@ ...@@ -99,81 +49,6 @@
:y2="calculatedHeight" :y2="calculatedHeight"
stroke="#000"> stroke="#000">
</line> </line>
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
:x="positionFlag(deployment)"
y="0"
width="134"
:height="svgContainerHeight(deployment.tag)">
<rect
class="rect-text-metric deploy-info-rect rect-metric"
x="1"
y="1"
rx="2"
width="132"
:height="svgContainerHeight(deployment.tag) - 2">
</rect>
<text
class="deploy-info-text text-metric-bold"
transform="translate(5, 2)">
Deployed
</text>
<!--The date info-->
<g transform="translate(5, 20)">
<text class="deploy-info-text">
{{formatDate(deployment.time)}}
</text>
<text
class="deploy-info-text text-metric-bold"
x="62">
{{formatTime(deployment.time)}}
</text>
</g>
<line
class="divider-line"
x1="0"
y1="38"
x2="132"
:y2="38"
stroke="#000">
</line>
<!--Commit information-->
<g transform="translate(5, 40)">
<icon
name="commit"
:width="12"
:height="12"
:y="3">
</icon>
<a :xlink:href="deployment.commitUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)">
{{refText(deployment)}}
</text>
</a>
</g>
<!--Tag information-->
<g
transform="translate(5, 55)"
v-if="deployment.tag">
<icon
name="label"
:width="12"
:height="12"
:y="5">
</icon>
<a :xlink:href="deployment.tagUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)"
y="2">
{{deployment.tag}}
</text>
</a>
</g>
</svg>
</g> </g>
<svg <svg
height="0" height="0"
......
<script> <script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import Icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: { props: {
...@@ -7,14 +9,15 @@ ...@@ -7,14 +9,15 @@
type: Number, type: Number,
required: true, required: true,
}, },
currentFlagPosition: {
type: Number,
required: true,
},
currentData: { currentData: {
type: Object, type: Object,
required: true, required: true,
}, },
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: { graphHeight: {
type: Number, type: Number,
required: true, required: true,
...@@ -23,71 +26,173 @@ ...@@ -23,71 +26,173 @@
type: Number, type: Number,
required: true, required: true,
}, },
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: { showFlagContent: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
}, },
data() { components: {
return { Icon,
circleColorRgb: '#8fbce8',
};
}, },
computed: { computed: {
formatTime() { formatTime() {
return timeFormat(this.currentData.time); return this.deploymentFlagData ?
timeFormat(this.deploymentFlagData.time) :
timeFormat(this.currentData.time);
}, },
formatDate() { formatDate() {
return dateFormat(this.currentData.time); return this.deploymentFlagData ?
dateFormat(this.deploymentFlagData.time) :
dateFormat(this.currentData.time);
}, },
calculatedHeight() { cursorStyle() {
return this.graphHeight - this.graphHeightOffset; const xCoordinate = this.deploymentFlagData ?
this.deploymentFlagData.xPos :
this.currentXCoordinate;
const offsetTop = 20 * this.realPixelRatio;
const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
return {
top: `${offsetTop}px`,
left: `${offsetLeft}px`,
height: `${height}px`,
};
},
flagOrientation() {
if (this.currentXCoordinate * this.realPixelRatio > 120) {
return 'left';
}
return 'right';
},
},
methods: {
seriesMetricValue(series) {
const index = this.deploymentFlagData ?
this.deploymentFlagData.seriesIndex :
this.currentDataIndex;
const value = series.values[index] &&
series.values[index].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
seriesMetricLabel(index, series) {
if (this.timeSeries.length < 2) {
return this.legendTitle;
}
if (series.metricTag) {
return series.metricTag;
}
return `series ${index + 1}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
}, },
}, },
}; };
</script> </script>
<template> <template>
<g class="mouse-over-flag"> <div
class="prometheus-graph-cursor"
:style="cursorStyle"
>
<div
v-if="showFlagContent"
class="prometheus-graph-flag popover"
:class="flagOrientation"
>
<div class="arrow"></div>
<div class="popover-title">
<h5 v-if="this.deploymentFlagData">
Deployed
</h5>
{{formatDate}} at
<strong>{{formatTime}}</strong>
</div>
<div
v-if="this.deploymentFlagData"
class="popover-content deploy-meta-content"
>
<div>
<icon
name="commit"
:size="12">
</icon>
<a :href="deploymentFlagData.commitUrl">
{{deploymentFlagData.sha.slice(0, 8)}}
</a>
</div>
<div
v-if="deploymentFlagData.tag">
<icon
name="label"
:size="12">
</icon>
<a :href="deploymentFlagData.tagUrl">
{{deploymentFlagData.ref}}
</a>
</div>
</div>
<div class="popover-content">
<table>
<tr
v-for="(series, index) in timeSeries"
:key="index"
>
<td>
<svg width="15" height="6">
<line <line
class="selected-metric-line" :stroke="series.lineColor"
:x1="currentXCoordinate" :stroke-dasharray="strokeDashArray(series.lineStyle)"
:y1="0" stroke-width="4"
:x2="currentXCoordinate" x1="0"
:y2="calculatedHeight" x2="15"
transform="translate(-5, 20)"> y1="2"
y2="2">
</line> </line>
<svg
v-if="showFlagContent"
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
<rect
class="rect-metric"
x="4"
y="1"
rx="2"
width="90"
height="40"
transform="translate(-3, 20)">
</rect>
<text
class="text-metric text-metric-bold"
x="16"
y="35"
transform="translate(-5, 20)">
{{formatTime}}
</text>
<text
class="text-metric"
x="16"
y="15"
transform="translate(-5, 20)">
{{formatDate}}
</text>
</svg> </svg>
</g> </td>
<td>{{seriesMetricLabel(index, series)}}</td>
<td>
<strong>{{seriesMetricValue(series)}}</strong>
</td>
</tr>
</table>
</div>
</div>
</div>
</template> </template>
...@@ -29,15 +29,18 @@ const mixins = { ...@@ -29,15 +29,18 @@ const mixins = {
time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) { if (xPos >= 0) {
const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
deploymentDataArray.push({ deploymentDataArray.push({
id: deployment.id, id: deployment.id,
time, time,
sha: deployment.sha, sha: deployment.sha,
commitUrl: `${this.projectPath}/commit/${deployment.sha}`, commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag, tag: deployment.tag,
tagUrl: `${this.tagsPath}/${deployment.tag}`, tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
ref: deployment.ref.name, ref: deployment.ref.name,
xPos, xPos,
seriesIndex,
showDeploymentFlag: false, showDeploymentFlag: false,
}); });
} }
......
...@@ -14,7 +14,7 @@ const d3 = { ...@@ -14,7 +14,7 @@ const d3 = {
timeYear, timeYear,
}; };
export const dateFormat = d3.time('%b %-d, %Y'); export const dateFormat = d3.time('%a, %b %-d');
export const timeFormat = d3.time('%-I:%M%p'); export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d'); export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left; export const bisectDate = d3.bisector(d => d.time).left;
......
import Todos from './todos';
export default () => new Todos();
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
import { visitUrl } from './lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from './users_select'; import UsersSelect from '~/users_select';
import { isMetaClick } from './lib/utils/common_utils'; import { isMetaClick } from '~/lib/utils/common_utils';
export default class Todos { export default class Todos {
constructor() { constructor() {
......
...@@ -192,6 +192,17 @@ ...@@ -192,6 +192,17 @@
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
.name,
small.aliases,
small.params {
float: left;
}
small.aliases,
small.params {
padding: 2px 5px;
}
small.description { small.description {
float: right; float: right;
padding: 3px 5px; padding: 3px 5px;
...@@ -209,6 +220,7 @@ ...@@ -209,6 +220,7 @@
} }
ul > li { ul > li {
@include clearfix;
white-space: nowrap; white-space: nowrap;
} }
......
...@@ -408,6 +408,73 @@ ...@@ -408,6 +408,73 @@
} }
} }
.prometheus-graph-cursor {
position: absolute;
background: $theme-gray-600;
width: 1px;
}
.prometheus-graph-flag {
display: block;
min-width: 160px;
h5 {
padding: 0;
margin: 0;
font-size: 14px;
line-height: 1.2;
}
table {
border-collapse: collapse;
padding: 0;
margin: 0;
}
td {
vertical-align: middle;
+ td {
padding-left: 5px;
vertical-align: top;
}
}
.deploy-meta-content {
border-bottom: 1px solid $white-dark;
svg {
height: 15px;
vertical-align: bottom;
}
}
&.popover {
&.left {
left: auto;
right: 0;
margin-right: 10px;
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
}
> .arrow {
top: 40px;
}
> .popover-title,
> .popover-content {
padding: 5px 8px;
font-size: 12px;
white-space: nowrap;
}
}
}
.prometheus-svg-container { .prometheus-svg-container {
position: relative; position: relative;
height: 0; height: 0;
......
...@@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController ...@@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController
def require_pages_enabled! def require_pages_enabled!
not_found unless @project.pages_available? not_found unless @project.pages_available?
end end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
end end
...@@ -4,6 +4,7 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::BoardsController < Projects::ApplicationController
include BoardsResponses include BoardsResponses
include IssuableCollections include IssuableCollections
before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show] before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
......
...@@ -197,10 +197,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -197,10 +197,6 @@ class Projects::IssuesController < Projects::ApplicationController
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
def render_issue_json def render_issue_json
if @issue.valid? if @issue.valid?
render json: serializer.represent(@issue) render json: serializer.represent(@issue)
......
...@@ -147,6 +147,7 @@ module ApplicationSettingsHelper ...@@ -147,6 +147,7 @@ module ApplicationSettingsHelper
:after_sign_up_text, :after_sign_up_text,
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:circuitbreaker_access_retries, :circuitbreaker_access_retries,
:circuitbreaker_check_interval, :circuitbreaker_check_interval,
......
...@@ -274,6 +274,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -274,6 +274,7 @@ class ApplicationSetting < ActiveRecord::Base
{ {
after_sign_up_text: nil, after_sign_up_text: nil,
akismet_enabled: false, akismet_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days', default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
......
...@@ -371,7 +371,7 @@ class Commit ...@@ -371,7 +371,7 @@ class Commit
# #
# Returns a symbol # Returns a symbol
def uri_type(path) def uri_type(path)
entry = @raw.tree.path(path) entry = @raw.rugged_tree_entry(path)
if entry[:type] == :blob if entry[:type] == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob blob.image? || blob.video? ? :raw : :blob
......
...@@ -1461,6 +1461,7 @@ class Project < ActiveRecord::Base ...@@ -1461,6 +1461,7 @@ class Project < ActiveRecord::Base
import_finish import_finish
remove_import_jid remove_import_jid
update_project_counter_caches update_project_counter_caches
after_create_default_branch
end end
def update_project_counter_caches def update_project_counter_caches
...@@ -1474,6 +1475,27 @@ class Project < ActiveRecord::Base ...@@ -1474,6 +1475,27 @@ class Project < ActiveRecord::Base
end end
end end
def after_create_default_branch
return unless default_branch
# Ensure HEAD points to the default branch in case it is not master
change_head(default_branch)
if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
params = {
name: default_branch,
push_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}],
merge_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}]
}
ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true)
end
end
def remove_import_jid def remove_import_jid
return unless import_jid return unless import_jid
......
class ProjectTeam class ProjectTeam
include BulkMemberAccessLoad include BulkMemberAccessLoad
prepend EE::ProjectTeam
attr_accessor :project attr_accessor :project
def initialize(project) def initialize(project)
...@@ -40,8 +42,6 @@ class ProjectTeam ...@@ -40,8 +42,6 @@ class ProjectTeam
end end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
return false if group_member_lock
ProjectMember.add_users( ProjectMember.add_users(
project, project,
users, users,
...@@ -173,12 +173,4 @@ class ProjectTeam ...@@ -173,12 +173,4 @@ class ProjectTeam
def group def group
project.group project.group
end end
def group_member_lock
group && group.membership_lock
end
def merge_max!(first_hash, second_hash)
first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
end
end end
class GroupChildEntity < Grape::Entity class GroupChildEntity < Grape::Entity
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
include RequestAwareEntity include RequestAwareEntity
include MarkupHelper
expose :id, :name, :description, :visibility, :full_name, expose :id, :name, :description, :visibility, :full_name,
:created_at, :updated_at, :avatar_url :created_at, :updated_at, :avatar_url
...@@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity ...@@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity
number_with_delimiter(instance.member_count) number_with_delimiter(instance.member_count)
end end
expose :markdown_description do |instance|
markdown_description
end
private private
def membership def membership
...@@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity ...@@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity
def type def type
object.class.name.downcase object.class.name.downcase
end end
def markdown_description
markdown_field(object, :description)
end
end end
...@@ -168,24 +168,7 @@ class GitPushService < BaseService ...@@ -168,24 +168,7 @@ class GitPushService < BaseService
offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max
@push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
# Ensure HEAD points to the default branch in case it is not master @project.after_create_default_branch
project.change_head(branch_name)
# Set protection on the default branch if configured
if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch)
params = {
name: @project.default_branch,
push_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}],
merge_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}]
}
ProtectedBranches::CreateService.new(@project, current_user, params).execute
end
end end
def build_push_data def build_push_data
......
...@@ -2,8 +2,8 @@ module ProtectedBranches ...@@ -2,8 +2,8 @@ module ProtectedBranches
class CreateService < BaseService class CreateService < BaseService
attr_reader :protected_branch attr_reader :protected_branch
def execute def execute(skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
project.protected_branches.create(params) project.protected_branches.create(params)
end end
......
...@@ -315,6 +315,7 @@ ...@@ -315,6 +315,7 @@
Charts Charts
-# Shortcut to Issues > New Issue -# Shortcut to Issues > New Issue
- if project_nav_tab?(:issues)
%li.hidden %li.hidden
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
Create a new issue Create a new issue
...@@ -332,5 +333,6 @@ ...@@ -332,5 +333,6 @@
Commits Commits
-# Shortcut to issue boards -# Shortcut to issue boards
- if project_nav_tab?(:issues)
%li.hidden %li.hidden
= link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
---
title: Properly memoize ChangeAccess#validate_path_locks? to avoid excessive queries
merge_request:
author:
type: performance
---
title: Display graph values on hover within monitoring page
merge_request: 16261
author:
type: changed
---
title: Protected branch is now created for default branch on import
merge_request: 16198
author:
type: fixed
---
title: "Fix slash commands dropdown description mis-alignment on Firefox"
merge_request: 16125
author: Maurizio De Santis
type: fixed
---
title: Migrate existing data from KubernetesService to Clusters::Platforms::Kubernetes
merge_request: 15589
author:
type: changed
---
title: Rendering of emoji's in Group-Overview
merge_request: 16098
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: disables shortcut to issue boards when issues are not enabled
merge_request: 16020
author: Christiaan Van den Poel
type: fixed
---
title: Update scss-lint to 0.56.0
merge_request: 16278
author: Takuya Noguchi
type: other
class MigrateKubernetesServiceToNewClustersArchitectures < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME = 'KubernetesService'.freeze
disable_ddl_transaction!
class Project < ActiveRecord::Base
self.table_name = 'projects'
has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
has_many :clusters, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
has_many :services, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service'
has_one :kubernetes_service, -> { where(category: 'deployment', type: 'KubernetesService') }, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service', inverse_of: :project, foreign_key: :project_id
end
class Cluster < ActiveRecord::Base
self.table_name = 'clusters'
has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
has_many :projects, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
has_one :platform_kubernetes, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::PlatformsKubernetes'
accepts_nested_attributes_for :platform_kubernetes
enum platform_type: {
kubernetes: 1
}
enum provider_type: {
user: 0,
gcp: 1
}
end
class ClustersProject < ActiveRecord::Base
self.table_name = 'cluster_projects'
belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
end
class PlatformsKubernetes < ActiveRecord::Base
self.table_name = 'cluster_platforms_kubernetes'
belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
attr_encrypted :token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
end
class Service < ActiveRecord::Base
include EachBatch
self.table_name = 'services'
self.inheritance_column = :_type_disabled # Disable STI, otherwise KubernetesModel will be looked up
belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project', foreign_key: :project_id
scope :unmanaged_kubernetes_service, -> do
joins('LEFT JOIN projects ON projects.id = services.project_id')
.joins('LEFT JOIN cluster_projects ON cluster_projects.project_id = projects.id')
.joins('LEFT JOIN cluster_platforms_kubernetes ON cluster_platforms_kubernetes.cluster_id = cluster_projects.cluster_id')
.where(category: 'deployment', type: 'KubernetesService', template: false)
.where("services.properties LIKE '%api_url%'")
.where("(services.properties NOT LIKE CONCAT('%', cluster_platforms_kubernetes.api_url, '%')) OR cluster_platforms_kubernetes.api_url IS NULL")
.group(:id)
.order(id: :asc)
end
scope :kubernetes_service_without_template, -> do
where(category: 'deployment', type: 'KubernetesService', template: false)
end
def api_url
parsed_properties['api_url']
end
def ca_pem
parsed_properties['ca_pem']
end
def namespace
parsed_properties['namespace']
end
def token
parsed_properties['token']
end
private
def parsed_properties
@parsed_properties ||= JSON.parse(self.properties)
end
end
def find_dedicated_environement_scope(project)
environment_scopes = project.clusters.map(&:environment_scope)
return '*' if environment_scopes.exclude?('*') # KubernetesService should be added as a default cluster (environment_scope: '*') at first place
return 'migrated/*' if environment_scopes.exclude?('migrated/*') # If it's conflicted, the KubernetesService added as a migrated cluster
unique_iid = 0
# If it's still conflicted, finding an unique environment scope incrementaly
loop do
candidate = "migrated#{unique_iid}/*"
return candidate if environment_scopes.exclude?(candidate)
unique_iid += 1
end
end
def up
ActiveRecord::Base.transaction do
MigrateKubernetesServiceToNewClustersArchitectures::Service
.unmanaged_kubernetes_service.find_each(batch_size: 1) do |kubernetes_service|
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create(
enabled: kubernetes_service.active,
user_id: nil, # KubernetesService doesn't have
name: DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME,
provider_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.provider_types[:user],
platform_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.platform_types[:kubernetes],
projects: [kubernetes_service.project],
environment_scope: find_dedicated_environement_scope(kubernetes_service.project),
platform_kubernetes_attributes: {
api_url: kubernetes_service.api_url,
ca_cert: kubernetes_service.ca_pem,
namespace: kubernetes_service.namespace,
username: nil, # KubernetesService doesn't have
encrypted_password: nil, # KubernetesService doesn't have
encrypted_password_iv: nil, # KubernetesService doesn't have
token: kubernetes_service.token # encrypted_token and encrypted_token_iv
} )
end
end
MigrateKubernetesServiceToNewClustersArchitectures::Service
.kubernetes_service_without_template.each_batch(of: 100) do |kubernetes_service|
kubernetes_service.update_all(active: false)
end
end
def down
# noop
end
end
...@@ -32,7 +32,9 @@ options: ...@@ -32,7 +32,9 @@ options:
## AWS Elastic File System ## AWS Elastic File System
GitLab does not recommend using AWS Elastic File System (EFS). GitLab strongly recommends against using AWS Elastic File System (EFS).
Our support team will not be able to assist on performance issues related to
file system access.
Customers and users have reported that AWS EFS does not perform well for GitLab's Customers and users have reported that AWS EFS does not perform well for GitLab's
use-case. There are several issues that can cause problems. For these reasons use-case. There are several issues that can cause problems. For these reasons
......
...@@ -15,4 +15,4 @@ that to prioritize important jobs. ...@@ -15,4 +15,4 @@ that to prioritize important jobs.
to restart Sidekiq. to restart Sidekiq.
- **(EES/EEP)** [Extra Sidekiq operations](extra_sidekiq_processes.md): Configure an extra set of Sidekiq processes to ensure certain queues always have dedicated workers, no matter the amount of jobs that need to be processed. - **(EES/EEP)** [Extra Sidekiq operations](extra_sidekiq_processes.md): Configure an extra set of Sidekiq processes to ensure certain queues always have dedicated workers, no matter the amount of jobs that need to be processed.
- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. - [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer.
- **(EES/EEP)** [Speed up SSH operations](fast_ssh_key_lookup.md): Authorize SSH users via a fast, indexed lookup to the GitLab database. - [Speed up SSH operations](fast_ssh_key_lookup.md): Authorize SSH users via a fast, indexed lookup to the GitLab database.
...@@ -536,7 +536,7 @@ Parameters: ...@@ -536,7 +536,7 @@ Parameters:
- `id` (required) - The ID of a group - `id` (required) - The ID of a group
- `cn` (required) - The CN of a LDAP group - `cn` (required) - The CN of a LDAP group
- `group_access` (required) - Minimum access level for members of the LDAP group - `group_access` (required) - Minimum access level for members of the LDAP group
- `provider` (required) - LDAP provider for the LDAP group (when using several providers) - `provider` (required) - LDAP provider for the LDAP group
### Delete LDAP group link ### Delete LDAP group link
......
# Dynamic Application Security Testing (SAST) # Dynamic Application Security Testing (DAST)
> [Introduced][ee-4348] in [GitLab Enterprise Edition Ultimate][ee] 10.4. > [Introduced][ee-4348] in [GitLab Enterprise Edition Ultimate][ee] 10.4.
......
...@@ -4,7 +4,6 @@ module EE ...@@ -4,7 +4,6 @@ module EE
raise NotImplementedError unless defined?(super) raise NotImplementedError unless defined?(super)
super + [ super + [
:authorized_keys_enabled,
:check_namespace_plan, :check_namespace_plan,
:elasticsearch_aws, :elasticsearch_aws,
:elasticsearch_aws_access_key, :elasticsearch_aws_access_key,
......
...@@ -32,7 +32,6 @@ module EE ...@@ -32,7 +32,6 @@ module EE
module ClassMethods module ClassMethods
def defaults def defaults
super.merge( super.merge(
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
elasticsearch_url: ENV['ELASTIC_URL'] || 'http://localhost:9200', elasticsearch_url: ENV['ELASTIC_URL'] || 'http://localhost:9200',
elasticsearch_aws: false, elasticsearch_aws: false,
elasticsearch_aws_region: ENV['ELASTIC_REGION'] || 'us-east-1', elasticsearch_aws_region: ENV['ELASTIC_REGION'] || 'us-east-1',
......
module EE
module ProjectTeam
extend ActiveSupport::Concern
def add_users(users, access_level, current_user: nil, expires_at: nil)
raise NotImplementedError unless defined?(super)
return false if group_member_lock
super
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
raise NotImplementedError unless defined?(super)
return false if group_member_lock
super
end
private
def group_member_lock
group && group.membership_lock
end
end
end
@public
Feature: Explore Groups
Background:
Given group "TestGroup" has private project "Enterprise"
@javascript
Scenario: I should see group with private and internal projects as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit group "TestGroup" page
Then I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group issues for internal project as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit group "TestGroup" issues page
Then I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group merge requests for internal project as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit group "TestGroup" merge requests page
Then I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group with private, internal and public projects as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I visit group "TestGroup" page
Then I should see project "Community" items
And I should not see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group issues for public project as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I visit group "TestGroup" issues page
Then I should see project "Community" items
And I should not see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group merge requests for public project as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I visit group "TestGroup" merge requests page
Then I should see project "Community" items
And I should not see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group with private, internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit group "TestGroup" page
Then I should see project "Community" items
And I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group issues for internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit group "TestGroup" issues page
Then I should see project "Community" items
And I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group merge requests for internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit group "TestGroup" merge requests page
Then I should see project "Community" items
And I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group with public project in public groups area
Given group "TestGroup" has public project "Community"
When I visit the public groups area
Then I should see group "TestGroup"
@javascript
Scenario: I should see group with public project in public groups area as user
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
@javascript
Scenario: I should see group with internal project in public groups area as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
Feature: Invites
Background:
Given "John Doe" is owner of group "Owned"
And "John Doe" has invited "user@example.com" to group "Owned"
Scenario: Viewing invitation when signed out
When I visit the invitation page
Then I should be redirected to the sign in page
And I should see a notice telling me to sign in
Scenario: Signing in to view invitation
When I visit the invitation page
And I sign in as "Mary Jane"
Then I should be redirected to the invitation page
Scenario: Viewing invitation when signed in
Given I sign in as "Mary Jane"
And I visit the invitation page
Then I should see the invitation details
And I should see an "Accept invitation" button
And I should see a "Decline" button
Scenario: Viewing invitation as an existing member
Given I sign in as "John Doe"
And I visit the invitation page
Then I should see a message telling me I'm already a member
Scenario: Accepting the invitation
Given I sign in as "Mary Jane"
And I visit the invitation page
And I click the "Accept invitation" button
Then I should be redirected to the group page
And I should see a notice telling me I have access
Scenario: Declining the application when signed in
Given I sign in as "Mary Jane"
And I visit the invitation page
And I click the "Decline" button
Then I should be redirected to the dashboard
And I should see a notice telling me I have declined
Scenario: Declining the application when signed out
When I visit the invitation's decline page
Then I should be redirected to the sign in page
And I should see a notice telling me I have declined
class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedGroup
include SharedProject
step 'group "TestGroup" has private project "Enterprise"' do
group_has_project("TestGroup", "Enterprise", Gitlab::VisibilityLevel::PRIVATE)
end
step 'group "TestGroup" has internal project "Internal"' do
group_has_project("TestGroup", "Internal", Gitlab::VisibilityLevel::INTERNAL)
end
step 'group "TestGroup" has public project "Community"' do
group_has_project("TestGroup", "Community", Gitlab::VisibilityLevel::PUBLIC)
end
step '"John Doe" is owner of group "TestGroup"' do
group = Group.find_by(name: "TestGroup") || create(:group, name: "TestGroup")
user = create(:user, name: "John Doe")
group.add_owner(user)
end
step 'I visit group "TestGroup" page' do
visit group_path(Group.find_by(name: "TestGroup"))
end
step 'I visit group "TestGroup" issues page' do
visit issues_group_path(Group.find_by(name: "TestGroup"))
end
step 'I visit group "TestGroup" merge requests page' do
visit merge_requests_group_path(Group.find_by(name: "TestGroup"))
end
step 'I visit group "TestGroup" members page' do
visit group_group_members_path(Group.find_by(name: "TestGroup"))
end
step 'I should not see project "Enterprise" items' do
expect(page).not_to have_content "Enterprise"
end
step 'I should see project "Internal" items' do
expect(page).to have_content "Internal"
end
step 'I should not see project "Internal" items' do
expect(page).not_to have_content "Internal"
end
step 'I should see project "Community" items' do
expect(page).to have_content "Community"
end
step 'I change filter to Everyone\'s' do
click_link "Everyone's"
end
step 'I should see group member "John Doe"' do
expect(page).to have_content "John Doe"
end
protected
def group_has_project(groupname, projectname, visibility_level)
group = Group.find_by(name: groupname) || create(:group, name: groupname)
project = create(:project,
namespace: group,
name: projectname,
path: "#{groupname}-#{projectname}",
visibility_level: visibility_level
)
create(:issue,
title: "#{projectname} feature",
project: project
)
create(:merge_request,
title: "#{projectname} feature implemented",
source_project: project,
target_project: project
)
create(:closed_issue_event,
project: project
)
end
end
...@@ -41,8 +41,10 @@ class Spinach::Features::GroupHooks < Spinach::FeatureSteps ...@@ -41,8 +41,10 @@ class Spinach::Features::GroupHooks < Spinach::FeatureSteps
end end
step 'I click test hook button' do step 'I click test hook button' do
WebMock.enable!
stub_request(:post, @hook.url).to_return(status: 200) stub_request(:post, @hook.url).to_return(status: 200)
click_link 'Test' click_link 'Test'
WebMock.disable!
end end
step 'I click test hook button with invalid URL' do step 'I click test hook button with invalid URL' do
......
class Spinach::Features::Invites < Spinach::FeatureSteps
include SharedAuthentication
include SharedUser
include SharedGroup
step '"John Doe" has invited "user@example.com" to group "Owned"' do
user = User.find_by(name: "John Doe")
group = Group.find_by(name: "Owned")
group.add_developer("user@example.com", user)
end
step 'I visit the invitation page' do
group = Group.find_by(name: "Owned")
invite = group.group_members.invite.last
invite.generate_invite_token!
@raw_invite_token = invite.raw_invite_token
visit invite_path(@raw_invite_token)
end
step 'I should be redirected to the sign in page' do
expect(current_path).to eq(new_user_session_path)
end
step 'I should see a notice telling me to sign in' do
expect(page).to have_content "To accept this invitation, sign in"
end
step 'I should be redirected to the invitation page' do
expect(current_path).to eq(invite_path(@raw_invite_token))
end
step 'I should see the invitation details' do
expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.")
end
step "I should see a message telling me I'm already a member" do
expect(page).to have_content("However, you are already a member of this group.")
end
step 'I should see an "Accept invitation" button' do
expect(page).to have_link("Accept invitation")
end
step 'I should see a "Decline" button' do
expect(page).to have_link("Decline")
end
step 'I click the "Accept invitation" button' do
page.click_link "Accept invitation"
end
step 'I should be redirected to the group page' do
group = Group.find_by(name: "Owned")
expect(current_path).to eq(group_path(group))
end
step 'I should see a notice telling me I have access' do
expect(page).to have_content("You have been granted Developer access to group Owned.")
end
step 'I click the "Decline" button' do
page.click_link "Decline"
end
step 'I should be redirected to the dashboard' do
expect(current_path).to eq(dashboard_projects_path)
end
step 'I should see a notice telling me I have declined' do
expect(page).to have_content("You have declined the invitation to join group Owned.")
end
step "I visit the invitation's decline page" do
group = Group.find_by(name: "Owned")
invite = group.group_members.invite.last
invite.generate_invite_token!
@raw_invite_token = invite.raw_invite_token
visit decline_invite_path(@raw_invite_token)
end
end
...@@ -54,12 +54,6 @@ module API ...@@ -54,12 +54,6 @@ module API
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source) authorize_admin_source!(source_type, source)
## EE specific
if source_type == 'project' && source.group && source.group.membership_lock
not_allowed!
end
## EE specific
member = source.members.find_by(user_id: params[:user_id]) member = source.members.find_by(user_id: params[:user_id])
conflict!('Member already exists') if member conflict!('Member already exists') if member
......
...@@ -2,6 +2,7 @@ module Gitlab ...@@ -2,6 +2,7 @@ module Gitlab
module Checks module Checks
class ChangeAccess class ChangeAccess
include PathLocksHelper include PathLocksHelper
include Gitlab::Utils::StrongMemoize
ERROR_MESSAGES = { ERROR_MESSAGES = {
push_code: 'You are not allowed to push code to this project.', push_code: 'You are not allowed to push code to this project.',
...@@ -300,10 +301,12 @@ module Gitlab ...@@ -300,10 +301,12 @@ module Gitlab
end end
def validate_path_locks? def validate_path_locks?
@validate_path_locks ||= @project.feature_available?(:file_locks) && strong_memoize(:validate_path_locks) do
@project.feature_available?(:file_locks) &&
project.path_locks.any? && @newrev && @oldrev && project.path_locks.any? && @newrev && @oldrev &&
project.default_branch == @branch_name # locks protect default branch only project.default_branch == @branch_name # locks protect default branch only
end end
end
def path_locks_validation def path_locks_validation
lambda do |diff| lambda do |diff|
......
...@@ -173,8 +173,8 @@ module Gitlab ...@@ -173,8 +173,8 @@ module Gitlab
end end
def find_by_rugged(repository, sha, path, limit:) def find_by_rugged(repository, sha, path, limit:)
commit = repository.lookup(sha) rugged_commit = repository.lookup(sha)
root_tree = commit.tree root_tree = rugged_commit.tree
blob_entry = find_entry_by_path(repository, root_tree.oid, path) blob_entry = find_entry_by_path(repository, root_tree.oid, path)
......
...@@ -15,8 +15,6 @@ module Gitlab ...@@ -15,8 +15,6 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
delegate :tree, to: :rugged_commit
def ==(other) def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit) return false unless other.is_a?(Gitlab::Git::Commit)
...@@ -452,6 +450,11 @@ module Gitlab ...@@ -452,6 +450,11 @@ module Gitlab
) )
end end
# Is this the same as Blob.find_entry_by_path ?
def rugged_tree_entry(path)
rugged_commit.tree.path(path)
end
private private
def init_from_hash(hash) def init_from_hash(hash)
......
...@@ -1163,23 +1163,13 @@ module Gitlab ...@@ -1163,23 +1163,13 @@ module Gitlab
end end
def fetch_repository_as_mirror(repository) def fetch_repository_as_mirror(repository)
remote_name = "tmp-#{SecureRandom.hex}" gitaly_migrate(:remote_fetch_internal_remote) do |is_enabled|
# Notice that this feature flag is not for `fetch_repository_as_mirror`
# as a whole but for the fetching mechanism (file path or gitaly-ssh).
url, env = gitaly_migrate(:fetch_internal) do |is_enabled|
if is_enabled if is_enabled
repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository) gitaly_remote_client.fetch_internal_remote(repository)
[GITALY_INTERNAL_URL, repository.fetch_env]
else else
[repository.path, nil] rugged_fetch_repository_as_mirror(repository)
end end
end end
add_remote(remote_name, url, mirror_refmap: :all_refs)
fetch_remote(remote_name, env: env)
ensure
remove_remote(remote_name)
end end
def blob_at(sha, path) def blob_at(sha, path)
...@@ -2064,6 +2054,16 @@ module Gitlab ...@@ -2064,6 +2054,16 @@ module Gitlab
false false
end end
def rugged_fetch_repository_as_mirror(repository)
remote_name = "tmp-#{SecureRandom.hex}"
repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository)
add_remote(remote_name, GITALY_INTERNAL_URL, mirror_refmap: :all_refs)
fetch_remote(remote_name, env: repository.fetch_env)
ensure
remove_remote(remote_name)
end
def fetch_remote(remote_name = 'origin', env: nil) def fetch_remote(remote_name = 'origin', env: nil)
run_git(['fetch', remote_name], env: env).last.zero? run_git(['fetch', remote_name], env: env).last.zero?
end end
......
...@@ -23,6 +23,19 @@ module Gitlab ...@@ -23,6 +23,19 @@ module Gitlab
response.result response.result
end end
def fetch_internal_remote(repository)
request = Gitaly::FetchInternalRemoteRequest.new(
repository: @gitaly_repo,
remote_repository: repository.gitaly_repository
)
response = GitalyClient.call(@storage, :remote_service,
:fetch_internal_remote, request,
remote_storage: repository.storage)
response.result
end
end end
end end
end end
...@@ -10,6 +10,7 @@ module QA ...@@ -10,6 +10,7 @@ module QA
autoload :Namespace, 'qa/runtime/namespace' autoload :Namespace, 'qa/runtime/namespace'
autoload :Scenario, 'qa/runtime/scenario' autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser' autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
end end
## ##
......
...@@ -38,22 +38,49 @@ module QA ...@@ -38,22 +38,49 @@ module QA
Capybara.register_driver :chrome do |app| Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => { # This enables access to logs with `page.driver.manage.get_log(:browser)`
'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] loggingPrefs: {
browser: "ALL",
client: "ALL",
driver: "ALL",
server: "ALL"
} }
) )
Capybara::Selenium::Driver options = Selenium::WebDriver::Chrome::Options.new
.new(app, browser: :chrome, desired_capabilities: capabilities) options.add_argument("window-size=1240,1680")
# Chrome won't work properly in a Docker container in sandbox mode
options.add_argument("no-sandbox")
# Run headless by default unless CHROME_HEADLESS is false
if QA::Runtime::Env.chrome_headless?
options.add_argument("headless")
# Chrome documentation says this flag is needed for now
# https://developers.google.com/web/updates/2017/04/headless-chrome#cli
options.add_argument("disable-gpu")
end end
Capybara::Screenshot.register_driver(:chrome) do |driver, path| # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
driver.browser.save_screenshot(path) options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci?
Capybara::Selenium::Driver.new(
app,
browser: :chrome,
desired_capabilities: capabilities,
options: options
)
end end
# Keep only the screenshots generated from the last failing test suite # Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run Capybara::Screenshot.prune_strategy = :keep_last_run
# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
Capybara::Screenshot.register_driver(:chrome) do |driver, path|
driver.browser.save_screenshot(path)
end
Capybara.configure do |config| Capybara.configure do |config|
config.default_driver = :chrome config.default_driver = :chrome
config.javascript_driver = :chrome config.javascript_driver = :chrome
......
module QA
module Runtime
module Env
extend self
def chrome_headless?
(ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
end
def running_in_ci?
ENV['CI'] || ENV['CI_SERVER']
end
end
end
end
describe QA::Runtime::Env do
before do
allow(ENV).to receive(:[]).and_call_original
end
describe '.chrome_headless?' do
context 'when there is an env variable set' do
it 'returns false when falsey values specified' do
stub_env('CHROME_HEADLESS', 'false')
expect(described_class.chrome_headless?).to be_falsey
stub_env('CHROME_HEADLESS', 'no')
expect(described_class.chrome_headless?).to be_falsey
stub_env('CHROME_HEADLESS', '0')
expect(described_class.chrome_headless?).to be_falsey
end
it 'returns true when anything else specified' do
stub_env('CHROME_HEADLESS', 'true')
expect(described_class.chrome_headless?).to be_truthy
stub_env('CHROME_HEADLESS', '1')
expect(described_class.chrome_headless?).to be_truthy
stub_env('CHROME_HEADLESS', 'anything')
expect(described_class.chrome_headless?).to be_truthy
end
end
context 'when there is no env variable set' do
it 'returns the default, true' do
stub_env('CHROME_HEADLESS', nil)
expect(described_class.chrome_headless?).to be_truthy
end
end
end
describe '.running_in_ci?' do
context 'when there is an env variable set' do
it 'returns true if CI' do
stub_env('CI', 'anything')
expect(described_class.running_in_ci?).to be_truthy
end
it 'returns true if CI_SERVER' do
stub_env('CI_SERVER', 'anything')
expect(described_class.running_in_ci?).to be_truthy
end
end
context 'when there is no env variable set' do
it 'returns true' do
stub_env('CI', nil)
stub_env('CI_SERVER', nil)
expect(described_class.running_in_ci?).to be_falsey
end
end
end
def stub_env(name, value)
allow(ENV).to receive(:[]).with(name).and_return(value)
end
end
...@@ -71,6 +71,16 @@ describe Projects::BoardsController do ...@@ -71,6 +71,16 @@ describe Projects::BoardsController do
end end
end end
context 'issues are disabled' do
let(:project) { create(:project, :issues_disabled) }
it 'returns a not found 404 response' do
list_boards
expect(response).to have_gitlab_http_status(404)
end
end
def list_boards(format: :html) def list_boards(format: :html)
get :index, namespace_id: project.namespace, get :index, namespace_id: project.namespace,
project_id: project, project_id: project,
......
require "spec_helper"
describe ProjectTeam do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
describe '#add_users' do
let(:user1) { create(:user) }
let(:user2) { create(:user) }
context 'when group membership is locked' do
before do
group.update_attribute(:membership_lock, true)
end
it 'does not add the given users to the team' do
project.team.add_users([user1, user2], :reporter)
expect(project.team.reporter?(user1)).to be(false)
expect(project.team.reporter?(user2)).to be(false)
end
end
end
describe '#add_user' do
let(:user) { create(:user) }
context 'when group membership is locked' do
before do
group.update_attribute(:membership_lock, true)
end
it 'does not add the given user to the team' do
project.team.add_user(user, :reporter)
expect(project.team.reporter?(user)).to be(false)
end
end
end
end
...@@ -10,6 +10,10 @@ describe Projects::HashedStorage::MigrateRepositoryService do ...@@ -10,6 +10,10 @@ describe Projects::HashedStorage::MigrateRepositoryService do
set(:primary) { create(:geo_node, :primary) } set(:primary) { create(:geo_node, :primary) }
set(:secondary) { create(:geo_node) } set(:secondary) { create(:geo_node) }
before do
TestEnv.clean_test_path
end
it 'creates a Geo::HashedStorageMigratedEvent on success' do it 'creates a Geo::HashedStorageMigratedEvent on success' do
expect { service.execute }.to change(Geo::EventLog, :count).by(1) expect { service.execute }.to change(Geo::EventLog, :count).by(1)
......
require 'rails_helper' require 'rails_helper'
describe 'Issue Boards shortcut', :js do describe 'Issue Boards shortcut', :js do
context 'issues are enabled' do
let(:project) { create(:project) } let(:project) { create(:project) }
before do before do
...@@ -17,4 +18,21 @@ describe 'Issue Boards shortcut', :js do ...@@ -17,4 +18,21 @@ describe 'Issue Boards shortcut', :js do
wait_for_requests wait_for_requests
end end
end
context 'issues are not enabled' do
let(:project) { create(:project, :issues_disabled) }
before do
sign_in(create(:admin))
visit project_path(project)
end
it 'does not take user to the issue board index' do
find('body').native.send_keys('gb')
expect(page).to have_selector("body[data-page='projects:show']")
end
end
end end
require 'spec_helper'
describe 'Explore Groups', :js do
let(:user) { create :user }
let(:group) { create :group }
let!(:private_project) do
create :project, :private, namespace: group do |project|
create(:issue, project: internal_project)
create(:merge_request, source_project: project, target_project: project)
end
end
let!(:internal_project) do
create :project, :internal, namespace: group do |project|
create(:issue, project: project)
create(:merge_request, source_project: project, target_project: project)
end
end
let!(:public_project) do
create(:project, :public, namespace: group) do |project|
create(:issue, project: project)
create(:merge_request, source_project: project, target_project: project)
end
end
shared_examples 'renders public and internal projects' do
it do
visit_page
expect(page).to have_content(public_project.name)
expect(page).to have_content(internal_project.name)
expect(page).not_to have_content(private_project.name)
end
end
shared_examples 'renders only public project' do
it do
visit_page
expect(page).to have_content(public_project.name)
expect(page).not_to have_content(internal_project.name)
expect(page).not_to have_content(private_project.name)
end
end
shared_examples 'renders group in public groups area' do
it do
visit explore_groups_path
expect(page).to have_content(group.name)
end
end
context 'when signed in' do
before do
sign_in(user)
end
it_behaves_like 'renders public and internal projects' do
subject(:visit_page) { visit group_path(group) }
end
it_behaves_like 'renders public and internal projects' do
subject(:visit_page) { visit issues_group_path(group) }
end
it_behaves_like 'renders public and internal projects' do
subject(:visit_page) { visit merge_requests_group_path(group) }
end
it_behaves_like 'renders group in public groups area'
end
context 'when signed out' do
it_behaves_like 'renders only public project' do
subject(:visit_page) { visit group_path(group) }
end
it_behaves_like 'renders only public project' do
subject(:visit_page) { visit issues_group_path(group) }
end
it_behaves_like 'renders only public project' do
subject(:visit_page) { visit merge_requests_group_path(group) }
end
it_behaves_like 'renders group in public groups area'
end
end
...@@ -55,4 +55,20 @@ feature 'Group show page' do ...@@ -55,4 +55,20 @@ feature 'Group show page' do
end end
end end
end end
context 'group has a project with emoji in description', :js do
let(:user) { create(:user) }
let!(:project) { create(:project, description: ':smile:', namespace: group) }
before do
group.add_owner(user)
sign_in(user)
visit path
end
it 'shows the project info' do
expect(page).to have_content(project.title)
expect(page).to have_selector('gl-emoji[data-name="smile"]')
end
end
end end
require 'spec_helper'
describe 'Invites' do
let(:user) { create(:user) }
let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) }
let(:invite) { group.group_members.invite.last }
before do
project.add_master(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
invite.generate_invite_token!
end
context 'when signed out' do
before do
visit invite_path(invite.raw_invite_token)
end
it 'renders sign in page with sign in notice' do
expect(current_path).to eq(new_user_session_path)
expect(page).to have_content('To accept this invitation, sign in')
end
it 'sign in and redirects to invitation page' do
fill_in 'user_login', with: user.email
fill_in 'user_password', with: user.password
check 'user_remember_me'
click_button 'Sign in'
expect(current_path).to eq(invite_path(invite.raw_invite_token))
expect(page).to have_content(
'You have been invited by John Doe to join group Owned as Developer.'
)
expect(page).to have_link('Accept invitation')
expect(page).to have_link('Decline')
end
end
context 'when signed in as an exists member' do
before do
sign_in(owner)
end
it 'shows message user already a member' do
visit invite_path(invite.raw_invite_token)
expect(page).to have_content('However, you are already a member of this group.')
end
end
describe 'accepting the invitation' do
before do
sign_in(user)
visit invite_path(invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
page.click_link 'Accept invitation'
expect(current_path).to eq(group_path(group))
expect(page).to have_content(
'You have been granted Developer access to group Owned.'
)
end
end
describe 'declining the application' do
context 'when signed in' do
before do
sign_in(user)
visit invite_path(invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
page.click_link 'Decline'
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content(
'You have declined the invitation to join group Owned.'
)
end
end
context 'when signed out' do
before do
visit decline_invite_path(invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
expect(current_path).to eq(new_user_session_path)
expect(page).to have_content(
'You have declined the invitation to join group Owned.'
)
end
end
end
end
require 'rails_helper'
describe 'Issues shortcut', :js do
context 'New Issue shortcut' do
context 'issues are enabled' do
let(:project) { create(:project) }
before do
sign_in(create(:admin))
visit project_path(project)
end
it 'takes user to the new issue page' do
find('body').native.send_keys('i')
expect(page).to have_selector('#new_issue')
end
end
context 'issues are not enabled' do
let(:project) { create(:project, :issues_disabled) }
before do
sign_in(create(:admin))
visit project_path(project)
end
it 'does not take user to the new issue page' do
find('body').native.send_keys('i')
expect(page).to have_selector("body[data-page='projects:show']")
end
end
end
end
...@@ -11,168 +11,38 @@ const createComponent = (propsData) => { ...@@ -11,168 +11,38 @@ const createComponent = (propsData) => {
}; };
describe('MonitoringDeployment', () => { describe('MonitoringDeployment', () => {
const reducedDeploymentData = [deploymentData[0]];
reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name;
reducedDeploymentData[0].xPos = 10;
reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at);
describe('Methods', () => { describe('Methods', () => {
it('refText shows the ref when a tag is available', () => { it('should contain a hidden gradient', () => {
reducedDeploymentData[0].tag = '1.0';
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.refText(reducedDeploymentData[0]),
).toEqual(reducedDeploymentData[0].ref);
});
it('refText shows the sha when no tag is available', () => {
reducedDeploymentData[0].tag = null;
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.refText(reducedDeploymentData[0]),
).toContain('f5bcd1');
});
it('nameDeploymentClass creates a class with the prefix deploy-info-', () => {
const component = createComponent({ const component = createComponent({
showDeployInfo: false, showDeployInfo: true,
deploymentData: reducedDeploymentData, deploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440, graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
expect( expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
component.nameDeploymentClass(reducedDeploymentData[0]),
).toContain('deploy-info');
}); });
it('transformDeploymentGroup translates an available deployment', () => { it('transformDeploymentGroup translates an available deployment', () => {
const component = createComponent({ const component = createComponent({
showDeployInfo: false, showDeployInfo: false,
deploymentData: reducedDeploymentData, deploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440, graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
expect( expect(
component.transformDeploymentGroup(reducedDeploymentData[0]), component.transformDeploymentGroup({ xPos: 16 }),
).toContain('translate(11, 20)'); ).toContain('translate(11, 20)');
}); });
it('hides the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = false;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
});
it('positions the flag to the left when the xPos is too far right', () => {
reducedDeploymentData[0].showDeploymentFlag = false;
reducedDeploymentData[0].xPos = 250;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.positionFlag(reducedDeploymentData[0]),
).toBeLessThan(0);
});
it('shows the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.$el.querySelector('.js-deploy-info-box').style.display,
).not.toEqual('display: none;');
});
it('contains date, refs and the "deployed" text', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.$el.querySelectorAll('.deploy-info-text'),
).toContainText('Deployed');
expect(
component.$el.querySelectorAll('.deploy-info-text'),
).toContainText('Wed, May 31');
expect(
component.$el.querySelectorAll('.deploy-info-text'),
).toContainText(component.refText(reducedDeploymentData[0]));
});
it('contains a link to the commit contents', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'),
).not.toEqual('');
});
it('should contain a hidden gradient', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
});
describe('Computed props', () => { describe('Computed props', () => {
it('calculatedHeight', () => { it('calculatedHeight', () => {
const component = createComponent({ const component = createComponent({
showDeployInfo: true, showDeployInfo: true,
deploymentData: reducedDeploymentData, deploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440, graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
......
import Vue from 'vue'; import Vue from 'vue';
import GraphFlag from '~/monitoring/components/graph/flag.vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue';
import { deploymentData } from '../mock_data';
const createComponent = (propsData) => { const createComponent = (propsData) => {
const Component = Vue.extend(GraphFlag); const Component = Vue.extend(GraphFlag);
...@@ -9,11 +10,6 @@ const createComponent = (propsData) => { ...@@ -9,11 +10,6 @@ const createComponent = (propsData) => {
}).$mount(); }).$mount();
}; };
function getCoordinate(component, selector, coordinate) {
const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate);
return parseInt(coordinateVal, 10);
}
const defaultValuesComponent = { const defaultValuesComponent = {
currentXCoordinate: 200, currentXCoordinate: 200,
currentYCoordinate: 100, currentYCoordinate: 100,
...@@ -25,31 +21,111 @@ const defaultValuesComponent = { ...@@ -25,31 +21,111 @@ const defaultValuesComponent = {
graphHeight: 300, graphHeight: 300,
graphHeightOffset: 120, graphHeightOffset: 120,
showFlagContent: true, showFlagContent: true,
realPixelRatio: 1,
timeSeries: [{
values: [{
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
}],
}],
unitOfDisplay: 'ms',
currentDataIndex: 0,
legendTitle: 'Average',
};
const deploymentFlagData = {
...deploymentData[0],
ref: deploymentData[0].ref.name,
xPos: 10,
time: new Date(deploymentData[0].created_at),
}; };
describe('GraphFlag', () => { describe('GraphFlag', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { let component;
const component = createComponent(defaultValuesComponent);
it('has a line at the currentXCoordinate', () => {
component = createComponent(defaultValuesComponent);
expect(component.$el.style.left)
.toEqual(`${70 + component.currentXCoordinate}px`);
});
describe('Deployment flag', () => {
it('shows a deployment flag when deployment data provided', () => {
const deploymentFlagComponent = createComponent({
...defaultValuesComponent,
deploymentFlagData,
});
expect(
deploymentFlagComponent.$el.querySelector('.popover-title'),
).toContainText('Deployed');
});
it('contains the ref when a tag is available', () => {
const deploymentFlagComponent = createComponent({
...defaultValuesComponent,
deploymentFlagData: {
...deploymentFlagData,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
tag: true,
ref: '1.0',
},
});
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).toContainText('f5bcd1d9');
expect(getCoordinate(component, '.selected-metric-line', 'x1')) expect(
.toEqual(component.currentXCoordinate); deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
expect(getCoordinate(component, '.selected-metric-line', 'x2')) ).toContainText('1.0');
.toEqual(component.currentXCoordinate);
}); });
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { it('does not contain the ref when a tag is unavailable', () => {
const component = createComponent(defaultValuesComponent); const deploymentFlagComponent = createComponent({
...defaultValuesComponent,
deploymentFlagData: {
...deploymentFlagData,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
tag: false,
ref: '1.0',
},
});
const svg = component.$el.querySelector('.rect-text-metric'); expect(
expect(svg.tagName).toEqual('svg'); deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition); ).toContainText('f5bcd1d9');
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).not.toContainText('1.0');
});
}); });
describe('Computed props', () => { describe('Computed props', () => {
it('calculatedHeight', () => { beforeEach(() => {
const component = createComponent(defaultValuesComponent); component = createComponent(defaultValuesComponent);
});
it('formatTime', () => {
expect(component.formatTime).toMatch(/\d:17PM/);
});
it('formatDate', () => {
expect(component.formatDate).toEqual('Sun, Jun 4');
});
it('cursorStyle', () => {
expect(component.cursorStyle).toEqual({
top: '20px',
left: '270px',
height: '180px',
});
});
expect(component.calculatedHeight).toEqual(180); it('flagOrientation', () => {
expect(component.flagOrientation).toEqual('left');
}); });
}); });
}); });
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import Todos from '~/todos'; import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
describe('Todos', () => { describe('Todos', () => {
......
...@@ -319,6 +319,12 @@ describe Gitlab::Checks::ChangeAccess do ...@@ -319,6 +319,12 @@ describe Gitlab::Checks::ChangeAccess do
it 'allows the default branch even if it does not match push rule' do it 'allows the default branch even if it does not match push rule' do
expect { subject.exec }.not_to raise_error expect { subject.exec }.not_to raise_error
end end
it 'memoizes the validate_path_locks? call' do
expect(project.path_locks).to receive(:any?).once.and_call_original
2.times { subject.exec }
end
end end
end end
......
...@@ -146,7 +146,7 @@ describe Gitlab::Git::Blob, seed_helper: true do ...@@ -146,7 +146,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
context 'when sha references a tree' do context 'when sha references a tree' do
it 'returns nil' do it 'returns nil' do
tree = Gitlab::Git::Commit.find(repository, 'master').tree tree = repository.rugged.rev_parse('master^{tree}')
blob = Gitlab::Git::Blob.raw(repository, tree.oid) blob = Gitlab::Git::Blob.raw(repository, tree.oid)
...@@ -230,7 +230,7 @@ describe Gitlab::Git::Blob, seed_helper: true do ...@@ -230,7 +230,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end end
describe '.batch_lfs_pointers' do describe '.batch_lfs_pointers' do
let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree } let(:tree_object) { repository.rugged.rev_parse('master^{tree}') }
let(:non_lfs_blob) do let(:non_lfs_blob) do
Gitlab::Git::Blob.find( Gitlab::Git::Blob.find(
......
...@@ -55,7 +55,6 @@ describe Gitlab::Git::Commit, seed_helper: true do ...@@ -55,7 +55,6 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(@commit.parents).to eq(@gitlab_parents) } it { expect(@commit.parents).to eq(@gitlab_parents) }
it { expect(@commit.parent_id).to eq(@parents.first.oid) } it { expect(@commit.parent_id).to eq(@parents.first.oid) }
it { expect(@commit.no_commit_message).to eq("--no commit message") } it { expect(@commit.no_commit_message).to eq("--no commit message") }
it { expect(@commit.tree).to eq(@tree) }
after do after do
# Erase the new commit so other tests get the original repo # Erase the new commit so other tests get the original repo
......
...@@ -649,6 +649,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -649,6 +649,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
Gitlab::Shell.new.remove_repository(storage_path, 'my_project') Gitlab::Shell.new.remove_repository(storage_path, 'my_project')
end end
shared_examples 'repository mirror fecthing' do
it 'fetches a repository as a mirror remote' do it 'fetches a repository as a mirror remote' do
subject subject
...@@ -674,6 +675,15 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -674,6 +675,15 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
context 'with gitaly enabled' do
it_behaves_like 'repository mirror fecthing'
end
context 'with gitaly enabled', :skip_gitaly_mock do
it_behaves_like 'repository mirror fecthing'
end
end
describe '#remote_tags' do describe '#remote_tags' do
let(:remote_name) { 'upstream' } let(:remote_name) { 'upstream' }
let(:target_commit_id) { SeedRepo::Commit::ID } let(:target_commit_id) { SeedRepo::Commit::ID }
......
...@@ -31,4 +31,17 @@ describe Gitlab::GitalyClient::RemoteService do ...@@ -31,4 +31,17 @@ describe Gitlab::GitalyClient::RemoteService do
expect(client.remove_remote(remote_name)).to be(true) expect(client.remove_remote(remote_name)).to be(true)
end end
end end
describe '#fetch_internal_remote' do
let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
it 'sends an fetch_internal_remote message and returns the result value' do
expect_any_instance_of(Gitaly::RemoteService::Stub)
.to receive(:fetch_internal_remote)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return(double(result: true))
expect(client.fetch_internal_remote(remote_repository)).to be(true)
end
end
end end
...@@ -69,7 +69,7 @@ describe Gitlab::Shell do ...@@ -69,7 +69,7 @@ describe Gitlab::Shell do
end end
it 'does nothing' do it 'does nothing' do
expect(Gitlab::Utils).not_to receive(:system_silent) expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute)
gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
end end
...@@ -443,7 +443,7 @@ describe Gitlab::Shell do ...@@ -443,7 +443,7 @@ describe Gitlab::Shell do
end end
end end
context 'with gitlay' do context 'with gitaly' do
it_behaves_like '#add_repository' it_behaves_like '#add_repository'
end end
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb')
describe MigrateKubernetesServiceToNewClustersArchitectures, :migration do
context 'when unique KubernetesService exists' do
shared_examples 'KubernetesService migration' do
let(:sample_num) { 2 }
let(:projects) do
(1..sample_num).each_with_object([]) do |n, array|
array << MigrateKubernetesServiceToNewClustersArchitectures::Project.create!
end
end
let!(:kubernetes_services) do
projects.map do |project|
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
active: active,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"namespace\":\"prod\",\"api_url\":\"https://kubernetes#{project.id}.com\",\"ca_pem\":\"ca_pem#{project.id}\",\"token\":\"token#{project.id}\"}")
end
end
it 'migrates the KubernetesService to Platform::Kubernetes' do
expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(sample_num)
projects.each do |project|
project.clusters.last.tap do |cluster|
expect(cluster.enabled).to eq(active)
expect(cluster.platform_kubernetes.api_url).to eq(project.kubernetes_service.api_url)
expect(cluster.platform_kubernetes.ca_cert).to eq(project.kubernetes_service.ca_pem)
expect(cluster.platform_kubernetes.token).to eq(project.kubernetes_service.token)
expect(project.kubernetes_service).not_to be_active
end
end
end
end
context 'when KubernetesService is active' do
let(:active) { true }
it_behaves_like 'KubernetesService migration'
end
end
context 'when unique KubernetesService spawned from Service Template' do
let(:sample_num) { 2 }
let(:projects) do
(1..sample_num).each_with_object([]) do |n, array|
array << MigrateKubernetesServiceToNewClustersArchitectures::Project.create!
end
end
let!(:kubernetes_service_template) do
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
template: true,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}")
end
let!(:kubernetes_services) do
projects.map do |project|
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"namespace\":\"prod\",\"api_url\":\"#{kubernetes_service_template.api_url}\",\"ca_pem\":\"#{kubernetes_service_template.ca_pem}\",\"token\":\"#{kubernetes_service_template.token}\"}")
end
end
it 'migrates the KubernetesService to Platform::Kubernetes without template' do
expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(sample_num)
projects.each do |project|
project.clusters.last.tap do |cluster|
expect(cluster.platform_kubernetes.api_url).to eq(project.kubernetes_service.api_url)
expect(cluster.platform_kubernetes.ca_cert).to eq(project.kubernetes_service.ca_pem)
expect(cluster.platform_kubernetes.token).to eq(project.kubernetes_service.token)
expect(project.kubernetes_service).not_to be_active
end
end
end
end
context 'when managed KubernetesService exists' do
let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
let(:cluster) do
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
projects: [project],
name: 'sample-cluster',
platform_type: :kubernetes,
provider_type: :user,
platform_kubernetes_attributes: {
api_url: 'https://sample.kubernetes.com',
ca_cert: 'ca_pem-sample',
token: 'token-sample'
} )
end
let!(:kubernetes_service) do
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
active: cluster.enabled,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"api_url\":\"#{cluster.platform_kubernetes.api_url}\"}")
end
it 'does not migrate the KubernetesService and disables the kubernetes_service' do # Because the corresponding Platform::Kubernetes already exists
expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }
kubernetes_service.reload
expect(kubernetes_service).not_to be_active
end
end
context 'when production cluster has already been existed' do # i.e. There are no environment_scope conflicts
let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
let(:cluster) do
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
projects: [project],
name: 'sample-cluster',
platform_type: :kubernetes,
provider_type: :user,
environment_scope: 'production/*',
platform_kubernetes_attributes: {
api_url: 'https://sample.kubernetes.com',
ca_cert: 'ca_pem-sample',
token: 'token-sample'
} )
end
let!(:kubernetes_service) do
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
active: true,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"api_url\":\"https://debug.kube.com\"}")
end
it 'migrates the KubernetesService to Platform::Kubernetes' do
expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
kubernetes_service.reload
project.clusters.last.tap do |cluster|
expect(cluster.environment_scope).to eq('*')
expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url)
expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem)
expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token)
expect(kubernetes_service).not_to be_active
end
end
end
context 'when default cluster has already been existed' do
let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
let!(:cluster) do
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
projects: [project],
name: 'sample-cluster',
platform_type: :kubernetes,
provider_type: :user,
environment_scope: '*',
platform_kubernetes_attributes: {
api_url: 'https://sample.kubernetes.com',
ca_cert: 'ca_pem-sample',
token: 'token-sample'
} )
end
let!(:kubernetes_service) do
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
active: true,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"api_url\":\"https://debug.kube.com\"}")
end
it 'migrates the KubernetesService to Platform::Kubernetes with dedicated environment_scope' do # Because environment_scope is duplicated
expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
kubernetes_service.reload
project.clusters.last.tap do |cluster|
expect(cluster.environment_scope).to eq('migrated/*')
expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url)
expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem)
expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token)
expect(kubernetes_service).not_to be_active
end
end
end
context 'when default cluster and migrated cluster has already been existed' do
let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
let!(:cluster) do
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
projects: [project],
name: 'sample-cluster',
platform_type: :kubernetes,
provider_type: :user,
environment_scope: '*',
platform_kubernetes_attributes: {
api_url: 'https://sample.kubernetes.com',
ca_cert: 'ca_pem-sample',
token: 'token-sample'
} )
end
let!(:migrated_cluster) do
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create!(
projects: [project],
name: 'sample-cluster',
platform_type: :kubernetes,
provider_type: :user,
environment_scope: 'migrated/*',
platform_kubernetes_attributes: {
api_url: 'https://sample.kubernetes.com',
ca_cert: 'ca_pem-sample',
token: 'token-sample'
} )
end
let!(:kubernetes_service) do
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
active: true,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"api_url\":\"https://debug.kube.com\"}")
end
it 'migrates the KubernetesService to Platform::Kubernetes with dedicated environment_scope' do # Because environment_scope is duplicated
expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
kubernetes_service.reload
project.clusters.last.tap do |cluster|
expect(cluster.environment_scope).to eq('migrated0/*')
expect(cluster.platform_kubernetes.api_url).to eq(kubernetes_service.api_url)
expect(cluster.platform_kubernetes.ca_cert).to eq(kubernetes_service.ca_pem)
expect(cluster.platform_kubernetes.token).to eq(kubernetes_service.token)
expect(kubernetes_service).not_to be_active
end
end
end
context 'when KubernetesService has nullified parameters' do
let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
before do
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
active: false,
category: 'deployment',
type: 'KubernetesService',
properties: "{}")
end
it 'does not migrate the KubernetesService and disables the kubernetes_service' do
expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }
expect(project.kubernetes_service).not_to be_active
end
end
# Platforms::Kubernetes validates `token` reagdless of the activeness,
# whereas KubernetesService validates `token` if only it's activated
# However, in this migration file, there are no validations because of the re-defined model class
# therefore, we should safely add this raw to Platform::Kubernetes
context 'when KubernetesService has empty token' do
let(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
before do
MigrateKubernetesServiceToNewClustersArchitectures::Service.create!(
project: project,
active: false,
category: 'deployment',
type: 'KubernetesService',
properties: "{\"namespace\":\"prod\",\"api_url\":\"http://111.111.111.111\",\"ca_pem\":\"a\",\"token\":\"\"}")
end
it 'does not migrate the KubernetesService and disables the kubernetes_service' do
expect { migrate! }.to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }.by(1)
project.clusters.last.tap do |cluster|
expect(cluster.environment_scope).to eq('*')
expect(cluster.platform_kubernetes.namespace).to eq('prod')
expect(cluster.platform_kubernetes.api_url).to eq('http://111.111.111.111')
expect(cluster.platform_kubernetes.ca_cert).to eq('a')
expect(cluster.platform_kubernetes.token).to be_empty
expect(project.kubernetes_service).not_to be_active
end
end
end
context 'when KubernetesService does not exist' do
let!(:project) { MigrateKubernetesServiceToNewClustersArchitectures::Project.create! }
it 'does not migrate the KubernetesService' do
expect { migrate! }.not_to change { MigrateKubernetesServiceToNewClustersArchitectures::Cluster.count }
end
end
end
...@@ -181,7 +181,6 @@ eos ...@@ -181,7 +181,6 @@ eos
it { is_expected.to respond_to(:parents) } it { is_expected.to respond_to(:parents) }
it { is_expected.to respond_to(:date) } it { is_expected.to respond_to(:date) }
it { is_expected.to respond_to(:diffs) } it { is_expected.to respond_to(:diffs) }
it { is_expected.to respond_to(:tree) }
it { is_expected.to respond_to(:id) } it { is_expected.to respond_to(:id) }
it { is_expected.to respond_to(:to_patch) } it { is_expected.to respond_to(:to_patch) }
end end
......
...@@ -3489,9 +3489,51 @@ describe Project do ...@@ -3489,9 +3489,51 @@ describe Project do
expect(project).to receive(:import_finish) expect(project).to receive(:import_finish)
expect(project).to receive(:update_project_counter_caches) expect(project).to receive(:update_project_counter_caches)
expect(project).to receive(:remove_import_jid) expect(project).to receive(:remove_import_jid)
expect(project).to receive(:after_create_default_branch)
project.after_import project.after_import
end end
context 'branch protection' do
let(:project) { create(:project, :repository) }
it 'does not protect when branch protection is disabled' do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
project.after_import
expect(project.protected_branches).to be_empty
end
it "gives developer access to push when branch protection is set to 'developers can push'" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
project.after_import
expect(project.protected_branches).not_to be_empty
expect(project.default_branch).to eq(project.protected_branches.first.name)
expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it "gives developer access to merge when branch protection is set to 'developers can merge'" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
project.after_import
expect(project.protected_branches).not_to be_empty
expect(project.default_branch).to eq(project.protected_branches.first.name)
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it 'protects default branch' do
project.after_import
expect(project.protected_branches).not_to be_empty
expect(project.default_branch).to eq(project.protected_branches.first.name)
expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
end
end end
describe '#update_project_counter_caches' do describe '#update_project_counter_caches' do
......
...@@ -178,6 +178,30 @@ describe ProjectTeam do ...@@ -178,6 +178,30 @@ describe ProjectTeam do
end end
end end
describe '#add_users' do
let(:user1) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project) }
it 'add the given users to the team' do
project.team.add_users([user1, user2], :reporter)
expect(project.team.reporter?(user1)).to be(true)
expect(project.team.reporter?(user2)).to be(true)
end
end
describe '#add_user' do
let(:user) { create(:user) }
let(:project) { create(:project) }
it 'add the given user to the team' do
project.team.add_user(user, :reporter)
expect(project.team.reporter?(user)).to be(true)
end
end
describe "#human_max_access" do describe "#human_max_access" do
it 'returns Master role' do it 'returns Master role' do
user = create(:user) user = create(:user)
......
...@@ -193,7 +193,7 @@ describe API::Internal do ...@@ -193,7 +193,7 @@ describe API::Internal do
end end
describe "GET /internal/authorized_keys" do describe "GET /internal/authorized_keys" do
context "unsing an existing key's fingerprint" do context "using an existing key's fingerprint" do
it "finds the key" do it "finds the key" do
get(api('/internal/authorized_keys'), fingerprint: key.fingerprint, secret_token: secret_token) get(api('/internal/authorized_keys'), fingerprint: key.fingerprint, secret_token: secret_token)
......
...@@ -22,6 +22,7 @@ describe GroupChildEntity do ...@@ -22,6 +22,7 @@ describe GroupChildEntity do
avatar_url avatar_url
name name
description description
markdown_description
visibility visibility
type type
can_edit can_edit
...@@ -60,9 +61,10 @@ describe GroupChildEntity do ...@@ -60,9 +61,10 @@ describe GroupChildEntity do
end end
describe 'for a group', :nested_groups do describe 'for a group', :nested_groups do
let(:description) { 'Awesomeness' }
let(:object) do let(:object) do
create(:group, :nested, :with_avatar, create(:group, :nested, :with_avatar,
description: 'Awesomeness') description: description)
end end
before do before do
...@@ -96,6 +98,14 @@ describe GroupChildEntity do ...@@ -96,6 +98,14 @@ describe GroupChildEntity do
expect(json[:edit_path]).to eq(edit_group_path(object)) expect(json[:edit_path]).to eq(edit_group_path(object))
end end
context 'emoji in description' do
let(:description) { ':smile:' }
it 'has the correct markdown_description' do
expect(json[:markdown_description]).to eq('<p dir="auto"><gl-emoji title="smiling face with open mouth and smiling eyes" data-name="smile" data-unicode-version="6.0">😄</gl-emoji></p>')
end
end
it_behaves_like 'group child json' it_behaves_like 'group child json'
end end
end end
...@@ -86,12 +86,20 @@ describe Groups::DestroyService do ...@@ -86,12 +86,20 @@ describe Groups::DestroyService do
context 'potential race conditions' do context 'potential race conditions' do
context "when the `GroupDestroyWorker` task runs immediately" do context "when the `GroupDestroyWorker` task runs immediately" do
around do |example| around do |example|
old_strategy = DatabaseCleaner[:active_record, { connection: ActiveRecord::Base }].strategy connections = [ActiveRecord::Base, Geo::BaseRegistry]
DatabaseCleaner[:active_record, { connection: ActiveRecord::Base }].strategy = :deletion old_connections = connections.each_with_object({}) do |connection, memo|
memo[connection] = DatabaseCleaner[:active_record, { connection: connection }].strategy
DatabaseCleaner[:active_record, { connection: connection }].strategy = :deletion
memo
end
begin begin
example.run example.run
ensure ensure
DatabaseCleaner[:active_record, { connection: ActiveRecord::Base }].strategy = old_strategy old_connections.each do |connection, old_strategy|
DatabaseCleaner[:active_record, { connection: connection }].strategy = old_strategy
end
end end
end end
......
...@@ -19,5 +19,21 @@ describe ProtectedBranches::CreateService do ...@@ -19,5 +19,21 @@ describe ProtectedBranches::CreateService do
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end end
context 'when user does not have permission' do
let(:user) { create(:user) }
before do
project.add_developer(user)
end
it 'creates a new protected branch if we skip authorization step' do
expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1)
end
it 'raises Gitlab::Access:AccessDeniedError' do
expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end end
end end
...@@ -32,6 +32,7 @@ describe RepositoryImportWorker do ...@@ -32,6 +32,7 @@ describe RepositoryImportWorker do
expect_any_instance_of(Projects::ImportService).to receive(:execute) expect_any_instance_of(Projects::ImportService).to receive(:execute)
.and_return({ status: :ok }) .and_return({ status: :ok })
expect_any_instance_of(Project).to receive(:after_import).and_call_original
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
expect_any_instance_of(Project).to receive(:import_finish) expect_any_instance_of(Project).to receive(:import_finish)
......
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