Commit 7c627ac8 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2017-09-20' into 'master'

CE upstream - Wednesday

Closes gitaly#589, gitlab-qa#66, gitlab-ce#33287, gitlab-ce#36877, gitlab-ce#37893, gitlab-ce#37259 et gitlab-ce#37465

See merge request gitlab-org/gitlab-ee!2965
parents a52cd764 2f023955
...@@ -181,7 +181,8 @@ build-package: ...@@ -181,7 +181,8 @@ build-package:
# Review docs base # Review docs base
.review-docs: &review-docs .review-docs: &review-docs
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script:
- gem install gitlab --no-doc
services: [] services: []
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
...@@ -200,10 +201,9 @@ review-docs-deploy: ...@@ -200,10 +201,9 @@ review-docs-deploy:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$CI_COMMIT_REF_SLUG-built-from-ce-ee.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup on_stop: review-docs-cleanup
script: script:
- gem install gitlab --no-doc
- scripts/trigger-build-docs deploy - scripts/trigger-build-docs deploy
# Cleanup remote environment of gitlab-docs # Cleanup remote environment of gitlab-docs
...@@ -214,7 +214,6 @@ review-docs-cleanup: ...@@ -214,7 +214,6 @@ review-docs-cleanup:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_NAME
action: stop action: stop
script: script:
- gem install gitlab --no-doc
- scripts/trigger-build-docs cleanup - scripts/trigger-build-docs cleanup
# Retrieve knapsack and rspec_flaky reports # Retrieve knapsack and rspec_flaky reports
...@@ -524,6 +523,12 @@ db:seed_fu-mysql: ...@@ -524,6 +523,12 @@ db:seed_fu-mysql:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-mysql <<: *use-mysql
db:check-schema-pg:
<<: *db-migrate-reset
<<: *use-pg
script:
- sh scripts/schema_changed.sh
# Frontend-related jobs # Frontend-related jobs
gitlab:assets:compile: gitlab:assets:compile:
<<: *dedicated-runner <<: *dedicated-runner
......
...@@ -374,6 +374,7 @@ group :test do ...@@ -374,6 +374,7 @@ group :test do
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0' gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5' gem 'concurrent-ruby', '~> 1.0.5'
gem 'test-prof', '~> 0.2.5'
end end
gem 'octokit', '~> 4.6.2' gem 'octokit', '~> 4.6.2'
......
...@@ -912,6 +912,7 @@ GEM ...@@ -912,6 +912,7 @@ GEM
ffi ffi
sysexits (1.2.0) sysexits (1.2.0)
temple (0.7.7) temple (0.7.7)
test-prof (0.2.5)
test_after_commit (1.1.0) test_after_commit (1.1.0)
activerecord (>= 3.2) activerecord (>= 3.2)
text (1.3.1) text (1.3.1)
...@@ -1202,6 +1203,7 @@ DEPENDENCIES ...@@ -1202,6 +1203,7 @@ DEPENDENCIES
stackprof (~> 0.2.10) stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0) state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6) sys-filesystem (~> 1.1.6)
test-prof (~> 0.2.5)
test_after_commit (~> 1.1) test_after_commit (~> 1.1)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
......
The GitLab Enterprise Edition (EE) license (the “EE License”) The GitLab Enterprise Edition (EE) license (the “EE License”)
Copyright (c) 2011-2017 GitLab B.V. Copyright (c) 2011-2017 GitLab B.V.
With regard to the GitLab Software:
This software and associated documentation files (the "Software") may only be This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to, used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the GitLab Subscription Terms of Service, available and are in compliance with, the GitLab Subscription Terms of Service, available
at https://about.gitlab.com/terms/#subscription (the “EE Terms”), or other at https://about.gitlab.com/terms/#subscription (the “EE Terms”), or other
agreement governing the use of the Software, as agreed by you and GitLab, agreement governing the use of the Software, as agreed by you and GitLab,
and otherwise have a valid GitLab Enterprise Edition subscription for the c and otherwise have a valid GitLab Enterprise Edition subscription for the c
orrect number of user seats. Subject to the foregoing sentence, you are free to orrect number of user seats. Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that GitLab modify this Software and publish patches to the Software. You agree that GitLab
and/or its licensors (as applicable) retain all right, title and interest in and and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid GitLab Enterprise Edition subscription for the correct exploited with a valid GitLab Enterprise Edition subscription for the correct
number of user seats. Notwithstanding the foregoing, you may copy and modify number of user seats. Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a the Software for development and testing purposes, without requiring a
subscription. You agree that GitLab and/or its licensors (as applicable) retain subscription. You agree that GitLab and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software. and/or sell the Software.
This EE License applies only to the part of this Software that is not This EE License applies only to the part of this Software that is not
...@@ -30,8 +32,13 @@ license. The full text of this EE License shall be included in all copies or ...@@ -30,8 +32,13 @@ license. The full text of this EE License shall be included in all copies or
substantial portions of the Software. substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
For all third party components incorporated into the GitLab Software, those
components are licensed under the original license provided by the owner of the
applicable component.
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
</span> </span>
<a <a
v-if="deployKey.can_edit" v-if="deployKey.can_edit"
class="btn btn-small" class="btn btn-sm"
:href="editDeployKeyPath" :href="editDeployKeyPath"
> >
Edit Edit
......
import Cookies from 'js-cookie';
import _ from 'underscore';
import {
getCookieName,
getSelector,
hidePopover,
setupDismissButton,
mouseenter,
mouseleave,
} from './feature_highlight_helper';
export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = hidePopover.bind($selector);
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
.data('content', $popoverContent.prop('outerHTML'))
.popover({
html: true,
// Override the existing template to add custom CSS classes
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-content"></div>
</div>
`,
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave)
.on('inserted.bs.popover', setupDismissButton)
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll);
})
.on('hide.bs.popover', () => {
window.removeEventListener('scroll', hideOnScroll);
})
// Display feature highlight
.removeAttr('disabled');
};
export const shouldHighlightFeature = (id) => {
const element = document.querySelector(getSelector(id));
const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true';
return element && !previouslyDismissed;
};
export const highlightFeatures = (highlightOrder) => {
const featureId = highlightOrder.find(shouldHighlightFeature);
if (featureId) {
setupFeatureHighlightPopover(featureId);
return true;
}
return false;
};
import Cookies from 'js-cookie';
export const getCookieName = cookieId => `feature-highlighted-${cookieId}`;
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export const showPopover = function showPopover() {
if (this.hasClass('js-popover-show')) {
return false;
}
this.popover('show');
this.addClass('disable-animation js-popover-show');
return true;
};
export const hidePopover = function hidePopover() {
if (!this.hasClass('js-popover-show')) {
return false;
}
this.popover('hide');
this.removeClass('disable-animation js-popover-show');
return true;
};
export const dismiss = function dismiss(cookieId) {
Cookies.set(getCookieName(cookieId), true);
hidePopover.call(this);
this.hide();
};
export const mouseleave = function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $featureHighlight = $(this);
hidePopover.call($featureHighlight);
}
};
export const mouseenter = function mouseenter() {
const $featureHighlight = $(this);
const showedPopover = showPopover.call($featureHighlight);
if (showedPopover) {
$('.popover')
.on('mouseleave', mouseleave.bind($featureHighlight));
}
};
export const setupDismissButton = function setupDismissButton() {
const popoverId = this.getAttribute('aria-describedby');
const cookieId = this.dataset.highlight;
const $popover = $(this);
const dismissWrapper = dismiss.bind($popover, cookieId);
$(`#${popoverId} .dismiss-feature-highlight`)
.on('click', dismissWrapper);
};
import { highlightFeatures } from './feature_highlight';
import bp from '../breakpoints';
const highlightOrder = ['issue-boards'];
export default function domContentLoaded(order) {
if (bp.getBreakpointSize() === 'lg') {
highlightFeatures(order);
}
}
document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder));
import _ from 'underscore'; import _ from 'underscore';
(() => { /*
/* * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
* TODO: Make these methods more configurable (e.g. stringifyTime condensed or * non-condensed, abbreviateTimelengths)
* non-condensed, abbreviateTimelengths) * */
* */
/*
const utils = window.gl.utils = gl.utils || {}; * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
const prettyTime = utils.prettyTime = { * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
/* * or week length.
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } */
* Seconds can be negative or positive, zero or non-zero. Can be configured for any day
* or week length. export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
*/ const DAYS_PER_WEEK = daysPerWeek;
parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { const HOURS_PER_DAY = hoursPerDay;
const DAYS_PER_WEEK = daysPerWeek; const MINUTES_PER_HOUR = 60;
const HOURS_PER_DAY = hoursPerDay; const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_HOUR = 60; const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; const timePeriodConstraints = {
weeks: MINUTES_PER_WEEK,
const timePeriodConstraints = { days: MINUTES_PER_DAY,
weeks: MINUTES_PER_WEEK, hours: MINUTES_PER_HOUR,
days: MINUTES_PER_DAY, minutes: 1,
hours: MINUTES_PER_HOUR, };
minutes: 1,
};
let unorderedMinutes = prettyTime.secondsToMinutes(seconds); let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= (periodCount * minutesPerPeriod); unorderedMinutes -= (periodCount * minutesPerPeriod);
return periodCount; return periodCount;
}); });
}, }
/* /*
* Accepts a timeObject and returns a condensed string representation of it * Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/ */
stringifyTime(timeObject) { export function stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
const isNonZero = !!unitValue; const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
}, '').trim(); }, '').trim();
return reducedTime.length ? reducedTime : '0m'; return reducedTime.length ? reducedTime : '0m';
}, }
/* /*
* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
* the first non-zero unit/value pair. * the first non-zero unit/value pair.
*/ */
abbreviateTime(timeStr) { export function abbreviateTime(timeStr) {
return timeStr.split(' ') return timeStr.split(' ')
.filter(unitStr => unitStr.charAt(0) !== '0')[0]; .filter(unitStr => unitStr.charAt(0) !== '0')[0];
}, }
secondsToMinutes(seconds) {
return Math.abs(seconds / 60);
},
};
})(window.gl || (window.gl = {}));
...@@ -101,7 +101,6 @@ import './label_manager'; ...@@ -101,7 +101,6 @@ import './label_manager';
import './labels'; import './labels';
import './labels_select'; import './labels_select';
import './layout_nav'; import './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import './logo';
......
...@@ -11,6 +11,7 @@ export default class NewNavSidebar { ...@@ -11,6 +11,7 @@ export default class NewNavSidebar {
initDomElements() { initDomElements() {
this.$page = $('.page-with-sidebar'); this.$page = $('.page-with-sidebar');
this.$sidebar = $('.nav-sidebar'); this.$sidebar = $('.nav-sidebar');
this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
this.$overlay = $('.mobile-overlay'); this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav'); this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button'); this.$closeSidebar = $('.close-nav-button');
...@@ -55,6 +56,16 @@ export default class NewNavSidebar { ...@@ -55,6 +56,16 @@ export default class NewNavSidebar {
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
} }
NewNavSidebar.setCollapsedCookie(collapsed); NewNavSidebar.setCollapsedCookie(collapsed);
this.toggleSidebarOverflow();
}
toggleSidebarOverflow() {
if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) {
this.$innerScroll.css('overflow-y', 'scroll');
} else {
this.$innerScroll.css('overflow-y', '');
}
} }
render() { render() {
......
import stopwatchSvg from 'icons/_icon_stopwatch.svg'; import stopwatchSvg from 'icons/_icon_stopwatch.svg';
import { abbreviateTime } from '../../../lib/utils/pretty_time';
import '../../../lib/utils/pretty_time';
export default { export default {
name: 'time-tracking-collapsed-state', name: 'time-tracking-collapsed-state',
...@@ -79,7 +78,7 @@ export default { ...@@ -79,7 +78,7 @@ export default {
}, },
methods: { methods: {
abbreviateTime(timeStr) { abbreviateTime(timeStr) {
return gl.utils.prettyTime.abbreviateTime(timeStr); return abbreviateTime(timeStr);
}, },
}, },
template: ` template: `
......
import '../../../lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
const prettyTime = gl.utils.prettyTime;
export default { export default {
name: 'time-tracking-comparison-pane', name: 'time-tracking-comparison-pane',
...@@ -23,12 +21,12 @@ export default { ...@@ -23,12 +21,12 @@ export default {
}, },
}, },
computed: { computed: {
parsedRemaining() { parsedTimeRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent; const diffSeconds = this.timeEstimate - this.timeSpent;
return prettyTime.parseSeconds(diffSeconds); return parseSeconds(diffSeconds);
}, },
timeRemainingHumanReadable() { timeRemainingHumanReadable() {
return prettyTime.stringifyTime(this.parsedRemaining); return stringifyTime(this.parsedTimeRemaining);
}, },
timeRemainingTooltip() { timeRemainingTooltip() {
const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
...@@ -44,13 +42,6 @@ export default { ...@@ -44,13 +42,6 @@ export default {
timeRemainingStatusClass() { timeRemainingStatusClass() {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
}, },
/* Parsed time values */
parsedEstimate() {
return prettyTime.parseSeconds(this.timeEstimate);
},
parsedSpent() {
return prettyTime.parseSeconds(this.timeSpent);
},
}, },
template: ` template: `
<div class="time-tracking-comparison-pane"> <div class="time-tracking-comparison-pane">
......
...@@ -72,12 +72,12 @@ export default { ...@@ -72,12 +72,12 @@ export default {
<a <a
href="#modal_merge_info" href="#modal_merge_info"
data-toggle="modal" data-toggle="modal"
class="btn btn-small inline"> class="btn btn-sm inline">
Check out branch Check out branch
</a> </a>
<span class="dropdown prepend-left-10"> <span class="dropdown prepend-left-10">
<a <a
class="btn btn-small inline dropdown-toggle" class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Download as" aria-label="Download as"
role="button"> role="button">
......
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
<button <button
v-if="showDisabledButton" v-if="showDisabledButton"
type="button" type="button"
class="btn btn-success btn-small" class="btn btn-success btn-sm"
disabled="true"> disabled="true">
Merge Merge
</button> </button>
......
...@@ -11,7 +11,7 @@ export default { ...@@ -11,7 +11,7 @@ export default {
<status-icon status="failed" /> <status-icon status="failed" />
<button <button
type="button" type="button"
class="btn btn-success btn-small" class="btn btn-success btn-sm"
disabled="true"> disabled="true">
Merge Merge
</button> </button>
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc; return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
}, },
mergeButtonClass() { mergeButtonClass() {
const defaultClass = 'btn btn-small btn-success accept-merge-request'; const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`; const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`; const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
...@@ -231,7 +231,7 @@ export default { ...@@ -231,7 +231,7 @@ export default {
v-if="shouldShowMergeOptionsDropdown" v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
type="button" type="button"
class="btn btn-small btn-info dropdown-toggle js-merge-moment" class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Select merge moment"> aria-label="Select merge moment">
<i <i
......
...@@ -52,4 +52,3 @@ ...@@ -52,4 +52,3 @@
@import "framework/snippets"; @import "framework/snippets";
@import "framework/memory_graph"; @import "framework/memory_graph";
@import "framework/responsive-tables"; @import "framework/responsive-tables";
@import "framework/feature_highlight";
...@@ -46,15 +46,6 @@ ...@@ -46,15 +46,6 @@
} }
} }
@mixin btn-svg {
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
}
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
background-color: $light; background-color: $light;
border-color: $border-light; border-color: $border-light;
...@@ -132,7 +123,6 @@ ...@@ -132,7 +123,6 @@
.btn { .btn {
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
@include btn-svg;
color: $gl-text-color; color: $gl-text-color;
...@@ -140,7 +130,6 @@ ...@@ -140,7 +130,6 @@
outline: 0; outline: 0;
} }
&.btn-small,
&.btn-sm { &.btn-sm {
padding: 4px 10px; padding: 4px 10px;
font-size: 13px; font-size: 13px;
...@@ -236,6 +225,13 @@ ...@@ -236,6 +225,13 @@
} }
} }
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
svg, svg,
.fa { .fa {
&:not(:last-child) { &:not(:last-child) {
......
.feature-highlight {
position: relative;
margin-left: $gl-padding;
width: 20px;
height: 20px;
cursor: pointer;
&::before {
content: '';
display: block;
position: absolute;
top: 6px;
left: 6px;
width: 8px;
height: 8px;
background-color: $blue-500;
border-radius: 50%;
box-shadow: 0 0 0 rgba($blue-500, 0.4);
animation: pulse-highlight 2s infinite;
}
&:hover::before,
&.disable-animation::before {
animation: none;
}
&[disabled]::before {
display: none;
}
}
.is-showing-fly-out {
.feature-highlight {
display: none;
}
}
.feature-highlight-popover-content {
display: none;
hr {
margin: $gl-padding * 0.5 0;
}
.btn-link {
@include btn-svg;
svg path {
fill: currentColor;
}
}
.dismiss-feature-highlight {
padding: 0;
}
svg:first-child {
width: 100%;
background-color: $indigo-50;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom: 1px solid darken($gray-normal, 8%);
}
}
.popover .feature-highlight-popover-content {
display: block;
}
.feature-highlight-popover {
padding: 0;
.popover-content {
padding: 0;
}
}
.feature-highlight-popover-sub-content {
padding: 9px 14px;
}
@include keyframes(pulse-highlight) {
0% {
box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
}
70% {
box-shadow: 0 0 0 10px transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
...@@ -192,7 +192,11 @@ $new-sidebar-collapsed-width: 50px; ...@@ -192,7 +192,11 @@ $new-sidebar-collapsed-width: 50px;
.nav-sidebar-inner-scroll { .nav-sidebar-inner-scroll {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: scroll; overflow: auto;
@media (min-width: $screen-sm-min) {
overflow: hidden;
}
} }
.with-performance-bar .nav-sidebar { .with-performance-bar .nav-sidebar {
......
...@@ -346,6 +346,10 @@ ...@@ -346,6 +346,10 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.mr-widget-body-controls {
flex-wrap: wrap;
}
.mr_source_commit, .mr_source_commit,
.mr_target_commit { .mr_target_commit {
margin-bottom: 0; margin-bottom: 0;
......
...@@ -778,6 +778,7 @@ ul.notes { ...@@ -778,6 +778,7 @@ ul.notes {
background-color: transparent; background-color: transparent;
border: none; border: none;
outline: 0; outline: 0;
color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve; transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled { &.is-disabled {
...@@ -801,7 +802,7 @@ ul.notes { ...@@ -801,7 +802,7 @@ ul.notes {
} }
svg { svg {
fill: $gray-darkest; fill: currentColor;
height: 16px; height: 16px;
width: 16px; width: 16px;
} }
......
...@@ -56,7 +56,6 @@ ...@@ -56,7 +56,6 @@
.tree-content-holder { .tree-content-holder {
display: flex; display: flex;
max-height: 100vh;
min-height: 300px; min-height: 300px;
} }
...@@ -156,7 +155,7 @@ ...@@ -156,7 +155,7 @@
list-style-type: none; list-style-type: none;
background: $gray-normal; background: $gray-normal;
display: inline-block; display: inline-block;
padding: 10px 18px; padding: #{$gl-padding / 2} $gl-padding;
border-right: 1px solid $white-dark; border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
white-space: nowrap; white-space: nowrap;
...@@ -180,10 +179,9 @@ ...@@ -180,10 +179,9 @@
a { a {
@include str-truncated(100px); @include str-truncated(100px);
color: $black; color: $black;
width: 100px;
text-align: center;
vertical-align: middle; vertical-align: middle;
text-decoration: none; text-decoration: none;
margin-right: 12px;
&.close { &.close {
width: auto; width: auto;
...@@ -193,6 +191,10 @@ ...@@ -193,6 +191,10 @@
} }
} }
.close-icon:hover {
color: $hint-color;
}
.close-icon, .close-icon,
.unsaved-icon { .unsaved-icon {
float: right; float: right;
......
...@@ -73,9 +73,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -73,9 +73,6 @@ class Projects::IssuesController < Projects::ApplicationController
@noteable = @issue @noteable = @issue
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@discussions = @issue.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
......
...@@ -57,7 +57,7 @@ class GroupsFinder < UnionFinder ...@@ -57,7 +57,7 @@ class GroupsFinder < UnionFinder
end end
def owned_groups def owned_groups
current_user&.groups || Group.none current_user&.owned_groups || Group.none
end end
def include_public_groups? def include_public_groups?
......
...@@ -249,6 +249,8 @@ class IssuableFinder ...@@ -249,6 +249,8 @@ class IssuableFinder
end end
def by_scope(items) def by_scope(items)
return items.none if current_user_related? && !current_user
case params[:scope] case params[:scope]
when 'created-by-me', 'authored' when 'created-by-me', 'authored'
items.where(author_id: current_user.id) items.where(author_id: current_user.id)
......
...@@ -5,6 +5,25 @@ module AutoDevopsHelper ...@@ -5,6 +5,25 @@ module AutoDevopsHelper
can?(current_user, :admin_pipeline, project) && can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled? && project.has_auto_devops_implicitly_disabled? &&
!project.repository.gitlab_ci_yml && !project.repository.gitlab_ci_yml &&
project.ci_services.active.none? !project.ci_service
end
def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain?
missing_service = !project.kubernetes_service&.active?
if missing_service
params = {
kubernetes: link_to('Kubernetes service', edit_project_service_path(project, 'kubernetes'))
}
if missing_domain
_('Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly.') % params
else
_('Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly.') % params
end
elsif missing_domain
_('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
end
end end
end end
...@@ -87,10 +87,14 @@ module SubmoduleHelper ...@@ -87,10 +87,14 @@ module SubmoduleHelper
namespace = @project.namespace.full_path namespace = @project.namespace.full_path
end end
[ begin
namespace_project_path(namespace, base), [
namespace_project_tree_path(namespace, base, commit) namespace_project_path(namespace, base),
] namespace_project_tree_path(namespace, base, commit)
]
rescue ActionController::UrlGenerationError
[nil, nil]
end
end end
def sanitize_submodule_url(url) def sanitize_submodule_url(url)
......
...@@ -43,6 +43,7 @@ module Ci ...@@ -43,6 +43,7 @@ module Ci
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
delegate :id, to: :project, prefix: true delegate :id, to: :project, prefix: true
delegate :full_path, to: :project, prefix: true
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
validates :sha, presence: { unless: :importing? } validates :sha, presence: { unless: :importing? }
...@@ -349,7 +350,7 @@ module Ci ...@@ -349,7 +350,7 @@ module Ci
return @config_processor if defined?(@config_processor) return @config_processor if defined?(@config_processor)
@config_processor ||= begin @config_processor ||= begin
Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path) Gitlab::Ci::YamlProcessor.new(ci_yaml_file)
rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message self.yaml_errors = e.message
nil nil
......
...@@ -196,7 +196,7 @@ class Project < ActiveRecord::Base ...@@ -196,7 +196,7 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops accepts_nested_attributes_for :auto_devops, update_only: true
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
......
...@@ -6,6 +6,10 @@ class ProjectAutoDevops < ActiveRecord::Base ...@@ -6,6 +6,10 @@ class ProjectAutoDevops < ActiveRecord::Base
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def has_domain?
domain.present?
end
def variables def variables
variables = [] variables = []
variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present? variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
......
...@@ -841,10 +841,6 @@ class Repository ...@@ -841,10 +841,6 @@ class Repository
} }
end end
def user_to_committer(user)
Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
def can_be_merged?(source_sha, target_branch) def can_be_merged?(source_sha, target_branch)
our_commit = rugged.branches[target_branch].target our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha) their_commit = rugged.lookup(source_sha)
...@@ -885,54 +881,34 @@ class Repository ...@@ -885,54 +881,34 @@ class Repository
end end
def revert( def revert(
user, commit, branch_name, user, commit, branch_name, message,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
with_branch(
user,
branch_name,
start_branch_name: start_branch_name,
start_repository: start_project.repository.raw_repository) do |start_commit|
revert_tree_id = check_revert_content(commit, start_commit.sha)
unless revert_tree_id
raise Repository::CreateTreeError.new('Failed to revert commit')
end
committer = user_to_committer(user) with_cache_hooks do
raw_repository.revert(
create_commit(message: commit.revert_message(user), user: user,
author: committer, commit: commit.raw,
committer: committer, branch_name: branch_name,
tree: revert_tree_id, message: message,
parents: [start_commit.sha]) start_branch_name: start_branch_name,
start_repository: start_project.repository.raw_repository
)
end end
end end
def cherry_pick( def cherry_pick(
user, commit, branch_name, user, commit, branch_name, message,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
with_branch(
user,
branch_name,
start_branch_name: start_branch_name,
start_repository: start_project.repository.raw_repository) do |start_commit|
cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) with_cache_hooks do
unless cherry_pick_tree_id raw_repository.cherry_pick(
raise Repository::CreateTreeError.new('Failed to cherry-pick commit') user: user,
end commit: commit.raw,
branch_name: branch_name,
committer = user_to_committer(user) message: message,
start_branch_name: start_branch_name,
create_commit(message: commit.cherry_pick_message(user), start_repository: start_project.repository.raw_repository
author: { )
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [start_commit.sha])
end end
end end
...@@ -944,36 +920,6 @@ class Repository ...@@ -944,36 +920,6 @@ class Repository
end end
end end
def check_revert_content(target_commit, source_sha)
args = [target_commit.sha, source_sha]
args << { mainline: 1 } if target_commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
tree_id = revert_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def check_cherry_pick_content(target_commit, source_sha)
args = [target_commit.sha, source_sha]
args << 1 if target_commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
tree_id = cherry_pick_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0
end
def merged_to_root_ref?(branch_name) def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name) branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref) root_ref_commit = commit(root_ref)
......
...@@ -11,6 +11,7 @@ class GroupPolicy < BasePolicy ...@@ -11,6 +11,7 @@ class GroupPolicy < BasePolicy
condition(:has_access) { access_level != GroupMember::NO_ACCESS } condition(:has_access) { access_level != GroupMember::NO_ACCESS }
condition(:guest) { access_level >= GroupMember::GUEST } condition(:guest) { access_level >= GroupMember::GUEST }
condition(:developer) { access_level >= GroupMember::DEVELOPER }
condition(:owner) { access_level >= GroupMember::OWNER } condition(:owner) { access_level >= GroupMember::OWNER }
condition(:master) { access_level >= GroupMember::MASTER } condition(:master) { access_level >= GroupMember::MASTER }
condition(:reporter) { access_level >= GroupMember::REPORTER } condition(:reporter) { access_level >= GroupMember::REPORTER }
...@@ -44,11 +45,11 @@ class GroupPolicy < BasePolicy ...@@ -44,11 +45,11 @@ class GroupPolicy < BasePolicy
rule { admin } .enable :read_group rule { admin } .enable :read_group
rule { has_projects } .enable :read_group rule { has_projects } .enable :read_group
rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label rule { reporter }.enable :admin_label
rule { master }.policy do rule { master }.policy do
enable :create_projects enable :create_projects
enable :admin_milestones
enable :admin_pipeline enable :admin_pipeline
enable :admin_build enable :admin_build
end end
......
...@@ -158,6 +158,7 @@ class ProjectPolicy < BasePolicy ...@@ -158,6 +158,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:developer_access) }.policy do rule { can?(:developer_access) }.policy do
enable :admin_merge_request enable :admin_merge_request
enable :admin_milestone
enable :update_merge_request enable :update_merge_request
enable :create_commit_status enable :create_commit_status
enable :update_commit_status enable :update_commit_status
...@@ -181,7 +182,6 @@ class ProjectPolicy < BasePolicy ...@@ -181,7 +182,6 @@ class ProjectPolicy < BasePolicy
enable :update_project_snippet enable :update_project_snippet
enable :update_environment enable :update_environment
enable :update_deployment enable :update_deployment
enable :admin_milestone
enable :admin_project_snippet enable :admin_project_snippet
enable :admin_project_member enable :admin_project_member
enable :admin_note enable :admin_note
......
...@@ -11,15 +11,19 @@ module Commits ...@@ -11,15 +11,19 @@ module Commits
def commit_change(action) def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action) raise NotImplementedError unless repository.respond_to?(action)
# rubocop:disable GitlabSecurity/PublicSend
message = @commit.public_send(:"#{action}_message", current_user)
# rubocop:disable GitlabSecurity/PublicSend # rubocop:disable GitlabSecurity/PublicSend
repository.public_send( repository.public_send(
action, action,
current_user, current_user,
@commit, @commit,
@branch_name, @branch_name,
message,
start_project: @start_project, start_project: @start_project,
start_branch_name: @start_branch) start_branch_name: @start_branch)
rescue Repository::CreateTreeError rescue Gitlab::Git::Repository::CreateTreeError
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content." This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg raise ChangeError, error_msg
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= image_tag @appearance.logo_url, class: 'appearance-logo-preview' = image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- if @appearance.persisted? - if @appearance.persisted?
%br %br
= link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo" = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
%hr %hr
= f.hidden_field :logo_cache = f.hidden_field :logo_cache
= f.file_field :logo, class: "" = f.file_field :logo, class: ""
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview' = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted? - if @appearance.persisted?
%br %br
= link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo" = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
%hr %hr
= f.hidden_field :header_logo_cache = f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: "" = f.file_field :header_logo, class: ""
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
- @hooks.each do |hook| - @hooks.each do |hook|
%li %li
.controls .controls
= render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: hook, button_class: 'btn-small' = render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: hook, button_class: 'btn-sm'
= link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm' = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url .monospace= hook.url
......
<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
<path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/>
<g transform="translate(11 23)">
<path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
<path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
<g transform="translate(5 10)">
<use fill="black" filter="url(#a)" xlink:href="#b"/>
<use fill="#F9F9F9" xlink:href="#b"/>
</g>
<g transform="translate(5 42)">
<use fill="black" filter="url(#c)" xlink:href="#d"/>
<use fill="#FEF0E8" xlink:href="#d"/>
<path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
<path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
<path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/>
</g>
</g>
<path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/>
<g transform="translate(145 28)">
<mask id="f" fill="white">
<use xlink:href="#e"/>
</mask>
<use fill="#FFFFFF" xlink:href="#e"/>
<path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/>
<g transform="translate(5 10)">
<use fill="black" filter="url(#g)" xlink:href="#h"/>
<use fill="#F9F9F9" xlink:href="#h"/>
</g>
<g transform="translate(5 42)">
<use fill="black" filter="url(#i)" xlink:href="#j"/>
<use fill="#FEF0E8" xlink:href="#j"/>
<path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/>
<path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
<path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/>
</g>
</g>
<path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/>
<g transform="translate(78 16)">
<path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
<g transform="translate(5 10)">
<use fill="black" filter="url(#k)" xlink:href="#l"/>
<use fill="#EFEDF8" xlink:href="#l"/>
<path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
<path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
<path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
</g>
<g transform="translate(5 42)">
<use fill="black" filter="url(#m)" xlink:href="#n"/>
<use fill="#F9F9F9" xlink:href="#n"/>
</g>
<g transform="translate(5 74)">
<rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/>
<use fill="black" filter="url(#o)" xlink:href="#p"/>
<use fill="#F9F9F9" xlink:href="#p"/>
</g>
<path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
</g>
</g>
</svg>
...@@ -127,20 +127,6 @@ ...@@ -127,20 +127,6 @@
= link_to project_boards_path(@project), title: boards_link_text do = link_to project_boards_path(@project), title: boards_link_text do
%span %span
= boards_link_text = boards_link_text
.feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } }
.feature-highlight-popover-content
= render 'feature_highlight/issue_boards.svg'
.feature-highlight-popover-sub-content
%span= _('Use')
= link_to 'Issue Boards', project_boards_path(@project)
%span= _('to create customized software development workflows like')
%strong= _('Scrum')
%span= _('or')
%strong= _('Kanban')
%hr
%button.btn-link.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it! Don't show this again")
= custom_icon('thumbs_up')
= nav_link(controller: :labels) do = nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do = link_to project_labels_path(@project), title: 'Labels' do
......
...@@ -27,6 +27,8 @@ ...@@ -27,6 +27,8 @@
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
%div{ class: container_class } %div{ class: container_class }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
.prepend-top-20 .prepend-top-20
.empty_wrapper .empty_wrapper
%h3.page-title-empty %h3.page-title-empty
......
...@@ -70,7 +70,10 @@ ...@@ -70,7 +70,10 @@
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button", %span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
= render "shared/icons/icon_status_success.svg" %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
= render 'shared/icons/icon_status_success_solid.svg'
%template{ 'v-else' => '' }
= render 'shared/icons/icon_resolve_discussion.svg'
%span.line-resolve-text %span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
......
...@@ -3,11 +3,15 @@ ...@@ -3,11 +3,15 @@
= form_for @project, url: project_pipelines_settings_path(@project) do |f| = form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature %fieldset.builds-feature
.form-group .form-group
%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) %h5 Auto DevOps (Beta)
%p %p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration. Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
This will happen starting with the next event (e.g.: push) that occurs to the project.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md') = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- message = auto_devops_warning_message(@project)
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form| = f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio .radio
= form.label :enabled_true do = form.label :enabled_true do
...@@ -15,26 +19,24 @@ ...@@ -15,26 +19,24 @@
%strong Enable Auto DevOps %strong Enable Auto DevOps
%br %br
%span.descr %span.descr
The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
in the project.
.radio .radio
= form.label :enabled_false do = form.label :enabled_false do
= form.radio_button :enabled, 'false' = form.radio_button :enabled, 'false'
%strong Disable Auto DevOps %strong Disable Auto DevOps
%br %br
%span.descr %span.descr
A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery. An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continious Integration and Delivery.
.radio .radio
= form.label :enabled do = form.label :enabled_nil do
= form.radio_button :enabled, nil = form.radio_button :enabled, ''
%strong %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
Instance default (status: #{current_application_settings.auto_devops_enabled?})
%br %br
%span.descr %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. Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%br %br
%p %p
Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed. You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com' = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr %hr
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your CI/CD configuration, like job timeout. Update your CI/CD configuration, like job timeout or Auto DevOps.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content.no-animate{ class: ('expanded' if expanded) }
= render 'projects/pipelines_settings/show' = render 'projects/pipelines_settings/show'
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%span.append-right-10.inline %span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm' = link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm'
= render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: hook, button_class: 'btn-small' = render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: hook, button_class: 'btn-sm'
= link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do = link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do
%span.sr-only Remove %span.sr-only Remove
= icon('trash') = icon('trash')
- dropdown_toggle_text = @ref || @project.default_branch - dropdown_toggle_text = @ref || @project.default_branch
= form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do = form_tag nil, method: :get, class: "project-refs-form project-refs-target-form" do
= hidden_field_tag :destination, destination = hidden_field_tag :destination, destination
- if defined?(path) - if defined?(path)
= hidden_field_tag :path, path = hidden_field_tag :path, path
...@@ -7,14 +7,10 @@ ...@@ -7,14 +7,10 @@
= hidden_field_tag key, value, id: nil = hidden_field_tag key, value, id: nil
.dropdown .dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, find: ['branches']), field_name: 'ref', input_field_name: 'new-branch', submit_form_on_click: true, visit: false }, { toggle_class: "js-project-refs-dropdown" } = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, find: ['branches']), field_name: 'ref', input_field_name: 'new-branch', submit_form_on_click: true, visit: false }, { toggle_class: "js-project-refs-dropdown" }
%ul.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
%li = dropdown_title _("Create a new branch")
= dropdown_title _("Create a new branch") = dropdown_input _("Create a new branch")
%li = dropdown_title _("Select existing branch"), options: {close: false}
= dropdown_input _("Create a new branch") = dropdown_filter _("Search branches and tags")
%li = dropdown_content
= dropdown_title _("Select existing branch"), options: {close: false} = dropdown_loading
%li
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }} {{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent) - if can?(current_user, :admin_list, current_board_parent)
%button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button", %button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm", "@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"', "v-if" => 'list.type !== "closed"',
"aria-label" => "New issue", "aria-label" => "New issue",
......
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg>
---
title: Fixes project denial of service via gitmodules using Extended ASCII.
merge_request: 14301
author:
type: fixed
---
title: made read-only APIs for public merge requests available without authentication
merge_request: 13291
author: haseebeqx
---
title: Update x/x discussions resolved checkmark icon to be green when all discussions
resolved
merge_request:
author:
type: fixed
---
title: Fix Auto DevOps banner to be shown on empty projects
merge_request:
author:
type: fixed
---
title: Handle if Auto DevOps domain is not set in project settings
merge_request:
author:
type: added
---
title: Fix the filesystem shard health check to check all configured shards
merge_request: 14341
author:
type: fixed
---
title: Add 'closed_at' attribute to Issues API
merge_request: 14316
author: Vitaliy @blackst0ne Klachkov
type: added
---
title: File uploaders do not perform hard check, only soft check
merge_request:
author:
type: fixed
---
title: Allow developer role to admin milestones
merge_request:
author:
type: changed
---
title: Eliminate N+1 queries referencing issues
merge_request:
author:
type: fixed
---
title: Remove unnecessary loading of discussions in `IssuesController#show`
merge_request:
author:
type: fixed
...@@ -127,7 +127,7 @@ module ActiveRecord ...@@ -127,7 +127,7 @@ module ActiveRecord
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
where = inddef.scan(/WHERE (.+)$/).flatten[0] where = inddef.scan(/WHERE (.+)$/).flatten[0]
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass| opclasses = Hash[inddef.scan(/\((.+?)\)(?:$| WHERE )/).flatten[0].split(',').map do |column_and_opclass|
column, opclass = column_and_opclass.split(' ').map(&:strip) column, opclass = column_and_opclass.split(' ').map(&:strip)
[column, opclass] if opclass [column, opclass] if opclass
end.compact] end.compact]
......
class CleanStagesStatusesMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class Stage < ActiveRecord::Base
include ::EachBatch
self.table_name = 'ci_stages'
end
def up
Gitlab::BackgroundMigration.steal('MigrateStageStatus')
Stage.where('status IS NULL').each_batch(of: 50) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first
Gitlab::BackgroundMigration::MigrateStageStatus.new.perform(*range)
end
end
def down
# noop
end
end
...@@ -317,7 +317,7 @@ ActiveRecord::Schema.define(version: 20170918223303) do ...@@ -317,7 +317,7 @@ ActiveRecord::Schema.define(version: 20170918223303) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree, opclasses: {"id)"=>"WHERE"} add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
...@@ -2145,8 +2145,8 @@ ActiveRecord::Schema.define(version: 20170918223303) do ...@@ -2145,8 +2145,8 @@ ActiveRecord::Schema.define(version: 20170918223303) do
add_foreign_key "protected_tag_create_access_levels", "protected_tags", name: "fk_f7dfda8c51", on_delete: :cascade add_foreign_key "protected_tag_create_access_levels", "protected_tags", name: "fk_f7dfda8c51", on_delete: :cascade
add_foreign_key "protected_tag_create_access_levels", "users" add_foreign_key "protected_tag_create_access_levels", "users"
add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade
add_foreign_key "push_rules", "projects", name: "fk_83b29894de", on_delete: :cascade
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "push_rules", "projects", name: "fk_83b29894de", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
......
...@@ -95,6 +95,7 @@ Example response: ...@@ -95,6 +95,7 @@ Example response:
"username" : "root" "username" : "root"
}, },
"updated_at" : "2016-01-04T15:31:51.081Z", "updated_at" : "2016-01-04T15:31:51.081Z",
"closed_at" : null,
"id" : 76, "id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z", "created_at" : "2016-01-04T15:31:51.081Z",
...@@ -207,6 +208,7 @@ Example response: ...@@ -207,6 +208,7 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/1", "web_url": "http://example.com/example/example/issues/1",
...@@ -315,6 +317,7 @@ Example response: ...@@ -315,6 +317,7 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : "2016-01-05T15:31:46.176Z",
"user_notes_count": 1, "user_notes_count": 1,
"due_date": "2016-07-22", "due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1", "web_url": "http://example.com/example/example/issues/1",
...@@ -364,7 +367,8 @@ Example response: ...@@ -364,7 +367,8 @@ Example response:
"id" : 11, "id" : 11,
"title" : "v3.0", "title" : "v3.0",
"created_at" : "2016-01-04T15:31:39.788Z", "created_at" : "2016-01-04T15:31:39.788Z",
"updated_at" : "2016-01-04T15:31:39.788Z" "updated_at" : "2016-01-04T15:31:39.788Z",
"closed_at" : "2016-01-05T15:31:46.176Z"
}, },
"author" : { "author" : {
"state" : "active", "state" : "active",
...@@ -473,6 +477,7 @@ Example response: ...@@ -473,6 +477,7 @@ Example response:
}, },
"description" : null, "description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z", "updated_at" : "2016-01-07T12:44:33.959Z",
"closed_at" : null,
"milestone" : null, "milestone" : null,
"subscribed" : true, "subscribed" : true,
"user_notes_count": 0, "user_notes_count": 0,
...@@ -543,6 +548,7 @@ Example response: ...@@ -543,6 +548,7 @@ Example response:
"project_id" : 4, "project_id" : 4,
"description" : null, "description" : null,
"updated_at" : "2016-01-07T12:55:16.213Z", "updated_at" : "2016-01-07T12:55:16.213Z",
"closed_at" : "2016-01-08T12:55:16.213Z",
"iid" : 15, "iid" : 15,
"labels" : [ "labels" : [
"bug" "bug"
...@@ -626,6 +632,7 @@ Example response: ...@@ -626,6 +632,7 @@ Example response:
"state": "opened", "state": "opened",
"created_at": "2016-04-05T21:41:45.652Z", "created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z", "updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null,
"labels": [], "labels": [],
"milestone": null, "milestone": null,
"assignees": [{ "assignees": [{
...@@ -704,6 +711,7 @@ Example response: ...@@ -704,6 +711,7 @@ Example response:
"state": "opened", "state": "opened",
"created_at": "2016-04-05T21:41:45.652Z", "created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z", "updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null,
"labels": [], "labels": [],
"milestone": null, "milestone": null,
"assignees": [{ "assignees": [{
......
...@@ -252,6 +252,8 @@ The `cache:key` variable can use any of the [predefined variables](../variables/ ...@@ -252,6 +252,8 @@ The `cache:key` variable can use any of the [predefined variables](../variables/
The default key is **default** across the project, therefore everything is The default key is **default** across the project, therefore everything is
shared between each pipelines and jobs by default, starting from GitLab 9.0. shared between each pipelines and jobs by default, starting from GitLab 9.0.
>**Note:** The `cache:key` variable cannot contain the `/` character.
--- ---
**Example configurations** **Example configurations**
...@@ -276,7 +278,7 @@ To enable per-job and per-branch caching: ...@@ -276,7 +278,7 @@ To enable per-job and per-branch caching:
```yaml ```yaml
cache: cache:
key: "$CI_JOB_NAME/$CI_COMMIT_REF_NAME" key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
untracked: true untracked: true
``` ```
...@@ -284,7 +286,7 @@ To enable per-branch and per-stage caching: ...@@ -284,7 +286,7 @@ To enable per-branch and per-stage caching:
```yaml ```yaml
cache: cache:
key: "$CI_JOB_STAGE/$CI_COMMIT_REF_NAME" key: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
untracked: true untracked: true
``` ```
...@@ -293,7 +295,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace ...@@ -293,7 +295,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml ```yaml
cache: cache:
key: "%CI_JOB_STAGE%/%CI_COMMIT_REF_NAME%" key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
untracked: true untracked: true
``` ```
...@@ -302,7 +304,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace ...@@ -302,7 +304,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace
```yaml ```yaml
cache: cache:
key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME" key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
untracked: true untracked: true
``` ```
......
...@@ -9,8 +9,18 @@ There are a few rules to get your merge request accepted: ...@@ -9,8 +9,18 @@ There are a few rules to get your merge request accepted:
**approved by a [backend maintainer][projects]**. **approved by a [backend maintainer][projects]**.
1. If your merge request includes only frontend changes [^1], it must be 1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][projects]**. **approved by a [frontend maintainer][projects]**.
1. If your merge request includes UX changes [^1], it must
be **approved by a [UX team member][team]**.
1. If your merge request includes adding a new JavaScript library [^1], it must be
**approved by a [frontend lead][team]**.
1. If your merge request includes adding a new UI/UX paradigm [^1], it must be
**approved by a [UX lead][team]**.
1. If your merge request includes frontend and backend changes [^1], it must 1. If your merge request includes frontend and backend changes [^1], it must
be **approved by a [frontend and a backend maintainer][projects]**. be **approved by a [frontend and a backend maintainer][projects]**.
1. If your merge request includes UX and frontend changes [^1], it must
be **approved by a [UX team member and a frontend maintainer][team]**.
1. If your merge request includes UX, frontend and backend changes [^1], it must
be **approved by a [UX team member, a frontend and a backend maintainer][team]**.
1. If your merge request includes a new dependency or a filesystem change, it must 1. If your merge request includes a new dependency or a filesystem change, it must
be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details. be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details.
1. To lower the amount of merge requests maintainers need to review, you can 1. To lower the amount of merge requests maintainers need to review, you can
......
...@@ -106,21 +106,84 @@ CE and EE. ...@@ -106,21 +106,84 @@ CE and EE.
## Previewing the changes live ## Previewing the changes live
If you want to preview your changes live, you can use the manual `build-docs` If you want to preview the doc changes of your merge request live, you can use
job in your merge request. the manual `review-docs-deploy` job in your merge request.
TIP: **Tip:**
If your branch contains only documentation changes, you can use
[special branch names](#testing) to avoid long running pipelines.
![Manual trigger a docs build](img/manual_build_docs.png) ![Manual trigger a docs build](img/manual_build_docs.png)
This job will: This job will:
1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs) 1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
project named after the scheme: `<CE/EE-branch-slug>-built-from-ce-ee` project named after the scheme: `preview-<branch-slug>`
1. Trigger a pipeline and build the docs site with your changes 1. Trigger a cross project pipeline and build the docs site with your changes
Look for the docs URL at the output of the `build-docs` job. After a few minutes, the Review App will be deployed and you will be able to
preview the changes. The docs URL can be found in two places:
>**Note:**
- In the merge request widget
- In the output of the `review-docs-deploy` job, which also includes the
triggered pipeline so that you can investigate whether something went wrong
In case the Review App URL returns 404, follow these steps to debug:
1. **Did you follow the URL from the merge request widget?** If yes, then check if
the link is the same as the one in the job output. It can happen that if the
branch name slug is longer than 35 characters, it is automatically
truncated. That means that the merge request widget will not show the proper
URL due to a limitation of how `environment: url` works, but you can find the
real URL from the output of the `review-docs-deploy` job.
1. **Did you follow the URL from the job output?** If yes, then it means that
either the site is not yet deployed or something went wrong with the remote
pipeline. Give it a few minutes and it should appear online, otherwise you
can check the status of the remote pipeline from the link in the job output.
If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
TIP: **Tip:**
Someone that has no merge rights to the CE/EE projects (think of forks from
contributors) will not be able to run the manual job. In that case, you can
ask someone from the GitLab team who has the permissions to do that for you.
NOTE: **Note:**
Make sure that you always delete the branch of the merge request you were Make sure that you always delete the branch of the merge request you were
working on. If you don't, the remote docs branch won't be removed either, working on. If you don't, the remote docs branch won't be removed either,
and the server where the Review Apps are hosted will eventually be out of and the server where the Review Apps are hosted will eventually be out of
disk space. disk space.
### Behind the scenes
If you want to know the hot details, here's what's really happening:
1. You manually run the `review-docs-deploy` job in a CE/EE merge request.
1. The job runs the [`scirpts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
script with the `deploy` flag, which in turn:
1. Takes your branch name and applies the following:
- The slug of the branch name is used to avoid special characters since
ultimately this will be used by NGINX.
- The `preview-` prefix is added to avoid conflicts if there's a remote branch
with the same name that you created in the merge request.
- The final branch name is truncated to 42 characters to avoid filesystem
limitations with long branch names (> 63 chars).
1. The remote branch is then created if it doesn't exist (meaning you can
re-run the manual job as many times as you want and this step will be skipped).
1. A new cross-project pipeline is triggered in the docs project.
1. The preview URL is shown both at the job output and in the merge request
widget. You also get the link to the remote pipeline.
1. In the docs project, the pipeline is created and it
[skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
to lower the build time.
1. Once the docs site is built, the HTML files are uploaded as artifacts.
1. A specific Runner tied only to the docs project, runs the Review App job
that downloads the artifacts and uses `rsync` to transfer the files over
to a location where NGINX serves them.
The following GitLab features are used among others:
- [Manual actions](../ci/yaml/README.md#manual-actions)
- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
- [Review Apps](../ci/review_apps/index.md)
- [Artifacts](../ci/yaml/README.md#artifacts)
- [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
...@@ -364,6 +364,7 @@ module API ...@@ -364,6 +364,7 @@ module API
end end
class IssueBasic < ProjectEntity class IssueBasic < ProjectEntity
expose :closed_at
expose :labels do |issue, options| expose :labels do |issue, options|
# Avoids an N+1 query since labels are preloaded # Avoids an N+1 query since labels are preloaded
issue.labels.map(&:title).sort issue.labels.map(&:title).sort
......
...@@ -2,7 +2,7 @@ module API ...@@ -2,7 +2,7 @@ module API
class MergeRequests < Grape::API class MergeRequests < Grape::API
include PaginationParams include PaginationParams
before { authenticate! } before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata helpers ::Gitlab::IssuableMetadata
...@@ -55,6 +55,7 @@ module API ...@@ -55,6 +55,7 @@ module API
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`' desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
end end
get do get do
authenticate! unless params[:scope] == 'all'
merge_requests = find_merge_requests merge_requests = find_merge_requests
options = { with: Entities::MergeRequestBasic, options = { with: Entities::MergeRequestBasic,
......
...@@ -34,7 +34,8 @@ module Banzai ...@@ -34,7 +34,8 @@ module Banzai
{ namespace: :owner }, { namespace: :owner },
{ group: [:owners, :group_members] }, { group: [:owners, :group_members] },
:invited_groups, :invited_groups,
:project_members :project_members,
:project_feature
] ]
} }
), ),
......
module Gitlab
module Ci
module Build
module Policy
def self.fabricate(specs)
specifications = specs.to_h.map do |spec, value|
self.const_get(spec.to_s.camelize).new(value)
end
specifications.compact
end
end
end
end
end
module Gitlab
module Ci
module Build
module Policy
class Kubernetes < Policy::Specification
def initialize(spec)
unless spec.to_sym == :active
raise UnknownPolicyError
end
end
def satisfied_by?(pipeline)
pipeline.has_kubernetes_active?
end
end
end
end
end
end
module Gitlab
module Ci
module Build
module Policy
class Refs < Policy::Specification
def initialize(refs)
@patterns = Array(refs)
end
def satisfied_by?(pipeline)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
matches_path?(path, pipeline) &&
matches_pattern?(pattern, pipeline)
end
end
private
def matches_path?(path, pipeline)
return true unless path
pipeline.project_full_path == path
end
def matches_pattern?(pattern, pipeline)
return true if pipeline.tag? && pattern == 'tags'
return true if pipeline.branch? && pattern == 'branches'
return true if pipeline.source == pattern
return true if pipeline.source&.pluralize == pattern
if pattern.first == "/" && pattern.last == "/"
Regexp.new(pattern[1...-1]) =~ pipeline.ref
else
pattern == pipeline.ref
end
end
end
end
end
end
end
module Gitlab
module Ci
module Build
module Policy
##
# Abstract class that defines an interface of job policy
# specification.
#
# Used for job's only/except policy configuration.
#
class Specification
UnknownPolicyError = Class.new(StandardError)
def initialize(spec)
@spec = spec
end
def satisfied_by?(pipeline)
raise NotImplementedError
end
end
end
end
end
end
...@@ -5,12 +5,11 @@ module Gitlab ...@@ -5,12 +5,11 @@ module Gitlab
include Gitlab::Ci::Config::Entry::LegacyValidationHelpers include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
attr_reader :path, :cache, :stages, :jobs attr_reader :cache, :stages, :jobs
def initialize(config, path = nil) def initialize(config)
@ci_config = Gitlab::Ci::Config.new(config) @ci_config = Gitlab::Ci::Config.new(config)
@config = @ci_config.to_hash @config = @ci_config.to_hash
@path = path
unless @ci_config.valid? unless @ci_config.valid?
raise ValidationError, @ci_config.errors.first raise ValidationError, @ci_config.errors.first
...@@ -21,28 +20,12 @@ module Gitlab ...@@ -21,28 +20,12 @@ module Gitlab
raise ValidationError, e.message raise ValidationError, e.message
end end
def builds_for_stage_and_ref(stage, ref, tag = false, source = nil)
jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _|
build_attributes(name)
end
end
def builds def builds
@jobs.map do |name, _| @jobs.map do |name, _|
build_attributes(name) build_attributes(name)
end end
end end
def stage_seeds(pipeline)
seeds = @stages.uniq.map do |stage|
builds = pipeline_stage_builds(stage, pipeline)
Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
end
seeds.compact
end
def build_attributes(name) def build_attributes(name)
job = @jobs[name.to_sym] || {} job = @jobs[name.to_sym] || {}
...@@ -70,6 +53,32 @@ module Gitlab ...@@ -70,6 +53,32 @@ module Gitlab
}.compact } }.compact }
end end
def pipeline_stage_builds(stage, pipeline)
selected_jobs = @jobs.select do |_, job|
next unless job[:stage] == stage
only_specs = Gitlab::Ci::Build::Policy
.fabricate(job.fetch(:only, {}))
except_specs = Gitlab::Ci::Build::Policy
.fabricate(job.fetch(:except, {}))
only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
end
selected_jobs.map { |_, job| build_attributes(job[:name]) }
end
def stage_seeds(pipeline)
seeds = @stages.uniq.map do |stage|
builds = pipeline_stage_builds(stage, pipeline)
Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
end
seeds.compact
end
def self.validation_message(content) def self.validation_message(content)
return 'Please provide content of .gitlab-ci.yml' if content.blank? return 'Please provide content of .gitlab-ci.yml' if content.blank?
...@@ -83,34 +92,6 @@ module Gitlab ...@@ -83,34 +92,6 @@ module Gitlab
private private
def pipeline_stage_builds(stage, pipeline)
builds = builds_for_stage_and_ref(
stage, pipeline.ref, pipeline.tag?, pipeline.source)
builds.select do |build|
job = @jobs[build.fetch(:name).to_sym]
has_kubernetes = pipeline.has_kubernetes_active?
only_kubernetes = job.dig(:only, :kubernetes)
except_kubernetes = job.dig(:except, :kubernetes)
[!only_kubernetes && !except_kubernetes,
only_kubernetes && has_kubernetes,
except_kubernetes && !has_kubernetes].any?
end
end
def jobs_for_ref(ref, tag = false, source = nil)
@jobs.select do |_, job|
process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source)
end
end
def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil)
jobs_for_ref(ref, tag, source).select do |_, job|
job[:stage] == stage
end
end
def initial_parsing def initial_parsing
## ##
# Global config # Global config
...@@ -203,51 +184,6 @@ module Gitlab ...@@ -203,51 +184,6 @@ module Gitlab
raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
end end
end end
def process?(only_params, except_params, ref, tag, source)
if only_params.present?
return false unless matching?(only_params, ref, tag, source)
end
if except_params.present?
return false if matching?(except_params, ref, tag, source)
end
true
end
def matching?(patterns, ref, tag, source)
patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
matches_path?(path) && matches_pattern?(pattern, ref, tag, source)
end
end
def matches_path?(path)
return true unless path
path == self.path
end
def matches_pattern?(pattern, ref, tag, source)
return true if tag && pattern == 'tags'
return true if !tag && pattern == 'branches'
return true if source_to_pattern(source) == pattern
if pattern.first == "/" && pattern.last == "/"
Regexp.new(pattern[1...-1]) =~ ref
else
pattern == ref
end
end
def source_to_pattern(source)
if %w[api external web].include?(source)
source
else
source&.pluralize
end
end
end end
end end
end end
...@@ -57,6 +57,15 @@ module Gitlab ...@@ -57,6 +57,15 @@ module Gitlab
def version def version
Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first) Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first)
end end
def check_namespace!(*objects)
expected_namespace = self.name + '::'
objects.each do |object|
unless object.class.name.start_with?(expected_namespace)
raise ArgumentError, "expected object in #{expected_namespace}, got #{object}"
end
end
end
end end
end end
end end
...@@ -413,6 +413,10 @@ module Gitlab ...@@ -413,6 +413,10 @@ module Gitlab
end end
end end
def merge_commit?
parent_ids.size > 1
end
private private
def init_from_hash(hash) def init_from_hash(hash)
......
...@@ -15,9 +15,7 @@ module Gitlab ...@@ -15,9 +15,7 @@ module Gitlab
end end
# Refactoring aid # Refactoring aid
unless new_repository.is_a?(Gitlab::Git::Repository) Gitlab::Git.check_namespace!(new_repository)
raise "expected a Gitlab::Git::Repository, got #{new_repository}"
end
@repository = new_repository @repository = new_repository
end end
......
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
InvalidRef = Class.new(StandardError) InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError) GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError) DeleteBranchError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError)
class << self class << self
# Unlike `new`, `create` takes the storage path, not the storage name # Unlike `new`, `create` takes the storage path, not the storage name
...@@ -684,6 +685,88 @@ module Gitlab ...@@ -684,6 +685,88 @@ module Gitlab
nil nil
end end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
Gitlab::Git.check_namespace!(commit, start_repository)
revert_tree_id = check_revert_content(commit, start_commit.sha)
raise CreateTreeError unless revert_tree_id
committer = user_to_committer(user)
create_commit(message: message,
author: committer,
committer: committer,
tree: revert_tree_id,
parents: [start_commit.sha])
end
end
def check_revert_content(target_commit, source_sha)
args = [target_commit.sha, source_sha]
args << { mainline: 1 } if target_commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
tree_id = revert_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def cherry_pick(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)
cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
raise CreateTreeError unless cherry_pick_tree_id
committer = user_to_committer(user)
create_commit(message: message,
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [start_commit.sha])
end
end
def check_cherry_pick_content(target_commit, source_sha)
args = [target_commit.sha, source_sha]
args << 1 if target_commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
tree_id = cherry_pick_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0
end
def user_to_committer(user)
Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
def create_commit(params = {}) def create_commit(params = {})
params[:message].delete!("\r") params[:message].delete!("\r")
...@@ -835,7 +918,7 @@ module Gitlab ...@@ -835,7 +918,7 @@ module Gitlab
end end
def with_repo_branch_commit(start_repository, start_branch_name) def with_repo_branch_commit(start_repository, start_branch_name)
raise "expected Gitlab::Git::Repository, got #{start_repository}" unless start_repository.is_a?(Gitlab::Git::Repository) Gitlab::Git.check_namespace!(start_repository)
return yield nil if start_repository.empty_repo? return yield nil if start_repository.empty_repo?
......
...@@ -221,7 +221,7 @@ module Gitlab ...@@ -221,7 +221,7 @@ module Gitlab
repository: @gitaly_repo, repository: @gitaly_repo,
left_commit_id: parent_id, left_commit_id: parent_id,
right_commit_id: commit.id, right_commit_id: commit.id,
paths: options.fetch(:paths, []) paths: options.fetch(:paths, []).map { |path| GitalyClient.encode(path) }
} }
end end
......
...@@ -58,7 +58,7 @@ module Gitlab ...@@ -58,7 +58,7 @@ module Gitlab
end end
def repository_storages def repository_storages
@repository_storage ||= Gitlab::CurrentSettings.current_application_settings.repository_storages @repository_storage ||= storages_paths.keys
end end
def storages_paths def storages_paths
......
...@@ -17,7 +17,8 @@ module Gitlab ...@@ -17,7 +17,8 @@ module Gitlab
'it' => 'Italiano', 'it' => 'Italiano',
'uk' => 'Українська', 'uk' => 'Українська',
'ja' => '日本語', 'ja' => '日本語',
'ko' => '한국어' 'ko' => '한국어',
'nl_NL' => 'Nederlands'
}.freeze }.freeze
def available_locales def available_locales
......
...@@ -11,7 +11,7 @@ module QA ...@@ -11,7 +11,7 @@ module QA
end end
def go_to_admin_area def go_to_admin_area
within_top_menu { click_link 'Admin area' } within_top_menu { find('.admin-icon').click }
end end
def sign_out def sign_out
......
#!/bin/sh
schema_changed() {
if [ ! -z "$(git diff --name-only -- db/schema.rb)" ]; then
printf "db/schema.rb after rake db:migrate:reset is different from one in the repository"
printf "The diff is as follows:\n"
diff=$(git diff -p --binary -- db/schema.rb)
printf "%s" "$diff"
exit 1
else
printf "db/schema.rb after rake db:migrate:reset matches one in the repository"
fi
}
schema_changed
...@@ -2,13 +2,6 @@ ...@@ -2,13 +2,6 @@
require 'gitlab' require 'gitlab'
#
# Give the remote branch a different name than the current one
# in order to avoid conflicts
#
@docs_branch = "#{ENV["CI_COMMIT_REF_SLUG"]}-built-from-ce-ee"
GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
# #
# Configure credentials to be used with gitlab gem # Configure credentials to be used with gitlab gem
# #
...@@ -17,6 +10,26 @@ Gitlab.configure do |config| ...@@ -17,6 +10,26 @@ Gitlab.configure do |config|
config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token which has only Developer access to gitlab-docs config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token which has only Developer access to gitlab-docs
end end
#
# The remote docs project
#
GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
#
# Truncate the remote docs branch name if it's more than 63 characters
# otherwise we hit the filesystem limit and the directory name where
# NGINX serves the site won't match the branch name.
#
def docs_branch
# The maximum string length a file can have on a filesystem (ext4)
# is 63 characters. Let's use something smaller to be 100% sure.
max = 42
# Prefix the remote branch with 'preview-' in order to avoid
# name conflicts in the rare case the branch name already
# exists in the docs repo and truncate to max length.
"preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
end
# #
# Dummy way to find out in which repo we are, CE or EE # Dummy way to find out in which repo we are, CE or EE
# #
...@@ -28,18 +41,18 @@ end ...@@ -28,18 +41,18 @@ end
# Create a remote branch in gitlab-docs # Create a remote branch in gitlab-docs
# #
def create_remote_branch def create_remote_branch
Gitlab.create_branch(GITLAB_DOCS_REPO, @docs_branch, 'master') Gitlab.create_branch(GITLAB_DOCS_REPO, docs_branch, 'master')
puts "Remote branch '#{@docs_branch}' created" puts "Remote branch '#{docs_branch}' created"
rescue Gitlab::Error::BadRequest rescue Gitlab::Error::BadRequest
puts "Remote branch '#{@docs_branch}' already exists" puts "Remote branch '#{docs_branch}' already exists"
end end
# #
# Remove a remote branch in gitlab-docs # Remove a remote branch in gitlab-docs
# #
def remove_remote_branch def remove_remote_branch
Gitlab.delete_branch(GITLAB_DOCS_REPO, @docs_branch) Gitlab.delete_branch(GITLAB_DOCS_REPO, docs_branch)
puts "Remote branch '#{@docs_branch}' deleted" puts "Remote branch '#{docs_branch}' deleted"
end end
# #
...@@ -50,11 +63,11 @@ def trigger_pipeline ...@@ -50,11 +63,11 @@ def trigger_pipeline
param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE' param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE'
# The review app URL # The review app URL
app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}" app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
# Create the pipeline # Create the pipeline
puts "=> Triggering a pipeline..." puts "=> Triggering a pipeline..."
pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["DOCS_TRIGGER_TOKEN"], @docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] }) pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["CI_JOB_TOKEN"], docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
puts "=> Pipeline created:" puts "=> Pipeline created:"
puts "" puts ""
...@@ -77,4 +90,8 @@ when 'deploy' ...@@ -77,4 +90,8 @@ when 'deploy'
trigger_pipeline trigger_pipeline
when 'cleanup' when 'cleanup'
remove_remote_branch remove_remote_branch
else
puts "Please provide a valid option:
deploy - Creates the remote branch and triggers a pipeline
cleanup - Deletes the remote branch and stops the Review App"
end end
...@@ -10,6 +10,7 @@ describe HealthController do ...@@ -10,6 +10,7 @@ describe HealthController do
before do before do
allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip]) allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip])
stub_storage_settings({}) # Hide the broken storage
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end end
......
require 'spec_helper'
describe Projects::PipelinesSettingsController do
set(:user) { create(:user) }
set(:project_auto_devops) { create(:project_auto_devops) }
let(:project) { project_auto_devops.project }
before do
project.add_master(user)
sign_in(user)
end
describe 'PATCH update' do
before do
patch :update,
namespace_id: project.namespace.to_param,
project_id: project,
project: {
auto_devops_attributes: params
}
end
context 'when updating the auto_devops settings' do
let(:params) { { enabled: '', domain: 'mepmep.md' } }
it 'redirects to the settings page' do
expect(response).to have_http_status(302)
expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.")
end
context 'following the instance default' do
let(:params) { { enabled: '' } }
it 'allows enabled to be set to nil' do
project_auto_devops.reload
expect(project_auto_devops.enabled).to be_nil
end
end
end
end
end
...@@ -20,8 +20,7 @@ describe 'User edits files' do ...@@ -20,8 +20,7 @@ describe 'User edits files' do
it 'inserts a content of a file', js: true do it 'inserts a content of a file', js: true do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
find('.file-editor', match: :first)
wait_for_requests
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
...@@ -38,8 +37,7 @@ describe 'User edits files' do ...@@ -38,8 +37,7 @@ describe 'User edits files' do
it 'commits an edited file', js: true do it 'commits an edited file', js: true do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
find('.file-editor', match: :first)
wait_for_requests
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
...@@ -56,7 +54,7 @@ describe 'User edits files' do ...@@ -56,7 +54,7 @@ describe 'User edits files' do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
wait_for_requests find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
...@@ -67,15 +65,13 @@ describe 'User edits files' do ...@@ -67,15 +65,13 @@ describe 'User edits files' do
click_link('Changes') click_link('Changes')
wait_for_requests
expect(page).to have_content('*.rbca') expect(page).to have_content('*.rbca')
end end
it 'shows the diff of an edited file', js: true do it 'shows the diff of an edited file', js: true do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
find('.file-editor', match: :first)
wait_for_requests
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
click_link('Preview changes') click_link('Preview changes')
...@@ -104,7 +100,7 @@ describe 'User edits files' do ...@@ -104,7 +100,7 @@ describe 'User edits files' do
"A fork of this project has been created that you can make changes in, so you can submit a merge request." "A fork of this project has been created that you can make changes in, so you can submit a merge request."
) )
wait_for_requests find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
...@@ -120,7 +116,7 @@ describe 'User edits files' do ...@@ -120,7 +116,7 @@ describe 'User edits files' do
click_link('Fork') click_link('Fork')
wait_for_requests find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"title": { "type": "string" }, "title": { "type": "string" },
"description": { "type": ["string", "null"] }, "description": { "type": ["string", "null"] },
"state": { "type": "string" }, "state": { "type": "string" },
"closed_at": { "type": "date" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"updated_at": { "type": "date" }, "updated_at": { "type": "date" },
"labels": { "labels": {
......
{
"type": "array",
"items": { "$ref": "admin.json" }
}
{
"type": "array",
"items": { "$ref": "basic.json" }
}
...@@ -147,6 +147,12 @@ describe SubmoduleHelper do ...@@ -147,6 +147,12 @@ describe SubmoduleHelper do
expect(helper.submodule_links(submodule_item)).to eq([nil, nil]) expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
end end
it 'sanitizes invalid URL with extended ASCII' do
stub_url('é')
expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
end
it 'returns original' do it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce') stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
......
import Cookies from 'js-cookie';
import {
getCookieName,
getSelector,
showPopover,
hidePopover,
dismiss,
mouseleave,
mouseenter,
setupDismissButton,
} from '~/feature_highlight/feature_highlight_helper';
describe('feature highlight helper', () => {
describe('getCookieName', () => {
it('returns `feature-highlighted-` prefix', () => {
const cookieId = 'cookieId';
expect(getCookieName(cookieId)).toEqual(`feature-highlighted-${cookieId}`);
});
});
describe('getSelector', () => {
it('returns js-feature-highlight selector', () => {
const highlightId = 'highlightId';
expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`);
});
});
describe('showPopover', () => {
it('returns true when popover is shown', () => {
const context = {
hasClass: () => false,
popover: () => {},
addClass: () => {},
};
expect(showPopover.call(context)).toEqual(true);
});
it('returns false when popover is already shown', () => {
const context = {
hasClass: () => true,
};
expect(showPopover.call(context)).toEqual(false);
});
it('shows popover', (done) => {
const context = {
hasClass: () => false,
popover: () => {},
addClass: () => {},
};
spyOn(context, 'popover').and.callFake((method) => {
expect(method).toEqual('show');
done();
});
showPopover.call(context);
});
it('adds disable-animation and js-popover-show class', (done) => {
const context = {
hasClass: () => false,
popover: () => {},
addClass: () => {},
};
spyOn(context, 'addClass').and.callFake((classNames) => {
expect(classNames).toEqual('disable-animation js-popover-show');
done();
});
showPopover.call(context);
});
});
describe('hidePopover', () => {
it('returns true when popover is hidden', () => {
const context = {
hasClass: () => true,
popover: () => {},
removeClass: () => {},
};
expect(hidePopover.call(context)).toEqual(true);
});
it('returns false when popover is already hidden', () => {
const context = {
hasClass: () => false,
};
expect(hidePopover.call(context)).toEqual(false);
});
it('hides popover', (done) => {
const context = {
hasClass: () => true,
popover: () => {},
removeClass: () => {},
};
spyOn(context, 'popover').and.callFake((method) => {
expect(method).toEqual('hide');
done();
});
hidePopover.call(context);
});
it('removes disable-animation and js-popover-show class', (done) => {
const context = {
hasClass: () => true,
popover: () => {},
removeClass: () => {},
};
spyOn(context, 'removeClass').and.callFake((classNames) => {
expect(classNames).toEqual('disable-animation js-popover-show');
done();
});
hidePopover.call(context);
});
});
describe('dismiss', () => {
const context = {
hide: () => {},
};
beforeEach(() => {
spyOn(Cookies, 'set').and.callFake(() => {});
spyOn(hidePopover, 'call').and.callFake(() => {});
spyOn(context, 'hide').and.callFake(() => {});
dismiss.call(context);
});
it('sets cookie to true', () => {
expect(Cookies.set).toHaveBeenCalled();
});
it('calls hide popover', () => {
expect(hidePopover.call).toHaveBeenCalled();
});
it('calls hide', () => {
expect(context.hide).toHaveBeenCalled();
});
});
describe('mouseleave', () => {
it('calls hide popover if .popover:hover is false', () => {
const fakeJquery = {
length: 0,
};
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
spyOn(hidePopover, 'call');
mouseleave();
expect(hidePopover.call).toHaveBeenCalled();
});
it('does not call hide popover if .popover:hover is true', () => {
const fakeJquery = {
length: 1,
};
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
spyOn(hidePopover, 'call');
mouseleave();
expect(hidePopover.call).not.toHaveBeenCalled();
});
});
describe('mouseenter', () => {
const context = {};
it('shows popover', () => {
spyOn(showPopover, 'call').and.returnValue(false);
mouseenter.call(context);
expect(showPopover.call).toHaveBeenCalled();
});
it('registers mouseleave event if popover is showed', (done) => {
spyOn(showPopover, 'call').and.returnValue(true);
spyOn($.fn, 'on').and.callFake((eventName) => {
expect(eventName).toEqual('mouseleave');
done();
});
mouseenter.call(context);
});
it('does not register mouseleave event if popover is not showed', () => {
spyOn(showPopover, 'call').and.returnValue(false);
const spy = spyOn($.fn, 'on').and.callFake(() => {});
mouseenter.call(context);
expect(spy).not.toHaveBeenCalled();
});
});
describe('setupDismissButton', () => {
it('registers click event callback', (done) => {
const context = {
getAttribute: () => 'popoverId',
dataset: {
highlight: 'cookieId',
},
};
spyOn($.fn, 'on').and.callFake((event) => {
expect(event).toEqual('click');
done();
});
setupDismissButton.call(context);
});
});
});
import domContentLoaded from '~/feature_highlight/feature_highlight_options';
import bp from '~/breakpoints';
describe('feature highlight options', () => {
describe('domContentLoaded', () => {
const highlightOrder = [];
beforeEach(() => {
// Check for when highlightFeatures is called
spyOn(highlightOrder, 'find').and.callFake(() => {});
});
it('should not call highlightFeatures when breakpoint is xs', () => {
spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
domContentLoaded(highlightOrder);
expect(bp.getBreakpointSize).toHaveBeenCalled();
expect(highlightOrder.find).not.toHaveBeenCalled();
});
it('should not call highlightFeatures when breakpoint is sm', () => {
spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
domContentLoaded(highlightOrder);
expect(bp.getBreakpointSize).toHaveBeenCalled();
expect(highlightOrder.find).not.toHaveBeenCalled();
});
it('should not call highlightFeatures when breakpoint is md', () => {
spyOn(bp, 'getBreakpointSize').and.returnValue('md');
domContentLoaded(highlightOrder);
expect(bp.getBreakpointSize).toHaveBeenCalled();
expect(highlightOrder.find).not.toHaveBeenCalled();
});
it('should call highlightFeatures when breakpoint is lg', () => {
spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
domContentLoaded(highlightOrder);
expect(bp.getBreakpointSize).toHaveBeenCalled();
expect(highlightOrder.find).toHaveBeenCalled();
});
});
});
import Cookies from 'js-cookie';
import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
import * as featureHighlight from '~/feature_highlight/feature_highlight';
describe('feature highlight', () => {
describe('setupFeatureHighlightPopover', () => {
const selector = '.js-feature-highlight[data-highlight=test]';
beforeEach(() => {
setFixtures(`
<div>
<div class="js-feature-highlight" data-highlight="test" disabled>
Trigger
</div>
</div>
<div class="feature-highlight-popover-content">
Content
<div class="dismiss-feature-highlight">
Dismiss
</div>
</div>
`);
spyOn(window, 'addEventListener');
spyOn(window, 'removeEventListener');
featureHighlight.setupFeatureHighlightPopover('test', 0);
});
it('setups popover content', () => {
const $popoverContent = $('.feature-highlight-popover-content');
const outerHTML = $popoverContent.prop('outerHTML');
expect($(selector).data('content')).toEqual(outerHTML);
});
it('setups mouseenter', () => {
const showSpy = spyOn(featureHighlightHelper.showPopover, 'call');
$(selector).trigger('mouseenter');
expect(showSpy).toHaveBeenCalled();
});
it('setups debounced mouseleave', (done) => {
const hideSpy = spyOn(featureHighlightHelper.hidePopover, 'call');
$(selector).trigger('mouseleave');
// Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
setTimeout(() => {
expect(hideSpy).toHaveBeenCalled();
done();
}, 0);
});
it('setups inserted.bs.popover', () => {
$(selector).trigger('mouseenter');
const popoverId = $(selector).attr('aria-describedby');
const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click');
$(`#${popoverId} .dismiss-feature-highlight`).click();
expect(spyEvent).toHaveBeenTriggered();
});
it('setups show.bs.popover', () => {
$(selector).trigger('show.bs.popover');
expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
});
it('setups hide.bs.popover', () => {
$(selector).trigger('hide.bs.popover');
expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
});
it('removes disabled attribute', () => {
expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
});
it('displays popover', () => {
expect($(selector).attr('aria-describedby')).toBeFalsy();
$(selector).trigger('mouseenter');
expect($(selector).attr('aria-describedby')).toBeTruthy();
});
});
describe('shouldHighlightFeature', () => {
it('should return false if element is not found', () => {
spyOn(document, 'querySelector').and.returnValue(null);
spyOn(Cookies, 'get').and.returnValue(null);
expect(featureHighlight.shouldHighlightFeature()).toBeFalsy();
});
it('should return false if previouslyDismissed', () => {
spyOn(document, 'querySelector').and.returnValue(document.createElement('div'));
spyOn(Cookies, 'get').and.returnValue('true');
expect(featureHighlight.shouldHighlightFeature()).toBeFalsy();
});
it('should return true if element is found and not previouslyDismissed', () => {
spyOn(document, 'querySelector').and.returnValue(document.createElement('div'));
spyOn(Cookies, 'get').and.returnValue(null);
expect(featureHighlight.shouldHighlightFeature()).toBeTruthy();
});
});
describe('highlightFeatures', () => {
it('calls setupFeatureHighlightPopover if shouldHighlightFeature returns true', () => {
// Mimic shouldHighlightFeature set to true
const highlightOrder = ['issue-boards'];
spyOn(highlightOrder, 'find').and.returnValue(highlightOrder[0]);
expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(true);
});
it('does not call setupFeatureHighlightPopover if shouldHighlightFeature returns false', () => {
// Mimic shouldHighlightFeature set to false
const highlightOrder = ['issue-boards'];
spyOn(highlightOrder, 'find').and.returnValue(null);
expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(false);
});
});
});
This diff is collapsed.
...@@ -96,7 +96,7 @@ describe('MRWidgetReadyToMerge', () => { ...@@ -96,7 +96,7 @@ describe('MRWidgetReadyToMerge', () => {
}); });
describe('mergeButtonClass', () => { describe('mergeButtonClass', () => {
const defaultClass = 'btn btn-small btn-success accept-merge-request'; const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`; const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`; const inActionClass = `${defaultClass} btn-info`;
......
require 'spec_helper'
describe Gitlab::Ci::Build::Policy::Kubernetes do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when kubernetes service is active' do
set(:project) { create(:kubernetes_project) }
it 'is satisfied by a kubernetes pipeline' do
expect(described_class.new('active'))
.to be_satisfied_by(pipeline)
end
end
context 'when kubernetes service is inactive' do
set(:project) { create(:project) }
it 'is not satisfied by a pipeline without kubernetes available' do
expect(described_class.new('active'))
.not_to be_satisfied_by(pipeline)
end
end
context 'when kubernetes policy is invalid' do
it 'raises an error' do
expect { described_class.new('unknown') }
.to raise_error(described_class::UnknownPolicyError)
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Policy::Refs do
describe '#satisfied_by?' do
context 'when matching ref' do
let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'master') }
it 'is satisfied when pipeline branch matches' do
expect(described_class.new(%w[master deploy]))
.to be_satisfied_by(pipeline)
end
it 'is not satisfied when pipeline branch does not match' do
expect(described_class.new(%w[feature fix]))
.not_to be_satisfied_by(pipeline)
end
end
context 'when maching tags' do
context 'when pipeline runs for a tag' do
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: 'feature', tag: true)
end
it 'is satisfied when tags matcher is specified' do
expect(described_class.new(%w[master tags]))
.to be_satisfied_by(pipeline)
end
end
context 'when pipeline is not created for a tag' do
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: 'feature', tag: false)
end
it 'is not satisfied when tag match is specified' do
expect(described_class.new(%w[master tags]))
.not_to be_satisfied_by(pipeline)
end
end
end
context 'when also matching a path' do
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: 'master')
end
it 'is satisfied when provided patch matches specified one' do
expect(described_class.new(%W[master@#{pipeline.project_full_path}]))
.to be_satisfied_by(pipeline)
end
it 'is not satisfied when path differs' do
expect(described_class.new(%w[master@some/fork/repository]))
.not_to be_satisfied_by(pipeline)
end
end
context 'when maching a source' do
let(:pipeline) { build_stubbed(:ci_pipeline, source: :push) }
it 'is satisifed when provided source keyword matches' do
expect(described_class.new(%w[pushes]))
.to be_satisfied_by(pipeline)
end
it 'is not satisfied when provided source keyword does not match' do
expect(described_class.new(%w[triggers]))
.not_to be_satisfied_by(pipeline)
end
end
context 'when matching a ref by a regular expression' do
let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'docs-something') }
it 'is satisfied when regexp matches pipeline ref' do
expect(described_class.new(['/docs-.*/']))
.to be_satisfied_by(pipeline)
end
it 'is not satisfied when regexp does not match pipeline ref' do
expect(described_class.new(['/fix-.*/']))
.not_to be_satisfied_by(pipeline)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Policy do
let(:policy) { spy('policy specification') }
before do
stub_const("#{described_class}::Something", policy)
end
describe '.fabricate' do
context 'when policy exists' do
it 'fabricates and initializes relevant policy' do
specs = described_class.fabricate(something: 'some value')
expect(specs).to be_an Array
expect(specs).to be_one
expect(policy).to have_received(:new).with('some value')
end
end
context 'when some policies are not defined' do
it 'gracefully skips unknown policies' do
expect { described_class.fabricate(unknown: 'first') }
.to raise_error(NameError)
end
end
context 'when passing a nil value as specs' do
it 'returns an empty array' do
specs = described_class.fabricate(nil)
expect(specs).to be_an Array
expect(specs).to be_empty
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.
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