Commit 2e98683c authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into ee-dispatcher-dashboard-issues

parents 94aacb23 1b16ffac
......@@ -351,7 +351,7 @@ group :development, :test do
gem 'rubocop', '~> 0.52.0'
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 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false
......
......@@ -724,6 +724,9 @@ GEM
rake
raindrops (0.18.0)
rake (12.3.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2)
......@@ -838,7 +841,11 @@ GEM
safe_yaml (1.0.4)
sanitize (2.1.0)
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)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
......@@ -848,9 +855,9 @@ GEM
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
scss_lint (0.54.0)
scss_lint (0.56.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
sass (~> 3.5.3)
securecompare (1.0.0)
seed-fu (2.3.6)
activerecord (>= 3.1)
......@@ -1200,7 +1207,7 @@ DEPENDENCIES
rugged (~> 0.26.0)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0)
scss_lint (~> 0.56.0)
seed-fu (= 2.3.6)
select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5)
......
......@@ -57,12 +57,12 @@ class GfmAutoComplete {
displayTpl(value) {
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
// 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) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>';
}
if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>';
tpl += ' <small class="params"><%- params.join(" ") %></small>';
}
if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>';
......
......@@ -141,7 +141,8 @@ export default {
<div
v-if="group.description"
class="description">
{{group.description}}
<span v-html="group.description">
</span>
</div>
</div>
<group-folder
......
......@@ -71,7 +71,7 @@ export default class GroupsStore {
id: rawGroupItem.id,
name: rawGroupItem.name,
fullName: rawGroupItem.full_name,
description: rawGroupItem.description,
description: rawGroupItem.markdown_description,
visibility: rawGroupItem.visibility,
avatarUrl: rawGroupItem.avatar_url,
relativePath: rawGroupItem.relative_path,
......
......@@ -69,8 +69,8 @@
currentFlagPosition: 0,
showFlag: false,
showFlagContent: false,
showDeployInfo: true,
timeSeries: [],
realPixelRatio: 1,
};
},
......@@ -87,10 +87,7 @@
},
innerViewBox() {
if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
}
return '0 0 0 0';
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
},
axisTransform() {
......@@ -102,6 +99,10 @@
paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
};
},
deploymentFlagData() {
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
},
},
methods: {
......@@ -122,6 +123,10 @@
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight;
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.formatDeployments();
},
......@@ -261,6 +266,11 @@
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
......@@ -269,24 +279,21 @@
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<graph-flag
v-if="showFlag"
:current-x-coordinate="currentXCoordinate"
:current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
/>
</svg>
</svg>
<graph-flag
:real-pixel-ratio="realPixelRatio"
:current-x-coordinate="currentXCoordinate"
:current-data="currentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
/>
</div>
</div>
</template>
<script>
import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
showDeployInfo: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
......@@ -20,14 +13,6 @@
type: Number,
required: true,
},
graphWidth: {
type: Number,
required: true,
},
},
components: {
Icon,
},
computed: {
......@@ -37,52 +22,17 @@
},
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) {
return `translate(${Math.floor(deployment.xPos) + 1}, 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;
return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
},
};
</script>
<template>
<g
class="deploy-info"
v-if="showDeployInfo">
<g class="deploy-info">
<g
v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
<rect
x="0"
......@@ -99,81 +49,6 @@
:y2="calculatedHeight"
stroke="#000">
</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>
<svg
height="0"
......
<script>
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 {
props: {
......@@ -7,14 +9,15 @@
type: Number,
required: true,
},
currentFlagPosition: {
type: Number,
required: true,
},
currentData: {
type: Object,
required: true,
},
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: {
type: Number,
required: true,
......@@ -23,71 +26,173 @@
type: Number,
required: true,
},
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: {
type: Boolean,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
},
data() {
return {
circleColorRgb: '#8fbce8',
};
components: {
Icon,
},
computed: {
formatTime() {
return timeFormat(this.currentData.time);
return this.deploymentFlagData ?
timeFormat(this.deploymentFlagData.time) :
timeFormat(this.currentData.time);
},
formatDate() {
return dateFormat(this.currentData.time);
return this.deploymentFlagData ?
dateFormat(this.deploymentFlagData.time) :
dateFormat(this.currentData.time);
},
cursorStyle() {
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}`;
},
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
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>
<template>
<g class="mouse-over-flag">
<line
class="selected-metric-line"
:x1="currentXCoordinate"
:y1="0"
:x2="currentXCoordinate"
:y2="calculatedHeight"
transform="translate(-5, 20)">
</line>
<svg
<div
class="prometheus-graph-cursor"
:style="cursorStyle"
>
<div
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>
</g>
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
:stroke="series.lineColor"
:stroke-dasharray="strokeDashArray(series.lineStyle)"
stroke-width="4"
x1="0"
x2="15"
y1="2"
y2="2">
</line>
</svg>
</td>
<td>{{seriesMetricLabel(index, series)}}</td>
<td>
<strong>{{seriesMetricValue(series)}}</strong>
</td>
</tr>
</table>
</div>
</div>
</div>
</template>
......@@ -29,15 +29,18 @@ const mixins = {
time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) {
const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag,
tagUrl: `${this.tagsPath}/${deployment.tag}`,
tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
ref: deployment.ref.name,
xPos,
seriesIndex,
showDeploymentFlag: false,
});
}
......
......@@ -14,7 +14,7 @@ const d3 = {
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 dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
......
......@@ -192,6 +192,17 @@
overflow-y: auto;
overflow-x: hidden;
.name,
small.aliases,
small.params {
float: left;
}
small.aliases,
small.params {
padding: 2px 5px;
}
small.description {
float: right;
padding: 3px 5px;
......@@ -209,6 +220,7 @@
}
ul > li {
@include clearfix;
white-space: nowrap;
}
......
......@@ -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 {
position: relative;
height: 0;
......
......@@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController
def require_pages_enabled!
not_found unless @project.pages_available?
end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
end
......@@ -4,6 +4,7 @@ class Projects::BoardsController < Projects::ApplicationController
include BoardsResponses
include IssuableCollections
before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
......
......@@ -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)
end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
def render_issue_json
if @issue.valid?
render json: serializer.represent(@issue)
......
......@@ -371,7 +371,7 @@ class Commit
#
# Returns a symbol
def uri_type(path)
entry = @raw.tree.path(path)
entry = @raw.rugged_tree_entry(path)
if entry[:type] == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob
......
......@@ -1461,6 +1461,7 @@ class Project < ActiveRecord::Base
import_finish
remove_import_jid
update_project_counter_caches
after_create_default_branch
end
def update_project_counter_caches
......@@ -1474,6 +1475,27 @@ class Project < ActiveRecord::Base
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
return unless import_jid
......
class ProjectTeam
include BulkMemberAccessLoad
prepend EE::ProjectTeam
attr_accessor :project
def initialize(project)
......@@ -40,8 +42,6 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
return false if group_member_lock
ProjectMember.add_users(
project,
users,
......@@ -173,12 +173,4 @@ class ProjectTeam
def group
project.group
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
class GroupChildEntity < Grape::Entity
include ActionView::Helpers::NumberHelper
include RequestAwareEntity
include MarkupHelper
expose :id, :name, :description, :visibility, :full_name,
:created_at, :updated_at, :avatar_url
......@@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity
number_with_delimiter(instance.member_count)
end
expose :markdown_description do |instance|
markdown_description
end
private
def membership
......@@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity
def type
object.class.name.downcase
end
def markdown_description
markdown_field(object, :description)
end
end
......@@ -168,24 +168,7 @@ class GitPushService < BaseService
offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max
@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.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
@project.after_create_default_branch
end
def build_push_data
......
......@@ -2,8 +2,8 @@ module ProtectedBranches
class CreateService < BaseService
attr_reader :protected_branch
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
def execute(skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
project.protected_branches.create(params)
end
......
......@@ -315,9 +315,10 @@
Charts
-# Shortcut to Issues > New Issue
%li.hidden
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
Create a new issue
- if project_nav_tab?(:issues)
%li.hidden
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
Create a new issue
-# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds
......@@ -332,5 +333,6 @@
Commits
-# Shortcut to issue boards
%li.hidden
= link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
- if project_nav_tab?(:issues)
%li.hidden
= link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
---
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
# Dynamic Application Security Testing (SAST)
# Dynamic Application Security Testing (DAST)
> [Introduced][ee-4348] in [GitLab Enterprise Edition Ultimate][ee] 10.4.
......
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
end
step 'I click test hook button' do
WebMock.enable!
stub_request(:post, @hook.url).to_return(status: 200)
click_link 'Test'
WebMock.disable!
end
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
source = find_source(source_type, params[:id])
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])
conflict!('Member already exists') if member
......
......@@ -173,8 +173,8 @@ module Gitlab
end
def find_by_rugged(repository, sha, path, limit:)
commit = repository.lookup(sha)
root_tree = commit.tree
rugged_commit = repository.lookup(sha)
root_tree = rugged_commit.tree
blob_entry = find_entry_by_path(repository, root_tree.oid, path)
......
......@@ -15,8 +15,6 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
delegate :tree, to: :rugged_commit
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
......@@ -452,6 +450,11 @@ module Gitlab
)
end
# Is this the same as Blob.find_entry_by_path ?
def rugged_tree_entry(path)
rugged_commit.tree.path(path)
end
private
def init_from_hash(hash)
......
......@@ -1163,23 +1163,13 @@ module Gitlab
end
def fetch_repository_as_mirror(repository)
remote_name = "tmp-#{SecureRandom.hex}"
# 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|
gitaly_migrate(:remote_fetch_internal_remote) do |is_enabled|
if is_enabled
repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository)
[GITALY_INTERNAL_URL, repository.fetch_env]
gitaly_remote_client.fetch_internal_remote(repository)
else
[repository.path, nil]
rugged_fetch_repository_as_mirror(repository)
end
end
add_remote(remote_name, url, mirror_refmap: :all_refs)
fetch_remote(remote_name, env: env)
ensure
remove_remote(remote_name)
end
def blob_at(sha, path)
......@@ -2064,6 +2054,16 @@ module Gitlab
false
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)
run_git(['fetch', remote_name], env: env).last.zero?
end
......
......@@ -23,6 +23,19 @@ module Gitlab
response.result
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
......@@ -10,6 +10,7 @@ module QA
autoload :Namespace, 'qa/runtime/namespace'
autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
end
##
......
......@@ -38,22 +38,49 @@ module QA
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => {
'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
# This enables access to logs with `page.driver.manage.get_log(:browser)`
loggingPrefs: {
browser: "ALL",
client: "ALL",
driver: "ALL",
server: "ALL"
}
)
Capybara::Selenium::Driver
.new(app, browser: :chrome, desired_capabilities: capabilities)
end
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument("window-size=1240,1680")
Capybara::Screenshot.register_driver(:chrome) do |driver, path|
driver.browser.save_screenshot(path)
# 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
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
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
# Keep only the screenshots generated from the last failing test suite
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|
config.default_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
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)
get :index, namespace_id: project.namespace,
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
set(:primary) { create(:geo_node, :primary) }
set(:secondary) { create(:geo_node) }
before do
TestEnv.clean_test_path
end
it 'creates a Geo::HashedStorageMigratedEvent on success' do
expect { service.execute }.to change(Geo::EventLog, :count).by(1)
......
require 'rails_helper'
describe 'Issue Boards shortcut', :js do
let(:project) { create(:project) }
context 'issues are enabled' do
let(:project) { create(:project) }
before do
create(:board, project: project)
before do
create(:board, project: project)
sign_in(create(:admin))
sign_in(create(:admin))
visit project_path(project)
visit project_path(project)
end
it 'takes user to issue board index' do
find('body').native.send_keys('gb')
expect(page).to have_selector('.boards-list')
wait_for_requests
end
end
it 'takes user to issue board index' do
find('body').native.send_keys('gb')
expect(page).to have_selector('.boards-list')
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')
wait_for_requests
expect(page).to have_selector("body[data-page='projects:show']")
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
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
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) => {
};
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', () => {
it('refText shows the ref when a tag is available', () => {
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-', () => {
it('should contain a hidden gradient', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
showDeployInfo: true,
deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.nameDeploymentClass(reducedDeploymentData[0]),
).toContain('deploy-info');
expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
});
it('transformDeploymentGroup translates an available deployment', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.transformDeploymentGroup(reducedDeploymentData[0]),
component.transformDeploymentGroup({ xPos: 16 }),
).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', () => {
it('calculatedHeight', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
......
import Vue from 'vue';
import GraphFlag from '~/monitoring/components/graph/flag.vue';
import { deploymentData } from '../mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(GraphFlag);
......@@ -9,11 +10,6 @@ const createComponent = (propsData) => {
}).$mount();
};
function getCoordinate(component, selector, coordinate) {
const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate);
return parseInt(coordinateVal, 10);
}
const defaultValuesComponent = {
currentXCoordinate: 200,
currentYCoordinate: 100,
......@@ -25,31 +21,111 @@ const defaultValuesComponent = {
graphHeight: 300,
graphHeightOffset: 120,
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', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
const component = createComponent(defaultValuesComponent);
let component;
expect(getCoordinate(component, '.selected-metric-line', 'x1'))
.toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.selected-metric-line', 'x2'))
.toEqual(component.currentXCoordinate);
it('has a line at the currentXCoordinate', () => {
component = createComponent(defaultValuesComponent);
expect(component.$el.style.left)
.toEqual(`${70 + component.currentXCoordinate}px`);
});
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
const component = createComponent(defaultValuesComponent);
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(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).toContainText('1.0');
});
it('does not contain the ref when a tag is unavailable', () => {
const deploymentFlagComponent = createComponent({
...defaultValuesComponent,
deploymentFlagData: {
...deploymentFlagData,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
tag: false,
ref: '1.0',
},
});
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).toContainText('f5bcd1d9');
const svg = component.$el.querySelector('.rect-text-metric');
expect(svg.tagName).toEqual('svg');
expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition);
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).not.toContainText('1.0');
});
});
describe('Computed props', () => {
it('calculatedHeight', () => {
const component = createComponent(defaultValuesComponent);
beforeEach(() => {
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');
});
});
});
......@@ -146,7 +146,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
context 'when sha references a tree' 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)
......@@ -230,7 +230,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
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
Gitlab::Git::Blob.find(
......
......@@ -55,7 +55,6 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(@commit.parents).to eq(@gitlab_parents) }
it { expect(@commit.parent_id).to eq(@parents.first.oid) }
it { expect(@commit.no_commit_message).to eq("--no commit message") }
it { expect(@commit.tree).to eq(@tree) }
after do
# Erase the new commit so other tests get the original repo
......
......@@ -649,29 +649,39 @@ describe Gitlab::Git::Repository, seed_helper: true do
Gitlab::Shell.new.remove_repository(storage_path, 'my_project')
end
it 'fetches a repository as a mirror remote' do
subject
shared_examples 'repository mirror fecthing' do
it 'fetches a repository as a mirror remote' do
subject
expect(refs(new_repository.path)).to eq(refs(repository.path))
end
expect(refs(new_repository.path)).to eq(refs(repository.path))
end
context 'with keep-around refs' do
let(:sha) { SeedRepo::Commit::ID }
let(:keep_around_ref) { "refs/keep-around/#{sha}" }
let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
context 'with keep-around refs' do
let(:sha) { SeedRepo::Commit::ID }
let(:keep_around_ref) { "refs/keep-around/#{sha}" }
let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
before do
repository.rugged.references.create(keep_around_ref, sha, force: true)
repository.rugged.references.create(tmp_ref, sha, force: true)
end
before do
repository.rugged.references.create(keep_around_ref, sha, force: true)
repository.rugged.references.create(tmp_ref, sha, force: true)
end
it 'includes the temporary and keep-around refs' do
subject
it 'includes the temporary and keep-around refs' do
subject
expect(refs(new_repository.path)).to include(keep_around_ref)
expect(refs(new_repository.path)).to include(tmp_ref)
expect(refs(new_repository.path)).to include(keep_around_ref)
expect(refs(new_repository.path)).to include(tmp_ref)
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
......
......@@ -31,4 +31,17 @@ describe Gitlab::GitalyClient::RemoteService do
expect(client.remove_remote(remote_name)).to be(true)
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
......@@ -181,7 +181,6 @@ eos
it { is_expected.to respond_to(:parents) }
it { is_expected.to respond_to(:date) }
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(:to_patch) }
end
......
......@@ -3489,9 +3489,51 @@ describe Project do
expect(project).to receive(:import_finish)
expect(project).to receive(:update_project_counter_caches)
expect(project).to receive(:remove_import_jid)
expect(project).to receive(:after_create_default_branch)
project.after_import
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
describe '#update_project_counter_caches' do
......
......@@ -178,6 +178,30 @@ describe ProjectTeam do
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
it 'returns Master role' do
user = create(:user)
......
......@@ -22,6 +22,7 @@ describe GroupChildEntity do
avatar_url
name
description
markdown_description
visibility
type
can_edit
......@@ -60,9 +61,10 @@ describe GroupChildEntity do
end
describe 'for a group', :nested_groups do
let(:description) { 'Awesomeness' }
let(:object) do
create(:group, :nested, :with_avatar,
description: 'Awesomeness')
description: description)
end
before do
......@@ -96,6 +98,14 @@ describe GroupChildEntity do
expect(json[:edit_path]).to eq(edit_group_path(object))
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'
end
end
......@@ -86,12 +86,20 @@ describe Groups::DestroyService do
context 'potential race conditions' do
context "when the `GroupDestroyWorker` task runs immediately" do
around do |example|
old_strategy = DatabaseCleaner[:active_record, { connection: ActiveRecord::Base }].strategy
DatabaseCleaner[:active_record, { connection: ActiveRecord::Base }].strategy = :deletion
connections = [ActiveRecord::Base, Geo::BaseRegistry]
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
example.run
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
......
......@@ -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.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
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
......@@ -32,6 +32,7 @@ describe RepositoryImportWorker do
expect_any_instance_of(Projects::ImportService).to receive(:execute)
.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(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