Commit e36daa0f authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'fix/gb/stage-id-reference-background-migration'

# Conflicts:
#   app/models/concerns/each_batch.rb
#   spec/models/concerns/each_batch_spec.rb
parents 2719b2f0 7c35ecf7
......@@ -362,6 +362,7 @@ db:migrate:reset-mysql:
- git fetch origin v8.14.10
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
......
......@@ -386,7 +386,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.9.0'
gem 'gitaly', '~> 0.13.0'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -278,7 +278,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.9.0)
gitaly (0.13.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -980,7 +980,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.9.0)
gitaly (~> 0.13.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
......
......@@ -2,6 +2,7 @@
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
const UNFOLD_COUNT = 20;
let isBound = false;
......@@ -10,7 +11,11 @@ class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff();
$diffFile.each((index, file) => {
if (!$.data(file, 'singleFileDiff')) {
$.data(file, 'singleFileDiff', new SingleFileDiff(file));
}
});
FilesCommentButton.init($diffFile);
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global Commit */
/* global NotificationsForm */
/* global TreeView */
/* global NotificationsDropdown */
/* global GroupAvatar */
/* global LineHighlighter */
......@@ -25,7 +21,6 @@
/* global ProjectAvatar */
/* global CompareAutocomplete */
/* global ProjectNew */
/* global Star */
/* global ProjectShow */
/* global Labels */
/* global Shortcuts */
......@@ -54,6 +49,15 @@ import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import Star from './star';
import Todos from './todos';
import TreeView from './tree';
import UsagePing from './usage_ping';
import UsernameValidator from './username_validator';
import VersionCheckImage from './version_check_image';
import Wikis from './wikis';
import ZenMode from './zen_mode';
import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
......@@ -128,7 +132,7 @@ import PerformanceBar from './performance_bar';
break;
case 'sessions:new':
new UsernameValidator();
new ActiveTabMemoizer();
new SigninTabsMemoizer();
new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents();
break;
case 'projects:boards:show':
......@@ -164,7 +168,7 @@ import PerformanceBar from './performance_bar';
new UsersSelect();
break;
case 'dashboard:todos:index':
new gl.Todos();
new Todos();
break;
case 'dashboard:projects:index':
case 'dashboard:projects:starred':
......@@ -318,7 +322,7 @@ import PerformanceBar from './performance_bar';
new gl.Members();
new UsersSelect();
break;
case 'projects:settings:members:show':
case 'projects:project_members:index':
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
new gl.MemberExpirationDate();
......@@ -380,7 +384,7 @@ import PerformanceBar from './performance_bar';
new BlobViewer();
break;
case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break;
case 'search:show':
new Search();
......@@ -396,6 +400,7 @@ import PerformanceBar from './performance_bar';
initSettingsPanels();
break;
case 'projects:settings:ci_cd:show':
case 'groups:settings:ci_cd:show':
new gl.ProjectVariables();
break;
case 'ci:lints:create':
......@@ -432,7 +437,7 @@ import PerformanceBar from './performance_bar';
new Admin();
switch (path[1]) {
case 'cohorts':
new gl.UsagePing();
new UsagePing();
break;
case 'groups':
new UsersSelect();
......@@ -484,7 +489,7 @@ import PerformanceBar from './performance_bar';
new NotificationsDropdown();
break;
case 'wikis':
new gl.Wikis();
new Wikis();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'), true);
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
/* global ZenMode */
/* global Autosave */
/* global dateFormat */
/* global Pikaday */
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
(function() {
this.IssuableForm = (function() {
......
......@@ -4,13 +4,13 @@
import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility';
import './flash';
import './task_list';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new gl.TaskList({
this.taskList = new TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
......
<script>
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
export default {
mixins: [animateMixin],
......@@ -46,7 +47,7 @@
if (this.canUpdate) {
// eslint-disable-next-line no-new
new gl.TaskList({
new TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
......
......@@ -143,26 +143,13 @@ import './render_math';
import './right_sidebar';
import './search';
import './search_autocomplete';
import './signin_tabs_memoizer';
import './single_file_diff';
import './smart_interval';
import './snippets_list';
import './star';
import './subscription';
import './subscription_select';
import './syntax_highlight';
import './task_list';
import './todos';
import './tree';
import './usage_ping';
import './user';
import './user_tabs';
import './username_validator';
import './users_select';
import './version_check_image';
import './visibility_select';
import './wikis';
import './zen_mode';
// eslint-disable-next-line global-require, import/no-commonjs
if (process.env.NODE_ENV !== 'production') require('./test_utils/');
......
......@@ -2,7 +2,7 @@
/* global MergeRequestTabs */
import 'vendor/jquery.waitforimages';
import './task_list';
import TaskList from './task_list';
import './merge_request_tabs';
(function() {
......@@ -25,7 +25,7 @@ import './merge_request_tabs';
this.initMRBtnListeners();
this.initCommitMessageListeners();
if ($("a.btn-close").length) {
this.taskList = new gl.TaskList({
this.taskList = new TaskList({
dataType: 'merge_request',
fieldName: 'description',
selector: '.detail-page-description',
......
......@@ -21,7 +21,7 @@ import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
import './task_list';
import TaskList from './task_list';
window.autosize = autosize;
window.Dropzone = Dropzone;
......@@ -71,7 +71,7 @@ export default class Notes {
this.addBinding();
this.setPollingInterval();
this.setupMainTargetNoteForm();
this.taskList = new gl.TaskList({
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes'
......
......@@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
import { setupPipelineVariableList } from './setup_pipeline_variable_list';
Vue.use(Translate);
......@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list'));
});
function insertRow($row) {
const $rowClone = $row.clone();
$rowClone.removeAttr('data-is-persisted');
$rowClone.find('input, textarea').val('');
$row.after($rowClone);
}
function removeRow($row) {
const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
$row.hide();
$row
.find('.js-destroy-input')
.val(1);
} else {
$row.remove();
}
}
function checkIfRowTouched($row) {
return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0);
}
function setupPipelineVariableList(parent = document) {
const $parent = $(parent);
$parent.on('click', '.js-row-remove-button', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
removeRow($row);
e.preventDefault();
});
// Remove any empty rows except the last r
$parent.on('blur', '.js-user-input', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
const isTouched = checkIfRowTouched($row);
if ($row.is(':not(:last-child)') && !isTouched) {
removeRow($row);
}
});
// Always make sure there is an empty last row
$parent.on('input', '.js-user-input', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (isTouched) {
insertRow($lastRow);
}
});
// Clear out the empty last row so it
// doesn't get submitted and throw validation errors
$parent.closest('form').on('submit', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (!isTouched) {
$lastRow.find('input, textarea').attr('name', '');
}
});
}
export {
setupPipelineVariableList,
insertRow,
removeRow,
};
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
import VisibilitySelect from './visibility_select';
function highlightChanges($elm) {
$elm.addClass('highlight-changes');
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
......@@ -30,7 +32,7 @@ function highlightChanges($elm) {
ProjectNew.prototype.initVisibilitySelect = function() {
const visibilityContainer = document.querySelector('.js-visibility-select');
if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
const visibilitySelect = new VisibilitySelect(visibilityContainer);
visibilitySelect.init();
const $visibilitySelect = $(visibilityContainer).find('select');
......
......@@ -6,7 +6,7 @@ import AccessorUtilities from './lib/utils/accessor';
* Memorize the last selected tab after reloading a page.
* Does that setting the current selected tab in the localStorage
*/
class ActiveTabMemoizer {
export default class SigninTabsMemoizer {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
......@@ -51,5 +51,3 @@ class ActiveTabMemoizer {
return window.localStorage.getItem(this.currentTabKey);
}
}
window.ActiveTabMemoizer = ActiveTabMemoizer;
......@@ -2,18 +2,13 @@
import FilesCommentButton from './files_comment_button';
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
WRAPPER = '<div class="diff-content"></div>';
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) {
export default class SingleFileDiff {
constructor(file) {
this.file = file;
this.toggleDiff = this.toggleDiff.bind(this);
this.content = $('.diff-content', this.file);
......@@ -37,7 +32,7 @@ window.SingleFileDiff = (function() {
}).bind(this));
}
SingleFileDiff.prototype.toggleDiff = function($target, cb) {
toggleDiff($target, cb) {
if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
......@@ -58,9 +53,9 @@ window.SingleFileDiff = (function() {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb);
}
};
}
SingleFileDiff.prototype.getContentHTML = function(cb) {
getContentHTML(cb) {
this.collapsedContent.hide();
this.loadingContent.show();
$.get(this.diffForPath, (function(_this) {
......@@ -84,15 +79,5 @@ window.SingleFileDiff = (function() {
if (cb) cb();
};
})(this));
};
return SingleFileDiff;
})();
$.fn.singleFileDiff = function() {
return this.each(function() {
if (!$.data(this, 'singleFileDiff')) {
return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
}
});
};
}
}
/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */
window.gl.SnippetsList = function() {
var $holder = $('.snippets-list-holder');
function SnippetsList() {
const $holder = $('.snippets-list-holder');
$holder.find('.pagination').on('ajax:success', (e, data) => {
$holder.replaceWith(data.html);
});
};
}
window.gl.SnippetsList = SnippetsList;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */
window.Star = (function() {
function Star() {
export default class Star {
constructor() {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
var $starIcon, $starSpan, $this, toggleStar;
$this = $(this);
......@@ -23,6 +23,4 @@ window.Star = (function() {
new Flash('Star toggle failed. Try again later.', 'alert');
});
}
return Star;
})();
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
window.SubscriptionSelect = (function() {
function SubscriptionSelect() {
class SubscriptionSelect {
constructor() {
$('.js-subscription-event').each(function(i, el) {
var fieldName;
fieldName = $(el).data("field-name");
......@@ -28,6 +28,6 @@ window.SubscriptionSelect = (function() {
});
});
}
}
return SubscriptionSelect;
})();
window.SubscriptionSelect = SubscriptionSelect;
......@@ -2,7 +2,7 @@
import 'deckar01-task_list';
class TaskList {
export default class TaskList {
constructor(options = {}) {
this.selector = options.selector;
this.dataType = options.dataType;
......@@ -48,6 +48,3 @@ class TaskList {
});
}
}
window.gl = window.gl || {};
window.gl.TaskList = TaskList;
......@@ -2,7 +2,7 @@
import UsersSelect from './users_select';
class Todos {
export default class Todos {
constructor() {
this.initFilters();
this.bindEvents();
......@@ -159,6 +159,3 @@ class Todos {
}
}
}
window.gl = window.gl || {};
gl.Todos = Todos;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
window.TreeView = (function() {
function TreeView() {
export default class TreeView {
constructor() {
this.initKeyNav();
// Code browser tree slider
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
......@@ -22,7 +22,7 @@ window.TreeView = (function() {
$('span.log_loading:first').removeClass('hide');
}
TreeView.prototype.initKeyNav = function() {
initKeyNav() {
var li, liSelected;
li = $("tr.tree-item");
liSelected = null;
......@@ -60,7 +60,5 @@ window.TreeView = (function() {
}
}
});
};
return TreeView;
})();
}
}
function UsagePing() {
export default function UsagePing() {
const usageDataUrl = $('.usage-data').data('endpoint');
$.ajax({
......@@ -10,6 +10,3 @@ function UsagePing() {
},
});
}
window.gl = window.gl || {};
window.gl.UsagePing = UsagePing;
/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
class User {
constructor({ action }) {
......@@ -17,7 +18,7 @@ class User {
}
initTabs() {
return new window.gl.UserTabs({
return new UserTabs({
parentEl: '.user-profile',
action: this.action
});
......
......@@ -60,7 +60,7 @@ content on the Users#show page.
</div>
*/
class UserTabs {
export default class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
......@@ -171,6 +171,3 @@ class UserTabs {
return this.$parentEl.find('.nav-links .active a').data('action');
}
}
window.gl = window.gl || {};
window.gl.UserTabs = UserTabs;
......@@ -8,7 +8,7 @@ const successMessageSelector = '.username .validation-success';
const pendingMessageSelector = '.username .validation-pending';
const invalidMessageSelector = '.username .gl-field-error';
class UsernameValidator {
export default class UsernameValidator {
constructor() {
this.inputElement = $('#new_user_username');
this.inputDomElement = this.inputElement.get(0);
......@@ -129,5 +129,3 @@ class UsernameValidator {
$inputErrorMessage.show();
}
}
window.UsernameValidator = UsernameValidator;
......@@ -3,6 +3,3 @@ export default class VersionCheckImage {
imageElement.off('error').on('error', () => imageElement.hide());
}
}
window.gl = window.gl || {};
gl.VersionCheckImage = VersionCheckImage;
class VisibilitySelect {
export default class VisibilitySelect {
constructor(container) {
if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
this.container = container;
......@@ -19,6 +19,3 @@ class VisibilitySelect {
this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
}
}
window.gl = window.gl || {};
window.gl.VisibilitySelect = VisibilitySelect;
/* eslint-disable no-param-reassign */
/* global Breakpoints */
import 'vendor/jquery.nicescroll';
import './breakpoints';
class Wikis {
export default class Wikis {
constructor() {
this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar');
......@@ -63,6 +62,3 @@ class Wikis {
}
}
}
window.gl = window.gl || {};
window.gl.Wikis = Wikis;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */
/* global Mousetrap */
// Zen Mode (full screen) textarea
......@@ -35,8 +35,8 @@ window.Dropzone = Dropzone;
// **Target** a.js-zen-leave
//
window.ZenMode = (function() {
function ZenMode() {
export default class ZenMode {
constructor() {
this.active_backdrop = null;
this.active_textarea = null;
$(document).on('click', '.js-zen-enter', function(e) {
......@@ -66,7 +66,7 @@ window.ZenMode = (function() {
});
}
ZenMode.prototype.enter = function(backdrop) {
enter(backdrop) {
Mousetrap.pause();
this.active_backdrop = $(backdrop);
this.active_backdrop.addClass('fullscreen');
......@@ -74,9 +74,9 @@ window.ZenMode = (function() {
// Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style');
return this.active_textarea.focus();
};
}
ZenMode.prototype.exit = function() {
exit() {
if (this.active_textarea) {
Mousetrap.unpause();
this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen');
......@@ -85,13 +85,11 @@ window.ZenMode = (function() {
this.active_backdrop = null;
return Dropzone.forElement('.div-dropzone').enable();
}
};
}
ZenMode.prototype.scrollTo = function(zen_area) {
scrollTo(zen_area) {
return $.scrollTo(zen_area, 0, {
offset: -150
});
};
return ZenMode;
})();
}
}
......@@ -153,6 +153,7 @@ $code_line_height: 1.6;
* Padding
*/
$gl-padding: 16px;
$gl-col-padding: 15px;
$gl-btn-padding: 10px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
......@@ -443,6 +444,7 @@ $logs-p-color: #333;
/*
* Forms
*/
$input-height: 34px;
$input-danger-bg: #f2dede;
$input-danger-border: $red-400;
$input-group-addon-bg: #f7f8fa;
......@@ -574,6 +576,12 @@ $stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6;
/*
Pipeline Schedules
*/
$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
/*
Filtered Search
*/
......
......@@ -31,6 +31,12 @@ $new-sidebar-width: 220px;
&:hover {
background-color: $border-color;
}
.project-title,
.group-title {
overflow: hidden;
text-overflow: ellipsis;
}
}
.settings-avatar {
......
......@@ -74,3 +74,84 @@
margin-right: 3px;
}
}
.pipeline-variable-list {
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
list-style: none;
clear: both;
}
.pipeline-variable-row {
display: flex;
align-items: flex-end;
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
}
@media (max-width: $screen-sm-max) {
padding-right: $gl-col-padding;
}
&:last-child {
& .pipeline-variable-row-remove-button {
display: none;
}
@media (max-width: $screen-sm-max) {
& .pipeline-variable-value-input {
margin-right: $pipeline-variable-remove-button-width;
}
}
@media (max-width: $screen-xs-max) {
.pipeline-variable-row-body {
margin-right: $pipeline-variable-remove-button-width;
}
}
}
}
.pipeline-variable-row-body {
display: flex;
width: calc(75% - #{$gl-col-padding});
padding-left: $gl-col-padding;
@media (max-width: $screen-sm-max) {
width: 100%;
}
@media (max-width: $screen-xs-max) {
display: block;
}
}
.pipeline-variable-key-input {
margin-right: $gl-btn-padding;
@media (max-width: $screen-xs-max) {
margin-bottom: $gl-btn-padding;
}
}
.pipeline-variable-row-remove-button {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
width: $pipeline-variable-remove-button-width;
height: $input-height;
padding: 0;
background: transparent;
border: 0;
color: $gl-text-color-secondary;
@include transition(color);
&:hover,
&:focus {
outline: none;
color: $gl-text-color;
}
}
......@@ -70,7 +70,7 @@ module MembershipActions
def members_page_url
if membershipable.is_a?(Project)
project_settings_members_path(membershipable)
project_project_members_path(membershipable)
else
polymorphic_url([membershipable, :members])
end
......
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
labels = LabelsFinder.new(current_user).execute
respond_to do |format|
format.json { render json: LabelSerializer.new.represent_appearance(labels) }
end
end
def labels
finder_params = { project_ids: projects.select(:id) }
labels = LabelsFinder.new(current_user, finder_params).execute
GlobalLabel.build_collection(labels)
end
end
......@@ -2,13 +2,13 @@ class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects
before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update]
def index
respond_to do |format|
format.html do
@milestone_states = GlobalMilestone.states_count(@projects)
@milestone_states = GlobalMilestone.states_count(group_projects, group)
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
......@@ -22,49 +22,41 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def create
project_ids = params[:milestone][:project_ids].reject(&:blank?)
title = milestone_params[:title]
@milestone = Milestones::CreateService.new(group, current_user, milestone_params).execute
if create_milestones(project_ids)
redirect_to milestone_path(title)
if @milestone.persisted?
redirect_to milestone_path
else
render_new_with_error(project_ids.empty?)
render "new"
end
end
def show
end
def update
@milestone.milestones.each do |milestone|
Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
end
redirect_back_or_default(default: milestone_path(@milestone.title))
def edit
render_404 if @milestone.is_legacy_group_milestone?
end
private
def create_milestones(project_ids)
return false unless project_ids.present?
def update
# Keep this compatible with legacy group milestones where we have to update
# all projects milestones states at once.
if @milestone.is_legacy_group_milestone?
update_params = milestone_params.select { |key| key == "state_event" }
milestones = @milestone.milestones
else
update_params = milestone_params
milestones = [@milestone]
end
ActiveRecord::Base.transaction do
@projects.where(id: project_ids).each do |project|
Milestones::CreateService.new(project, current_user, milestone_params).execute
end
milestones.each do |milestone|
Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
end
true
rescue ActiveRecord::ActiveRecordError => e
flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}"
false
redirect_to milestone_path
end
def render_new_with_error(empty_project_ids)
@milestone = Milestone.new(milestone_params)
@milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids
render :new
end
private
def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group)
......@@ -74,16 +66,31 @@ class Groups::MilestonesController < Groups::ApplicationController
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
def milestone_path
if @milestone.is_legacy_group_milestone?
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
else
group_milestone_path(group, @milestone.iid)
end
end
def milestones
@milestones = GroupMilestone.build_collection(@group, @projects, params)
search_params = params.merge(group_ids: group.id)
milestones = MilestonesFinder.new(search_params).execute
legacy_milestones = GroupMilestone.build_collection(group, group_projects, params)
milestones + legacy_milestones
end
def milestone
@milestone = GroupMilestone.build(@group, @projects, params[:title])
@milestone =
if params[:title]
GroupMilestone.build(group, group_projects, params[:title])
else
group.milestones.find_by_iid(params[:id])
end
render_404 unless @milestone
end
end
module Groups
module Settings
class CiCdController < Groups::ApplicationController
before_action :authorize_admin_pipeline!
def show
define_secret_variables
end
private
def define_secret_variables
@variable = Ci::GroupVariable.new(group: group)
.present(current_user: current_user)
@variables = group.variables.order_key_asc
.map { |variable| variable.present(current_user: current_user) }
end
def authorize_admin_pipeline!
return render_404 unless can?(current_user, :admin_pipeline, group)
end
end
end
end
module Groups
class VariablesController < Groups::ApplicationController
before_action :variable, only: [:show, :update, :destroy]
before_action :authorize_admin_build!
def index
redirect_to group_settings_ci_cd_path(group)
end
def show
end
def update
if variable.update(variable_params)
redirect_to group_variables_path(group),
notice: 'Variable was successfully updated.'
else
render "show"
end
end
def create
@variable = group.variables.create(variable_params)
.present(current_user: current_user)
if @variable.persisted?
redirect_to group_settings_ci_cd_path(group),
notice: 'Variable was successfully created.'
else
render "show"
end
end
def destroy
if variable.destroy
redirect_to group_settings_ci_cd_path(group),
status: 302,
notice: 'Variable was successfully removed.'
else
redirect_to group_settings_ci_cd_path(group),
status: 302,
notice: 'Failed to remove the variable.'
end
end
private
def variable_params
params.require(:variable).permit(*variable_params_attributes)
end
def variable_params_attributes
%i[key value protected]
end
def variable
@variable ||= group.variables.find(params[:id]).present(current_user: current_user)
end
def authorize_admin_build!
return render_404 unless can?(current_user, :admin_build, group)
end
end
end
......@@ -22,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.'
end
redirect_to project_settings_members_path(project)
redirect_to project_project_members_path(project)
end
def update
......@@ -36,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to project_settings_members_path(project), status: 302
redirect_to project_project_members_path(project), status: 302
end
format.js { head :ok }
end
......
......@@ -13,20 +13,16 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to :html
def index
@milestones =
case params[:state]
when 'all' then @project.milestones
when 'closed' then @project.milestones.closed
else @project.milestones.active
end
@sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort)
@milestones = milestones.sort(@sort)
respond_to do |format|
format.html do
@project_namespace = @project.namespace.becomes(Namespace)
@milestones = @milestones.includes(:project)
# We need to show group milestones in the JSON response
# so that people can filter by and assign group milestones,
# but we don't need to show them on the project milestones page itself.
@milestones = @milestones.for_projects
@milestones = @milestones.page(params[:page])
end
format.json do
......@@ -45,12 +41,13 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def show
@project_namespace = @project.namespace.becomes(Namespace)
end
def create
@milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute
if @milestone.save
if @milestone.valid?
redirect_to project_milestone_path(@project, @milestone)
else
render "new"
......@@ -85,6 +82,18 @@ class Projects::MilestonesController < Projects::ApplicationController
protected
def milestones
@milestones ||= begin
if @project.group && can?(current_user, :read_group, @project.group)
group = @project.group
end
search_params = params.merge(project_ids: @project.id, group_ids: group&.id)
MilestonesFinder.new(search_params).execute
end
end
def milestone
@milestone ||= @project.milestones.find_by!(iid: params[:id])
end
......
class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, except: [:index, :new, :create]
before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update]
before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
before_action :schedule, only: [:edit, :update, :destroy, :take_ownership]
def index
@scope = params[:scope]
@all_schedules = PipelineSchedulesFinder.new(@project).execute
......@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
redirect_to pipeline_schedules_path(@project), status: 302
else
redirect_to pipeline_schedules_path(@project),
status: 302,
status: :forbidden,
alert: _("Failed to remove the pipeline schedule")
end
end
......@@ -66,6 +66,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def schedule_params
params.require(:schedule)
.permit(:description, :cron, :cron_timezone, :ref, :active)
.permit(:description, :cron, :cron_timezone, :ref, :active,
variables_attributes: [:id, :key, :value, :_destroy] )
end
def authorize_update_pipeline_schedule!
return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule)
end
def authorize_admin_pipeline_schedule!
return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
end
end
......@@ -6,8 +6,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
sort = params[:sort].presence || sort_value_name
redirect_to project_settings_members_path(@project, sort: sort)
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
@skip_groups += @project.group.ancestors.pluck(:id) if @project.group
@project_members = MembersFinder.new(@project, current_user).execute
if params[:search].present?
@project_members = @project_members.joins(:user).merge(User.search(params[:search]))
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
@project_members = @project_members.sort(@sort).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
end
def update
......@@ -19,7 +34,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def resend_invite
redirect_path = project_settings_members_path(@project)
redirect_path = project_project_members_path(@project)
@project_member = @project.project_members.find(params[:id])
......@@ -42,7 +57,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404
end
redirect_to(project_settings_members_path(project),
redirect_to(project_project_members_path(project),
notice: notice)
end
......
......@@ -21,7 +21,10 @@ module Projects
end
def define_secret_variables
@variable = Ci::Variable.new
@variable = Ci::Variable.new(project: project)
.present(current_user: current_user)
@variables = project.variables.order_key_asc
.map { |variable| variable.present(current_user: current_user) }
end
def define_triggers_variables
......
module Projects
module Settings
class MembersController < Projects::ApplicationController
include SortingHelper
def show
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
@skip_groups += @project.group.ancestors.pluck(:id) if @project.group
@project_members = MembersFinder.new(@project, current_user).execute
if params[:search].present?
@project_members = @project_members.joins(:user).merge(User.search(params[:search]))
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
@project_members = @project_members.sort(@sort).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
end
end
end
end
class Projects::VariablesController < Projects::ApplicationController
before_action :variable, only: [:show, :update, :destroy]
before_action :authorize_admin_build!
layout 'project_settings'
......@@ -8,37 +9,39 @@ class Projects::VariablesController < Projects::ApplicationController
end
def show
@variable = @project.variables.find(params[:id])
end
def update
@variable = @project.variables.find(params[:id])
if @variable.update_attributes(variable_params)
redirect_to project_variables_path(project), notice: 'Variable was successfully updated.'
if variable.update(variable_params)
redirect_to project_variables_path(project),
notice: 'Variable was successfully updated.'
else
render action: "show"
render "show"
end
end
def create
@variable = @project.variables.new(variable_params)
@variable = project.variables.create(variable_params)
.present(current_user: current_user)
if @variable.save
flash[:notice] = 'Variables were successfully updated.'
redirect_to project_settings_ci_cd_path(project)
if @variable.persisted?
redirect_to project_settings_ci_cd_path(project),
notice: 'Variable was successfully created.'
else
render "show"
end
end
def destroy
@key = @project.variables.find(params[:id])
@key.destroy
redirect_to project_settings_ci_cd_path(project),
status: 302,
notice: 'Variable was successfully removed.'
if variable.destroy
redirect_to project_settings_ci_cd_path(project),
status: 302,
notice: 'Variable was successfully removed.'
else
redirect_to project_settings_ci_cd_path(project),
status: 302,
notice: 'Failed to remove the variable.'
end
end
private
......@@ -50,4 +53,8 @@ class Projects::VariablesController < Projects::ApplicationController
def variable_params_attributes
%i[id key value protected _destroy]
end
def variable
@variable ||= project.variables.find(params[:id]).present(current_user: current_user)
end
end
......@@ -147,9 +147,17 @@ class IssuableFinder
@milestones =
if milestones?
scope = Milestone.where(project_id: projects)
if project?
group_id = project.group&.id
project_id = project.id
end
group_id = group.id if group
scope.where(title: params[:milestone_title])
search_params =
{ title: params[:milestone_title], project_ids: project_id, group_ids: group_id }
MilestonesFinder.new(search_params).execute
else
Milestone.none
end
......@@ -331,11 +339,6 @@ class IssuableFinder
items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
else
items = items.with_milestone(params[:milestone_title])
items_projects = projects(items)
if items_projects
items = items.where(milestones: { project_id: items_projects })
end
end
end
......
# Search for milestones
#
# params - Hash
# project_ids: Array of project ids or single project id.
# group_ids: Array of group ids or single group id.
# order - Orders by field default due date asc.
# title - filter by title.
# state - filters by state.
class MilestonesFinder
def execute(projects, params)
milestones = Milestone.of_projects(projects)
milestones = milestones.reorder("due_date ASC")
case params[:state]
when 'closed' then milestones.closed
when 'all' then milestones
else milestones.active
attr_reader :params, :project_ids, :group_ids
def initialize(params = {})
@project_ids = Array(params[:project_ids])
@group_ids = Array(params[:group_ids])
@params = params
end
def execute
return Milestone.none if project_ids.empty? && group_ids.empty?
items = Milestone.all
items = by_groups_and_projects(items)
items = by_title(items)
items = by_state(items)
order(items)
end
private
def by_groups_and_projects(items)
items.for_projects_and_groups(project_ids, group_ids)
end
def by_title(items)
if params[:title]
items.where(title: params[:title])
else
items
end
end
def by_state(items)
Milestone.filter_by_state(items, params[:state])
end
def order(items)
if params.has_key?(:order)
items.reorder(params[:order])
else
items.reorder('due_date ASC')
end
end
end
......@@ -97,7 +97,7 @@ module GitlabRoutingHelper
## Members
def project_members_url(project, *args)
project_project_members_url(project)
project_project_members_url(project, *args)
end
def project_member_path(project_member, *args)
......
......@@ -54,8 +54,10 @@ module MilestonesHelper
def milestone_class_for_state(param, check, match_blank_param = false)
if match_blank_param
'active' if param.blank? || param == check
elsif param == check
'active'
else
'active' if param == check
check
end
end
......@@ -147,4 +149,14 @@ module MilestonesHelper
labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
def group_milestone_route(milestone, params = {})
params = nil if params.empty?
if milestone.is_legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
group_milestone_path(@group, milestone.iid, milestone: params)
end
end
end
......@@ -267,15 +267,15 @@ module ProjectsHelper
def tab_ability_map
{
environments: :read_environment,
milestones: :read_milestone,
snippets: :read_project_snippet,
settings: :admin_project,
builds: :read_build,
labels: :read_label,
issues: :read_issue,
team: :read_project_member,
wiki: :read_wiki
environments: :read_environment,
milestones: :read_milestone,
snippets: :read_project_snippet,
settings: :admin_project,
builds: :read_build,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
wiki: :read_wiki
}
end
......
......@@ -75,7 +75,7 @@ module SearchHelper
{ category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "Current Project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "Current Project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "Current Project", label: "Members", url: project_settings_members_path(@project) },
{ category: "Current Project", label: "Members", url: project_project_members_path(@project) },
{ category: "Current Project", label: "Wiki", url: project_wikis_path(@project) }
]
else
......
......@@ -200,8 +200,10 @@ module Ci
variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
variables += secret_variables(environment: environment)
variables += trigger_request.user_variables if trigger_request
variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
variables += persisted_environment_variables if environment
variables
......
module Ci
class GroupVariable < ActiveRecord::Base
extend Ci::Model
include HasVariable
include Presentable
belongs_to :group
validates :key, uniqueness: { scope: :group_id }
scope :unprotected, -> { where(protected: false) }
end
end
......@@ -9,17 +9,21 @@ module Ci
belongs_to :owner, class_name: 'User'
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
has_many :variables, class_name: 'Ci::PipelineScheduleVariable'
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :description, presence: true
validates :variables, variable_duplicates: true
before_save :set_next_run_at
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
accepts_nested_attributes_for :variables, allow_destroy: true
def owned_by?(current_user)
owner == current_user
end
......@@ -56,5 +60,9 @@ module Ci
Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
.next_time_from(next_run_at)
end
def job_variables
variables&.map(&:to_runner_variable) || []
end
end
end
module Ci
class PipelineScheduleVariable < ActiveRecord::Base
extend Ci::Model
include HasVariable
belongs_to :pipeline_schedule
end
end
......@@ -2,6 +2,7 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
include HasVariable
include Presentable
belongs_to :project
......
......@@ -18,38 +18,56 @@ module EachBatch
# relation.update_all(updated_at: Time.now)
# end
#
# The supplied block is also passed an optional batch index:
#
# User.each_batch do |relation, index|
# puts index # => 1, 2, 3, ...
# end
#
# You can also specify an alternative column to use for ordering the rows:
#
# User.each_batch(column: :created_at) do |relation|
# ...
# end
#
# This will produce SQL queries along the lines of:
#
# User Load (0.7ms) SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 41654) ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1000
# (0.7ms) SELECT COUNT(*) FROM "users" WHERE ("users"."id" >= 41654) AND ("users"."id" < 42687)
#
# of - The number of rows to retrieve per batch.
def each_batch(of: 1000)
# column - The column to use for ordering the batches.
def each_batch(of: 1000, column: primary_key)
unless column
raise ArgumentError,
'the column: argument must be set to a column name to use for ordering rows'
end
start = except(:select)
.select(primary_key)
.reorder(primary_key => :asc)
.select(column)
.reorder(column => :asc)
.take
return unless start
start_id = start[primary_key]
start_id = start[column]
arel_table = self.arel_table
1.step do |index|
stop = except(:select)
.select(primary_key)
.where(arel_table[primary_key].gteq(start_id))
.reorder(primary_key => :asc)
.select(column)
.where(arel_table[column].gteq(start_id))
.reorder(column => :asc)
.offset(of)
.limit(1)
.take
relation = where(arel_table[primary_key].gteq(start_id))
relation = where(arel_table[column].gteq(start_id))
if stop
stop_id = stop[primary_key]
stop_id = stop[column]
start_id = stop_id
relation = relation.where(arel_table[primary_key].lt(stop_id))
relation = relation.where(arel_table[column].lt(stop_id))
end
# Any ORDER BYs are useless for this relation and can lead to less
......
......@@ -8,7 +8,8 @@ module InternalId
def set_iid
if iid.blank?
records = project.send(self.class.name.tableize)
parent = project || group
records = parent.send(self.class.name.tableize)
records = records.with_deleted if self.paranoid?
max_iid = records.maximum(:iid)
......
......@@ -30,6 +30,7 @@ module Issuable
belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User'
belongs_to :milestone
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent
def authors_loaded?
# We check first if we're loaded to not load unnecessarily.
......
......@@ -70,6 +70,22 @@ module Milestoneish
due_date && due_date.past?
end
def is_group_milestone?
false
end
def is_project_milestone?
false
end
def is_legacy_group_milestone?
false
end
def is_dashboard_milestone?
false
end
private
def count_issues_by_state(user)
......
......@@ -3,6 +3,8 @@ module ShaAttribute
module ClassMethods
def sha_attribute(name)
return unless table_exists?
column = columns.find { |c| c.name == name.to_s }
# In case the table doesn't exist we won't be able to find the column,
......
......@@ -2,4 +2,8 @@ class DashboardMilestone < GlobalMilestone
def issues_finder_params
{ authorized_only: true }
end
def is_dashboard_milestone?
true
end
end
......@@ -2,7 +2,7 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
delegate :color, :description, to: :@first_label
delegate :color, :text_color, :description, to: :@first_label
def for_display
@first_label
......
......@@ -2,6 +2,7 @@ class GlobalMilestone
include Milestoneish
EPOCH = DateTime.parse('1970-01-01')
STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
attr_accessor :title, :milestones
alias_attribute :name, :title
......@@ -11,7 +12,10 @@ class GlobalMilestone
end
def self.build_collection(projects, params)
child_milestones = MilestonesFinder.new.execute(projects, params)
params =
{ project_ids: projects.map(&:id), state: params[:state] }
child_milestones = MilestonesFinder.new(params).execute
milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
milestones_relation = Milestone.where(id: grouped.map(&:id))
......@@ -28,13 +32,42 @@ class GlobalMilestone
new(title, child_milestones)
end
def self.states_count(projects)
relation = MilestonesFinder.new.execute(projects, state: 'all')
milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
def self.states_count(projects, group = nil)
legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
group_milestones_count = group_milestones_states_count(group)
legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
legacy_group_milestones_count + group_milestones_count
end
end
def self.group_milestones_states_count(group)
return STATE_COUNT_HASH unless group
params = { group_ids: [group.id], state: 'all', order: nil }
relation = MilestonesFinder.new(params).execute
grouped_by_state = relation.group(:state).count
{
opened: grouped_by_state['active'] || 0,
closed: grouped_by_state['closed'] || 0,
all: grouped_by_state.values.sum
}
end
# Counts the legacy group milestones which must be grouped by title
def self.legacy_group_milestone_states_count(projects)
return STATE_COUNT_HASH unless projects
params = { project_ids: projects.map(&:id), state: 'all', order: nil }
relation = MilestonesFinder.new(params).execute
project_milestones_by_state_and_title = relation.group(:state, :title).count
opened = count_by_state(milestones_by_state_and_title, 'active')
closed = count_by_state(milestones_by_state_and_title, 'closed')
all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
opened = count_by_state(project_milestones_by_state_and_title, 'active')
closed = count_by_state(project_milestones_by_state_and_title, 'closed')
all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
{
opened: opened,
......
......@@ -18,10 +18,12 @@ class Group < Namespace
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
......@@ -248,6 +250,14 @@ class Group < Namespace
}
end
def secret_variables_for(ref, project)
list_of_ids = [self] + ancestors
variables = Ci::GroupVariable.where(group: list_of_ids)
variables = variables.unprotected unless project.protected_for?(ref)
variables = variables.group_by(&:group_id)
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end
protected
def update_two_factor_requirement
......
......@@ -16,4 +16,8 @@ class GroupMilestone < GlobalMilestone
def issues_finder_params
{ group_id: group.id }
end
def is_legacy_group_milestone?
true
end
end
......@@ -849,7 +849,10 @@ class MergeRequest < ActiveRecord::Base
#
def all_commit_shas
if persisted?
merge_request_diffs.preload(:merge_request_diff_commits).flat_map(&:commit_shas).uniq
column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)')
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq
elsif compare_commits
compare_commits.to_a.reverse.map(&:id)
else
......
......@@ -18,17 +18,32 @@ class Milestone < ActiveRecord::Base
cache_markdown_field :description
belongs_to :project
belongs_to :group
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :for_projects, -> { where(group: nil).includes(:project) }
scope :for_projects_and_groups, -> (project_ids, group_ids) do
conditions = []
conditions << arel_table[:project_id].in(project_ids) if project_ids.compact.any?
conditions << arel_table[:group_id].in(group_ids) if group_ids.compact.any?
where(conditions.reduce(:or))
end
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
validate :uniqueness_of_title, if: :title_changed?
validate :milestone_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title
......@@ -63,6 +78,14 @@ class Milestone < ActiveRecord::Base
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def filter_by_state(milestones, state)
case state
when 'closed' then milestones.closed
when 'all' then milestones
else milestones.active
end
end
end
def self.reference_prefix
......@@ -138,6 +161,8 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid, full: false)
return if is_group_milestone?
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
......@@ -152,6 +177,10 @@ class Milestone < ActiveRecord::Base
id
end
def for_display
self
end
def can_be_closed?
active? && issues.opened.count.zero?
end
......@@ -164,8 +193,45 @@ class Milestone < ActiveRecord::Base
write_attribute(:title, sanitize_title(value)) if value.present?
end
def safe_title
title.to_slug.normalize.to_s
end
def parent
group || project
end
def is_group_milestone?
group_id.present?
end
def is_project_milestone?
project_id.present?
end
private
# Milestone titles must be unique across project milestones and group milestones
def uniqueness_of_title
if project
relation = Milestone.for_projects_and_groups([project_id], [project.group&.id])
elsif group
project_ids = group.projects.map(&:id)
relation = Milestone.for_projects_and_groups(project_ids, [group.id])
end
title_exists = relation.find_by_title(title)
errors.add(:title, "already being used for another group or project milestone.") if title_exists
end
# Milestone should be either a project milestone or a group milestone
def milestone_type_check
if group_id && project_id
field = project_id_changed? ? :project_id : :group_id
errors.add(field, "milestone should belong either to a project or a group.")
end
end
def milestone_format_reference(format = :iid)
raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
......
module Ci
class PipelineSchedulePolicy < PipelinePolicy
alias_method :pipeline_schedule, :subject
condition(:owner_of_schedule) do
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end
rule { can?(:master_access) | owner_of_schedule }.policy do
enable :update_pipeline_schedule
enable :admin_pipeline_schedule
end
end
end
......@@ -31,6 +31,8 @@ class GroupPolicy < BasePolicy
rule { master }.policy do
enable :create_projects
enable :admin_milestones
enable :admin_pipeline
enable :admin_build
end
rule { owner }.policy do
......
......@@ -162,7 +162,6 @@ class ProjectPolicy < BasePolicy
enable :create_pipeline
enable :update_pipeline
enable :create_pipeline_schedule
enable :update_pipeline_schedule
enable :create_merge_request
enable :create_wiki
enable :push_code
......@@ -188,7 +187,6 @@ class ProjectPolicy < BasePolicy
enable :admin_build
enable :admin_container_image
enable :admin_pipeline
enable :admin_pipeline_schedule
enable :admin_environment
enable :admin_deployment
enable :admin_pages
......
module Ci
class GroupVariablePresenter < Gitlab::View::Presenter::Delegated
presents :variable
def placeholder
'GROUP_VARIABLE'
end
def form_path
if variable.persisted?
group_variable_path(group, variable)
else
group_variables_path(group)
end
end
def edit_path
group_variable_path(group, variable)
end
def delete_path
group_variable_path(group, variable)
end
end
end
module Ci
class VariablePresenter < Gitlab::View::Presenter::Delegated
presents :variable
def placeholder
'PROJECT_VARIABLE'
end
def form_path
if variable.persisted?
project_variable_path(project, variable)
else
project_variables_path(project)
end
end
def edit_path
project_variable_path(project, variable)
end
def delete_path
project_variable_path(project, variable)
end
end
end
class LabelEntity < Grape::Entity
expose :id
expose :id, if: ->(label, _) { !label.is_a?(GlobalLabel) }
expose :title
expose :color
expose :description
......
module Boards
class CreateService < BaseService
def execute
if project.boards.empty?
create_board!
else
project.boards.first
end
create_board! if can_create_board?
end
private
def can_create_board?
project.boards.size == 0
end
def create_board!
board = project.boards.create
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :closed)
board = project.boards.create(params)
if board.persisted?
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :closed)
end
board
end
......
......@@ -2,8 +2,11 @@ class IssuableBaseService < BaseService
private
def create_milestone_note(issuable)
milestone = issuable.milestone
return if milestone && milestone.is_group_milestone?
SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone)
issuable, issuable.project, current_user, milestone)
end
def create_labels_note(issuable, old_labels)
......@@ -89,10 +92,12 @@ class IssuableBaseService < BaseService
milestone_id = params[:milestone_id]
return unless milestone_id
if milestone_id == IssuableFinder::NONE ||
project.milestones.find_by(id: milestone_id).nil?
params[:milestone_id] = ''
end
params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE
milestone =
Milestone.for_projects_and_groups([project.id], [project.group&.id]).find_by_id(milestone_id)
params[:milestone_id] = '' unless milestone
end
def filter_labels
......
......@@ -61,8 +61,18 @@ module Issues
end
def cloneable_milestone_id
@new_project.milestones
.find_by(title: @old_issue.milestone.try(:title)).try(:id)
title = @old_issue.milestone&.title
return unless title
if @new_project.group && can?(current_user, :read_group, @new_project.group)
group_id = @new_project.group.id
end
params =
{ title: title, project_ids: @new_project.id, group_ids: group_id }
milestones = MilestonesFinder.new(params).execute
milestones.first&.id
end
def rewrite_notes
......
......@@ -35,11 +35,12 @@ module MergeRequests
# target branch manually
def close_merge_requests
commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a
merge_requests = @project.merge_requests.preload(:merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request|
commit_ids.include?(merge_request.diff_head_sha)
commit_ids.include?(merge_request.diff_head_sha) &&
merge_request.merge_request_diff.state != 'empty'
end
filter_merge_requests(merge_requests).each do |merge_request|
......
module Milestones
class BaseService < ::BaseService
# Parent can either a group or a project
attr_accessor :parent, :current_user, :params
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
end
end
end
module Milestones
class CloseService < Milestones::BaseService
def execute(milestone)
if milestone.close
if milestone.close && milestone.is_project_milestone?
event_service.close_milestone(milestone, current_user)
end
......
module Milestones
class CreateService < Milestones::BaseService
def execute
milestone = project.milestones.new(params)
milestone = parent.milestones.new(params)
if milestone.save
if milestone.save && milestone.is_project_milestone?
event_service.open_milestone(milestone, current_user)
end
......
module Milestones
class ReopenService < Milestones::BaseService
def execute(milestone)
if milestone.activate
if milestone.activate && milestone.is_project_milestone?
event_service.reopen_milestone(milestone, current_user)
end
......
......@@ -5,9 +5,9 @@ module Milestones
case state
when 'activate'
Milestones::ReopenService.new(project, current_user, {}).execute(milestone)
Milestones::ReopenService.new(parent, current_user, {}).execute(milestone)
when 'close'
Milestones::CloseService.new(project, current_user, {}).execute(milestone)
Milestones::CloseService.new(parent, current_user, {}).execute(milestone)
end
if params.present?
......
......@@ -146,32 +146,6 @@ module QuickActions
end
end
desc do
"Change assignee#{'(s)' if issuable.allows_multiple_assignees?}"
end
explanation do |users|
users = issuable.allows_multiple_assignees? ? users : users.take(1)
"Change #{'assignee'.pluralize(users.size)} to #{users.map(&:to_reference).to_sentence}."
end
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
end
condition do
issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
parse_params do |assignee_param|
extract_users(assignee_param)
end
command :reassign do |users|
@updates[:assignee_ids] =
if issuable.allows_multiple_assignees?
users.map(&:id)
else
[users.last.id]
end
end
desc 'Set milestone'
explanation do |milestone|
"Sets the milestone to #{milestone.to_reference}." if milestone
......
# VariableDuplicatesValidator
#
# This validtor is designed for especially the following condition
# - Use `accepts_nested_attributes_for :xxx` in a parent model
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
duplicates = value.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any?
record.errors.add(attribute, "Duplicate variables: #{duplicates.join(", ")}")
end
end
end
= form_for [@project.namespace.becomes(Namespace), @project, @variable] do |f|
= form_for @variable, as: :variable, url: @variable.form_path do |f|
= form_errors(@variable)
.form-group
= f.label :key, "Key", class: "label-light"
= f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
= f.text_field :key, class: "form-control", placeholder: @variable.placeholder, required: true
.form-group
= f.label :value, "Value", class: "label-light"
= f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
= f.text_area :value, class: "form-control", placeholder: @variable.placeholder
.form-group
.checkbox
= f.label :protected do
......
.row.prepend-top-default.append-bottom-default
.col-lg-4
= render "projects/variables/content"
= render "ci/variables/content"
.col-lg-8
%h5.prepend-top-0
Add a variable
= render "projects/variables/form", btn_text: "Add new variable"
= render "ci/variables/form", btn_text: "Add new variable"
%hr
%h5.prepend-top-0
Your variables (#{@project.variables.size})
- if @project.variables.empty?
Your variables (#{@variables.size})
- if @variables.empty?
%p.settings-message.text-center.append-bottom-0
No variables found, add one with the form above.
- else
= render "projects/variables/table"
= render "ci/variables/table"
%button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
- page_title "Variables"
.row.prepend-top-default.append-bottom-default
.col-lg-3
= render "ci/variables/content"
.col-lg-9
%h5.prepend-top-0
Update variable
= render "ci/variables/form", btn_text: "Save variable"
......@@ -11,18 +11,18 @@
%th Protected
%th
%tbody
- @project.variables.order_key_asc.each do |variable|
- @variables.each do |variable|
- if variable.id?
%tr
%td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }******
%td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
= link_to project_variable_path(@project, variable), class: "btn btn-transparent btn-variable-edit" do
= link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
Update
= icon("pencil")
= link_to project_variable_path(@project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
= link_to variable.delete_path, class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
%span.sr-only
Remove
= icon("trash")
......@@ -12,3 +12,8 @@
= link_to projects_group_path(@group), title: 'Projects' do
%span
Projects
= nav_link(controller: :ci_cd) do
= link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do
%span
Pipelines
= form_for [@group, @milestone], html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row
= form_errors(@milestone)
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: '' } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix
.error-alert
= render "shared/milestones/form_dates", f: f
.form-actions
- if @milestone.new_record?
= f.submit 'Create milestone', class: "btn-create btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
- else
= f.submit 'Update milestone', class: "btn-create btn"
= link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel"
= render 'shared/milestones/milestone',
milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title),
milestone_path: group_milestone_route(milestone),
issues_path: issues_group_path(@group, milestone_title: milestone.title),
merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
milestone: milestone
- page_title "Milestones"
- render "header_title"
%h3.page-title
Edit Milestone
= render "form"
......@@ -9,11 +9,6 @@
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
New milestone
.row-content-block
Only milestones from
%strong= @group.name
group are listed here.
.milestones
%ul.content-list
- if @milestones.blank?
......
......@@ -4,40 +4,4 @@
%h3.page-title
New Milestone
%p.light
This will create milestone in every selected project
%hr
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row
- if @milestone.errors.any?
#error_explanation
.alert.alert-danger
%ul
- @milestone.errors.full_messages.each do |msg|
%li
= msg
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: '' } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix
.error-alert
.form-group
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
{ selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
= render "shared/milestones/form_dates", f: f
.form-actions
= f.submit 'Create milestone', class: "btn-create btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
= render "form"
= render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.is_legacy_group_milestone?
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
- page_title "Pipelines"
= render "groups/settings_head"
= render 'ci/variables/index'
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