Commit 042330c7 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-12-06

# Conflicts:
#	Gemfile.lock
#	app/assets/javascripts/dispatcher.js
#	app/assets/stylesheets/framework/dropdowns.scss
#	app/assets/stylesheets/pages/issues.scss
#	app/assets/stylesheets/pages/projects.scss
#	app/helpers/appearances_helper.rb
#	app/models/ci/build.rb
#	app/services/boards/issues/list_service.rb
#	app/uploaders/job_artifact_uploader.rb
#	app/uploaders/legacy_artifact_uploader.rb
#	app/workers/post_receive.rb
#	app/workers/project_cache_worker.rb
#	doc/user/permissions.md
#	doc/user/project/merge_requests/index.md
#	lib/gitlab/sidekiq_config.rb
#	locale/gitlab.pot
#	spec/factories/appearances.rb
#	spec/factories/ci/job_artifacts.rb
#	spec/lib/gitlab/email/handler_spec.rb
#	spec/lib/gitlab/sidekiq_config_spec.rb
#	spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
#	spec/models/ci/build_spec.rb
#	spec/models/project_spec.rb
#	spec/serializers/pipeline_serializer_spec.rb
#	spec/uploaders/job_artifact_uploader_spec.rb
#	spec/uploaders/legacy_artifact_uploader_spec.rb

[ci skip]
parents 309a5f15 68360783
......@@ -416,7 +416,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.58.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.59.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -300,7 +300,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.58.0)
gitaly-proto (0.59.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -382,8 +382,11 @@ GEM
grpc (1.4.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
<<<<<<< HEAD
gssapi (1.2.0)
ffi (>= 1.0.1)
=======
>>>>>>> upstream/master
haml (4.0.7)
tilt
haml_lint (0.26.0)
......@@ -1069,7 +1072,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.58.0)
gitaly-proto (~> 0.59.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
......@@ -150,8 +150,8 @@ export default class Clusters {
}
toggle() {
this.toggleButton.classList.toggle('checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
this.toggleButton.classList.toggle('is-checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
}
showToken() {
......
import Flash from '../flash';
import { s__ } from '../locale';
import ClustersService from './services/clusters_service';
/**
* Toggles loading and disabled classes.
* @param {HTMLElement} button
*/
const toggleLoadingButton = (button) => {
if (button.getAttribute('disabled')) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', true);
}
button.classList.toggle('is-loading');
};
/**
* Toggles checked class for the given button
* @param {HTMLElement} button
*/
const toggleValue = (button) => {
button.classList.toggle('is-checked');
};
/**
* Handles toggle buttons in the cluster's table.
*
* When the user clicks the toggle button for each cluster, it:
* - toggles the button
* - shows a loading and disables button
* - Makes a put request to the given endpoint
* Once we receive the response, either:
* 1) Show updated status in case of successfull response
* 2) Show initial status in case of failed response
*/
export default function setClusterTableToggles() {
document.querySelectorAll('.js-toggle-cluster-list')
.forEach(button => button.addEventListener('click', (e) => {
const toggleButton = e.currentTarget;
const endpoint = toggleButton.getAttribute('data-endpoint');
toggleValue(toggleButton);
toggleLoadingButton(toggleButton);
const value = toggleButton.classList.contains('is-checked');
ClustersService.updateCluster(endpoint, { cluster: { enabled: value } })
.then(() => {
toggleLoadingButton(toggleButton);
})
.catch(() => {
toggleLoadingButton(toggleButton);
toggleValue(toggleButton);
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
});
}));
}
......@@ -17,4 +17,8 @@ export default class ClusterService {
installApplication(appId) {
return axios.post(this.appInstallEndpointMap[appId]);
}
static updateCluster(endpoint, data) {
return axios.put(endpoint, data);
}
}
......@@ -31,9 +31,12 @@ import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
/* global Sidebar */
<<<<<<< HEAD
/* global WeightSelect */
/* global AdminEmailSelect */
=======
>>>>>>> upstream/master
import IssuableTemplateSelectors from './templates/issuable_template_selectors';
import Flash from './flash';
import CommitsList from './commits';
......@@ -294,7 +297,10 @@ import initGroupAnalytics from './init_group_analytics';
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
<<<<<<< HEAD
new WeightSelect();
=======
>>>>>>> upstream/master
new IssuableTemplateSelectors();
break;
case 'projects:merge_requests:creations:new':
......@@ -330,18 +336,21 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'projects:snippets:show':
initNotes();
new ZenMode();
break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
new GLForm($('.snippet-form'), true);
new ZenMode();
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
new GLForm($('.snippet-form'), false);
new ZenMode();
break;
case 'projects:releases:edit':
new ZenMode();
......@@ -609,6 +618,7 @@ import initGroupAnalytics from './init_group_analytics';
new LineHighlighter();
new BlobViewer();
initNotes();
new ZenMode();
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
......@@ -621,7 +631,15 @@ import initGroupAnalytics from './init_group_analytics';
import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch((err) => {
Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
Flash(s__('ClusterIntegration|Problem setting up the cluster'));
throw err;
});
break;
case 'projects:clusters:index':
import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index')
.then(clusterIndex => clusterIndex.default())
.catch((err) => {
Flash(s__('ClusterIntegration|Problem setting up the clusters list'));
throw err;
});
break;
......
......@@ -28,7 +28,7 @@ export default class IssuableIndex {
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success(response) {
$('#issue_email').val(response.new_issue_address).focus();
$('#issuable_email').val(response.new_address).focus();
},
beforeSend() {
$('.incoming-email-token-reset').text('resetting...');
......
......@@ -129,7 +129,7 @@ import { addDelimiter } from './lib/utils/text_utility';
};
MergeRequest.prototype.hideCloseButton = function() {
const el = document.querySelector('.merge-request .issuable-actions');
const el = document.querySelector('.merge-request .js-issuable-actions');
const closeDropdownItem = el.querySelector('li.close-item');
if (closeDropdownItem) {
closeDropdownItem.classList.add('hidden');
......
<script>
import projectFeatureToggle from './project_feature_toggle.vue';
import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default {
props: {
......
<script>
export default {
props: {
name: {
type: String,
required: false,
default: '',
},
value: {
type: Boolean,
required: true,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
},
model: {
prop: 'value',
event: 'change',
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
},
};
</script>
<template>
<label class="toggle-wrapper">
<input
v-if="name"
type="hidden"
:name="name"
:value="value"
/>
<button
type="button"
aria-label="Toggle"
class="project-feature-toggle"
data-enabled-text="Enabled"
data-disabled-text="Disabled"
:class="{ checked: value, disabled: disabledInput }"
@click="toggleFeature"
/>
</label>
</template>
<script>
import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from './project_feature_toggle.vue';
import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external';
......
<script>
import loadingIcon from './loading_icon.vue';
export default {
props: {
name: {
type: String,
required: false,
default: '',
},
value: {
type: Boolean,
required: true,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
enabledText: {
type: String,
required: false,
default: 'Enabled',
},
disabledText: {
type: String,
required: false,
default: 'Disabled',
},
},
components: {
loadingIcon,
},
model: {
prop: 'value',
event: 'change',
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
},
};
</script>
<template>
<label class="toggle-wrapper">
<input
type="hidden"
:name="name"
:value="value"
/>
<button
type="button"
aria-label="Toggle"
class="project-feature-toggle"
:data-enabled-text="enabledText"
:data-disabled-text="disabledText"
:class="{
'is-checked': value,
'is-disabled': disabledInput,
'is-loading': isLoading
}"
@click="toggleFeature"
>
<loadingIcon class="loading-icon" />
</button>
</label>
</template>
......@@ -43,6 +43,7 @@
@import "framework/tabs";
@import "framework/timeline";
@import "framework/tooltips";
@import "framework/toggle";
@import "framework/typography";
@import "framework/zen";
@import "framework/blank";
......
......@@ -1031,6 +1031,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
}
<<<<<<< HEAD
.new-epic-dropdown {
.dropdown-menu {
padding-left: $gl-padding-top;
......@@ -1060,6 +1061,8 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
}
=======
>>>>>>> upstream/master
.dropdown-content-faded-mask {
position: relative;
......
/**
* Toggle button
*
* @usage
* ### Active and Inactive text should be provided as data attributes:
* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* </button>
* ### Checked should have `is-checked` class
* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* </button>
* ### Disabled should have `is-disabled` class
* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* </button>
* ### Loading should have `is-loading` and an icon with `loading-icon` class
* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon"></i>
* </button>
*/
.project-feature-toggle {
position: relative;
border: 0;
outline: 0;
display: block;
width: 100px;
height: 24px;
cursor: pointer;
user-select: none;
background: $feature-toggle-color-disabled;
border-radius: 12px;
padding: 3px;
transition: all .4s ease;
&::selection,
&::before::selection,
&::after::selection {
background: none;
}
&::before {
color: $feature-toggle-text-color;
font-size: 12px;
line-height: 24px;
position: absolute;
top: 0;
left: 25px;
right: 5px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
animation: animate-disabled .2s ease-in;
content: attr(data-disabled-text);
}
&::after {
position: relative;
display: block;
content: "";
width: 22px;
height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
}
.loading-icon {
display: none;
font-size: 12px;
color: $white-light;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&.is-loading {
&::before {
display: none;
}
.loading-icon {
display: block;
&::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
&.is-checked {
background: $feature-toggle-color-enabled;
&::before {
left: 5px;
right: 25px;
animation: animate-enabled .2s ease-in;
content: attr(data-enabled-text);
}
&::after {
left: calc(100% - 22px);
}
}
&.is-disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: $screen-xs-min) {
width: 50px;
&::before,
&.is-checked::before {
display: none;
}
}
@keyframes animate-enabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes animate-disabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
}
......@@ -14,3 +14,17 @@
}
@include new-style-dropdown('.clusters-dropdown ');
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
}
.empty-state .svg-content img {
width: 145px;
}
.top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0;
}
}
......@@ -13,6 +13,41 @@
.author_link {
white-space: nowrap;
}
@media (max-width: $screen-xs-max) {
display: block;
}
}
.detail-page-header-body {
position: relative;
line-height: 35px;
display: flex;
flex-grow: 1;
@media (min-width: $screen-sm-min) {
padding-left: 0;
padding-right: 0;
}
}
.detail-page-header-actions {
@include new-style-dropdown;
align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
@media (max-width: $screen-xs-max) {
width: 100%;
margin-top: 10px;
> .issue-btn-group {
> .btn {
width: 100%;
}
}
}
}
.detail-page-description {
......
......@@ -624,50 +624,16 @@
margin-top: 0;
height: auto;
align-self: center;
@media (max-width: $screen-xs-max) {
position: absolute;
top: 0;
left: 0;
}
}
.issuable-header {
position: relative;
padding-left: 45px;
padding-right: 45px;
line-height: 35px;
display: flex;
flex-grow: 1;
@media (min-width: $screen-sm-min) {
float: left;
padding-left: 0;
padding-right: 0;
}
}
.issuable-actions {
@include new-style-dropdown;
align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
@media (min-width: $screen-sm-min) {
float: right;
}
}
.issuable-gutter-toggle {
@media (max-width: $screen-sm-max) {
position: absolute;
top: 0;
right: 0;
margin-left: $btn-side-margin;
}
}
.issuable-meta {
flex: 1;
display: inline-block;
font-size: 14px;
line-height: 24px;
......
......@@ -134,26 +134,11 @@ ul.related-merge-requests > li {
}
@media (max-width: $screen-xs-max) {
.detail-page-header,
.issuable-header {
display: block;
.detail-page-header {
.issuable-meta {
line-height: 18px;
}
}
.issuable-actions {
margin-top: 10px;
.issue-btn-group {
width: 100%;
.btn {
width: 100%;
}
}
}
}
.issue-form {
......@@ -164,6 +149,7 @@ ul.related-merge-requests > li {
}
}
<<<<<<< HEAD
.issues-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
......@@ -185,6 +171,9 @@ ul.related-merge-requests > li {
}
.issue-email-modal-btn {
=======
.issuable-email-modal-btn {
>>>>>>> upstream/master
padding: 0;
color: $gl-link-color;
background-color: transparent;
......
......@@ -134,93 +134,6 @@
}
}
.project-feature-toggle {
position: relative;
border: 0;
outline: 0;
display: block;
width: 100px;
height: 24px;
cursor: pointer;
user-select: none;
background: $feature-toggle-color-disabled;
border-radius: 12px;
padding: 3px;
transition: all .4s ease;
&::selection,
&::before::selection,
&::after::selection {
background: none;
}
&::before {
color: $feature-toggle-text-color;
font-size: 12px;
line-height: 24px;
position: absolute;
top: 0;
left: 25px;
right: 5px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
animation: animate-disabled .2s ease-in;
content: attr(data-disabled-text);
}
&::after {
position: relative;
display: block;
content: "";
width: 22px;
height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
}
&.checked {
background: $feature-toggle-color-enabled;
&::before {
left: 5px;
right: 25px;
animation: animate-enabled .2s ease-in;
content: attr(data-enabled-text);
}
&::after {
left: calc(100% - 22px);
}
}
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: $screen-xs-min) {
width: 50px;
&::before,
&.checked::before {
display: none;
}
}
@keyframes animate-enabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes animate-disabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
}
.project-home-panel,
.group-home-panel {
padding-top: 24px;
......@@ -1269,6 +1182,7 @@ a.allowed-to-push {
}
}
<<<<<<< HEAD
/* EE-specific styles */
.project-mirror-settings {
.fingerprint-verified {
......@@ -1340,4 +1254,9 @@ a.allowed-to-push {
display: flex;
justify-content: center;
}
=======
.issuable-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
>>>>>>> upstream/master
}
......@@ -8,11 +8,11 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000
def index
if project.cluster
redirect_to project_cluster_path(project, project.cluster)
else
redirect_to new_project_cluster_path(project)
end
@scope = params[:scope] || 'all'
@clusters = ClustersFinder.new(project, current_user, @scope).execute.page(params[:page])
@active_count = ClustersFinder.new(project, current_user, :active).execute.count
@inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count
@all_count = @active_count + @inactive_count
end
def new
......@@ -39,10 +39,20 @@ class Projects::ClustersController < Projects::ApplicationController
.execute(cluster)
if cluster.valid?
flash[:notice] = "Cluster was successfully updated."
redirect_to project_cluster_path(project, project.cluster)
respond_to do |format|
format.json do
head :no_content
end
format.html do
flash[:notice] = "Cluster was successfully updated."
redirect_to project_cluster_path(project, cluster)
end
end
else
render :show
respond_to do |format|
format.json { head :bad_request }
format.html { render :show }
end
end
end
......@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user)
end
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:provider_type,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
])
end
def update_params
if cluster.managed?
params.require(:cluster).permit(
......
......@@ -292,15 +292,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
if params[:merge_when_pipeline_succeeds].present?
return :failed unless @merge_request.head_pipeline
return :failed unless @merge_request.actual_head_pipeline
if @merge_request.head_pipeline.active?
if @merge_request.actual_head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params)
.execute(@merge_request)
:merge_when_pipeline_succeeds
elsif @merge_request.head_pipeline.success?
elsif @merge_request.actual_head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
@merge_request.merge_async(current_user.id, params)
......
......@@ -136,11 +136,11 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), status: 302, alert: ex.message
end
def new_issue_address
def new_issuable_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
current_user.reset_incoming_email_token!
render json: { new_issue_address: @project.new_issue_address(current_user) }
render json: { new_address: @project.new_issuable_address(current_user, params[:issuable_type]) }
end
def archive
......
class ClustersFinder
def initialize(project, user, scope)
@project = project
@user = user
@scope = scope || :active
end
def execute
clusters = project.clusters
filter_by_scope(clusters)
end
private
attr_reader :project, :user, :scope
def filter_by_scope(clusters)
case scope.to_sym
when :all
clusters
when :inactive
clusters.disabled
when :active
clusters.enabled
else
raise "Invalid scope #{scope}"
end
end
end
module AppearancesHelper
def brand_title
<<<<<<< HEAD
brand_item&.title.presence || 'GitLab Enterprise Edition'
=======
brand_item&.title.presence || 'GitLab Community Edition'
>>>>>>> upstream/master
end
def brand_image
......
......@@ -86,7 +86,7 @@ module ButtonHelper
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span),
button_content,
(href ? button_content : title),
class: "#{title.downcase}-selector",
href: (href if href),
data: {
......
......@@ -51,6 +51,28 @@ module Ci
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
<<<<<<< HEAD
=======
scope :matches_tag_ids, -> (tag_ids) do
matcher = ::ActsAsTaggableOn::Tagging
.where(taggable_type: CommitStatus)
.where(context: 'tags')
.where('taggable_id = ci_builds.id')
.where.not(tag_id: tag_ids).select('1')
where("NOT EXISTS (?)", matcher)
end
scope :with_any_tags, -> do
matcher = ::ActsAsTaggableOn::Tagging
.where(taggable_type: CommitStatus)
.where(context: 'tags')
.where('taggable_id = ci_builds.id').select('1')
where("EXISTS (?)", matcher)
end
>>>>>>> upstream/master
mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
......@@ -339,10 +361,13 @@ module Ci
project.running_or_pending_build_count(force: true)
end
<<<<<<< HEAD
def browsable_artifacts?
artifacts_metadata?
end
=======
>>>>>>> upstream/master
def artifacts_metadata_entry(path, **options)
artifacts_metadata.use_file do |metadata_path|
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
......
......@@ -113,7 +113,7 @@ module Ci
def can_pick?(build)
return false if self.ref_protected? && !build.protected?
assignable_for?(build.project) && accepting_tags?(build)
assignable_for?(build.project_id) && accepting_tags?(build)
end
def only_for?(project)
......@@ -172,8 +172,8 @@ module Ci
end
end
def assignable_for?(project)
is_shared? || projects.exists?(id: project.id)
def assignable_for?(project_id)
is_shared? || projects.exists?(id: project_id)
end
def accepting_tags?(build)
......
......@@ -55,6 +55,10 @@ module Clusters
end
end
def created?
status_name == :created
end
def applications
[
application_helm || build_application_helm,
......
......@@ -150,6 +150,13 @@ class MergeRequest < ActiveRecord::Base
'!'
end
# Use this method whenever you need to make sure the head_pipeline is synced with the
# branch head commit, for example checking if a merge request can be merged.
# For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
def actual_head_pipeline
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
end
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
......@@ -848,8 +855,9 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds?
return true unless head_pipeline
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
end
def environments_for(current_user)
......@@ -1023,7 +1031,7 @@ class MergeRequest < ActiveRecord::Base
return true if autocomplete_precheck
return false unless mergeable?(skip_ci_check: true)
return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
return false if last_diff_sha != diff_head_sha
true
......
......@@ -88,6 +88,13 @@ class Milestone < ActiveRecord::Base
else milestones.active
end
end
def predefined?(milestone)
milestone == Any ||
milestone == None ||
milestone == Upcoming ||
milestone == Started
end
end
def self.reference_prefix
......
......@@ -194,7 +194,6 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
......@@ -762,13 +761,14 @@ class Project < ActiveRecord::Base
Gitlab::Routing.url_helpers.project_url(self)
end
def new_issue_address(author)
def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
author.ensure_incoming_email_token!
suffix = address_type == 'merge_request' ? '+merge-request' : ''
Gitlab::IncomingEmail.reply_address(
"#{full_path}+#{author.incoming_email_token}")
"#{full_path}#{suffix}+#{author.incoming_email_token}")
end
def build_commit_note(commit)
......
......@@ -5,5 +5,9 @@ module Clusters
def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created?
end
end
end
......@@ -179,7 +179,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def pipeline
@pipeline ||= head_pipeline
@pipeline ||= actual_head_pipeline
end
def issues_sentence(project, issues)
......
......@@ -50,7 +50,7 @@ class MergeRequestEntity < IssuableEntity
end
expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
# Booleans
expose :merge_ongoing?, as: :merge_ongoing
......
......@@ -55,6 +55,7 @@ module Boards
def without_board_labels(issues)
return issues unless board_label_ids.any?
<<<<<<< HEAD
label_links = LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
.where(label_id: board_label_ids)
......@@ -63,6 +64,13 @@ module Boards
end
issues.where.not(label_links.limit(1).arel.exists)
=======
issues.where.not(issues_label_links.limit(1).arel.exists)
end
def issues_label_links
LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id").where(label_id: board_label_ids)
>>>>>>> upstream/master
end
def with_list_label(issues)
......
......@@ -32,7 +32,7 @@ module Ci
.new(pipeline, command, SEQUENCE)
sequence.build! do |pipeline, sequence|
update_merge_requests_head_pipeline if pipeline.persisted?
schedule_head_pipeline_update
if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
......@@ -41,15 +41,18 @@ module Ci
pipeline.process!
end
end
pipeline
end
private
def update_merge_requests_head_pipeline
return unless pipeline.latest?
def commit
@commit ||= project.commit(origin_sha || origin_ref)
end
MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref)
.update_all(head_pipeline_id: @pipeline.id)
def sha
commit.try(:id)
end
def cancel_pending_pipelines
......@@ -72,5 +75,15 @@ module Ci
@pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created")
end
def schedule_head_pipeline_update
related_merge_requests.each do |merge_request|
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
end
def related_merge_requests
MergeRequest.where(source_project: pipeline.project, source_branch: pipeline.ref)
end
end
end
......@@ -23,6 +23,16 @@ module Ci
valid = true
if Feature.enabled?('ci_job_request_with_tags_matcher')
# pick builds that does not have other tags than runner's one
builds = builds.matches_tag_ids(runner.tags.ids)
# pick builds that have at least one tag
unless runner.run_untagged?
builds = builds.with_any_tags
end
end
builds.find do |build|
next unless runner.can_pick?(build)
......
......@@ -5,6 +5,8 @@ module Clusters
def execute(access_token = nil)
@access_token = access_token
raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end
......@@ -25,5 +27,9 @@ module Clusters
@cluster_params = params.merge(user: current_user, projects: [project])
end
def can_create_cluster?
project.clusters.empty?
end
end
end
......@@ -12,8 +12,12 @@ module MergeRequests
merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid?
compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created
# compare branches only if branches are valid, otherwise
# compare_branches may raise an error
if merge_request.can_be_created
compare_branches
assign_title_and_description
end
merge_request
end
......
......@@ -35,6 +35,12 @@ module MergeRequests
super
end
# expose issuable create method so it can be called from email
# handler CreateMergeRequestHandler
def create(merge_request)
super
end
private
def update_merge_requests_head_pipeline(merge_request)
......
......@@ -77,6 +77,7 @@ module MergeRequests
end
merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
end
......
<<<<<<< HEAD
class JobArtifactUploader < ObjectStoreUploader
storage_options Gitlab.config.artifacts
=======
class JobArtifactUploader < GitlabUploader
storage :file
>>>>>>> upstream/master
def self.local_store_path
Gitlab.config.artifacts.path
......@@ -15,8 +20,29 @@ class JobArtifactUploader < ObjectStoreUploader
model.size
end
<<<<<<< HEAD
private
=======
def store_dir
default_local_path
end
def cache_dir
File.join(self.class.local_store_path, 'tmp/cache')
end
def work_dir
File.join(self.class.local_store_path, 'tmp/work')
end
private
def default_local_path
File.join(self.class.local_store_path, default_path)
end
>>>>>>> upstream/master
def default_path
creation_date = model.created_at.utc.strftime('%Y_%m_%d')
......
<<<<<<< HEAD
class LegacyArtifactUploader < ObjectStoreUploader
storage_options Gitlab.config.artifacts
=======
class LegacyArtifactUploader < GitlabUploader
storage :file
>>>>>>> upstream/master
def self.local_store_path
Gitlab.config.artifacts.path
......@@ -9,8 +14,29 @@ class LegacyArtifactUploader < ObjectStoreUploader
File.join(self.local_store_path, 'tmp/uploads/')
end
<<<<<<< HEAD
private
=======
def store_dir
default_local_path
end
def cache_dir
File.join(self.class.local_store_path, 'tmp/cache')
end
def work_dir
File.join(self.class.local_store_path, 'tmp/work')
end
private
def default_local_path
File.join(self.class.local_store_path, default_path)
end
>>>>>>> upstream/master
def default_path
File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s)
end
......
......@@ -201,7 +201,7 @@
= nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
%span
Cluster
Clusters
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
......
.issues-footer.text-center
%button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } }
Email a new issue to this project
- name = issuable_type == 'issue' ? 'issue' : 'merge request'
#issue-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
.issuable-footer.text-center
%button.issuable-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issuable-email-modal" } }
Email a new #{name} to this project
#issuable-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
.modal-dialog{ role: "document" }
.modal-content
.modal-header
%button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
%span{ aria: { hidden: "true" } }= icon("times")
%h4.modal-title
Create new issue by email
Create new #{name} by email
.modal-body
%p
You can create a new issue inside this project by sending an email to the following email address:
You can create a new #{name} inside this project by sending an email to the following email address:
.email-modal-input-group.input-group
= text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn
= clipboard_button(target: '#issue_email')
= clipboard_button(target: '#issuable_email')
%p
The subject will be used as the title of the new issue, and the message will be the description.
= link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
and styling with
= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
are supported.
= render 'by_email_description'
%p
This is a private email address, generated just for you.
Anyone who gets ahold of it can create issues as if they were you.
Anyone who gets ahold of it can create issues or merge requests as if they were you.
You should
= link_to 'reset it', new_issue_address_project_path(@project), class: 'incoming-email-token-reset'
= link_to 'reset it', new_issuable_address_project_path(@project, issuable_type: issuable_type), class: 'incoming-email-token-reset'
if that ever happens.
......@@ -25,7 +25,7 @@
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
= markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: "Go full screen", data: { container: "body" } }
= sprite_icon("screen-full")
.md-write-holder
......
.gl-responsive-table-row
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
.table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern")
.table-mobile-content= cluster.environment_scope
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
.table-mobile-content= cluster.platform_kubernetes&.actual_namespace
.table-section.section-10
.table-mobile-header{ role: "rowheader" }
.table-mobile-content
%button{ type: "button",
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?,
data: { "enabled-text": s_("ClusterIntegration|Active"),
"disabled-text": s_("ClusterIntegration|Inactive"),
endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
= icon("spinner spin", class: "loading-icon")
.row.empty-state
.col-xs-12
.svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-xs-12.text-center
.text-content
%h4= s_('ClusterIntegration|Integrate cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
%p
= link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
......@@ -5,12 +5,11 @@
= field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button',
class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}",
'aria-label': s_('ClusterIntegration|Toggle Cluster'),
class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster),
data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } }
data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } }
- if can?(current_user, :update_cluster, @cluster)
.form-group
= field.submit _('Save'), class: 'btn btn-success'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon("angle-left")
.fade-right= icon("angle-right")
%ul.nav-links.scrolling-tabs
%li{ class: ('active' if @scope == 'active') }>
= link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do
= s_("ClusterIntegration|Active")
%span.badge= @active_count
%li{ class: ('active' if @scope == 'inactive') }>
= link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do
= s_("ClusterIntegration|Inactive")
%span.badge= @inactive_count
%li{ class: ('active' if @scope.nil? || @scope == 'all') }>
= link_to project_clusters_path(@project), class: "js-all-tab" do
= s_("ClusterIntegration|All")
%span.badge= @all_count
- breadcrumb_title "Clusters"
- page_title "Clusters"
.clusters-container
- if !@clusters.empty?
= render "tabs"
.ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment pattern")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
= paginate @clusters, theme: "gitlab"
- elsif @scope == 'all'
= render "empty_state"
- else
= render "tabs"
.prepend-top-20.text-center
= s_("ClusterIntegration|There are no clusters to show")
- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title "Cluster"
- add_to_breadcrumbs "Clusters", project_clusters_path(@project)
- breadcrumb_title @cluster.id
- page_title _("Cluster")
- expanded = Rails.env.test?
......@@ -28,7 +29,6 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster')
.settings-content
- if @cluster.managed?
= render 'projects/clusters/gcp/show'
......
The subject will be used as the title of the new issue, and the message will be the description.
= link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
and styling with
= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
are supported.
......@@ -2,7 +2,7 @@
- @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
......@@ -29,6 +29,6 @@
.issues-holder
= render 'issues'
- if new_issue_email
= render 'issue_by_email', email: new_issue_email
= render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
- else
= render 'shared/empty_states/issues', button_path: new_project_issue_path(@project)
......@@ -15,8 +15,8 @@
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issuable')
.clearfix.detail-page-header
.issuable-header
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
= icon('check', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
......@@ -25,9 +25,6 @@
= icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs Open
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.issuable-meta
- if @issue.confidential
.issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon')
......@@ -35,7 +32,10 @@
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
......
The subject will be used as the source branch name for the new merge request and the target branch will be the default branch for the project.
......@@ -4,22 +4,22 @@
.alert.alert-danger
%p The source project of this merge request has been removed.
.clearfix.detail-page-header
.issuable-header
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
= icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
= @merge_request.state_human_name
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.issuable-meta
- if @merge_request.discussion_locked?
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request")
.issuable-actions.js-issuable-actions
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
......
......@@ -4,6 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
......@@ -25,6 +26,8 @@
.merge-requests-holder
= render 'merge_requests'
- if new_merge_request_email
= render 'projects/issuable_by_email', email: new_merge_request_email, issuable_type: 'merge_request'
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
......@@ -3,7 +3,7 @@
.git-clone-holder.input-group
.input-group-btn
- if allowed_protocols_present?
.clone-dropdown-btn.btn.btn-static
.clone-dropdown-btn.btn
%span
= enabled_project_button(project, enabled_protocol)
- else
......
.detail-page-header.clearfix
.snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
%span.sr-only
= visibility_level_label(@snippet.visibility_level)
= visibility_level_icon(@snippet.visibility_level, fw: false)
%span.creator
Authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.detail-page-header
.detail-page-header-body
.snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
%span.sr-only
= visibility_level_label(@snippet.visibility_level)
= visibility_level_icon(@snippet.visibility_level, fw: false)
%span.creator
Authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
.detail-page-header-actions
- if @snippet.project_id?
= render "projects/snippets/actions"
- else
......
......@@ -38,8 +38,7 @@ class EmailReceiverWorker
"You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
when Gitlab::Email::InvalidNoteError,
Gitlab::Email::InvalidIssueError
when Gitlab::Email::InvalidRecordError
can_retry = true
e.message
end
......
class PostReceive
include ApplicationWorker
<<<<<<< HEAD
prepend EE::PostReceive
=======
>>>>>>> upstream/master
def perform(gl_repository, identifier, changes)
project, is_wiki = Gitlab::GlRepository.parse(gl_repository)
......
# Worker for updating any project specific caches.
class ProjectCacheWorker
include ApplicationWorker
<<<<<<< HEAD
prepend EE::Workers::ProjectCacheWorker
=======
>>>>>>> upstream/master
LEASE_TIMEOUT = 15.minutes.to_i
......
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
sidekiq_options queue: 'pipeline_default'
def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id)
pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
return unless pipeline && pipeline.latest?
raise ArgumentError, 'merge request sha does not equal pipeline sha' if merge_request.diff_head_sha != pipeline.sha
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
end
---
title: Allow creation of merge request from email
merge_request: 13817
author: janp
type: added
---
title: Make sure head pippeline always corresponds with the head sha of an MR
merge_request:
author:
type: fixed
---
title: Init zen mode in snippets pages
merge_request:
author:
type: fixed
---
title: Perform SQL matching of Build&Runner tags to greatly speed-up job picking
merge_request:
author:
type: performance
......@@ -786,6 +786,8 @@ test:
# user: YOUR_USERNAME
pages:
path: tmp/tests/pages
artifacts:
path: tmp/tests/artifacts
repositories:
storages:
default:
......
......@@ -490,7 +490,7 @@ constraints(ProjectUrlConstrainer.new) do
get :download_export
get :activity
get :refs
put :new_issue_address
put :new_issuable_address
end
end
end
......
......@@ -413,6 +413,20 @@ ActiveRecord::Schema.define(version: 20171124182517) do
add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
create_table "ci_job_artifacts", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "job_id", null: false
t.integer "file_type", null: false
t.integer "size", limit: 8
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.datetime_with_timezone "expire_at"
t.string "file"
end
add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
create_table "ci_pipeline_schedule_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
......
......@@ -58,7 +58,9 @@ Runs the following rake tasks:
It will check that each component was setup according to the installation guide and suggest fixes for issues found.
You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
You may also have a look at our Trouble Shooting Guides:
- [Trouble Shooting Guide (GitLab)](http://docs.gitlab.com/ee/README.html#troubleshooting)
- [Trouble Shooting Guide (Omnibus Gitlab)](http://docs.gitlab.com/omnibus/README.html#troubleshooting)
**Omnibus Installation**
......
......@@ -17,6 +17,9 @@ Taking the trigger term as `project-name`, the commands are:
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
your GitLab.com projects, you need to [add the `gitlab` keyword at the beginning of the command](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html#usage).
## Issue commands
It is possible to create new issue, display issue details and search up to 5 issues.
......
......@@ -43,7 +43,10 @@ The following table depicts the various user permission levels in a project.
| See environments | | ✓ | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Create new environments | | | ✓ | ✓ | ✓ |
<<<<<<< HEAD
| Manage related issues | | ✓ | ✓ | ✓ | ✓ |
=======
>>>>>>> upstream/master
| Stop environments | | | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
......
......@@ -27,6 +27,10 @@ With GitLab merge requests, you can:
- [Resolve merge conflicts from the UI](#resolve-conflicts)
- Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
<<<<<<< HEAD
=======
- [Create new merge requests by email](#create_by_email)
>>>>>>> upstream/master
With **[GitLab Enterprise Edition][ee]**, you can also:
......@@ -138,6 +142,14 @@ those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
## Create new merge requests by email
You can create a new merge request by sending an email to a user-specific email
address. The address can be obtained on the merge requests page by clicking on
a **Email a new merge request to this project** button. The subject will be
used as the source branch name for the new merge request and the target branch
will be the default branch for the project.
## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing
......
......@@ -50,6 +50,10 @@ module Gitlab
postgresql? && version.to_f >= 9.3
end
def self.replication_slots_supported?
postgresql? && version.to_f >= 9.4
end
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
......
require 'gitlab/email/handler/create_merge_request_handler'
require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
require 'gitlab/email/handler/unsubscribe_handler'
......@@ -11,6 +12,7 @@ module Gitlab
EE::ServiceDeskHandler,
UnsubscribeHandler,
CreateNoteHandler,
CreateMergeRequestHandler,
CreateIssueHandler
].freeze
......
require 'gitlab/email/handler/base_handler'
require 'gitlab/email/handler/reply_processing'
module Gitlab
module Email
module Handler
class CreateMergeRequestHandler < BaseHandler
include ReplyProcessing
attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key)
super(mail, mail_key)
if m = /\A([^\+]*)\+merge-request\+(.*)/.match(mail_key.to_s)
@project_path, @incoming_email_token = m.captures
end
end
def can_handle?
@project_path && @incoming_email_token
end
def execute
raise ProjectNotFound unless project
validate_permission!(:create_merge_request)
verify_record!(
record: create_merge_request,
invalid_exception: InvalidMergeRequestError,
record_name: 'merge_request')
end
def author
@author ||= User.find_by(incoming_email_token: incoming_email_token)
end
def project
@project ||= Project.find_by_full_path(project_path)
end
def metrics_params
super.merge(project: project&.full_path)
end
private
def create_merge_request
merge_request = MergeRequests::BuildService.new(project, author, merge_request_params).execute
if merge_request.errors.any?
merge_request
else
MergeRequests::CreateService.new(project, author).create(merge_request)
end
end
def merge_request_params
{
source_project_id: project.id,
source_branch: mail.subject,
target_project_id: project.id
}
end
end
end
end
end
......@@ -13,8 +13,10 @@ module Gitlab
UserBlockedError = Class.new(ProcessingError)
UserNotAuthorizedError = Class.new(ProcessingError)
NoteableNotFoundError = Class.new(ProcessingError)
InvalidNoteError = Class.new(ProcessingError)
InvalidIssueError = Class.new(ProcessingError)
InvalidRecordError = Class.new(ProcessingError)
InvalidNoteError = Class.new(InvalidRecordError)
InvalidIssueError = Class.new(InvalidRecordError)
InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError)
class Receiver
......
......@@ -213,11 +213,17 @@ module Gitlab
end
def shas_with_signatures(repository, shas)
shas.select do |sha|
begin
Rugged::Commit.extract_signature(repository.rugged, sha)
rescue Rugged::OdbError
false
GitalyClient.migrate(:filter_shas_with_signatures) do |is_enabled|
if is_enabled
Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
else
shas.select do |sha|
begin
Rugged::Commit.extract_signature(repository.rugged, sha)
rescue Rugged::OdbError
false
end
end
end
end
end
......
......@@ -777,24 +777,21 @@ module Gitlab
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
Gitlab::Git.check_namespace!(commit, start_repository)
revert_tree_id = check_revert_content(commit, start_commit.sha)
raise CreateTreeError unless revert_tree_id
committer = user_to_committer(user)
gitaly_migrate(:revert) do |is_enabled|
args = {
user: user,
commit: commit,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_repository
}
create_commit(message: message,
author: committer,
committer: committer,
tree: revert_tree_id,
parents: [start_commit.sha])
if is_enabled
gitaly_operations_client.user_revert(args)
else
rugged_revert(args)
end
end
end
......@@ -1783,6 +1780,28 @@ module Gitlab
end
end
def rugged_revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
Gitlab::Git.check_namespace!(commit, start_repository)
revert_tree_id = check_revert_content(commit, start_commit.sha)
raise CreateTreeError unless revert_tree_id
committer = user_to_committer(user)
create_commit(message: message,
author: committer,
committer: committer,
tree: revert_tree_id,
parents: [start_commit.sha])
end
end
def gitaly_add_branch(branch_name, user, target)
gitaly_operation_client.user_create_branch(branch_name, user, target)
rescue GRPC::FailedPrecondition => ex
......
......@@ -250,6 +250,26 @@ module Gitlab
consume_commits_response(response)
end
def filter_shas_with_signatures(shas)
request = Gitaly::FilterShasWithSignaturesRequest.new(repository: @gitaly_repo)
enum = Enumerator.new do |y|
shas.each_slice(20) do |revs|
request.shas = GitalyClient.encode_repeated(revs)
y.yield request
request = Gitaly::FilterShasWithSignaturesRequest.new
end
end
response = GitalyClient.call(@repository.storage, :commit_service, :filter_shas_with_signatures, enum)
response.flat_map do |msg|
msg.shas.map { |sha| EncodingHelper.encode!(sha) }
end
end
private
def call_commit_diff(request_params, options = {})
......
......@@ -124,7 +124,31 @@ module Gitlab
end
def user_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
request = Gitaly::UserCherryPickRequest.new(
call_cherry_pick_or_revert(:cherry_pick,
user: user,
commit: commit,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_repository)
end
def user_revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
call_cherry_pick_or_revert(:revert,
user: user,
commit: commit,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_repository)
end
private
def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
request_class = "Gitaly::User#{rpc.to_s.camelcase}Request".constantize
request = request_class.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit: commit.to_gitaly_commit,
......@@ -137,11 +161,15 @@ module Gitlab
response = GitalyClient.call(
@repository.storage,
:operation_service,
:user_cherry_pick,
:"user_#{rpc}",
request,
remote_storage: start_repository.storage
)
handle_cherry_pick_or_revert_response(response)
end
def handle_cherry_pick_or_revert_response(response)
if response.pre_receive_error.presence
raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error
elsif response.commit_error.presence
......
......@@ -20,9 +20,13 @@ module Gitlab
end
def self.workers
<<<<<<< HEAD
@workers ||=
find_workers(Rails.root.join('app', 'workers')) +
find_workers(Rails.root.join('ee', 'app', 'workers'))
=======
@workers ||= find_workers(Rails.root.join('app', 'workers'))
>>>>>>> upstream/master
end
def self.default_queues
......
This diff is collapsed.
......@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count }
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(project.cluster).to be_gcp
expect(project.cluster).to be_kubernetes
expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
expect(project.clusters.first).to be_gcp
expect(project.clusters.first).to be_kubernetes
end
end
end
......
......@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Platforms::Kubernetes.count }
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
expect(project.clusters.first).to be_user
expect(project.clusters.first).to be_kubernetes
end
end
end
......
......@@ -15,14 +15,72 @@ describe Projects::ClustersController do
sign_in(user)
end
context 'when project has a cluster' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
context 'when project has one or more clusters' do
let(:project) { create(:project) }
let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
it 'lists available clusters' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
it 'assigns counters to correct values' do
go
expect(assigns(:active_count)).to eq(1)
expect(assigns(:inactive_count)).to eq(1)
end
context 'when page is specified' do
let(:last_page) { project.clusters.page.total_pages }
it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
before do
allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
create_list(:cluster, 2, :provided_by_gcp, projects: [project])
get :index, namespace_id: project.namespace, project_id: project, page: last_page
end
it 'redirects to the page' do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:clusters).current_page).to eq(last_page)
end
end
context 'when only enabled clusters are requested' do
it 'returns only enabled clusters' do
get :index, namespace_id: project.namespace, project_id: project, scope: 'active'
expect(assigns(:clusters)).to all(have_attributes(enabled: true))
end
end
context 'when only disabled clusters are requested' do
it 'returns only disabled clusters' do
get :index, namespace_id: project.namespace, project_id: project, scope: 'inactive'
expect(assigns(:clusters)).to all(have_attributes(enabled: false))
end
end
end
context 'when project does not have a cluster' do
it { expect(go).to redirect_to(new_project_cluster_path(project)) }
let(:project) { create(:project) }
it 'returns an empty state page' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index, partial: :empty_state)
expect(assigns(:clusters)).to eq([])
end
it 'assigns counters to zero' do
go
expect(assigns(:active_count)).to eq(0)
expect(assigns(:inactive_count)).to eq(0)
end
end
end
......@@ -146,7 +204,7 @@ describe Projects::ClustersController do
go
cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
end
......@@ -180,28 +238,77 @@ describe Projects::ClustersController do
sign_in(user)
end
context 'when changing parameters' do
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
context 'when format is json' do
context 'when changing parameters' do
context 'when valid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
}
}
}
}
end
it "updates and redirects back to show page" do
go_json
cluster.reload
expect(response).to have_http_status(:no_content)
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
end
context 'when invalid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
platform_kubernetes_attributes: {
namespace: 'my invalid namespace #@'
}
}
}
end
it "rejects changes" do
go_json
expect(response).to have_http_status(:bad_request)
end
end
end
end
it "updates and redirects back to show page" do
go
context 'when format is html' do
context 'when update enabled' do
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
}
}
end
cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
it "updates and redirects back to show page" do
go
cluster.reload
expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
end
end
end
......@@ -228,6 +335,13 @@ describe Projects::ClustersController do
project_id: project,
id: cluster)
end
def go_json
put :update, params.merge(namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json)
end
end
describe 'DELETE destroy' do
......
......@@ -325,12 +325,12 @@ describe Projects::MergeRequestsController do
end
context 'when the pipeline succeeds is passed' do
def merge_when_pipeline_succeeds
post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
let!(:head_pipeline) do
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
end
before do
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
def merge_when_pipeline_succeeds
post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
end
it 'returns :merge_when_pipeline_succeeds' do
......@@ -355,6 +355,18 @@ describe Projects::MergeRequestsController do
project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
end
context 'and head pipeline is not the current one' do
before do
head_pipeline.update(sha: 'not_current_sha')
end
it 'returns :failed' do
merge_when_pipeline_succeeds
expect(json_response).to eq('status' => 'failed')
end
end
it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds
......
......@@ -449,11 +449,12 @@ describe ProjectsController do
end
end
describe 'PUT #new_issue_address' do
describe 'PUT #new_issuable_address for issue' do
subject do
put :new_issue_address,
put :new_issuable_address,
namespace_id: project.namespace,
id: project
id: project,
issuable_type: 'issue'
user.reload
end
......@@ -472,7 +473,35 @@ describe ProjectsController do
end
it 'changes projects new issue address' do
expect { subject }.to change { project.new_issue_address(user) }
expect { subject }.to change { project.new_issuable_address(user, 'issue') }
end
end
describe 'PUT #new_issuable_address for merge request' do
subject do
put :new_issuable_address,
namespace_id: project.namespace,
id: project,
issuable_type: 'merge_request'
user.reload
end
before do
sign_in(user)
project.team << [user, :developer]
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
end
it 'has http status 200' do
expect(response).to have_http_status(200)
end
it 'changes the user incoming email token' do
expect { subject }.to change { user.incoming_email_token }
end
it 'changes projects new merge request address' do
expect { subject }.to change { project.new_issuable_address(user, 'merge_request') }
end
end
......
......@@ -2,8 +2,13 @@
FactoryGirl.define do
factory :appearance do
<<<<<<< HEAD
title "GitLab Enterprise Edition"
description "Open source software to collaborate on code"
=======
title "MepMep"
description "This is my Community Edition instance"
>>>>>>> upstream/master
new_project_guidelines "Custom project guidelines"
end
end
......@@ -5,10 +5,13 @@ FactoryGirl.define do
job factory: :ci_build
file_type :archive
<<<<<<< HEAD
trait :remote_store do
file_store JobArtifactUploader::REMOTE_STORE
end
=======
>>>>>>> upstream/master
after :build do |artifact|
artifact.project ||= artifact.job.project
end
......
......@@ -28,5 +28,9 @@ FactoryGirl.define do
provider_type :gcp
provider_gcp factory: [:cluster_provider_gcp, :creating]
end
trait :disabled do
enabled false
end
end
end
......@@ -376,16 +376,16 @@ describe 'Issues' do
end
it 'changes incoming email address token', :js do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
find('.issuable-email-modal-btn').click
previous_token = find('input#issuable_email').value
find('.incoming-email-token-reset').click
wait_for_requests
expect(page).to have_no_field('issue_email', with: previous_token)
new_token = project1.new_issue_address(user.reload)
expect(page).to have_no_field('issuable_email', with: previous_token)
new_token = project1.new_issuable_address(user.reload, 'issue')
expect(page).to have_field(
'issue_email',
'issuable_email',
with: new_token
)
end
......@@ -666,8 +666,8 @@ describe 'Issues' do
end
it 'click the button to show modal for the new email' do
page.within '#issue-email-modal' do
email = project.new_issue_address(user)
page.within '#issuable-email-modal' do
email = project.new_issuable_address(user, 'issue')
expect(page).to have_selector("input[value='#{email}']")
end
......
......@@ -20,10 +20,14 @@ feature 'Pipelines for Merge Requests', :js do
end
before do
visit project_merge_request_path(project, merge_request)
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
scenario 'user visits merge request pipelines tab' do
visit project_merge_request_path(project, merge_request)
expect(page.find('.ci-widget')).to have_content('pending')
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
......@@ -31,6 +35,15 @@ feature 'Pipelines for Merge Requests', :js do
expect(page).to have_selector('.stage-cell')
end
scenario 'pipeline sha does not equal last commit sha' do
pipeline.update_attribute(:sha, '19e2e9b4ef76b422ce1154af39a91323ccc57434')
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page.find('.ci-widget')).to have_content(
'Could not connect to the CI server. Please check your settings and try again')
end
end
context 'without pipelines' do
......
......@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE'
end
......@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do
it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.')
expect(page).to have_link('Create on GKE')
expect(page).to have_link('Add cluster')
end
end
end
......@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE'
end
......
......@@ -16,6 +16,7 @@ feature 'User Cluster', :js do
before do
visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Add an existing cluster'
end
......@@ -94,7 +95,7 @@ feature 'User Cluster', :js do
it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.')
expect(page).to have_link('Add an existing cluster')
expect(page).to have_link('Add cluster')
end
end
end
......
......@@ -14,12 +14,78 @@ feature 'Clusters', :js do
context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
end
click_link 'Create on GKE'
it 'sees empty state' do
expect(page).to have_link('Add cluster')
expect(page).to have_selector('.empty-state')
end
end
context 'when user has a cluster and visits cluster index page' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
before do
visit project_clusters_path(project)
end
it 'user sees a table with one cluster' do
# One is the header row, the other the cluster row
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end
it 'user sees navigation tabs' do
expect(page.find('.js-active-tab').text).to include('Active')
expect(page.find('.js-active-tab .badge').text).to include('1')
expect(page.find('.js-inactive-tab').text).to include('Inactive')
expect(page.find('.js-inactive-tab .badge').text).to include('0')
expect(page.find('.js-all-tab').text).to include('All')
expect(page.find('.js-all-tab .badge').text).to include('1')
end
context 'inline update of cluster' do
it 'user can update cluster' do
expect(page).to have_selector('.js-toggle-cluster-list')
end
context 'with sucessfull request' do
it 'user sees updated cluster' do
expect do
page.find('.js-toggle-cluster-list').click
wait_for_requests
end.to change { cluster.reload.enabled }
expect(page).not_to have_selector('.is-checked')
expect(cluster.reload).not_to be_enabled
end
end
context 'with failed request' do
it 'user sees not update cluster and error message' do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
page.find('.js-toggle-cluster-list').click
expect(page).to have_content('Something went wrong on our end.')
expect(page).to have_selector('.is-checked')
expect(cluster.reload).to be_enabled
end
end
end
context 'when user clicks on a cluster' do
before do
click_link cluster.name
end
it 'user sees a new page' do
expect(page).to have_button('Create cluster')
it 'user sees a cluster details page' do
expect(page).to have_button('Save')
expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment