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 ...@@ -416,7 +416,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # 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 gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -300,7 +300,7 @@ GEM ...@@ -300,7 +300,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.58.0) gitaly-proto (0.59.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -382,8 +382,11 @@ GEM ...@@ -382,8 +382,11 @@ GEM
grpc (1.4.5) grpc (1.4.5)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
<<<<<<< HEAD
gssapi (1.2.0) gssapi (1.2.0)
ffi (>= 1.0.1) ffi (>= 1.0.1)
=======
>>>>>>> upstream/master
haml (4.0.7) haml (4.0.7)
tilt tilt
haml_lint (0.26.0) haml_lint (0.26.0)
...@@ -1069,7 +1072,7 @@ DEPENDENCIES ...@@ -1069,7 +1072,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.58.0) gitaly-proto (~> 0.59.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
...@@ -150,8 +150,8 @@ export default class Clusters { ...@@ -150,8 +150,8 @@ export default class Clusters {
} }
toggle() { toggle() {
this.toggleButton.classList.toggle('checked'); this.toggleButton.classList.toggle('is-checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
} }
showToken() { 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 { ...@@ -17,4 +17,8 @@ export default class ClusterService {
installApplication(appId) { installApplication(appId) {
return axios.post(this.appInstallEndpointMap[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'; ...@@ -31,9 +31,12 @@ import projectImport from './project_import';
import Labels from './labels'; import Labels from './labels';
import LabelManager from './label_manager'; import LabelManager from './label_manager';
/* global Sidebar */ /* global Sidebar */
<<<<<<< HEAD
/* global WeightSelect */ /* global WeightSelect */
/* global AdminEmailSelect */ /* global AdminEmailSelect */
=======
>>>>>>> upstream/master
import IssuableTemplateSelectors from './templates/issuable_template_selectors'; import IssuableTemplateSelectors from './templates/issuable_template_selectors';
import Flash from './flash'; import Flash from './flash';
import CommitsList from './commits'; import CommitsList from './commits';
...@@ -294,7 +297,10 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -294,7 +297,10 @@ import initGroupAnalytics from './init_group_analytics';
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
<<<<<<< HEAD
new WeightSelect(); new WeightSelect();
=======
>>>>>>> upstream/master
new IssuableTemplateSelectors(); new IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:creations:new': case 'projects:merge_requests:creations:new':
...@@ -330,18 +336,21 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -330,18 +336,21 @@ import initGroupAnalytics from './init_group_analytics';
break; break;
case 'projects:snippets:show': case 'projects:snippets:show':
initNotes(); initNotes();
new ZenMode();
break; break;
case 'projects:snippets:new': case 'projects:snippets:new':
case 'projects:snippets:edit': case 'projects:snippets:edit':
case 'projects:snippets:create': case 'projects:snippets:create':
case 'projects:snippets:update': case 'projects:snippets:update':
new GLForm($('.snippet-form'), true); new GLForm($('.snippet-form'), true);
new ZenMode();
break; break;
case 'snippets:new': case 'snippets:new':
case 'snippets:edit': case 'snippets:edit':
case 'snippets:create': case 'snippets:create':
case 'snippets:update': case 'snippets:update':
new GLForm($('.snippet-form'), false); new GLForm($('.snippet-form'), false);
new ZenMode();
break; break;
case 'projects:releases:edit': case 'projects:releases:edit':
new ZenMode(); new ZenMode();
...@@ -609,6 +618,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -609,6 +618,7 @@ import initGroupAnalytics from './init_group_analytics';
new LineHighlighter(); new LineHighlighter();
new BlobViewer(); new BlobViewer();
initNotes(); initNotes();
new ZenMode();
break; break;
case 'import:fogbugz:new_user_map': case 'import:fogbugz:new_user_map':
new UsersSelect(); new UsersSelect();
...@@ -621,7 +631,15 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -621,7 +631,15 @@ import initGroupAnalytics from './init_group_analytics';
import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap .then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch((err) => { .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; throw err;
}); });
break; break;
......
...@@ -28,7 +28,7 @@ export default class IssuableIndex { ...@@ -28,7 +28,7 @@ export default class IssuableIndex {
url: $('.incoming-email-token-reset').attr('href'), url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json', dataType: 'json',
success(response) { success(response) {
$('#issue_email').val(response.new_issue_address).focus(); $('#issuable_email').val(response.new_address).focus();
}, },
beforeSend() { beforeSend() {
$('.incoming-email-token-reset').text('resetting...'); $('.incoming-email-token-reset').text('resetting...');
......
...@@ -129,7 +129,7 @@ import { addDelimiter } from './lib/utils/text_utility'; ...@@ -129,7 +129,7 @@ import { addDelimiter } from './lib/utils/text_utility';
}; };
MergeRequest.prototype.hideCloseButton = function() { 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'); const closeDropdownItem = el.querySelector('li.close-item');
if (closeDropdownItem) { if (closeDropdownItem) {
closeDropdownItem.classList.add('hidden'); closeDropdownItem.classList.add('hidden');
......
<script> <script>
import projectFeatureToggle from './project_feature_toggle.vue'; import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default { export default {
props: { 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> <script>
import projectFeatureSetting from './project_feature_setting.vue'; 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 projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external'; 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 @@ ...@@ -43,6 +43,7 @@
@import "framework/tabs"; @import "framework/tabs";
@import "framework/timeline"; @import "framework/timeline";
@import "framework/tooltips"; @import "framework/tooltips";
@import "framework/toggle";
@import "framework/typography"; @import "framework/typography";
@import "framework/zen"; @import "framework/zen";
@import "framework/blank"; @import "framework/blank";
......
...@@ -1031,6 +1031,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1031,6 +1031,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
} }
<<<<<<< HEAD
.new-epic-dropdown { .new-epic-dropdown {
.dropdown-menu { .dropdown-menu {
padding-left: $gl-padding-top; padding-left: $gl-padding-top;
...@@ -1060,6 +1061,8 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1060,6 +1061,8 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
} }
=======
>>>>>>> upstream/master
.dropdown-content-faded-mask { .dropdown-content-faded-mask {
position: relative; 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 @@ ...@@ -14,3 +14,17 @@
} }
@include new-style-dropdown('.clusters-dropdown '); @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 @@ ...@@ -13,6 +13,41 @@
.author_link { .author_link {
white-space: nowrap; 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 { .detail-page-description {
......
...@@ -624,50 +624,16 @@ ...@@ -624,50 +624,16 @@
margin-top: 0; margin-top: 0;
height: auto; height: auto;
align-self: center; 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 { .issuable-gutter-toggle {
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
position: absolute; margin-left: $btn-side-margin;
top: 0;
right: 0;
} }
} }
.issuable-meta { .issuable-meta {
flex: 1;
display: inline-block; display: inline-block;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
......
...@@ -134,26 +134,11 @@ ul.related-merge-requests > li { ...@@ -134,26 +134,11 @@ ul.related-merge-requests > li {
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.detail-page-header, .detail-page-header {
.issuable-header {
display: block;
.issuable-meta { .issuable-meta {
line-height: 18px; line-height: 18px;
} }
} }
.issuable-actions {
margin-top: 10px;
.issue-btn-group {
width: 100%;
.btn {
width: 100%;
}
}
}
} }
.issue-form { .issue-form {
...@@ -164,6 +149,7 @@ ul.related-merge-requests > li { ...@@ -164,6 +149,7 @@ ul.related-merge-requests > li {
} }
} }
<<<<<<< HEAD
.issues-footer { .issues-footer {
padding-top: $gl-padding; padding-top: $gl-padding;
padding-bottom: 37px; padding-bottom: 37px;
...@@ -185,6 +171,9 @@ ul.related-merge-requests > li { ...@@ -185,6 +171,9 @@ ul.related-merge-requests > li {
} }
.issue-email-modal-btn { .issue-email-modal-btn {
=======
.issuable-email-modal-btn {
>>>>>>> upstream/master
padding: 0; padding: 0;
color: $gl-link-color; color: $gl-link-color;
background-color: transparent; background-color: transparent;
......
...@@ -134,93 +134,6 @@ ...@@ -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, .project-home-panel,
.group-home-panel { .group-home-panel {
padding-top: 24px; padding-top: 24px;
...@@ -1269,6 +1182,7 @@ a.allowed-to-push { ...@@ -1269,6 +1182,7 @@ a.allowed-to-push {
} }
} }
<<<<<<< HEAD
/* EE-specific styles */ /* EE-specific styles */
.project-mirror-settings { .project-mirror-settings {
.fingerprint-verified { .fingerprint-verified {
...@@ -1340,4 +1254,9 @@ a.allowed-to-push { ...@@ -1340,4 +1254,9 @@ a.allowed-to-push {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
=======
.issuable-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
>>>>>>> upstream/master
} }
...@@ -8,11 +8,11 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -8,11 +8,11 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000 STATUS_POLLING_INTERVAL = 10_000
def index def index
if project.cluster @scope = params[:scope] || 'all'
redirect_to project_cluster_path(project, project.cluster) @clusters = ClustersFinder.new(project, current_user, @scope).execute.page(params[:page])
else @active_count = ClustersFinder.new(project, current_user, :active).execute.count
redirect_to new_project_cluster_path(project) @inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count
end @all_count = @active_count + @inactive_count
end end
def new def new
...@@ -39,10 +39,20 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -39,10 +39,20 @@ class Projects::ClustersController < Projects::ApplicationController
.execute(cluster) .execute(cluster)
if cluster.valid? if cluster.valid?
flash[:notice] = "Cluster was successfully updated." respond_to do |format|
redirect_to project_cluster_path(project, project.cluster) 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 else
render :show respond_to do |format|
format.json { head :bad_request }
format.html { render :show }
end
end end
end end
...@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user) .present(current_user: current_user)
end 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 def update_params
if cluster.managed? if cluster.managed?
params.require(:cluster).permit( params.require(:cluster).permit(
......
...@@ -292,15 +292,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -292,15 +292,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false)) @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
if params[:merge_when_pipeline_succeeds].present? 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 ::MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params) .new(@project, current_user, merge_params)
.execute(@merge_request) .execute(@merge_request)
:merge_when_pipeline_succeeds :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 # This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time # the tests finish at about the same time
@merge_request.merge_async(current_user.id, params) @merge_request.merge_async(current_user.id, params)
......
...@@ -136,11 +136,11 @@ class ProjectsController < Projects::ApplicationController ...@@ -136,11 +136,11 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), status: 302, alert: ex.message redirect_to edit_project_path(@project), status: 302, alert: ex.message
end end
def new_issue_address def new_issuable_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation? return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
current_user.reset_incoming_email_token! 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 end
def archive 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 module AppearancesHelper
def brand_title def brand_title
<<<<<<< HEAD
brand_item&.title.presence || 'GitLab Enterprise Edition' brand_item&.title.presence || 'GitLab Enterprise Edition'
=======
brand_item&.title.presence || 'GitLab Community Edition'
>>>>>>> upstream/master
end end
def brand_image def brand_image
......
...@@ -86,7 +86,7 @@ module ButtonHelper ...@@ -86,7 +86,7 @@ module ButtonHelper
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span), content_tag (href ? :a : :span),
button_content, (href ? button_content : title),
class: "#{title.downcase}-selector", class: "#{title.downcase}-selector",
href: (href if href), href: (href if href),
data: { data: {
......
...@@ -51,6 +51,28 @@ module Ci ...@@ -51,6 +51,28 @@ module Ci
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) } 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_file, LegacyArtifactUploader, mount_on: :artifacts_file
mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
...@@ -339,10 +361,13 @@ module Ci ...@@ -339,10 +361,13 @@ module Ci
project.running_or_pending_build_count(force: true) project.running_or_pending_build_count(force: true)
end end
<<<<<<< HEAD
def browsable_artifacts? def browsable_artifacts?
artifacts_metadata? artifacts_metadata?
end end
=======
>>>>>>> upstream/master
def artifacts_metadata_entry(path, **options) def artifacts_metadata_entry(path, **options)
artifacts_metadata.use_file do |metadata_path| artifacts_metadata.use_file do |metadata_path|
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
......
...@@ -113,7 +113,7 @@ module Ci ...@@ -113,7 +113,7 @@ module Ci
def can_pick?(build) def can_pick?(build)
return false if self.ref_protected? && !build.protected? return false if self.ref_protected? && !build.protected?
assignable_for?(build.project) && accepting_tags?(build) assignable_for?(build.project_id) && accepting_tags?(build)
end end
def only_for?(project) def only_for?(project)
...@@ -172,8 +172,8 @@ module Ci ...@@ -172,8 +172,8 @@ module Ci
end end
end end
def assignable_for?(project) def assignable_for?(project_id)
is_shared? || projects.exists?(id: project.id) is_shared? || projects.exists?(id: project_id)
end end
def accepting_tags?(build) def accepting_tags?(build)
......
...@@ -55,6 +55,10 @@ module Clusters ...@@ -55,6 +55,10 @@ module Clusters
end end
end end
def created?
status_name == :created
end
def applications def applications
[ [
application_helm || build_application_helm, application_helm || build_application_helm,
......
...@@ -150,6 +150,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -150,6 +150,13 @@ class MergeRequest < ActiveRecord::Base
'!' '!'
end 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 # Pattern used to extract `!123` merge request references from text
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
...@@ -848,8 +855,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -848,8 +855,9 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state? def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds? 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 end
def environments_for(current_user) def environments_for(current_user)
...@@ -1023,7 +1031,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -1023,7 +1031,7 @@ class MergeRequest < ActiveRecord::Base
return true if autocomplete_precheck return true if autocomplete_precheck
return false unless mergeable?(skip_ci_check: true) 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 return false if last_diff_sha != diff_head_sha
true true
......
...@@ -88,6 +88,13 @@ class Milestone < ActiveRecord::Base ...@@ -88,6 +88,13 @@ class Milestone < ActiveRecord::Base
else milestones.active else milestones.active
end end
end end
def predefined?(milestone)
milestone == Any ||
milestone == None ||
milestone == Upcoming ||
milestone == Started
end
end end
def self.reference_prefix def self.reference_prefix
......
...@@ -194,7 +194,6 @@ class Project < ActiveRecord::Base ...@@ -194,7 +194,6 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics' has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project' 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' has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry, # Container repositories need to remove data from the container registry,
...@@ -762,13 +761,14 @@ class Project < ActiveRecord::Base ...@@ -762,13 +761,14 @@ class Project < ActiveRecord::Base
Gitlab::Routing.url_helpers.project_url(self) Gitlab::Routing.url_helpers.project_url(self)
end end
def new_issue_address(author) def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author return unless Gitlab::IncomingEmail.supports_issue_creation? && author
author.ensure_incoming_email_token! author.ensure_incoming_email_token!
suffix = address_type == 'merge_request' ? '+merge-request' : ''
Gitlab::IncomingEmail.reply_address( Gitlab::IncomingEmail.reply_address(
"#{full_path}+#{author.incoming_email_token}") "#{full_path}#{suffix}+#{author.incoming_email_token}")
end end
def build_commit_note(commit) def build_commit_note(commit)
......
...@@ -5,5 +5,9 @@ module Clusters ...@@ -5,5 +5,9 @@ module Clusters
def gke_cluster_url def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end end
def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created?
end
end end
end end
...@@ -179,7 +179,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -179,7 +179,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def pipeline def pipeline
@pipeline ||= head_pipeline @pipeline ||= actual_head_pipeline
end end
def issues_sentence(project, issues) def issues_sentence(project, issues)
......
...@@ -50,7 +50,7 @@ class MergeRequestEntity < IssuableEntity ...@@ -50,7 +50,7 @@ class MergeRequestEntity < IssuableEntity
end end
expose :merge_commit_message expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
# Booleans # Booleans
expose :merge_ongoing?, as: :merge_ongoing expose :merge_ongoing?, as: :merge_ongoing
......
...@@ -55,6 +55,7 @@ module Boards ...@@ -55,6 +55,7 @@ module Boards
def without_board_labels(issues) def without_board_labels(issues)
return issues unless board_label_ids.any? return issues unless board_label_ids.any?
<<<<<<< HEAD
label_links = LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") label_links = LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
.where(label_id: board_label_ids) .where(label_id: board_label_ids)
...@@ -63,6 +64,13 @@ module Boards ...@@ -63,6 +64,13 @@ module Boards
end end
issues.where.not(label_links.limit(1).arel.exists) 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 end
def with_list_label(issues) def with_list_label(issues)
......
...@@ -32,7 +32,7 @@ module Ci ...@@ -32,7 +32,7 @@ module Ci
.new(pipeline, command, SEQUENCE) .new(pipeline, command, SEQUENCE)
sequence.build! do |pipeline, sequence| sequence.build! do |pipeline, sequence|
update_merge_requests_head_pipeline if pipeline.persisted? schedule_head_pipeline_update
if sequence.complete? if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines? cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
...@@ -41,15 +41,18 @@ module Ci ...@@ -41,15 +41,18 @@ module Ci
pipeline.process! pipeline.process!
end end
end end
pipeline
end end
private private
def update_merge_requests_head_pipeline def commit
return unless pipeline.latest? @commit ||= project.commit(origin_sha || origin_ref)
end
MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref) def sha
.update_all(head_pipeline_id: @pipeline.id) commit.try(:id)
end end
def cancel_pending_pipelines def cancel_pending_pipelines
...@@ -72,5 +75,15 @@ module Ci ...@@ -72,5 +75,15 @@ module Ci
@pipeline_created_counter ||= Gitlab::Metrics @pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created") .counter(:pipelines_created_total, "Counter of pipelines created")
end 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
end end
...@@ -23,6 +23,16 @@ module Ci ...@@ -23,6 +23,16 @@ module Ci
valid = true 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| builds.find do |build|
next unless runner.can_pick?(build) next unless runner.can_pick?(build)
......
...@@ -5,6 +5,8 @@ module Clusters ...@@ -5,6 +5,8 @@ module Clusters
def execute(access_token = nil) def execute(access_token = nil)
@access_token = access_token @access_token = access_token
raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
create_cluster.tap do |cluster| create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end end
...@@ -25,5 +27,9 @@ module Clusters ...@@ -25,5 +27,9 @@ module Clusters
@cluster_params = params.merge(user: current_user, projects: [project]) @cluster_params = params.merge(user: current_user, projects: [project])
end end
def can_create_cluster?
project.clusters.empty?
end
end end
end end
...@@ -12,8 +12,12 @@ module MergeRequests ...@@ -12,8 +12,12 @@ module MergeRequests
merge_request.target_branch = find_target_branch merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid? merge_request.can_be_created = branches_valid?
compare_branches if branches_present? # compare branches only if branches are valid, otherwise
assign_title_and_description if merge_request.can_be_created # compare_branches may raise an error
if merge_request.can_be_created
compare_branches
assign_title_and_description
end
merge_request merge_request
end end
......
...@@ -35,6 +35,12 @@ module MergeRequests ...@@ -35,6 +35,12 @@ module MergeRequests
super super
end end
# expose issuable create method so it can be called from email
# handler CreateMergeRequestHandler
def create(merge_request)
super
end
private private
def update_merge_requests_head_pipeline(merge_request) def update_merge_requests_head_pipeline(merge_request)
......
...@@ -77,6 +77,7 @@ module MergeRequests ...@@ -77,6 +77,7 @@ module MergeRequests
end end
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end end
end end
......
<<<<<<< HEAD
class JobArtifactUploader < ObjectStoreUploader class JobArtifactUploader < ObjectStoreUploader
storage_options Gitlab.config.artifacts storage_options Gitlab.config.artifacts
=======
class JobArtifactUploader < GitlabUploader
storage :file
>>>>>>> upstream/master
def self.local_store_path def self.local_store_path
Gitlab.config.artifacts.path Gitlab.config.artifacts.path
...@@ -15,8 +20,29 @@ class JobArtifactUploader < ObjectStoreUploader ...@@ -15,8 +20,29 @@ class JobArtifactUploader < ObjectStoreUploader
model.size model.size
end end
<<<<<<< HEAD
private 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 def default_path
creation_date = model.created_at.utc.strftime('%Y_%m_%d') creation_date = model.created_at.utc.strftime('%Y_%m_%d')
......
<<<<<<< HEAD
class LegacyArtifactUploader < ObjectStoreUploader class LegacyArtifactUploader < ObjectStoreUploader
storage_options Gitlab.config.artifacts storage_options Gitlab.config.artifacts
=======
class LegacyArtifactUploader < GitlabUploader
storage :file
>>>>>>> upstream/master
def self.local_store_path def self.local_store_path
Gitlab.config.artifacts.path Gitlab.config.artifacts.path
...@@ -9,8 +14,29 @@ class LegacyArtifactUploader < ObjectStoreUploader ...@@ -9,8 +14,29 @@ class LegacyArtifactUploader < ObjectStoreUploader
File.join(self.local_store_path, 'tmp/uploads/') File.join(self.local_store_path, 'tmp/uploads/')
end end
<<<<<<< HEAD
private 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 def default_path
File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s) File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s)
end end
......
...@@ -201,7 +201,7 @@ ...@@ -201,7 +201,7 @@
= nav_link(controller: [:clusters, :user, :gcp]) do = nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
%span %span
Cluster Clusters
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo? - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do = nav_link(path: 'pipelines#charts') do
......
.issues-footer.text-center - name = issuable_type == 'issue' ? 'issue' : 'merge request'
%button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } }
Email a new issue to this project
#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-dialog{ role: "document" }
.modal-content .modal-content
.modal-header .modal-header
%button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } } %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
%span{ aria: { hidden: "true" } }= icon("times") %span{ aria: { hidden: "true" } }= icon("times")
%h4.modal-title %h4.modal-title
Create new issue by email Create new #{name} by email
.modal-body .modal-body
%p %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 .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 .input-group-btn
= clipboard_button(target: '#issue_email') = clipboard_button(target: '#issuable_email')
%p %p
The subject will be used as the title of the new issue, and the message will be the description. = render 'by_email_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.
%p %p
This is a private email address, generated just for you. 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 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. if that ever happens.
...@@ -25,7 +25,7 @@ ...@@ -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-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: "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" }) = 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") = sprite_icon("screen-full")
.md-write-holder .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 @@ ...@@ -5,12 +5,11 @@
= field.hidden_field :enabled, { class: 'js-toggle-input'} = field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button', %button{ type: 'button',
class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @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'), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @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) - if can?(current_user, :update_cluster, @cluster)
.form-group .form-group
= field.submit _('Save'), class: 'btn btn-success' = 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 - @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") - page_title _("Cluster")
- expanded = Rails.env.test? - expanded = Rails.env.test?
...@@ -28,7 +29,6 @@ ...@@ -28,7 +29,6 @@
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster') %p= s_('ClusterIntegration|See and edit the details for your cluster')
.settings-content .settings-content
- if @cluster.managed? - if @cluster.managed?
= render 'projects/clusters/gcp/show' = 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 @@ ...@@ -2,7 +2,7 @@
- @can_bulk_update = can?(current_user, :admin_issue, @project) - @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues" - 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 - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
...@@ -29,6 +29,6 @@ ...@@ -29,6 +29,6 @@
.issues-holder .issues-holder
= render 'issues' = render 'issues'
- if new_issue_email - 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 - else
= render 'shared/empty_states/issues', button_path: new_project_issue_path(@project) = render 'shared/empty_states/issues', button_path: new_project_issue_path(@project)
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issuable') = webpack_bundle_tag('issuable')
.clearfix.detail-page-header .detail-page-header
.issuable-header .detail-page-header-body
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
= icon('check', class: "hidden-sm hidden-md hidden-lg") = icon('check', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs %span.hidden-xs
...@@ -25,9 +25,6 @@ ...@@ -25,9 +25,6 @@
= icon('circle-o', class: "hidden-sm hidden-md hidden-lg") = icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs Open %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 .issuable-meta
- if @issue.confidential - if @issue.confidential
.issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon') .issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon')
...@@ -35,7 +32,10 @@ ...@@ -35,7 +32,10 @@
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue") = 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 .clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options 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 @@ ...@@ -4,22 +4,22 @@
.alert.alert-danger .alert.alert-danger
%p The source project of this merge request has been removed. %p The source project of this merge request has been removed.
.clearfix.detail-page-header .detail-page-header
.issuable-header .detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) } .issuable-status-box.status-box{ class: status_box_class(@merge_request) }
= icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg") = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs %span.hidden-xs
= @merge_request.state_human_name = @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 .issuable-meta
- if @merge_request.discussion_locked? - if @merge_request.discussion_locked?
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request") = 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 .clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options Options
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project - new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests" - page_title "Merge Requests"
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
...@@ -25,6 +26,8 @@ ...@@ -25,6 +26,8 @@
.merge-requests-holder .merge-requests-holder
= render 'merge_requests' = render 'merge_requests'
- if new_merge_request_email
= render 'projects/issuable_by_email', email: new_merge_request_email, issuable_type: 'merge_request'
- else - else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.git-clone-holder.input-group .git-clone-holder.input-group
.input-group-btn .input-group-btn
- if allowed_protocols_present? - if allowed_protocols_present?
.clone-dropdown-btn.btn.btn-static .clone-dropdown-btn.btn
%span %span
= enabled_project_button(project, enabled_protocol) = enabled_project_button(project, enabled_protocol)
- else - else
......
.detail-page-header.clearfix .detail-page-header
.snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } } .detail-page-header-body
%span.sr-only .snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
= visibility_level_label(@snippet.visibility_level) %span.sr-only
= visibility_level_icon(@snippet.visibility_level, fw: false) = visibility_level_label(@snippet.visibility_level)
%span.creator = visibility_level_icon(@snippet.visibility_level, fw: false)
Authored %span.creator
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') Authored
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} = 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? - if @snippet.project_id?
= render "projects/snippets/actions" = render "projects/snippets/actions"
- else - else
......
...@@ -38,8 +38,7 @@ class EmailReceiverWorker ...@@ -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." "You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError 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." "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, when Gitlab::Email::InvalidRecordError
Gitlab::Email::InvalidIssueError
can_retry = true can_retry = true
e.message e.message
end end
......
class PostReceive class PostReceive
include ApplicationWorker include ApplicationWorker
<<<<<<< HEAD
prepend EE::PostReceive prepend EE::PostReceive
=======
>>>>>>> upstream/master
def perform(gl_repository, identifier, changes) def perform(gl_repository, identifier, changes)
project, is_wiki = Gitlab::GlRepository.parse(gl_repository) project, is_wiki = Gitlab::GlRepository.parse(gl_repository)
......
# Worker for updating any project specific caches. # Worker for updating any project specific caches.
class ProjectCacheWorker class ProjectCacheWorker
include ApplicationWorker include ApplicationWorker
<<<<<<< HEAD
prepend EE::Workers::ProjectCacheWorker prepend EE::Workers::ProjectCacheWorker
=======
>>>>>>> upstream/master
LEASE_TIMEOUT = 15.minutes.to_i 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: ...@@ -786,6 +786,8 @@ test:
# user: YOUR_USERNAME # user: YOUR_USERNAME
pages: pages:
path: tmp/tests/pages path: tmp/tests/pages
artifacts:
path: tmp/tests/artifacts
repositories: repositories:
storages: storages:
default: default:
......
...@@ -490,7 +490,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -490,7 +490,7 @@ constraints(ProjectUrlConstrainer.new) do
get :download_export get :download_export
get :activity get :activity
get :refs get :refs
put :new_issue_address put :new_issuable_address
end end
end end
end end
......
...@@ -413,6 +413,20 @@ ActiveRecord::Schema.define(version: 20171124182517) do ...@@ -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", ["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 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| create_table "ci_pipeline_schedule_variables", force: :cascade do |t|
t.string "key", null: false t.string "key", null: false
t.text "value" t.text "value"
......
...@@ -58,7 +58,9 @@ Runs the following rake tasks: ...@@ -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. 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** **Omnibus Installation**
......
...@@ -17,6 +17,9 @@ Taking the trigger term as `project-name`, the commands are: ...@@ -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 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 | | `/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 ## Issue commands
It is possible to create new issue, display issue details and search up to 5 issues. 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. ...@@ -43,7 +43,10 @@ The following table depicts the various user permission levels in a project.
| See environments | | ✓ | ✓ | ✓ | ✓ | | See environments | | ✓ | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Create new environments | | | ✓ | ✓ | ✓ | | Create new environments | | | ✓ | ✓ | ✓ |
<<<<<<< HEAD
| Manage related issues | | ✓ | ✓ | ✓ | ✓ | | Manage related issues | | ✓ | ✓ | ✓ | ✓ |
=======
>>>>>>> upstream/master
| Stop environments | | | ✓ | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ | | Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ |
......
...@@ -27,6 +27,10 @@ With GitLab merge requests, you can: ...@@ -27,6 +27,10 @@ With GitLab merge requests, you can:
- [Resolve merge conflicts from the UI](#resolve-conflicts) - [Resolve merge conflicts from the UI](#resolve-conflicts)
- Enable [fast-forward merge requests](#fast-forward-merge-requests) - 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 - 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: With **[GitLab Enterprise Edition][ee]**, you can also:
...@@ -138,6 +142,14 @@ those conflicts in the GitLab UI. ...@@ -138,6 +142,14 @@ those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md) [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 ## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing GitLab implements Git's powerful feature to revert any commit with introducing
......
...@@ -50,6 +50,10 @@ module Gitlab ...@@ -50,6 +50,10 @@ module Gitlab
postgresql? && version.to_f >= 9.3 postgresql? && version.to_f >= 9.3
end end
def self.replication_slots_supported?
postgresql? && version.to_f >= 9.4
end
def self.nulls_last_order(field, direction = 'ASC') def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}" order = "#{field} #{direction}"
......
require 'gitlab/email/handler/create_merge_request_handler'
require 'gitlab/email/handler/create_note_handler' require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler' require 'gitlab/email/handler/create_issue_handler'
require 'gitlab/email/handler/unsubscribe_handler' require 'gitlab/email/handler/unsubscribe_handler'
...@@ -11,6 +12,7 @@ module Gitlab ...@@ -11,6 +12,7 @@ module Gitlab
EE::ServiceDeskHandler, EE::ServiceDeskHandler,
UnsubscribeHandler, UnsubscribeHandler,
CreateNoteHandler, CreateNoteHandler,
CreateMergeRequestHandler,
CreateIssueHandler CreateIssueHandler
].freeze ].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 ...@@ -13,8 +13,10 @@ module Gitlab
UserBlockedError = Class.new(ProcessingError) UserBlockedError = Class.new(ProcessingError)
UserNotAuthorizedError = Class.new(ProcessingError) UserNotAuthorizedError = Class.new(ProcessingError)
NoteableNotFoundError = Class.new(ProcessingError) NoteableNotFoundError = Class.new(ProcessingError)
InvalidNoteError = Class.new(ProcessingError) InvalidRecordError = Class.new(ProcessingError)
InvalidIssueError = Class.new(ProcessingError) InvalidNoteError = Class.new(InvalidRecordError)
InvalidIssueError = Class.new(InvalidRecordError)
InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError) UnknownIncomingEmail = Class.new(ProcessingError)
class Receiver class Receiver
......
...@@ -213,11 +213,17 @@ module Gitlab ...@@ -213,11 +213,17 @@ module Gitlab
end end
def shas_with_signatures(repository, shas) def shas_with_signatures(repository, shas)
shas.select do |sha| GitalyClient.migrate(:filter_shas_with_signatures) do |is_enabled|
begin if is_enabled
Rugged::Commit.extract_signature(repository.rugged, sha) Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
rescue Rugged::OdbError else
false shas.select do |sha|
begin
Rugged::Commit.extract_signature(repository.rugged, sha)
rescue Rugged::OdbError
false
end
end
end end
end end
end end
......
...@@ -777,24 +777,21 @@ module Gitlab ...@@ -777,24 +777,21 @@ module Gitlab
end end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch( gitaly_migrate(:revert) do |is_enabled|
branch_name, args = {
start_branch_name: start_branch_name, user: user,
start_repository: start_repository commit: commit,
) do |start_commit| branch_name: branch_name,
message: message,
Gitlab::Git.check_namespace!(commit, start_repository) start_branch_name: start_branch_name,
start_repository: 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, if is_enabled
author: committer, gitaly_operations_client.user_revert(args)
committer: committer, else
tree: revert_tree_id, rugged_revert(args)
parents: [start_commit.sha]) end
end end
end end
...@@ -1783,6 +1780,28 @@ module Gitlab ...@@ -1783,6 +1780,28 @@ module Gitlab
end end
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) def gitaly_add_branch(branch_name, user, target)
gitaly_operation_client.user_create_branch(branch_name, user, target) gitaly_operation_client.user_create_branch(branch_name, user, target)
rescue GRPC::FailedPrecondition => ex rescue GRPC::FailedPrecondition => ex
......
...@@ -250,6 +250,26 @@ module Gitlab ...@@ -250,6 +250,26 @@ module Gitlab
consume_commits_response(response) consume_commits_response(response)
end 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 private
def call_commit_diff(request_params, options = {}) def call_commit_diff(request_params, options = {})
......
...@@ -124,7 +124,31 @@ module Gitlab ...@@ -124,7 +124,31 @@ module Gitlab
end end
def user_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) 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, repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly, user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit: commit.to_gitaly_commit, commit: commit.to_gitaly_commit,
...@@ -137,11 +161,15 @@ module Gitlab ...@@ -137,11 +161,15 @@ module Gitlab
response = GitalyClient.call( response = GitalyClient.call(
@repository.storage, @repository.storage,
:operation_service, :operation_service,
:user_cherry_pick, :"user_#{rpc}",
request, request,
remote_storage: start_repository.storage 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 if response.pre_receive_error.presence
raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error
elsif response.commit_error.presence elsif response.commit_error.presence
......
...@@ -20,9 +20,13 @@ module Gitlab ...@@ -20,9 +20,13 @@ module Gitlab
end end
def self.workers def self.workers
<<<<<<< HEAD
@workers ||= @workers ||=
find_workers(Rails.root.join('app', 'workers')) + find_workers(Rails.root.join('app', 'workers')) +
find_workers(Rails.root.join('ee', 'app', 'workers')) find_workers(Rails.root.join('ee', 'app', 'workers'))
=======
@workers ||= find_workers(Rails.root.join('app', 'workers'))
>>>>>>> upstream/master
end end
def self.default_queues def self.default_queues
......
This diff is collapsed.
...@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do ...@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do
expect(ClusterProvisionWorker).to receive(:perform_async) expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count } expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count } .and change { Clusters::Providers::Gcp.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.cluster).to be_gcp expect(project.clusters.first).to be_gcp
expect(project.cluster).to be_kubernetes expect(project.clusters.first).to be_kubernetes
end end
end end
end end
......
...@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do ...@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do
expect(ClusterProvisionWorker).to receive(:perform_async) expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count } expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Platforms::Kubernetes.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 end
end end
......
...@@ -15,14 +15,72 @@ describe Projects::ClustersController do ...@@ -15,14 +15,72 @@ describe Projects::ClustersController do
sign_in(user) sign_in(user)
end end
context 'when project has a cluster' do context 'when project has one or more clusters' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } 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 end
context 'when project does not have a cluster' do 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
end end
...@@ -146,7 +204,7 @@ describe Projects::ClustersController do ...@@ -146,7 +204,7 @@ describe Projects::ClustersController do
go go
cluster.reload 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(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey expect(cluster.enabled).to be_falsey
end end
...@@ -180,28 +238,77 @@ describe Projects::ClustersController do ...@@ -180,28 +238,77 @@ describe Projects::ClustersController do
sign_in(user) sign_in(user)
end end
context 'when changing parameters' do context 'when format is json' do
let(:params) do context 'when changing parameters' do
{ context 'when valid parameters are used' do
cluster: { let(:params) do
enabled: false, {
name: 'my-new-cluster-name', cluster: {
platform_kubernetes_attributes: { enabled: false,
namespace: 'my-namespace' 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
end
it "updates and redirects back to show page" do context 'when format is html' do
go 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 it "updates and redirects back to show page" do
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) go
expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey cluster.reload
expect(cluster.name).to eq('my-new-cluster-name') expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') 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 end
end end
...@@ -228,6 +335,13 @@ describe Projects::ClustersController do ...@@ -228,6 +335,13 @@ describe Projects::ClustersController do
project_id: project, project_id: project,
id: cluster) id: cluster)
end end
def go_json
put :update, params.merge(namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json)
end
end end
describe 'DELETE destroy' do describe 'DELETE destroy' do
......
...@@ -325,12 +325,12 @@ describe Projects::MergeRequestsController do ...@@ -325,12 +325,12 @@ describe Projects::MergeRequestsController do
end end
context 'when the pipeline succeeds is passed' do context 'when the pipeline succeeds is passed' do
def merge_when_pipeline_succeeds let!(:head_pipeline) do
post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1') create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
end end
before do def merge_when_pipeline_succeeds
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request) post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
end end
it 'returns :merge_when_pipeline_succeeds' do it 'returns :merge_when_pipeline_succeeds' do
...@@ -355,6 +355,18 @@ describe Projects::MergeRequestsController do ...@@ -355,6 +355,18 @@ describe Projects::MergeRequestsController do
project.update_column(:only_allow_merge_if_pipeline_succeeds, true) project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
end 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 it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds merge_when_pipeline_succeeds
......
...@@ -449,11 +449,12 @@ describe ProjectsController do ...@@ -449,11 +449,12 @@ describe ProjectsController do
end end
end end
describe 'PUT #new_issue_address' do describe 'PUT #new_issuable_address for issue' do
subject do subject do
put :new_issue_address, put :new_issuable_address,
namespace_id: project.namespace, namespace_id: project.namespace,
id: project id: project,
issuable_type: 'issue'
user.reload user.reload
end end
...@@ -472,7 +473,35 @@ describe ProjectsController do ...@@ -472,7 +473,35 @@ describe ProjectsController do
end end
it 'changes projects new issue address' do 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
end end
......
...@@ -2,8 +2,13 @@ ...@@ -2,8 +2,13 @@
FactoryGirl.define do FactoryGirl.define do
factory :appearance do factory :appearance do
<<<<<<< HEAD
title "GitLab Enterprise Edition" title "GitLab Enterprise Edition"
description "Open source software to collaborate on code" 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" new_project_guidelines "Custom project guidelines"
end end
end end
...@@ -5,10 +5,13 @@ FactoryGirl.define do ...@@ -5,10 +5,13 @@ FactoryGirl.define do
job factory: :ci_build job factory: :ci_build
file_type :archive file_type :archive
<<<<<<< HEAD
trait :remote_store do trait :remote_store do
file_store JobArtifactUploader::REMOTE_STORE file_store JobArtifactUploader::REMOTE_STORE
end end
=======
>>>>>>> upstream/master
after :build do |artifact| after :build do |artifact|
artifact.project ||= artifact.job.project artifact.project ||= artifact.job.project
end end
......
...@@ -28,5 +28,9 @@ FactoryGirl.define do ...@@ -28,5 +28,9 @@ FactoryGirl.define do
provider_type :gcp provider_type :gcp
provider_gcp factory: [:cluster_provider_gcp, :creating] provider_gcp factory: [:cluster_provider_gcp, :creating]
end end
trait :disabled do
enabled false
end
end end
end end
...@@ -376,16 +376,16 @@ describe 'Issues' do ...@@ -376,16 +376,16 @@ describe 'Issues' do
end end
it 'changes incoming email address token', :js do it 'changes incoming email address token', :js do
find('.issue-email-modal-btn').click find('.issuable-email-modal-btn').click
previous_token = find('input#issue_email').value previous_token = find('input#issuable_email').value
find('.incoming-email-token-reset').click find('.incoming-email-token-reset').click
wait_for_requests wait_for_requests
expect(page).to have_no_field('issue_email', with: previous_token) expect(page).to have_no_field('issuable_email', with: previous_token)
new_token = project1.new_issue_address(user.reload) new_token = project1.new_issuable_address(user.reload, 'issue')
expect(page).to have_field( expect(page).to have_field(
'issue_email', 'issuable_email',
with: new_token with: new_token
) )
end end
...@@ -666,8 +666,8 @@ describe 'Issues' do ...@@ -666,8 +666,8 @@ describe 'Issues' do
end end
it 'click the button to show modal for the new email' do it 'click the button to show modal for the new email' do
page.within '#issue-email-modal' do page.within '#issuable-email-modal' do
email = project.new_issue_address(user) email = project.new_issuable_address(user, 'issue')
expect(page).to have_selector("input[value='#{email}']") expect(page).to have_selector("input[value='#{email}']")
end end
......
...@@ -20,10 +20,14 @@ feature 'Pipelines for Merge Requests', :js do ...@@ -20,10 +20,14 @@ feature 'Pipelines for Merge Requests', :js do
end end
before do before do
visit project_merge_request_path(project, merge_request) merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end end
scenario 'user visits merge request pipelines tab' do 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 page.within('.merge-request-tabs') do
click_link('Pipelines') click_link('Pipelines')
end end
...@@ -31,6 +35,15 @@ feature 'Pipelines for Merge Requests', :js do ...@@ -31,6 +35,15 @@ feature 'Pipelines for Merge Requests', :js do
expect(page).to have_selector('.stage-cell') expect(page).to have_selector('.stage-cell')
end 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 end
context 'without pipelines' do context 'without pipelines' do
......
...@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do ...@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE' click_link 'Create on GKE'
end end
...@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do ...@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do
it 'user sees creation form with the successful message' 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_content('Cluster integration was successfully removed.')
expect(page).to have_link('Create on GKE') expect(page).to have_link('Add cluster')
end end
end end
end end
...@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do ...@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE' click_link 'Create on GKE'
end end
......
...@@ -16,6 +16,7 @@ feature 'User Cluster', :js do ...@@ -16,6 +16,7 @@ feature 'User Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Add an existing cluster' click_link 'Add an existing cluster'
end end
...@@ -94,7 +95,7 @@ feature 'User Cluster', :js do ...@@ -94,7 +95,7 @@ feature 'User Cluster', :js do
it 'user sees creation form with the successful message' 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_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 end
end end
......
...@@ -14,12 +14,78 @@ feature 'Clusters', :js do ...@@ -14,12 +14,78 @@ feature 'Clusters', :js do
context 'when user does not have a cluster and visits cluster index page' do context 'when user does not have a cluster and visits cluster index page' do
before do before do
visit project_clusters_path(project) 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
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 it 'user sees a cluster details page' do
expect(page).to have_button('Create cluster') expect(page).to have_button('Save')
expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end
end end
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