Commit 3d9b6bc2 authored by Bryce Johnson's avatar Bryce Johnson

Merge branch 'master' into backport-issues-controller-changes

parents e4348ae8 bc955cfc
......@@ -643,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
Max: 15
Max: 14
# Limit lines to 80 characters.
Metrics/LineLength:
......
......@@ -407,3 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.6'
......@@ -355,6 +355,8 @@ GEM
activesupport
grape (>= 0.16.0)
rake
grape_logging (1.6.0)
grape
grpc (1.4.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
......@@ -1035,6 +1037,7 @@ DEPENDENCIES
grape (~> 1.0)
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.6)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
......
......@@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
......
......@@ -13,6 +13,10 @@
type: String,
required: true,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
},
mixins: [
pipelinesMixin,
......@@ -95,6 +99,7 @@
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
/>
</div>
</div>
......
......@@ -160,6 +160,9 @@ import initChangesDropdown from './init_changes_dropdown';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
if (page === 'projects:merge_requests:index') {
new UserCallout({ setCalloutPerProject: true });
}
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
......@@ -342,6 +345,7 @@ import initChangesDropdown from './init_changes_dropdown';
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
new UserCallout({ setCalloutPerProject: true });
if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer();
......@@ -361,6 +365,9 @@ import initChangesDropdown from './init_changes_dropdown';
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
break;
case 'projects:pipelines:index':
new UserCallout({ setCalloutPerProject: true });
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
......@@ -418,6 +425,7 @@ import initChangesDropdown from './init_changes_dropdown';
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() {
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
......@@ -569,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown';
case 'edit':
shortcut_handler = new ShortcutsNavigation();
new ProjectNew();
import(/* webpackChunkName: 'project_permissions' */ './projects/permissions')
.then(permissions => permissions.default())
.catch(() => {});
break;
case 'new':
new ProjectNew();
......
......@@ -243,6 +243,7 @@ import bp from './breakpoints';
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
......
<script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import popover from '../../vue_shared/directives/popover';
export default {
export default {
props: {
pipeline: {
type: Object,
required: true,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
},
components: {
userAvatarLink,
},
directives: {
tooltip,
popover,
},
computed: {
user() {
return this.pipeline.user;
},
popoverOptions() {
return {
html: true,
delay: { hide: 600 },
trigger: 'hover',
placement: 'top',
title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>',
content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`,
};
},
},
};
};
</script>
<template>
<div class="table-section section-15 hidden-xs hidden-sm">
<div class="table-section section-15 hidden-xs hidden-sm pipeline-tags">
<a
:href="pipeline.path"
class="js-pipeline-url-link">
......@@ -57,6 +73,13 @@ export default {
:title="pipeline.yaml_errors">
yaml invalid
</span>
<a
v-if="pipeline.flags.auto_devops"
class="js-pipeline-url-autodevops label label-info autodevops-badge"
v-popover="popoverOptions"
role="button">
Auto DevOps
</a>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
......
......@@ -25,8 +25,8 @@
return {
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
......@@ -139,9 +139,7 @@
};
</script>
<template>
<div
class="pipelines-container"
:class="cssClass">
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
......@@ -200,6 +198,7 @@
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath"
/>
</div>
......
......@@ -17,6 +17,10 @@
required: false,
default: false,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
},
components: {
pipelinesTableRowComponent,
......@@ -54,6 +58,7 @@
:key="model.id"
:pipeline="model"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
/>
</div>
</template>
......@@ -25,6 +25,10 @@ export default {
required: false,
default: false,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
},
components: {
asyncButtonComponent,
......@@ -218,7 +222,10 @@ export default {
</div>
</div>
<pipeline-url :pipeline="pipeline" />
<pipeline-url
:pipeline="pipeline"
:auto-devops-help-path="autoDevopsHelpPath"
/>
<div class="table-section section-25">
<div
......
<script>
import projectFeatureToggle from './project_feature_toggle.vue';
export default {
props: {
name: {
type: String,
required: false,
default: '',
},
options: {
type: Array,
required: false,
default: () => [],
},
value: {
type: Number,
required: false,
default: 0,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
},
components: {
projectFeatureToggle,
},
computed: {
featureEnabled() {
return this.value !== 0;
},
displayOptions() {
if (this.featureEnabled) {
return this.options;
}
return [
[0, 'Enable feature to choose access level'],
];
},
displaySelectInput() {
return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
},
},
model: {
prop: 'value',
event: 'change',
},
methods: {
toggleFeature(featureEnabled) {
if (featureEnabled === false || this.options.length < 1) {
this.$emit('change', 0);
} else {
const [firstOptionValue] = this.options[this.options.length - 1];
this.$emit('change', firstOptionValue);
}
},
selectOption(e) {
this.$emit('change', Number(e.target.value));
},
},
};
</script>
<template>
<div class="project-feature-controls" :data-for="name">
<input
v-if="name"
type="hidden"
:name="name"
:value="value"
/>
<project-feature-toggle
:value="featureEnabled"
@change="toggleFeature"
:disabledInput="disabledInput"
/>
<div class="select-wrapper">
<select
class="form-control project-repo-select select-control"
@change="selectOption"
:disabled="displaySelectInput"
>
<option
v-for="[optionValue, optionName] in displayOptions"
:key="optionValue"
:value="optionValue"
:selected="optionValue === value"
>
{{optionName}}
</option>
</select>
<i aria-hidden="true" class="fa fa-chevron-down"></i>
</div>
</div>
</template>
<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>
export default {
props: {
label: {
type: String,
required: false,
default: null,
},
helpPath: {
type: String,
required: false,
default: null,
},
helpText: {
type: String,
required: false,
default: null,
},
},
};
</script>
<template>
<div class="project-feature-row">
<label v-if="label" class="label-light">
{{label}}
<a v-if="helpPath" :href="helpPath" target="_blank">
<i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i>
</a>
</label>
<span v-if="helpText" class="help-block">
{{helpText}}
</span>
<slot />
</div>
</template>
export const visibilityOptions = {
PRIVATE: 0,
INTERNAL: 10,
PUBLIC: 20,
};
export const visibilityLevelDescriptions = {
[visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
[visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.',
[visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.',
};
const selectorCache = [];
// workaround since we don't have a polyfill for classList.toggle 2nd parameter
export function toggleHiddenClass(element, hidden) {
if (hidden) {
element.classList.add('hidden');
} else {
element.classList.remove('hidden');
}
}
// hide external feature-specific settings when a given feature is disabled
export function toggleHiddenClassBySelector(selector, hidden) {
if (!selectorCache[selector]) {
selectorCache[selector] = document.querySelectorAll(selector);
}
selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden));
}
import Vue from 'vue';
import settingsPanel from './components/settings_panel.vue';
export default function initProjectPermissionsSettings() {
const mountPoint = document.querySelector('.js-project-permissions-form');
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
const componentProps = JSON.parse(componentPropsEl.innerHTML);
return new Vue({
el: mountPoint,
render: createElement => createElement(settingsPanel, { props: { ...componentProps } }),
});
}
......@@ -27,7 +27,7 @@ export default {
listEmptyMessage() {
return this.searchFailed ?
s__('ProjectsDropdown|Something went wrong on our end.') :
s__('ProjectsDropdown|No projects matched your query');
s__('ProjectsDropdown|Sorry, no projects matched your search');
},
},
};
......
......@@ -53,7 +53,7 @@ export default {
class="form-control"
ref="search"
v-model="searchQuery"
:placeholder="s__('ProjectsDropdown|Search projects')"
:placeholder="s__('ProjectsDropdown|Search your projects')"
/>
<i
v-if="!searchQuery"
......
......@@ -41,4 +41,8 @@ export default function initSettingsPanels() {
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
});
if (location.hash) {
expandSection($(location.hash));
}
}
import Cookies from 'js-cookie';
export default class UserCallout {
constructor(className = 'user-callout') {
constructor(options = {}) {
this.options = options;
const className = this.options.className || 'user-callout';
this.userCalloutBody = $(`.${className}`);
this.cookieName = this.userCalloutBody.data('uid');
this.isCalloutDismissed = Cookies.get(this.cookieName);
......@@ -17,7 +21,11 @@ export default class UserCallout {
dismissCallout(e) {
const $currentTarget = $(e.currentTarget);
if (this.options.setCalloutPerProject) {
Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('project-path') });
} else {
Cookies.set(this.cookieName, 'true', { expires: 365 });
}
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
......
/**
* Helper to user bootstrap popover in vue.js.
* Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover
*
* @example
* import popover from 'vue_shared/directives/popover.js';
* {
* directives: [popover]
* }
* <a v-popover="{options}">popover</a>
*/
export default {
bind(el, binding) {
$(el).popover(binding.value);
},
unbind(el) {
$(el).popover('destroy');
},
};
......@@ -19,6 +19,7 @@
@import "framework/flash";
@import "framework/forms";
@import "framework/gfm";
@import "framework/gitlab-theme";
@import "framework/header";
@import "framework/highlight";
@import "framework/issue_box";
......
......@@ -734,6 +734,11 @@
overflow: hidden;
}
@mixin dropdown-item-hover {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
// TODO: change global style and remove mixin
@mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu,
......@@ -760,6 +765,10 @@
padding: 8px 16px;
}
&.droplab-item-active button {
@include dropdown-item-hover;
}
a,
button,
.menu-item {
......@@ -779,6 +788,8 @@
&:hover,
&:active,
&:focus {
@include dropdown-item-hover;
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
......@@ -837,17 +848,30 @@
}
}
@media (max-width: $screen-xs-max) {
.navbar-gitlab {
li.header-projects,
li.header-more,
li.header-new,
li.header-user {
position: static;
}
}
header.navbar-gitlab .dropdown {
.dropdown-menu,
.dropdown-menu-nav {
width: 100%;
min-width: 100%;
}
}
}
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
@media (max-width: $screen-xs-max) {
display: table;
left: -50px;
min-width: 300px;
}
}
.projects-dropdown-container {
......
/**
* Styles the GitLab application with a specific color theme
*/
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
// Header
header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800);
.navbar-collapse {
color: $color-200;
}
.container-fluid {
.navbar-toggle {
border-left: 1px solid lighten($color-700, 10%);
}
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
background-color: rgba($color-200, .2);
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
svg {
fill: currentColor;
}
}
&.line-separator {
border-left: 1px solid rgba($color-200, .2);
}
}
}
.navbar-sub-nav {
color: $color-200;
}
.nav {
> li {
color: $color-200;
> a {
svg {
fill: $color-200;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
}
}
&:hover,
&:focus {
@media (min-width: $screen-sm-min) {
background-color: rgba($color-200, .2);
}
svg {
fill: currentColor;
}
}
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
&:hover {
svg {
fill: $color-900;
}
}
}
.impersonated-user,
.impersonated-user:hover {
svg {
fill: $color-900;
}
}
}
}
}
.title {
> a {
&:hover,
&:focus {
background-color: rgba($color-200, .2);
}
}
}
.search {
form {
background-color: rgba($color-200, .2);
&:hover {
background-color: rgba($color-200, .3);
}
}
.location-badge {
color: $color-100;
background-color: rgba($color-200, .1);
border-right: 1px solid $color-800;
}
.search-input::placeholder {
color: rgba($color-200, .8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($color-200, .8);
}
}
&.search-active {
form {
background-color: $white-light;
}
.location-badge {
color: $gl-text-color;
}
.search-input-wrap {
.search-icon {
color: rgba($color-200, .8);
}
}
}
}
.btn-sign-in {
background-color: $color-100;
color: $color-900;
}
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700;
> a {
color: $color-900;
}
svg {
fill: $color-900;
}
}
}
body {
&.ui_indigo {
@include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
}
&.ui_dark {
@include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
}
&.ui_blue {
@include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
}
&.ui_green {
@include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
}
&.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new {
background: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color;
.logo-text svg {
fill: $theme-gray-900;
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
color: $theme-gray-900;
}
&.active > a {
color: $white-light;
&:hover {
color: $white-light;
}
}
}
}
.container-fluid {
.navbar-toggle,
.navbar-toggle:hover {
color: $theme-gray-700;
border-left: 1px solid $theme-gray-200;
}
}
}
.search {
form {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $border-color;
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100;
.location-badge {
box-shadow: inset 0 0 0 1px $blue-100;
}
}
}
.search-input-wrap {
.search-icon {
color: $theme-gray-200;
}
}
.location-badge {
color: $theme-gray-700;
box-shadow: inset 0 0 0 1px $border-color;
background-color: $nav-badge-bg;
border-right: 0;
}
}
.nav-sidebar li.active {
> a {
color: $theme-gray-900;
}
svg {
fill: $theme-gray-900;
}
}
}
}
......@@ -111,7 +111,6 @@ header {
svg {
height: 16px;
width: 23px;
fill: currentColor;
}
}
......
......@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
// GitLab themes
$indigo-50: #f7f7ff;
$indigo-100: #ebebfa;
$indigo-200: #d1d1f0;
......@@ -86,6 +88,43 @@ $indigo-800: #393982;
$indigo-900: #292961;
$indigo-950: #1a1a40;
$theme-gray-50: #fafafa;
$theme-gray-100: #f2f2f2;
$theme-gray-200: #dfdfdf;
$theme-gray-300: #cccccc;
$theme-gray-400: #bababa;
$theme-gray-500: #a7a7a7;
$theme-gray-600: #949494;
$theme-gray-700: #707070;
$theme-gray-800: #4f4f4f;
$theme-gray-900: #2e2e2e;
$theme-gray-950: #1f1f1f;
$theme-blue-50: #f4f8fc;
$theme-blue-100: #e6edf5;
$theme-blue-200: #c8d7e6;
$theme-blue-300: #97b3cf;
$theme-blue-400: #648cb4;
$theme-blue-500: #4a79a8;
$theme-blue-600: #3e6fa0;
$theme-blue-700: #305c88;
$theme-blue-800: #25496e;
$theme-blue-900: #1a3652;
$theme-blue-950: #0f2235;
$theme-green-50: #f2faf6;
$theme-green-100: #e4f3ea;
$theme-green-200: #c0dfcd;
$theme-green-300: #8ac2a1;
$theme-green-400: #52a274;
$theme-green-500: #35935c;
$theme-green-600: #288a50;
$theme-green-700: #1c7441;
$theme-green-800: #145d33;
$theme-green-900: #0d4524;
$theme-green-950: #072d16;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
......@@ -540,6 +579,11 @@ $project-breadcrumb-color: #999;
$project-private-forks-notice-odd: $green-600;
$project-network-controls-color: #888;
$feature-toggle-color: #fff;
$feature-toggle-text-color: #fff;
$feature-toggle-color-disabled: #999;
$feature-toggle-color-enabled: #4a8bee;
/*
* Runners
*/
......
......@@ -9,10 +9,20 @@
header.navbar-gitlab-new {
color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
min-height: $new-navbar-height;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.header-content {
display: -webkit-flex;
display: flex;
......@@ -38,10 +48,10 @@ header.navbar-gitlab-new {
img {
height: 28px;
margin-right: 10px;
margin-right: 8px;
}
> a {
a {
display: -webkit-flex;
display: flex;
align-items: center;
......@@ -54,22 +64,6 @@ header.navbar-gitlab-new {
margin-right: 8px;
}
}
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
&:hover,
&:focus {
background-color: rgba($indigo-200, .2);
}
}
}
......@@ -106,7 +100,6 @@ header.navbar-gitlab-new {
.navbar-collapse {
padding-left: 0;
color: $indigo-200;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
......@@ -132,7 +125,6 @@ header.navbar-gitlab-new {
font-size: 14px;
text-align: center;
color: currentColor;
border-left: 1px solid lighten($indigo-700, 10%);
&:hover,
&:focus,
......@@ -167,42 +159,27 @@ header.navbar-gitlab-new {
will-change: color;
margin: 4px 2px;
padding: 6px 8px;
color: $indigo-200;
height: 32px;
@media (max-width: $screen-xs-max) {
padding: 0;
}
svg {
fill: $indigo-200;
}
&.header-user-dropdown-toggle {
margin-left: 2px;
.header-user-avatar {
border-color: $indigo-200;
margin-right: 0;
}
}
}
.header-new-dropdown-toggle {
margin-right: 0;
}
> a:hover,
> a:focus {
&:hover,
&:focus {
text-decoration: none;
outline: 0;
opacity: 1;
color: $white-light;
@media (min-width: $screen-sm-min) {
background-color: rgba($indigo-200, .2);
}
svg {
fill: currentColor;
}
......@@ -213,6 +190,11 @@ header.navbar-gitlab-new {
}
}
}
}
.header-new-dropdown-toggle {
margin-right: 0;
}
.impersonated-user,
.impersonated-user:hover {
......@@ -220,10 +202,6 @@ header.navbar-gitlab-new {
background-color: $white-light;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
svg {
fill: $indigo-900;
}
}
.impersonation-btn,
......@@ -241,8 +219,6 @@ header.navbar-gitlab-new {
&.active > a,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg {
fill: currentColor;
......@@ -256,7 +232,6 @@ header.navbar-gitlab-new {
display: -webkit-flex;
display: flex;
margin: 0 0 0 6px;
color: $indigo-200;
.dropdown-chevron {
position: relative;
......@@ -274,17 +249,6 @@ header.navbar-gitlab-new {
text-decoration: none;
outline: 0;
color: $white-light;
background-color: rgba($indigo-200, .2);
svg {
fill: currentColor;
}
}
&.active > a,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg {
fill: currentColor;
......@@ -309,7 +273,6 @@ header.navbar-gitlab-new {
}
&.line-separator {
border-left: 1px solid rgba($indigo-200, .2);
margin: 8px;
}
}
......@@ -339,17 +302,14 @@ header.navbar-gitlab-new {
height: 32px;
border: 0;
border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2);
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
background-color: rgba($indigo-200, .3);
box-shadow: none;
}
}
&.search-active form {
background-color: $white-light;
box-shadow: none;
.search-input {
......@@ -377,43 +337,26 @@ header.navbar-gitlab-new {
}
.search-input::placeholder {
color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($indigo-200, .8);
}
}
&.search-active {
.location-badge {
color: $gl-text-color;
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
.search-icon {
color: rgba($indigo-200, .8);
}
.clear-icon {
color: $white-light;
}
......@@ -517,8 +460,6 @@ header.navbar-gitlab-new {
.btn-sign-in {
margin-top: 3px;
background-color: $indigo-100;
color: $indigo-900;
font-weight: $gl-font-weight-bold;
&:hover {
......
......@@ -155,16 +155,9 @@ $new-sidebar-collapsed-width: 50px;
}
li.active {
box-shadow: inset 4px 0 0 $active-border;
> a {
color: $active-color;
font-weight: $gl-font-weight-bold;
}
svg {
fill: $active-color;
}
}
@media (max-width: $screen-xs-max) {
......
......@@ -226,6 +226,14 @@
vertical-align: baseline;
}
a.autodevops-badge {
color: $white-light;
}
a.autodevops-link {
color: $gl-link-color;
}
.commit-row-description {
font-size: 14px;
padding: 10px 15px;
......
......@@ -202,6 +202,10 @@
.btn-group.open .dropdown-toggle {
box-shadow: none;
}
.pipeline-tags .label-container {
white-space: normal;
}
}
.stage-cell {
......@@ -932,3 +936,8 @@ button.mini-pipeline-graph-dropdown-toggle {
.pipelines-container .top-area .nav-controls > .btn:last-child {
float: none;
}
.autodevops-title {
font-weight: $gl-font-weight-normal;
line-height: 1.5;
}
@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
.one {
background-color: $color-1;
border-top-left-radius: $border-radius-default;
}
.two {
background-color: $color-2;
border-top-right-radius: $border-radius-default;
}
.three {
background-color: $color-3;
border-bottom-left-radius: $border-radius-default;
}
.four {
background-color: $color-4;
border-bottom-right-radius: $border-radius-default;
}
}
.application-theme {
label {
margin-right: 20px;
text-align: center;
}
.preview {
font-size: 0;
margin-bottom: 10px;
&.indigo {
@include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
}
&.dark {
@include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
}
&.light {
@include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
}
&.blue {
@include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
}
&.green {
@include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
}
}
.preview-row {
display: block;
}
.quadrant {
display: inline-block;
height: 50px;
width: 80px;
}
}
.syntax-theme {
label {
margin-right: 20px;
......
......@@ -10,41 +10,6 @@
.edit-project,
.import-project {
.sharing-and-permissions {
.header {
padding-top: $gl-vert-padding;
}
.label-light {
margin-bottom: 0;
}
.help-block {
margin-top: 0;
}
.form-group {
margin-bottom: 5px;
}
> .form-group {
padding-left: 0;
}
select option[disabled] {
display: none;
}
}
select {
transition: background 2s ease-out;
&.highlight-changes {
background: $highlight-changes-color;
transition: none;
}
}
.help-block {
margin-bottom: 10px;
}
......@@ -90,6 +55,162 @@
}
}
.toggle-wrapper {
margin-top: 5px;
}
.project-feature-row > .toggle-wrapper {
margin: 10px 0;
}
.project-visibility-setting,
.project-feature-settings {
border: 1px solid $border-color;
padding: 10px 32px;
@media (max-width: $screen-xs-min) {
padding: 10px 20px;
}
}
.project-visibility-setting .request-access {
line-height: 2;
}
.project-feature-settings {
background: $gray-lighter;
border-top: none;
margin-bottom: 16px;
}
.project-repo-select {
transition: background 2s ease-out;
&:disabled {
opacity: 0.75;
}
.highlight-changes & {
background: $highlight-changes-color;
transition: none;
}
}
.project-feature-controls {
display: flex;
align-items: center;
margin: 8px 0;
max-width: 432px;
.toggle-wrapper {
flex: 0;
margin-right: 10px;
}
.select-wrapper {
flex: 1;
}
}
.project-feature-setting-group {
padding-left: 32px;
.project-feature-controls {
max-width: 400px;
}
@media (max-width: $screen-xs-min) {
padding-left: 20px;
}
}
.project-feature-toggle {
position: relative;
border: none;
outline: 0;
display: block;
width: 100px;
height: 24px;
cursor: pointer;
user-select: none;
background: $feature-toggle-color-disabled;
border-radius: 12px;
padding: 3px;
transition: all .4s ease;
&::selection,
&::before::selection,
&::after::selection {
background: none;
}
&::before {
color: $feature-toggle-text-color;
font-size: 12px;
line-height: 24px;
position: absolute;
top: 0;
left: 25px;
right: 5px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
animation: animate-disabled .2s ease-in;
content: attr(data-disabled-text);
}
&::after {
position: relative;
display: block;
content: "";
width: 22px;
height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
}
&.checked {
background: $feature-toggle-color-enabled;
&::before {
left: 5px;
right: 25px;
animation: animate-enabled .2s ease-in;
content: attr(data-enabled-text);
}
&::after {
left: calc(100% - 22px);
}
}
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: $screen-xs-min) {
width: 50px;
&::before,
&.checked::before {
display: none;
}
}
@keyframes animate-enabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes animate-disabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
}
.project-home-panel,
.group-home-panel {
padding-top: 24px;
......
......@@ -43,8 +43,10 @@
display: inline-block;
}
.blob-viewer[data-type="rich"] {
@media (min-width: $screen-md-min) {
.blob-viewer[data-type="rich"] {
margin: 20px;
}
}
.repository-view {
......
......@@ -2,7 +2,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
before_action :finder, only: [:edit, :update, :destroy]
def index
@broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page])
@broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new
end
......
class Admin::DashboardController < Admin::ApplicationController
def index
@projects = Project.without_deleted.with_route.limit(10)
@users = User.limit(10)
@groups = Group.with_route.limit(10)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
end
end
......@@ -17,7 +17,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def keys
@keys = user.keys
@keys = user.keys.order_id_desc
end
def new
......@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
:theme_id,
:twitter,
:username,
:website_url
......
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
@sort = params[:sort] || 'id_desc'
@groups =
if params[:parent_id] && Group.supports_nested_groups?
parent = Group.find_by(id: params[:parent_id])
......@@ -15,7 +17,7 @@ class Dashboard::GroupsController < Dashboard::ApplicationController
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.includes(:route)
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.sort(@sort)
@groups = @groups.page(params[:page])
respond_to do |format|
......
class Profiles::EmailsController < Profiles::ApplicationController
def index
@primary = current_user.email
@emails = current_user.emails
@emails = current_user.emails.order_id_desc
end
def create
......
......@@ -2,7 +2,7 @@ class Profiles::KeysController < Profiles::ApplicationController
skip_before_action :authenticate_user!, only: [:get_keys]
def index
@keys = current_user.keys
@keys = current_user.keys.order_id_desc
@key = Key.new
end
......
......@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id,
:layout,
:dashboard,
:project_view
:project_view,
:theme_id
)
end
end
......@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
......
......@@ -6,7 +6,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end
def update
if @project.update_attributes(update_params)
if @project.update(update_params)
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to project_settings_ci_cd_path(@project)
else
......@@ -16,14 +16,12 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
private
def create_params
params.require(:pipeline).permit(:ref)
end
def update_params
params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :auto_cancel_pending_pipelines, :ci_config_path
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
auto_devops_attributes: [:id, :domain, :enabled]
)
end
end
......@@ -47,6 +47,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
def import
@projects = current_user.authorized_projects.order_id_desc
end
def apply_import
source_project = Project.find(params[:source_project_id])
......
......@@ -8,6 +8,7 @@ module Projects
define_secret_variables
define_triggers_variables
define_badges_variables
define_auto_devops_variables
end
private
......@@ -42,6 +43,10 @@ module Projects
badge.new(@project, @ref).metadata
end
end
def define_auto_devops_variables
@auto_devops = @project.auto_devops || ProjectAutoDevops.new
end
end
end
end
......@@ -9,6 +9,7 @@ class MoveToProjectFinder
projects = @user.projects_where_can_admin_issues
projects = projects.search(search) if search.present?
projects = projects.excluding_project(from_project)
projects = projects.order_id_desc
# infinite scroll using offset
projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
......
......@@ -121,7 +121,7 @@ class ProjectsFinder < UnionFinder
end
def sort(items)
params[:sort].present? ? items.sort(params[:sort]) : items
params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc
end
def by_archived(projects)
......
......@@ -118,7 +118,7 @@ class TodosFinder
end
def sort(items)
params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
params[:sort] ? items.sort(params[:sort]) : items.order_id_desc
end
def by_action(items)
......
......@@ -115,6 +115,7 @@ module ApplicationSettingsHelper
:after_sign_up_text,
:akismet_api_key,
:akismet_enabled,
:auto_devops_enabled,
:clientside_sentry_dsn,
:clientside_sentry_enabled,
:container_registry_token_expire_delay,
......
module AutoDevopsHelper
def show_auto_devops_callout?(project)
show_callout?('auto_devops_settings_dismissed') &&
can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled?
end
end
......@@ -3,6 +3,10 @@ module GroupsHelper
can?(current_user, :change_visibility_level, group)
end
def can_change_share_with_group_lock?(group)
can?(current_user, :change_share_with_group_lock, group)
end
def group_icon(group)
if group.is_a?(String)
group = Group.find_by_full_path(group)
......@@ -65,6 +69,20 @@ module GroupsHelper
{ group_name: group.name }
end
def share_with_group_lock_help_text(group)
return default_help unless group.parent&.share_with_group_lock?
if group.share_with_group_lock?
if can?(current_user, :change_share_with_group_lock, group.parent)
ancestor_locked_but_you_can_override(group)
else
ancestor_locked_so_ask_the_owner(group)
end
else
ancestor_locked_and_has_been_overridden(group)
end
end
private
def group_title_link(group, hidable: false, show_avatar: false)
......@@ -80,4 +98,45 @@ module GroupsHelper
output.html_safe
end
end
def ancestor_group(group)
ancestor = oldest_consecutively_locked_ancestor(group)
if can?(current_user, :read_group, ancestor)
link_to ancestor.name, group_path(ancestor)
else
ancestor.name
end
end
def remove_the_share_with_group_lock_from_ancestor(group)
ancestor = oldest_consecutively_locked_ancestor(group)
text = s_("GroupSettings|remove the share with group lock from %{ancestor_group_name}") % { ancestor_group_name: ancestor.name }
if can?(current_user, :admin_group, ancestor)
link_to text, edit_group_path(ancestor)
else
text
end
end
def oldest_consecutively_locked_ancestor(group)
group.ancestors.find do |group|
!group.has_parent? || !group.parent.share_with_group_lock?
end
end
def default_help
s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.")
end
def ancestor_locked_but_you_can_override(group)
s_("GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
end
def ancestor_locked_so_ask_the_owner(group)
s_("GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
end
def ancestor_locked_and_has_been_overridden(group)
s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) }
end
end
......@@ -56,7 +56,7 @@ module IssuesHelper
end
def project_options(issuable, current_user, ability: :read_project)
projects = current_user.authorized_projects
projects = current_user.authorized_projects.order_id_desc
projects = projects.select do |project|
current_user.can?(ability, project)
end
......
......@@ -40,6 +40,10 @@ module PreferencesHelper
]
end
def user_application_theme
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
......
......@@ -15,9 +15,13 @@ module ProjectsHelper
end
def link_to_member_avatar(author, opts = {})
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
default_opts = { size: 16 }
opts = default_opts.merge(opts)
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar]
classes = %W[avatar avatar-inline s#{opts[:size]}]
classes << opts[:avatar_class] if opts[:avatar_class]
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: classes, alt: '')
end
def link_to_member(project, author, opts = {}, &block)
......@@ -29,7 +33,7 @@ module ProjectsHelper
author_html = ""
# Build avatar image tag
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
author_html << link_to_member_avatar(author, opts) if opts[:avatar]
# Build name span tag
if opts[:by_username]
......@@ -541,6 +545,43 @@ module ProjectsHelper
current_application_settings.restricted_visibility_levels || []
end
def project_permissions_settings(project)
feature = project.project_feature
{
visibilityLevel: project.visibility_level,
requestAccessEnabled: !!project.request_access_enabled,
issuesAccessLevel: feature.issues_access_level,
repositoryAccessLevel: feature.repository_access_level,
mergeRequestsAccessLevel: feature.merge_requests_access_level,
buildsAccessLevel: feature.builds_access_level,
wikiAccessLevel: feature.wiki_access_level,
snippetsAccessLevel: feature.snippets_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled
}
end
def project_permissions_panel_data(project)
data = {
currentSettings: project_permissions_settings(project),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
allowedVisibilityOptions: project_allowed_visibility_levels(project),
visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
}
data.to_json.html_safe
end
def project_allowed_visibility_levels(project)
Gitlab::VisibilityLevel.values.select do |level|
project.visibility_level_allowed?(level) && !restricted_levels.include?(level)
end
end
def find_file_path
return unless @project && !@project.empty_repo?
......
......@@ -92,7 +92,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group|
current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
{
category: "Groups",
id: group.id,
......@@ -104,7 +104,7 @@ module SearchHelper
# Autocomplete results for the current user's projects
def projects_autocomplete(term, limit = 5)
current_user.authorized_projects.search_by_title(term)
current_user.authorized_projects.order_id_desc.search_by_title(term)
.sorted_by_stars.non_archived.limit(limit).map do |p|
{
category: "Projects",
......
......@@ -33,7 +33,7 @@ class BroadcastMessage < ActiveRecord::Base
end
def self.current_and_future_messages
where('ends_at > :now', now: Time.zone.now).reorder(id: :asc)
where('ends_at > :now', now: Time.zone.now).order_id_asc
end
def active?
......
......@@ -216,6 +216,7 @@ module Ci
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += project.deployment_variables if has_environment?
variables += project.auto_devops_variables
variables += yaml_variables
variables += user_variables
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
......
......@@ -38,6 +38,7 @@ module Ci
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
after_initialize :set_config_source, if: :new_record?
after_create :keep_around_commits, unless: :importing?
enum source: {
......@@ -50,6 +51,12 @@ module Ci
external: 6
}
enum config_source: {
unknown_source: nil,
repository_source: 1,
auto_devops_source: 2
}
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
......@@ -316,6 +323,14 @@ module Ci
builds.latest.failed_but_allowed.any?
end
def set_config_source
if ci_yaml_from_repo
self.config_source = :repository_source
elsif implied_ci_yaml_file
self.config_source = :auto_devops_source
end
end
def config_processor
return unless ci_yaml_file
return @config_processor if defined?(@config_processor)
......@@ -342,11 +357,17 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
@ci_yaml_file = begin
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue Rugged::ReferenceError, GRPC::NotFound, GRPC::Internal
self.yaml_errors =
"Failed to load CI/CD config file at #{ci_yaml_file_path}"
@ci_yaml_file =
if auto_devops_source?
implied_ci_yaml_file
else
ci_yaml_from_repo
end
if @ci_yaml_file
@ci_yaml_file
else
self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
nil
end
end
......@@ -434,6 +455,23 @@ module Ci
private
def ci_yaml_from_repo
return unless project
return unless sha
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal
nil
end
def implied_ci_yaml_file
return unless project
if project.auto_devops_enabled?
Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
end
end
def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self)
end
......
......@@ -6,10 +6,6 @@ module Sortable
extend ActiveSupport::Concern
included do
# By default all models should be ordered
# by created_at field starting from newest
default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
......
......@@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
before_create :sync_share_with_group_lock_with_parent
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? }
# Legacy Storage specific hooks
after_update :move_dir, if: :path_changed?
......@@ -219,4 +223,14 @@ class Namespace < ActiveRecord::Base
errors.add(:parent_id, "has too deep level of nesting")
end
end
def sync_share_with_group_lock_with_parent
if parent&.share_with_group_lock?
self.share_with_group_lock = true
end
end
def force_share_with_group_lock_on_descendants
descendants.update_all(share_with_group_lock: true)
end
end
......@@ -187,9 +187,12 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
......@@ -466,6 +469,18 @@ class Project < ActiveRecord::Base
self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
def auto_devops_enabled?
if auto_devops&.enabled.nil?
current_application_settings.auto_devops_enabled?
else
auto_devops.enabled?
end
end
def has_auto_devops_implicitly_disabled?
auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
end
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end
......@@ -1378,6 +1393,10 @@ class Project < ActiveRecord::Base
Gitlab::Utils.slugify(full_path.to_s)
end
def has_ci?
repository.gitlab_ci_yml || auto_devops_enabled?
end
def predefined_variables
[
{ key: 'CI_PROJECT_ID', value: id.to_s, public: true },
......@@ -1423,6 +1442,12 @@ class Project < ActiveRecord::Base
deployment_service.predefined_variables
end
def auto_devops_variables
return [] unless auto_devops_enabled?
auto_devops&.variables || []
end
def append_or_update_attribute(name, value)
old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
......
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def variables
variables = []
variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
variables
end
end
......@@ -35,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
......@@ -72,7 +73,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> do
......@@ -259,11 +260,13 @@ class User < ActiveRecord::Base
end
def sort(method)
case method.to_s
order_method = method || 'id_desc'
case order_method.to_s
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
else
order_by(method)
order_by(order_method)
end
end
......@@ -371,7 +374,7 @@ class User < ActiveRecord::Base
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
Key.find_by(id: key_id)&.user
end
def find_by_full_path(path, follow_redirects: false)
......
......@@ -15,6 +15,11 @@ class GroupPolicy < BasePolicy
condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
condition(:has_parent, scope: :subject) { @subject.has_parent? }
condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? }
condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? }
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
......@@ -54,6 +59,8 @@ class GroupPolicy < BasePolicy
rule { ~can?(:view_globally) }.prevent :request_access
rule { has_access }.prevent :request_access
rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock
def access_level
return GroupMember::NO_ACCESS if @user.nil?
......
......@@ -16,6 +16,7 @@ class PipelineEntity < Grape::Entity
expose :flags do
expose :latest?, as: :latest
expose :stuck?, as: :stuck
expose :auto_devops_source?, as: :auto_devops
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
......
......@@ -16,8 +16,8 @@ module Ci
protected: project.protected_for?(ref)
)
result = validate(current_user,
ignore_skip_ci: ignore_skip_ci,
result = validate_project_and_git_items ||
validate_pipeline(ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors)
return result if result
......@@ -47,13 +47,13 @@ module Ci
private
def validate(triggering_user, ignore_skip_ci:, save_on_errors:)
def validate_project_and_git_items
unless project.builds_enabled?
return error('Pipeline is disabled')
end
unless allowed_to_trigger_pipeline?(triggering_user)
if can?(triggering_user, :create_pipeline, project)
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{ref}'")
else
return error('Insufficient permissions to create a new pipeline')
......@@ -67,7 +67,9 @@ module Ci
unless commit
return error('Commit not found')
end
end
def validate_pipeline(ignore_skip_ci:, save_on_errors:)
unless pipeline.config_processor
unless pipeline.ci_yaml_file
return error("Missing #{pipeline.ci_yaml_file_path} file")
......@@ -85,18 +87,18 @@ module Ci
end
end
def allowed_to_trigger_pipeline?(triggering_user)
if triggering_user
allowed_to_create?(triggering_user)
def allowed_to_trigger_pipeline?
if current_user
allowed_to_create?
else # legacy triggers don't have a corresponding user
!project.protected_for?(ref)
end
end
def allowed_to_create?(triggering_user)
access = Gitlab::UserAccess.new(triggering_user, project: project)
def allowed_to_create?
return unless can?(current_user, :create_pipeline, project)
can?(triggering_user, :create_pipeline, project) &&
access = Gitlab::UserAccess.new(current_user, project: project)
if branch?
access.can_update_branch?(ref)
elsif tag?
......
module UpdateVisibilityLevel
def valid_visibility_level_change?(target, new_visibility)
# check that user is allowed to set specified visibility_level
if new_visibility && new_visibility.to_i != target.visibility_level
unless can?(current_user, :change_visibility_level, target) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(target, new_visibility)
return false
end
end
true
end
end
module Groups
class UpdateService < Groups::BaseService
include UpdateVisibilityLevel
def execute
reject_parent_id!
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
unless can?(current_user, :change_visibility_level, group) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
return false unless valid_visibility_level_change?(group, params[:visibility_level])
deny_visibility_level(group, new_visibility)
return group
end
end
return false unless valid_share_with_group_lock_change?
group.assign_attributes(params)
......@@ -30,5 +25,19 @@ module Groups
def reject_parent_id!
params.except!(:parent_id)
end
def valid_share_with_group_lock_change?
return true unless changing_share_with_group_lock?
return true if can?(current_user, :change_share_with_group_lock, group)
group.errors.add(:share_with_group_lock, s_('GroupSettings|cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group'))
false
end
def changing_share_with_group_lock?
return false if params[:share_with_group_lock].nil?
params[:share_with_group_lock] != group.share_with_group_lock
end
end
end
module Projects
class UpdateService < BaseService
include UpdateVisibilityLevel
def execute
unless visibility_level_allowed?
unless valid_visibility_level_change?(project, params[:visibility_level])
return error('New visibility level not allowed!')
end
......@@ -28,22 +30,6 @@ module Projects
private
def visibility_level_allowed?
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return false
end
end
true
end
def renaming_project_with_container_registry_tags?
new_path = params[:path]
......
......@@ -115,7 +115,7 @@ module QuickActions
if issuable.allows_multiple_assignees?
issuable.assignees.pluck(:id) + users.map(&:id)
else
[users.last.id]
[users.first.id]
end
end
......
......@@ -9,18 +9,17 @@ module TestHooks
end
def execute
trigger_key = hook.class::TRIGGERS.key(trigger.to_sym)
trigger_data_method = "#{trigger}_data"
if !self.respond_to?(trigger_data_method, true) ||
!hook.class::TRIGGERS.value?(trigger.to_sym)
if trigger_key.nil? || !self.respond_to?(trigger_data_method, true)
return error('Testing not available for this hook')
end
error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
return hook.execute(sample_data, trigger)
return hook.execute(sample_data, trigger_key)
end
error(error_message)
......
......@@ -19,7 +19,7 @@ class WebHookService
def initialize(hook, data, hook_name)
@hook = hook
@data = data
@hook_name = hook_name
@hook_name = hook_name.to_s
end
def execute
......
......@@ -226,7 +226,17 @@
.help-block 0 for unlimited
%fieldset
%legend Continuous Integration
%legend Continuous Integration and Deployment
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :auto_devops_enabled do
= f.check_box :auto_devops_enabled
Enabled Auto DevOps (Beta) for projects by default
.help-block
It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
......
......@@ -28,17 +28,20 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
= render 'group_admin_settings', f: f
.form-group
%hr
= f.label :share_with_group_lock, class: 'control-label' do
Share with group lock
%label.control-label
= s_("GroupSettings|Share with group lock")
.col-sm-10
.checkbox
= f.check_box :share_with_group_lock
%span.descr Prevent sharing a project with another group within this group
= f.label :share_with_group_lock do
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
- group_link = link_to @group.name, group_path(@group)
= s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'group_admin_settings', f: f
.form-actions
= f.submit 'Save group', class: "btn btn-save"
......
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
= render "layouts/header/default"
......
%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do
%a{ href: "#", data: { toggle: "dropdown" } }
Projects
= custom_icon('caret_down')
......@@ -22,7 +22,7 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
%li.dropdown.hidden-lg
%li.header-more.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } }
More
= custom_icon('caret_down')
......
......@@ -3,6 +3,26 @@
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4.application-theme
%h4.prepend-top-0
GitLab navigation theme
%p Customize the appearance of the application header and navigation sidebar.
.col-lg-8.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
.preview{ class: theme.name.downcase }
.preview-row
.quadrant.one
.quadrant.two
.preview-row
.quadrant.three
.quadrant.four
= f.radio_button :theme_id, theme.id
= theme.name
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Syntax highlighting theme
......
// Remove body class for any previous theme, re-add current one
$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
$('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
......
- form = local_assigns.fetch(:form)
.form-group
.checkbox.builds-feature
.checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
= form.label :only_allow_merge_if_pipeline_succeeds do
= form.check_box :only_allow_merge_if_pipeline_succeeds
%strong Only allow merge requests to be merged if the pipeline succeeds
......
......@@ -2,6 +2,7 @@
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
} }
- content_for :page_specific_javascripts do
......
......@@ -66,90 +66,18 @@
%section.settings.sharing-permissions
.settings-header
%h4
Sharing and permissions
Permissions
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
.form_group.sharing-and-permissions
.row.js-visibility-select
.col-md-8
.label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block
.col-md-4.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
= f.fields_for :project_feature do |feature_fields|
%fieldset.features
.row
.col-md-8.project-feature
= feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block View and edit files in this project
.col-md-4.js-repo-access-level
= project_feature_access_select(:repository_access_level)
.row
.col-md-8.project-feature.nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
.col-md-4
= project_feature_access_select(:merge_requests_access_level)
.row
.col-md-8.project-feature.nested
= feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
%span.help-block Build, test, and deploy your changes
.col-md-4
= project_feature_access_select(:builds_access_level)
.row
.col-md-8.project-feature
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-4
= project_feature_access_select(:snippets_access_level)
.row
.col-md-8.project-feature
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-4
= project_feature_access_select(:issues_access_level)
.row
.col-md-8.project-feature
= feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
%span.help-block Pages for project documentation
.col-md-4
= project_feature_access_select(:wiki_access_level)
.form-group
= render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
.row.js-lfs-enabled.form-group.sharing-and-permissions
.col-md-8
= f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light'
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%span.help-block Manages large files such as audio, video and graphics files.
.col-md-4
.select-wrapper
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
= icon('chevron-down')
- if Gitlab.config.registry.enabled
.form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
.checkbox
= f.label :container_registry_enabled do
= f.check_box :container_registry_enabled
%strong Container Registry
%br
%span.descr Enable Container Registry for this project
= link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
%section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) }
%section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
.settings-header
%h4
Merge request settings
......
......@@ -16,6 +16,8 @@
- if @project.merge_requests.exists?
%div{ class: container_class }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
......
......@@ -2,9 +2,13 @@
- page_title "Pipelines"
= render "projects/pipelines/head"
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
"css-class" => container_class,
%div{ 'class' => container_class }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
"help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"all-path" => project_pipelines_path(@project),
......@@ -13,8 +17,8 @@
"finished-path" => project_pipelines_path(@project, scope: :finished),
"branches-path" => project_pipelines_path(@project, scope: :branches),
"tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml,
"has-ci" => @project.has_ci?,
"ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pipelines')
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pipelines')
......@@ -2,10 +2,41 @@
.col-lg-12
= form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature
- unless @repository.gitlab_ci_yml
.form-group
%p Pipelines need to be configured before you can begin using Continuous Integration.
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
%p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery.
%h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml
in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery.
.radio
= form.label :enabled do
= form.radio_button :enabled, nil
%strong
Instance default (status: #{current_application_settings.auto_devops_enabled?})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified.
%br
%p
Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
.form-group.append-bottom-default
= f.label :runners_token, "Runner token", class: 'label-light'
......
......@@ -8,7 +8,7 @@
= form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do
.form-group
= label_tag :source_project_id, "Project", class: 'control-label'
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
......
......@@ -6,7 +6,7 @@
- expanded = Rails.env.test?
%section.settings
%section.settings#js-general-pipeline-settings
.settings-header
%h4
General pipelines settings
......
......@@ -82,5 +82,8 @@
- view_path = default_project_view
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
%div{ class: project_child_container_class(view_path) }
= render view_path
......@@ -14,5 +14,7 @@
= render "projects/commits/head"
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
.bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.svg-container
= custom_icon('icon_autodevops')
.user-callout-copy
%h4= _('Auto DevOps (Beta)')
%p= _('Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
#{s_('AutoDevOps|Learn more in the')}
= link_to _('Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout'
This diff is collapsed.
---
title: Removes Sortable default scope.
merge_request: 13558
author:
type: fixed
---
title: Decrease Cyclomatic Complexity threshold to 14
merge_request: 13972
author: Maxim Rydkin
type: other
---
title: Redesign project feature permissions settings
merge_request: 14062
author:
type: changed
---
title: Add option in preferences to change navigation theme color
merge_request:
author:
type: added
---
title: Created callout for auto devops
merge_request:
author:
type: added
---
title: Fix a wrong `X-Gitlab-Event` header when testing webhooks
merge_request: 14108
author:
type: fixed
---
title: Make blob viewer for rich contents wider for mobile
merge_request: 14011
author: Takuya Noguchi
type: fixed
---
title: Update documentation for confidential issue
merge_request: 14117
author:
type: other
---
title: Gitaly feature toggles are on by default in development
merge_request: 13802
author:
type: other
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.
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.
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.
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