Commit ff7b545c authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge remote-tracking branch 'origin/master' into 18608-lock-issues

parents 55a75296 ff1deab6
......@@ -128,7 +128,7 @@ stages:
- export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- scripts/gitaly-test-spawn
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
- knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
when: always
......@@ -174,7 +174,8 @@ build-package:
# Review docs base
.review-docs: &review-docs
image: ruby:2.4-alpine
before_script: []
before_script:
- gem install gitlab --no-doc
services: []
variables:
SETUP_DB: "false"
......@@ -193,10 +194,9 @@ review-docs-deploy:
name: review-docs/$CI_COMMIT_REF_NAME
# 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
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
script:
- gem install gitlab --no-doc
- scripts/trigger-build-docs deploy
# Cleanup remote environment of gitlab-docs
......@@ -207,7 +207,6 @@ review-docs-cleanup:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
script:
- gem install gitlab --no-doc
- scripts/trigger-build-docs cleanup
# Retrieve knapsack and rspec_flaky reports
......@@ -413,12 +412,12 @@ downtime_check:
ee_compat_check:
<<: *rake-exec
only:
- branches@gitlab-org/gitlab-ce
except:
- master
- tags
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
allow_failure: yes
cache:
key: "ee_compat_check_repo"
......@@ -517,6 +516,12 @@ db:seed_fu-mysql:
<<: *db-seed_fu
<<: *use-mysql
db:check-schema-pg:
<<: *db-migrate-reset
<<: *use-pg
script:
- source scripts/schema_changed.sh
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-runner
......
......@@ -362,6 +362,7 @@ group :test do
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5'
gem 'test-prof', '~> 0.2.5'
end
gem 'octokit', '~> 4.6.2'
......
......@@ -882,6 +882,7 @@ GEM
ffi
sysexits (1.2.0)
temple (0.7.7)
test-prof (0.2.5)
test_after_commit (1.1.0)
activerecord (>= 3.2)
text (1.3.1)
......@@ -1163,6 +1164,7 @@ DEPENDENCIES
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.2.5)
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
......
Copyright (c) 2011-2017 GitLab B.V.
With regard to the GitLab Software:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
......@@ -17,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
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.
\ No newline at end of file
......@@ -73,7 +73,7 @@
</span>
<a
v-if="deployKey.can_edit"
class="btn btn-small"
class="btn btn-sm"
:href="editDeployKeyPath"
>
Edit
......
......@@ -4,6 +4,8 @@
import Vue from 'vue';
import '../mixins/discussion';
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
......
......@@ -4,6 +4,8 @@
import Vue from 'vue';
import '../mixins/discussion';
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
props: {
......
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';
(() => {
/*
* TODO: Make these methods more configurable (e.g. stringifyTime condensed or
* non-condensed, abbreviateTimelengths)
* */
const utils = window.gl.utils = gl.utils || {};
const prettyTime = utils.prettyTime = {
/*
* 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.
*/
parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
const DAYS_PER_WEEK = daysPerWeek;
const HOURS_PER_DAY = hoursPerDay;
const MINUTES_PER_HOUR = 60;
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,
days: MINUTES_PER_DAY,
hours: MINUTES_PER_HOUR,
minutes: 1,
};
/*
* TODO: Make these methods more configurable (e.g. stringifyTime condensed or
* non-condensed, abbreviateTimelengths)
* */
/*
* 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;
const HOURS_PER_DAY = hoursPerDay;
const MINUTES_PER_HOUR = 60;
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,
days: MINUTES_PER_DAY,
hours: MINUTES_PER_HOUR,
minutes: 1,
};
let unorderedMinutes = prettyTime.secondsToMinutes(seconds);
let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
return _.mapObject(timePeriodConstraints, (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
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/
/*
* 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.
*/
stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
}, '').trim();
return reducedTime.length ? reducedTime : '0m';
},
export function stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
}, '').trim();
return reducedTime.length ? reducedTime : '0m';
}
/*
* 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.
*/
/*
* 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.
*/
abbreviateTime(timeStr) {
return timeStr.split(' ')
.filter(unitStr => unitStr.charAt(0) !== '0')[0];
},
export function abbreviateTime(timeStr) {
return timeStr.split(' ')
.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';
import './labels';
import './labels_select';
import './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
......
......@@ -11,6 +11,7 @@ export default class NewNavSidebar {
initDomElements() {
this.$page = $('.page-with-sidebar');
this.$sidebar = $('.nav-sidebar');
this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
......@@ -55,6 +56,16 @@ export default class NewNavSidebar {
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : 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() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import findAndFollowLink from './shortcuts_dashboard_navigation';
......
import stopwatchSvg from 'icons/_icon_stopwatch.svg';
import '../../../lib/utils/pretty_time';
import { abbreviateTime } from '../../../lib/utils/pretty_time';
export default {
name: 'time-tracking-collapsed-state',
......@@ -79,7 +78,7 @@ export default {
},
methods: {
abbreviateTime(timeStr) {
return gl.utils.prettyTime.abbreviateTime(timeStr);
return abbreviateTime(timeStr);
},
},
template: `
......
import '../../../lib/utils/pretty_time';
const prettyTime = gl.utils.prettyTime;
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
export default {
name: 'time-tracking-comparison-pane',
......@@ -23,12 +21,12 @@ export default {
},
},
computed: {
parsedRemaining() {
parsedTimeRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent;
return prettyTime.parseSeconds(diffSeconds);
return parseSeconds(diffSeconds);
},
timeRemainingHumanReadable() {
return prettyTime.stringifyTime(this.parsedRemaining);
return stringifyTime(this.parsedTimeRemaining);
},
timeRemainingTooltip() {
const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
......@@ -44,13 +42,6 @@ export default {
timeRemainingStatusClass() {
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: `
<div class="time-tracking-comparison-pane">
......
......@@ -72,12 +72,12 @@ export default {
<a
href="#modal_merge_info"
data-toggle="modal"
class="btn btn-small inline">
class="btn btn-sm inline">
Check out branch
</a>
<span class="dropdown prepend-left-10">
<a
class="btn btn-small inline dropdown-toggle"
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
......
......@@ -12,6 +12,9 @@ export default {
ciIcon,
},
computed: {
hasPipeline() {
return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
},
hasCIError() {
const { hasCI, ciStatus } = this.mr;
......@@ -28,7 +31,9 @@ export default {
},
},
template: `
<div class="mr-widget-heading">
<div
v-if="hasPipeline || hasCIError"
class="mr-widget-heading">
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
......@@ -40,7 +45,7 @@ export default {
Could not connect to the CI server. Please check your settings and try again
</div>
</template>
<template v-else>
<template v-else-if="hasPipeline">
<div class="ci-status-icon append-right-10">
<a
class="icon-link"
......
......@@ -27,7 +27,7 @@ export default {
<button
v-if="showDisabledButton"
type="button"
class="btn btn-success btn-small"
class="btn btn-success btn-sm"
disabled="true">
Merge
</button>
......
......@@ -11,7 +11,7 @@ export default {
<status-icon status="failed" />
<button
type="button"
class="btn btn-success btn-small"
class="btn btn-success btn-sm"
disabled="true">
Merge
</button>
......
......@@ -29,6 +29,9 @@ export default {
statusIcon,
},
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
},
commitMessageLinkTitle() {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
......@@ -36,7 +39,7 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
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 inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
......@@ -56,7 +59,7 @@ export default {
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
} else if (this.mr.isPipelineActive) {
} else if (this.shouldShowMergeWhenPipelineSucceedsText) {
return 'Merge when pipeline succeeds';
}
......@@ -68,7 +71,7 @@ export default {
isMergeButtonDisabled() {
const { commitMessage } = this;
return Boolean(!commitMessage.length
|| !this.isMergeAllowed()
|| !this.shouldShowMergeControls()
|| this.isMakingRequest
|| this.mr.preventMerge);
},
......@@ -82,7 +85,12 @@ export default {
},
methods: {
isMergeAllowed() {
return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed);
return !this.mr.onlyAllowMergeIfPipelineSucceeds ||
this.mr.isPipelinePassing ||
this.mr.isPipelineSkipped;
},
shouldShowMergeControls() {
return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText;
},
updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription;
......@@ -202,8 +210,8 @@ export default {
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<div class="media space-children">
<span class="btn-group">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
<button
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
......@@ -219,7 +227,7 @@ export default {
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
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"
aria-label="Select merge moment">
<i
......@@ -260,8 +268,8 @@ export default {
</li>
</ul>
</span>
<div class="media-body space-children">
<template v-if="isMergeAllowed()">
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls()">
<label>
<input
id="remove-source-branch-input"
......@@ -286,7 +294,7 @@ export default {
</template>
<template v-else>
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
The pipeline for this merge request has not succeeded yet
</span>
</template>
</div>
......
......@@ -57,7 +57,7 @@ export default {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
},
shouldRenderPipelines() {
return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
return this.mr.relatedLinks;
......
......@@ -85,7 +85,9 @@ export default class MergeRequestStore {
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.hasCI = data.has_ci;
this.ciStatus = data.ci_status;
this.isPipelineFailed = this.ciStatus ? (this.ciStatus === 'failed' || this.ciStatus === 'canceled') : false;
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isPipelinePassing = this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings';
this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
......
......@@ -52,4 +52,3 @@
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive-tables";
@import "framework/feature_highlight";
......@@ -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) {
background-color: $light;
border-color: $border-light;
......@@ -132,7 +123,6 @@
.btn {
@include btn-default;
@include btn-white;
@include btn-svg;
color: $gl-text-color;
......@@ -140,7 +130,6 @@
outline: 0;
}
&.btn-small,
&.btn-sm {
padding: 4px 10px;
font-size: 13px;
......@@ -232,6 +221,13 @@
}
}
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
svg,
.fa {
&: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;
}
}
......@@ -6,3 +6,7 @@
.media-body {
flex: 1;
}
.media-body-wrap {
flex-grow: 1;
}
......@@ -192,7 +192,11 @@ $new-sidebar-collapsed-width: 50px;
.nav-sidebar-inner-scroll {
height: 100%;
width: 100%;
overflow: scroll;
overflow: auto;
@media (min-width: $screen-sm-min) {
overflow: hidden;
}
}
.with-performance-bar .nav-sidebar {
......
.info-well {
.admin-well-statistics,
.admin-well-features {
padding-bottom: 46px;
}
}
......@@ -356,6 +356,10 @@
}
}
.mr-widget-body-controls {
flex-wrap: wrap;
}
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
......
......@@ -784,6 +784,7 @@ ul.notes {
background-color: transparent;
border: none;
outline: 0;
color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
......@@ -807,7 +808,7 @@ ul.notes {
}
svg {
fill: $gray-darkest;
fill: currentColor;
height: 16px;
width: 16px;
}
......
......@@ -209,6 +209,11 @@
}
.stage-cell {
@media (min-width: $screen-md-min) {
min-width: 148px;
margin-right: -4px;
}
.mini-pipeline-graph-dropdown-toggle svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
......
......@@ -56,7 +56,6 @@
.tree-content-holder {
display: flex;
max-height: 100vh;
min-height: 300px;
}
......@@ -156,7 +155,7 @@
list-style-type: none;
background: $gray-normal;
display: inline-block;
padding: 10px 18px;
padding: #{$gl-padding / 2} $gl-padding;
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
white-space: nowrap;
......@@ -180,10 +179,9 @@
a {
@include str-truncated(100px);
color: $black;
width: 100px;
text-align: center;
vertical-align: middle;
text-decoration: none;
margin-right: 12px;
&.close {
width: auto;
......@@ -193,6 +191,10 @@
}
}
.close-icon:hover {
color: $hint-color;
}
.close-icon,
.unsaved-icon {
float: right;
......
......@@ -29,7 +29,7 @@ class Admin::LabelsController < Admin::ApplicationController
@label = Labels::UpdateService.new(label_params).execute(@label)
if @label.valid?
redirect_to admin_labels_path, notice: 'label was successfully updated.'
redirect_to admin_labels_path, notice: 'Label was successfully updated.'
else
render :edit
end
......
......@@ -38,7 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
@scopes = Gitlab::Auth::AVAILABLE_SCOPES
@scopes = Gitlab::Auth.available_scopes
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
......
......@@ -15,10 +15,14 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
render
end
end
format.json do
......
......@@ -20,7 +20,12 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie!
respond_to do |format|
format.html
format.html do
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
end
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
end
......
......@@ -17,6 +17,10 @@ class Projects::CompareController < Projects::ApplicationController
def show
apply_diff_view_cookie!
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
end
def diff_for_path
......
......@@ -9,14 +9,12 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
@forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
@total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size
@private_forks_count = @total_forks_count - forks.size
@public_forks_count = @total_forks_count - @private_forks_count
@sort = params[:sort] || 'id_desc'
@forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
@forks = @forks.order_by(@sort).page(params[:page])
@forks = forks.page(params[:page])
respond_to do |format|
format.html
......
......@@ -71,9 +71,6 @@ class Projects::IssuesController < Projects::ApplicationController
@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|
format.html
format.json do
......@@ -87,9 +84,9 @@ class Projects::IssuesController < Projects::ApplicationController
.inc_relations_for_view
.includes(:noteable)
.fresh
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
prepare_notes_for_rendering(notes)
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
discussions = Discussion.build_collection(notes, @issue)
......
......@@ -10,7 +10,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431
Gitlab::GitalyClient.allow_n_plus_1_calls do
render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
end
end
def diff_for_path
......
......@@ -56,6 +56,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
close_merge_request_without_source_project
check_if_can_be_merged
# Return if the response has already been rendered
return if response_body
respond_to do |format|
format.html do
# Build a note object for comment form
......@@ -70,6 +73,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
labels
set_pipeline_variables
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37432
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
end
format.json do
......
......@@ -8,19 +8,24 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :assign_commit
def show
@url = project_network_path(@project, @ref, @options.merge(format: :json))
@commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602
Gitlab::GitalyClient.allow_n_plus_1_calls do
@url = project_network_path(@project, @ref, @options.merge(format: :json))
@commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
respond_to do |format|
format.html do
if @options[:extended_sha1] && !@commit
flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
respond_to do |format|
format.html do
if @options[:extended_sha1] && !@commit
flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
end
end
end
format.json do
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
format.json do
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
end
end
render
end
end
......
......@@ -51,13 +51,16 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs)
contents.push(*tree.submodules)
@logs = contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
{
file_name: content.name,
commit: last_commit
}
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
@logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
{
file_name: content.name,
commit: last_commit
}
end
end
offset = (@offset + @limit)
......
......@@ -28,7 +28,7 @@ class Projects::UploadsController < Projects::ApplicationController
end
def image_or_video?
uploader && uploader.file.exists? && uploader.image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
......
......@@ -13,7 +13,10 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_logged_user, if: -> { current_user.present? }
def index
super
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434
Gitlab::GitalyClient.allow_n_plus_1_calls do
super
end
end
private
......
class ForkProjectsFinder < ProjectsFinder
def initialize(project, params: {}, current_user: nil)
project_ids = project.forks.includes(:creator).select(:id)
super(params: params, current_user: current_user, project_ids_relation: project_ids)
end
end
......@@ -57,7 +57,7 @@ class GroupsFinder < UnionFinder
end
def owned_groups
current_user&.groups || Group.none
current_user&.owned_groups || Group.none
end
def include_public_groups?
......
......@@ -244,6 +244,8 @@ class IssuableFinder
end
def by_scope(items)
return items.none if current_user_related? && !current_user
case params[:scope]
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
......
......@@ -5,6 +5,25 @@ module AutoDevopsHelper
can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled? &&
!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
......@@ -94,6 +94,12 @@ module MilestonesHelper
end
end
def milestone_tooltip_title(milestone)
if milestone.due_date
[milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
end
end
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'Past due')
......
......@@ -87,10 +87,14 @@ module SubmoduleHelper
namespace = @project.namespace.full_path
end
[
namespace_project_path(namespace, base),
namespace_project_tree_path(namespace, base, commit)
]
begin
[
namespace_project_path(namespace, base),
namespace_project_tree_path(namespace, base, commit)
]
rescue ActionController::UrlGenerationError
[nil, nil]
end
end
def sanitize_submodule_url(url)
......
......@@ -31,6 +31,7 @@ module Ci
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
delegate :id, to: :project, prefix: true
delegate :full_path, to: :project, prefix: true
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
validates :sha, presence: { unless: :importing? }
......@@ -336,7 +337,7 @@ module Ci
return @config_processor if defined?(@config_processor)
@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
self.yaml_errors = e.message
nil
......
......@@ -6,9 +6,7 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
has_many :deployments,
-> (env) { where(project_id: env.project_id) },
dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
......
......@@ -275,8 +275,6 @@ class Issue < ActiveRecord::Base
end
def update_project_counter_caches
return unless update_project_counter_caches?
Projects::OpenIssuesCountService.new(project).refresh_cache
end
......
......@@ -415,8 +415,11 @@ class MergeRequest < ActiveRecord::Base
end
def create_merge_request_diff
merge_request_diffs.create
reload_merge_request_diff
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do
merge_request_diffs.create
reload_merge_request_diff
end
end
def reload_merge_request_diff
......@@ -955,8 +958,6 @@ class MergeRequest < ActiveRecord::Base
end
def update_project_counter_caches
return unless update_project_counter_caches?
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
......
......@@ -162,9 +162,7 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid, full: false)
return if group_milestone? && format != :name
def to_reference(from_project = nil, format: :name, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
......@@ -241,6 +239,10 @@ class Milestone < ActiveRecord::Base
def milestone_format_reference(format = :iid)
raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
if group_milestone? && format == :iid
raise ArgumentError, 'Cannot refer to a group milestone by an internal id!'
end
if format == :name && !name.include?('"')
%("#{name}")
else
......
......@@ -61,8 +61,11 @@ module Network
@reserved[i] = []
end
commits_sort_by_ref.each do |commit|
place_chain(commit)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37436
Gitlab::GitalyClient.allow_n_plus_1_calls do
commits_sort_by_ref.each do |commit|
place_chain(commit)
end
end
# find parent spaces for not overlap lines
......
......@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base
protected
def validate_scopes
unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
unless revoked || scopes.all? { |scope| Gitlab::Auth.available_scopes.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes"
end
end
......
......@@ -192,7 +192,7 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
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 :members, to: :team, prefix: true
......
......@@ -6,6 +6,10 @@ class ProjectAutoDevops < ActiveRecord::Base
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def has_domain?
domain.present?
end
def variables
variables = []
variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
......
......@@ -146,7 +146,7 @@ class ProjectTeam
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
user.authorized_project?(project, min_access_level)
max_member_access(user.id) >= min_access_level
end
def human_max_access(user_id)
......
......@@ -834,10 +834,6 @@ class Repository
}
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)
our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha)
......@@ -859,54 +855,34 @@ class Repository
end
def revert(
user, commit, branch_name,
user, commit, branch_name, message,
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)
create_commit(message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
parents: [start_commit.sha])
with_cache_hooks do
raw_repository.revert(
user: user,
commit: commit.raw,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_project.repository.raw_repository
)
end
end
def cherry_pick(
user, commit, branch_name,
user, commit, branch_name, message,
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)
unless cherry_pick_tree_id
raise Repository::CreateTreeError.new('Failed to cherry-pick commit')
end
committer = user_to_committer(user)
create_commit(message: commit.cherry_pick_message(user),
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [start_commit.sha])
with_cache_hooks do
raw_repository.cherry_pick(
user: user,
commit: commit.raw,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_project.repository.raw_repository
)
end
end
......@@ -918,36 +894,6 @@ class Repository
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)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
......@@ -983,6 +929,7 @@ class Repository
def empty_repo?
!exists? || !has_visible_content?
end
cache_method :empty_repo?, memoize_only: true
def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank?
......
......@@ -9,6 +9,7 @@ class GroupPolicy < BasePolicy
condition(:has_access) { access_level != GroupMember::NO_ACCESS }
condition(:guest) { access_level >= GroupMember::GUEST }
condition(:developer) { access_level >= GroupMember::DEVELOPER }
condition(:owner) { access_level >= GroupMember::OWNER }
condition(:master) { access_level >= GroupMember::MASTER }
condition(:reporter) { access_level >= GroupMember::REPORTER }
......@@ -33,11 +34,11 @@ class GroupPolicy < BasePolicy
rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label
rule { master }.policy do
enable :create_projects
enable :admin_milestones
enable :admin_pipeline
enable :admin_build
end
......
......@@ -155,6 +155,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:developer_access) }.policy do
enable :admin_merge_request
enable :admin_milestone
enable :update_merge_request
enable :create_commit_status
enable :update_commit_status
......@@ -178,7 +179,6 @@ class ProjectPolicy < BasePolicy
enable :update_project_snippet
enable :update_environment
enable :update_deployment
enable :admin_milestone
enable :admin_project_snippet
enable :admin_project_member
enable :admin_note
......
......@@ -11,15 +11,19 @@ module Commits
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
# rubocop:disable GitlabSecurity/PublicSend
message = @commit.public_send(:"#{action}_message", current_user)
# rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
@commit,
@branch_name,
message,
start_project: @start_project,
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.
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
......
......@@ -6,15 +6,18 @@ class DeleteMergedBranchesService < BaseService
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
# Prevent deletion of protected branches
branches = branches.reject { |branch| project.protected_for?(branch) }
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37438
Gitlab::GitalyClient.allow_n_plus_1_calls do
branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
# Prevent deletion of protected branches
branches = branches.reject { |branch| project.protected_for?(branch) }
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
end
end
end
......
......@@ -187,6 +187,7 @@ class IssuableBaseService < BaseService
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
issuable.update_project_counter_caches
end
issuable
......@@ -198,8 +199,6 @@ class IssuableBaseService < BaseService
def after_create(issuable)
# To be overridden by subclasses
issuable.update_project_counter_caches
end
def before_update(issuable)
......@@ -208,8 +207,6 @@ class IssuableBaseService < BaseService
def after_update(issuable)
# To be overridden by subclasses
issuable.update_project_counter_caches
end
def update(issuable)
......@@ -234,6 +231,10 @@ class IssuableBaseService < BaseService
before_update(issuable)
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
update_project_counters = issuable.update_project_counter_caches?
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
......@@ -255,6 +256,8 @@ class IssuableBaseService < BaseService
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
issuable.update_project_counter_caches if update_project_counters
end
end
......
......@@ -29,6 +29,7 @@ module Issues
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
end
issue
......
......@@ -14,6 +14,7 @@ module MergeRequests
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
end
merge_request
......
......@@ -13,7 +13,10 @@ module MergeRequests
merge_request.source_branch = params[:source_branch]
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
create(merge_request)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37439
Gitlab::GitalyClient.allow_n_plus_1_calls do
create(merge_request)
end
end
def before_create(merge_request)
......
......@@ -4,7 +4,13 @@ module Notes
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = Notes::BuildService.new(project, current_user, params).execute
return note unless note.valid?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37440
note_valid = Gitlab::GitalyClient.allow_n_plus_1_calls do
note.valid?
end
return note unless note_valid
# We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands
......
......@@ -2,6 +2,11 @@ module Projects
# Base class for the various service classes that count project data (e.g.
# issues or forks).
class CountService
# The version of the cache format. This should be bumped whenever the
# underlying logic changes. This removes the need for explicitly flushing
# all caches.
VERSION = 1
def initialize(project)
@project = project
end
......@@ -37,7 +42,7 @@ module Projects
end
def cache_key
['projects', @project.id, cache_key_name]
['projects', 'count_service', VERSION, @project.id, cache_key_name]
end
end
end
......@@ -9,7 +9,7 @@ class AvatarUploader < GitlabUploader
end
def exists?
model.avatar.file && model.avatar.file.exists?
model.avatar.file && model.avatar.file.present?
end
# We set move_to_store and move_to_cache to 'false' to prevent stealing
......
......@@ -51,7 +51,7 @@ class GitlabUploader < CarrierWave::Uploader::Base
end
def exists?
file.try(:exists?)
file.present?
end
# Override this if you don't want to save files by default to the Rails.root directory
......
......@@ -21,7 +21,7 @@
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- if @appearance.persisted?
%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
= f.hidden_field :logo_cache
= f.file_field :logo, class: ""
......@@ -38,7 +38,7 @@
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%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
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
......
......@@ -7,7 +7,7 @@
.row
.col-md-4
.info-well
.well-segment.admin-well
.well-segment.admin-well.admin-well-statistics
%h4 Statistics
%p
Forks
......@@ -43,7 +43,7 @@
= number_with_delimiter(User.active.count)
.col-md-4
.info-well
.well-segment.admin-well
.well-segment.admin-well.admin-well-features
%h4 Features
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
......@@ -111,6 +111,10 @@
GitLab API
%span.pull-right
= API::API::version
%p
Gitaly
%span.pull-right
= Gitlab::GitalyClient.expected_server_version
- if Gitlab.config.pages.enabled
%p
GitLab Pages
......
......@@ -22,7 +22,7 @@
- @hooks.each do |hook|
%li
.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 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.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>
......@@ -117,20 +117,6 @@
= link_to project_boards_path(@project), title: boards_link_text do
%span
= 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
= link_to project_labels_path(@project), title: 'Labels' do
......
......@@ -74,7 +74,9 @@
%h4.prepend-top-0.warning-title
Change username
%p
Changing your username will change path to all personal projects!
Changing your username can have unintended side effects.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8
= form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group
......
......@@ -27,6 +27,8 @@
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
.prepend-top-20
.empty_wrapper
%h3.page-title-empty
......
......@@ -24,7 +24,7 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title) do
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
......
......@@ -23,7 +23,7 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title) do
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
......
......@@ -62,7 +62,10 @@
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":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
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
......
......@@ -16,7 +16,9 @@
New project
- if import_sources_enabled?
%p
Create or Import your project from popular Git services
A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
%p
All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
.col-lg-9.js-toggle-container
= form_for @project, html: { class: 'new_project' } do |f|
.create-project-options
......
......@@ -3,11 +3,15 @@
= form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature
.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)
%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')
- 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|
.radio
= form.label :enabled_true do
......@@ -15,26 +19,24 @@
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml
in the project.
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery.
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continious Integration and Delivery.
.radio
= form.label :enabled do
= form.radio_button :enabled, nil
%strong
Instance default (status: #{current_application_settings.auto_devops_enabled?})
= form.label :enabled_nil do
= form.radio_button :enabled, ''
%strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified.
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%br
%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'
%hr
......
......@@ -13,7 +13,7 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%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) }
= render 'projects/pipelines_settings/show'
......
......@@ -10,7 +10,7 @@
%span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= 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
%span.sr-only Remove
= icon('trash')
......@@ -22,11 +22,9 @@
- if @group.persisted?
.alert.alert-warning.prepend-top-10
%ul
%li Changing group path can have unintended side effects.
%li Renaming group path will rename directory for all related projects
%li It will change web url for access group and group projects.
%li It will change the git path to repositories under this group.
Changing group path can have unintended side effects.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
.form-group.group-name-holder
= f.label :name, class: 'control-label' do
......
- 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
- if defined?(path)
= hidden_field_tag :path, path
......@@ -7,14 +7,10 @@
= hidden_field_tag key, value, id: nil
.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]) }
%li
= dropdown_title _("Create a new branch")
%li
= dropdown_input _("Create a new branch")
%li
= dropdown_title _("Select existing branch"), options: {close: false}
%li
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Create a new branch")
= dropdown_input _("Create a new branch")
= dropdown_title _("Select existing branch"), options: {close: false}
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
......@@ -27,7 +27,7 @@
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- 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",
"v-if" => 'list.type !== "closed"',
"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>
......@@ -26,7 +26,7 @@
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title
- if issuable.milestone
%span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_remaining_days(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
%span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
None
......@@ -37,7 +37,7 @@
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
- else
%span.no-value None
......
......@@ -26,6 +26,6 @@
%span.assignee-icon
- assignees.each do |assignee|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
= link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(assignee, 16), class: "avatar s16", alt: '')
---
title: Add an API endpoint to determine the forks of a project
merge_request:
author:
type: added
---
title: Fix errors thrown in merge request widget with external CI service/integration
merge_request:
author:
type: fixed
---
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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment