diff options
author | Timothy Andrew <mail@timothyandrew.net> | 2016-09-07 14:58:01 +0530 |
---|---|---|
committer | Timothy Andrew <mail@timothyandrew.net> | 2016-09-07 14:58:01 +0530 |
commit | f5b9837c19f48255237eca1e618fc632a45b58b5 (patch) | |
tree | 2653e9aa1e3bf0a1db6630bb52863c30e6313d87 /app | |
parent | 72b9b87ca93615640dcc022f324b2af16ceaa4eb (diff) | |
parent | 1d5488699678d22644d24add4b89cede0419ad25 (diff) | |
download | gitlab-ce-f5b9837c19f48255237eca1e618fc632a45b58b5.tar.gz |
Merge remote-tracking branch 'origin/master' into 21170-cycle-analytics
Diffstat (limited to 'app')
221 files changed, 2407 insertions, 1779 deletions
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png Binary files differdeleted file mode 100644 index 5b55e12571c..00000000000 --- a/app/assets/images/icon-link.png +++ /dev/null diff --git a/app/assets/images/icon_anchor.svg b/app/assets/images/icon_anchor.svg new file mode 100644 index 00000000000..7e242586bad --- /dev/null +++ b/app/assets/images/icon_anchor.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#333" fill-rule="evenodd" d="M9.683 6.676l-.047-.048C8.27 5.26 6.07 5.243 4.726 6.588l-2.29 2.29c-1.344 1.344-1.328 3.544.04 4.91 1.366 1.368 3.564 1.385 4.908.04l1.753-1.752c-.695.074-1.457-.078-2.176-.444L5.934 12.66c-.634.634-1.67.625-2.312-.017-.642-.643-.65-1.677-.017-2.312L6.035 7.9c.634-.634 1.67-.625 2.312.017.024.024.048.05.07.075l.003-.002c.36.36.943.366 1.3.01.355-.356.35-.938-.01-1.3l-.027-.024zM6.58 9.586l.048.05c1.367 1.366 3.565 1.384 4.91.04l2.29-2.292c1.344-1.343 1.328-3.542-.04-4.91-1.366-1.366-3.564-1.384-4.908-.04L7.127 4.187c.695-.074 1.457.078 2.176.444l1.028-1.027c.635-.634 1.67-.624 2.313.017.643.644.652 1.678.018 2.312l-2.43 2.432c-.635.634-1.67.624-2.313-.018-.024-.024-.048-.05-.07-.075l-.003.004c-.36-.362-.943-.367-1.3-.01-.355.355-.35.937.01 1.3.01.007.018.015.027.023z"/></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 5ea6086ab77..d5e11e22be5 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -12,7 +12,7 @@ } Activities.prototype.updateTooltips = function() { - return gl.utils.localTimeAgo($('.js-timeago', '#activity')); + return gl.utils.localTimeAgo($('.js-timeago', '.content_list')); }; Activities.prototype.reloadActivities = function() { diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 8ac1ba7665e..5467e3edc69 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,6 +1,6 @@ (function(w) { $(function() { - $('.js-toggle-button').on('click', function(e) { + $('body').on('click', '.js-toggle-button', function(e) { e.preventDefault(); $(this) .find('.fa') diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index a612cf0f1ae..91c12570e09 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -54,4 +54,11 @@ $(() => { }); } }); + + gl.IssueBoardsSearch = new Vue({ + el: '#js-boards-seach', + data: { + filters: Store.state.filters + } + }); }); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index d7f4107cb02..7e86f001f44 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -21,15 +21,10 @@ }, data () { return { - query: '', filters: Store.state.filters }; }, watch: { - query () { - this.list.filters = this.getFilterData(); - this.list.getIssues(true); - }, filters: { handler () { this.list.page = 1; @@ -38,16 +33,6 @@ deep: true } }, - methods: { - getFilterData () { - const filters = this.filters; - let queryData = { search: this.query }; - - Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; }); - - return queryData; - } - }, ready () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index a6644e9eb8c..50fc11d7737 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -20,7 +20,8 @@ data () { return { scrollOffset: 250, - filters: Store.state.filters + filters: Store.state.filters, + showCount: false }; }, watch: { @@ -30,6 +31,15 @@ this.$els.list.scrollTop = 0; }, deep: true + }, + issues () { + this.$nextTick(() => { + if (this.scrollHeight() > this.listHeight()) { + this.showCount = true; + } else { + this.showCount = false; + } + }); } }, methods: { @@ -58,6 +68,7 @@ group: 'issues', sort: false, disabled: this.disabled, + filter: '.board-list-count', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index be2b8c568a8..91fd620fdb3 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -11,6 +11,7 @@ class List { this.loading = true; this.loadingMore = false; this.issues = []; + this.issuesSize = 0; if (obj.label) { this.label = new ListLabel(obj.label); @@ -51,17 +52,13 @@ class List { } nextPage () { - if (Math.floor(this.issues.length / 20) === this.page) { + if (this.issuesSize > this.issues.length) { this.page++; return this.getIssues(false); } } - canSearch () { - return this.type === 'backlog'; - } - getIssues (emptyIssues = true) { const filters = this.filters; let data = { page: this.page }; @@ -80,12 +77,13 @@ class List { .then((resp) => { const data = resp.json(); this.loading = false; + this.issuesSize = data.size; if (emptyIssues) { this.issues = []; } - this.createIssues(data); + this.createIssues(data.issues); }); } @@ -96,14 +94,20 @@ class List { } addIssue (issue, listFrom) { - this.issues.push(issue); + if (!this.findIssue(issue.id)) { + this.issues.push(issue); - if (this.label) { - issue.addLabel(this.label); - } + if (this.label) { + issue.addLabel(this.label); + } - if (listFrom) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id); + if (listFrom) { + this.issuesSize++; + gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + .then(() => { + listFrom.getIssues(false); + }); + } } } @@ -116,6 +120,7 @@ class List { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { + this.issuesSize--; issue.removeLabel(this.label); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 18f26a1f911..bd07ee0c161 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -15,7 +15,8 @@ author_id: gl.utils.getParameterValues('author_id')[0], assignee_id: gl.utils.getParameterValues('assignee_id')[0], milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]') + label_name: gl.utils.getParameterValues('label_name[]'), + search: '' }; }, addList (listObj) { diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 index f9f9f7999d4..b5ff3a81ed5 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,10 +1,7 @@ -Vue.http.interceptors.push((request, next) => { +Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - Vue.nextTick(() => { - setTimeout(() => { - Vue.activeResources--; - }, 500); + next(function (response) { + Vue.activeResources--; }); - next(); }); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 0d7d29bb0d0..4d066f13646 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -54,12 +54,14 @@ } Build.prototype.getInitialBuildTrace = function() { + var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] + return $.ajax({ url: this.build_url, dataType: 'json', success: function(build_data) { $('.js-build-output').html(build_data.trace_html); - if (build_data.status === 'success' || build_data.status === 'failed') { + if (removeRefreshStatuses.indexOf(build_data.status) >= 0) { return $('.js-build-refresh').remove(); } } diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 new file mode 100644 index 00000000000..8d3e29794a1 --- /dev/null +++ b/app/assets/javascripts/build_variables.js.es6 @@ -0,0 +1,6 @@ +$(function(){ + $('.reveal-variables').off('click').on('click',function(){ + $('.js-build').toggle().niceScroll(); + $(this).hide(); + }); +}); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ba64d2bcf0b..38cdc7b9fba 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -199,6 +199,7 @@ break; case 'labels': switch (path[2]) { + case 'new': case 'edit': new Labels(); } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 0179b320a3b..77b2082cba0 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -117,7 +117,7 @@ } }); } else { - return elements.show(); + return elements.show().removeClass('option-hidden'); } } }; @@ -190,9 +190,9 @@ currentIndex = -1; - NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden'; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")"; + SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; CURSOR_SELECT_SCROLL_PADDING = 5 @@ -556,7 +556,7 @@ if (isInput) { field = $(this.el); } else { - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']"); } if (el.hasClass(ACTIVE_CLASS)) { el.removeClass(ACTIVE_CLASS); @@ -565,10 +565,6 @@ } else { field.remove(); } - if (this.options.toggleLabel) { - this.updateLabel(selectedObject, el, this); - } - return selectedObject; } else if (el.hasClass(INDETERMINATE_CLASS)) { el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); @@ -578,7 +574,6 @@ if (!field.length && fieldName) { this.addInput(fieldName, value, selectedObject); } - return selectedObject; } else { if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); @@ -590,9 +585,6 @@ field.remove(); } el.addClass(ACTIVE_CLASS); - if (this.options.toggleLabel) { - this.updateLabel(selectedObject, el, this); - } if (value != null) { if (!field.length && fieldName) { this.addInput(fieldName, value, selectedObject); @@ -600,8 +592,14 @@ field.val(value).trigger('change'); } } - return selectedObject; } + + // Update label right after input has been added + if (this.options.toggleLabel) { + this.updateLabel(selectedObject, el, this); + } + + return selectedObject; }; GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 0f840821f53..9efad1ce943 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -10,21 +10,24 @@ ImporterStatus.prototype.initStatusPage = function() { $('.js-add-to-import').off('click').on('click', (function(_this) { return function(e) { - var $btn, $namespace_input, $target_field, $tr, id, new_namespace; + var $btn, $namespace_input, $target_field, $tr, id, target_namespace; $btn = $(e.currentTarget); $tr = $btn.closest('tr'); $target_field = $tr.find('.import-target'); $namespace_input = $target_field.find('input'); id = $tr.attr('id').replace('repo_', ''); - new_namespace = null; + target_namespace = null; + if ($namespace_input.length > 0) { - new_namespace = $namespace_input.prop('value'); - $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name'))); + target_namespace = $namespace_input.prop('value'); + $target_field.empty().append(target_namespace + "/" + ($target_field.data('project_name'))); } + $btn.disable().addClass('is-loading'); + return $.post(_this.import_url, { repo_id: id, - new_namespace: new_namespace + target_namespace: target_namespace }, { dataType: 'script' }); @@ -70,7 +73,7 @@ if ($('.js-importer-status').length) { var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); var importPath = $('.js-importer-status').data('import-path'); - + new ImporterStatus(jobsImportPath, importPath); } }); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 6838d9d8da1..e6422602ce8 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -127,7 +127,7 @@ Issue.prototype.initCanCreateBranch = function() { var $container; - $container = $('div#new-branch'); + $container = $('#new-branch'); if ($container.length === 0) { return; } @@ -139,7 +139,6 @@ if (data.can_create_branch) { $container.find('.checking').hide(); $container.find('.available').show(); - return $container.find('a').attr('disabled', false); } else { $container.find('.checking').hide(); return $container.find('.unavailable').show(); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 565dbeacdb3..bab23ff5ac0 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -164,7 +164,7 @@ instance.addInput(this.fieldName, label.id); } } - if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) { + if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) { selectedClass.push('is-active'); } if ($dropdown.hasClass('js-multiselect') && removesAll) { diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 218f24fe908..7d8eef1b495 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,54 +1,12 @@ (function() { - var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work; - Turbolinks.enableProgressBar(); - defaultClass = 'tanuki-shape'; - - pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek']; - - pieceIndex = 0; - - firstPiece = pieces[0]; - - currentTimer = null; - - delay = 150; - - clearHighlights = function() { - return $("." + defaultClass + ".highlight").attr('class', defaultClass); - }; - - start = function() { - clearHighlights(); - pieceIndex = 0; - if (pieces[0] !== firstPiece) { - pieces.reverse(); - } - if (currentTimer) { - clearInterval(currentTimer); - } - return currentTimer = setInterval(work, delay); - }; - - stop = function() { - clearInterval(currentTimer); - return clearHighlights(); - }; - - work = function() { - clearHighlights(); - $(pieces[pieceIndex]).attr('class', defaultClass + " highlight"); - if (pieceIndex === pieces.length - 1) { - pieceIndex = 0; - return pieces.reverse(); - } else { - return pieceIndex++; - } - }; - - $(document).on('page:fetch', start); + $(document).on('page:fetch', function() { + $('.tanuki-logo').addClass('animate'); + }); - $(document).on('page:change', stop); + $(document).on('page:change', function() { + $('.tanuki-logo').removeClass('animate'); + }); }).call(this); diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6 index 77bffbcb403..b56fd5aa658 100644 --- a/app/assets/javascripts/merge_conflict_resolver.js.es6 +++ b/app/assets/javascripts/merge_conflict_resolver.js.es6 @@ -75,10 +75,8 @@ class MergeConflictResolver { window.location.href = data.redirect_to; }) .error(() => { - new Flash('Something went wrong!'); - }) - .always(() => { this.vue.isSubmitting = false; + new Flash('Something went wrong!'); }); } diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 798f15e40a0..a787b11f2a9 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -4,6 +4,8 @@ this.ProjectNew = (function() { function ProjectNew() { this.toggleSettings = bind(this.toggleSettings, this); + this.$selects = $('.features select'); + $('.project-edit-container').on('ajax:before', (function(_this) { return function() { $('.project-edit-container').hide(); @@ -15,18 +17,24 @@ } ProjectNew.prototype.toggleSettings = function() { - this._showOrHide('#project_builds_enabled', '.builds-feature'); - return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature'); + var self = this; + + this.$selects.each(function () { + var $select = $(this), + className = $select.data('field').replace(/_/g, '-') + .replace('access-level', 'feature'); + self._showOrHide($select, '.' + className); + }); }; ProjectNew.prototype.toggleSettingsOnclick = function() { - return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings); + this.$selects.on('change', this.toggleSettings); }; ProjectNew.prototype._showOrHide = function(checkElement, container) { - var $container; - $container = $(container); - if ($(checkElement).prop('checked')) { + var $container = $(container); + + if ($(checkElement).val() !== '0') { return $container.show(); } else { return $container.hide(); diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6 new file mode 100644 index 00000000000..6f0996c0d2a --- /dev/null +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -0,0 +1,11 @@ +(global => { + global.gl = global.gl || {}; + + gl.SnippetsList = function() { + var $holder = $('.snippets-list-holder'); + + $holder.find('.pagination').on('ajax:success', (e, data) => { + $holder.replaceWith(data.html); + }); + } +})(window); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 6e677fa8cc6..23eda7d44ca 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -13,6 +13,7 @@ this.perPage = this.el.data('perPage'); this.clearListeners(); this.initBtnListeners(); + this.initFilters(); } Todos.prototype.clearListeners = function() { @@ -27,6 +28,31 @@ return $('.todo').on('click', this.goToTodoUrl); }; + Todos.prototype.initFilters = function() { + new UsersSelect(); + this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); + this.initFilterDropdown($('.js-type-search'), 'type'); + this.initFilterDropdown($('.js-action-search'), 'action_id'); + + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); + }; + + Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { + $dropdown.glDropdown({ + selectable: true, + filterable: searchFields ? true : false, + fieldName: fieldName, + search: { fields: searchFields }, + data: $dropdown.data('data'), + clicked: function() { + return $dropdown.closest('form.filter-form').submit(); + } + }) + }; + Todos.prototype.doneClicked = function(e) { var $this; e.preventDefault(); @@ -66,7 +92,7 @@ success: (function(_this) { return function(data) { $this.remove(); - $('.js-todos-list').remove(); + $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); return _this.updateBadges(data); }; })(this) diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js deleted file mode 100644 index 6c4d88cf407..00000000000 --- a/app/assets/javascripts/user.js +++ /dev/null @@ -1,29 +0,0 @@ -(function() { - this.User = (function() { - function User(opts) { - this.opts = opts; - $('.profile-groups-avatars').tooltip({ - "placement": "top" - }); - this.initTabs(); - $('.hide-project-limit-message').on('click', function(e) { - $.cookie('hide_project_limit_message', 'false', { - path: gon.relative_url_root || '/' - }); - $(this).parents('.project-limit-message').remove(); - return e.preventDefault(); - }); - } - - User.prototype.initTabs = function() { - return new UserTabs({ - parentEl: '.user-profile', - action: this.opts.action - }); - }; - - return User; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 new file mode 100644 index 00000000000..6889d3a7491 --- /dev/null +++ b/app/assets/javascripts/user.js.es6 @@ -0,0 +1,34 @@ +(global => { + global.User = class { + constructor(opts) { + this.opts = opts; + this.placeProfileAvatarsToTop(); + this.initTabs(); + this.hideProjectLimitMessage(); + } + + placeProfileAvatarsToTop() { + $('.profile-groups-avatars').tooltip({ + placement: 'top' + }); + } + + initTabs() { + return new UserTabs({ + parentEl: '.user-profile', + action: this.opts.action + }); + } + + hideProjectLimitMessage() { + $('.hide-project-limit-message').on('click', e => { + e.preventDefault(); + const path = gon.relative_url_root || '/'; + $.cookie('hide_project_limit_message', 'false', { + path: path + }); + $(this).parents('.project-limit-message').remove(); + }); + } + } +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index a306b8f3f29..d5cca1b10fb 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -24,6 +24,7 @@ @import "framework/issue_box.scss"; @import "framework/jquery.scss"; @import "framework/lists.scss"; +@import "framework/logo.scss"; @import "framework/markdown_area.scss"; @import "framework/mobile.scss"; @import "framework/modal.scss"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 6c3786b49bb..4618687a4be 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -200,13 +200,15 @@ svg { height: 15px; - width: auto; + width: 15px; position: relative; top: 2px; } svg, .fa { - margin-right: 3px; + &:not(:last-child) { + margin-right: 3px; + } } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 8984bce616c..5957dce89bc 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -53,7 +53,7 @@ pre { &.well-pre { border: 1px solid #eee; - background: #f9f9f9; + background: $gray-light; border-radius: 0; color: #555; } @@ -225,7 +225,7 @@ li.note { .milestone { &.milestone-closed { - background: #f9f9f9; + background: $gray-light; } .progress { margin-bottom: 0; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index be5c64c56d3..b0ba112476b 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -183,6 +183,13 @@ &.dropdown-menu-user-link { line-height: 16px; } + + .icon-play { + fill: $table-text-gray; + margin-right: 6px; + height: 12px; + width: 11px; + } } .dropdown-header { @@ -195,6 +202,12 @@ .separator + .dropdown-header { padding-top: 2px; } + + .unclickable { + cursor: not-allowed; + padding: 5px 8px; + color: $dropdown-header-color; + } } .dropdown-menu-large { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index d3e3fc50736..e3be45ba1dc 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -115,7 +115,7 @@ padding: 0; } td.blame-commit { - background: #f9f9f9; + background: $gray-light; min-width: 350px; .commit-author-link { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 43d55661541..37ff7e22ed1 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -19,7 +19,6 @@ input[type='text'].danger { } .form-actions { - margin: -$gl-padding; margin-top: 0; margin-bottom: -$gl-padding; padding: $gl-padding; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 0c607071840..1036219172e 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -2,16 +2,6 @@ * Application Header * */ -@mixin tanuki-logo-colors($path-color) { - fill: $path-color; - transition: all 0.8s; - - &:hover, - &.highlight { - fill: lighten($path-color, 25%); - transition: all 0.1s; - } -} header { transition: padding $sidebar-transition-duration; @@ -25,7 +15,7 @@ header { margin: 8px 0; text-align: center; - #tanuki-logo, img { + .tanuki-logo, img { height: 36px; } } @@ -94,7 +84,7 @@ header { .side-nav-toggle { position: absolute; left: -10px; - margin: 6px 0; + margin: 7px 0; font-size: 18px; padding: 6px 10px; border: none; @@ -146,6 +136,8 @@ header { } .title { + position: relative; + padding-right: 20px; margin: 0; font-size: 19px; max-width: 400px; @@ -158,7 +150,11 @@ header { vertical-align: top; white-space: nowrap; - @media (max-width: $screen-sm-max) { + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + max-width: 300px; + } + + @media (max-width: $screen-xs-max) { max-width: 190px; } @@ -170,11 +166,15 @@ header { } .dropdown-toggle-caret { - position: relative; - top: -2px; + color: $gl-text-color; + border: transparent; + background: transparent; + position: absolute; + right: 3px; width: 12px; - line-height: 12px; - margin-left: 5px; + line-height: 19px; + margin-top: (($header-height - 19) / 2); + padding: 0; font-size: 10px; text-align: center; cursor: pointer; @@ -205,26 +205,6 @@ header { } } -#tanuki-logo { - - #tanuki-left-ear, - #tanuki-right-ear, - #tanuki-nose { - @include tanuki-logo-colors($tanuki-red); - } - - #tanuki-left-eye, - #tanuki-right-eye { - @include tanuki-logo-colors($tanuki-orange); - } - - #tanuki-left-cheek, - #tanuki-right-cheek { - @include tanuki-logo-colors($tanuki-yellow); - } - -} - @media (max-width: $screen-xs-max) { header .container-fluid { font-size: 18px; diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss new file mode 100644 index 00000000000..3ee3fb4cee5 --- /dev/null +++ b/app/assets/stylesheets/framework/logo.scss @@ -0,0 +1,118 @@ +@mixin unique-keyframes { + $animation-name: unique-id(); + @include webkit-prefix(animation-name, $animation-name); + + @-webkit-keyframes #{$animation-name} { + @content; + } + @keyframes #{$animation-name} { + @content; + } +} + +@mixin tanuki-logo-colors($path-color) { + fill: $path-color; + transition: all 0.8s; + + &:hover { + fill: lighten($path-color, 25%); + transition: all 0.1s; + } +} + +@mixin tanuki-second-highlight-animations($tanuki-color) { + @include unique-keyframes { + 10%, 80% { + fill: #{$tanuki-color} + } + 20%, 90% { + fill: lighten($tanuki-color, 25%); + } + } +} + +@mixin tanuki-forth-highlight-animations($tanuki-color) { + @include unique-keyframes { + 30%, 60% { + fill: #{$tanuki-color}; + } + 40%, 70% { + fill: lighten($tanuki-color, 25%); + } + } +} + +.tanuki-logo { + + .tanuki-left-ear, + .tanuki-right-ear, + .tanuki-nose { + @include tanuki-logo-colors($tanuki-red); + } + + .tanuki-left-eye, + .tanuki-right-eye { + @include tanuki-logo-colors($tanuki-orange); + } + + .tanuki-left-cheek, + .tanuki-right-cheek { + @include tanuki-logo-colors($tanuki-yellow); + } + + &.animate { + .tanuki-shape { + @include webkit-prefix(animation-duration, 1.5s); + @include webkit-prefix(animation-iteration-count, infinite); + } + + .tanuki-left-cheek { + @include unique-keyframes { + 0%, 10%, 100% { + fill: lighten($tanuki-yellow, 25%); + } + 90% { + fill: $tanuki-yellow; + } + } + } + + .tanuki-left-eye { + @include tanuki-second-highlight-animations($tanuki-orange); + } + + .tanuki-left-ear { + @include tanuki-second-highlight-animations($tanuki-red); + } + + .tanuki-nose { + @include unique-keyframes { + 20%, 70% { + fill: $tanuki-red; + } + 30%, 80% { + fill: lighten($tanuki-red, 25%); + } + } + } + + .tanuki-right-eye { + @include tanuki-forth-highlight-animations($tanuki-orange); + } + + .tanuki-right-ear { + @include tanuki-forth-highlight-animations($tanuki-red); + } + + .tanuki-right-cheek { + @include unique-keyframes { + 40% { + fill: $tanuki-yellow; + } + 60% { + fill: lighten($tanuki-yellow, 25%); + } + } + } + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index d2d60ed7196..00f92cef9a4 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -9,43 +9,11 @@ border-radius: $radius; } -@mixin border-radius-left($radius) { - @include border-radius($radius 0 0 $radius) -} - -@mixin border-radius-right($radius) { - @include border-radius(0 0 $radius $radius) -} - -@mixin linear-gradient($from, $to) { - background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to)); - background-image: -webkit-linear-gradient($from, $to); - background-image: -moz-linear-gradient($from, $to); - background-image: -ms-linear-gradient($from, $to); - background-image: -o-linear-gradient($from, $to); -} - -@mixin transition($transition) { - -webkit-transition: $transition; - -moz-transition: $transition; - -ms-transition: $transition; - -o-transition: $transition; - transition: $transition; -} - /** * Prefilled mixins * Mixins with fixed values */ -@mixin shade { - @include box-shadow(0 0 3px #ddd); -} - -@mixin solid-shade { - @include box-shadow(0 0 0 3px #f1f1f1); -} - @mixin str-truncated($max_width: 82%) { display: inline-block; overflow: hidden; @@ -76,7 +44,7 @@ } &.active { - background: #f9f9f9; + background: $gray-light; a { font-weight: 600; } @@ -94,23 +62,6 @@ } } -@mixin input-big { - height: 36px; - padding: 5px 10px; - font-size: 16px; - line-height: 24px; - color: #7f8fa4; - background-color: #fff; - border-color: #e7e9ed; -} - -@mixin btn-big { - height: 36px; - padding: 5px 10px; - font-size: 16px; - line-height: 24px; -} - @mixin bulleted-list { > ul { list-style-type: disc; @@ -129,3 +80,8 @@ color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.1); } + +@mixin webkit-prefix($property, $value) { + #{'-webkit-' + $property}: $value; + #{$property}: $value; +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 9e924f99e9c..553768b2e68 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,4 +1,4 @@ -@mixin fade($gradient-direction, $rgba, $gradient-color) { +@mixin fade($gradient-direction, $gradient-color) { visibility: hidden; opacity: 0; z-index: 2; @@ -8,10 +8,7 @@ height: 30px; transition-duration: .3s; -webkit-transform: translateZ(0); - background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); - background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); - background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); - background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4)); &.scrolling { visibility: visible; @@ -71,7 +68,7 @@ .badge { font-weight: normal; background-color: #eee; - color: #78a; + color: $btn-transparent-color; vertical-align: baseline; } } @@ -141,7 +138,7 @@ } li a { - padding: 16px 10px 11px; + padding: 16px 15px 11px; } /* Small devices (phones, tablets, 768px and lower) */ @@ -161,6 +158,7 @@ > .dropdown { margin-right: $gl-padding-top; display: inline-block; + vertical-align: top; &:last-child { margin-right: 0; @@ -210,12 +208,6 @@ } } - .project-filter-form { - input { - background-color: $background-color; - } - } - @media (max-width: $screen-xs-max) { padding-bottom: 0; width: 100%; @@ -335,10 +327,6 @@ } } - .badge { - color: $gl-icon-color; - } - &:hover { a, i { color: $black; @@ -356,7 +344,7 @@ } .fade-right { - @include fade(left, rgba(255, 255, 255, 0.4), $background-color); + @include fade(left, $background-color); right: -5px; .fa { @@ -365,7 +353,7 @@ } .fade-left { - @include fade(right, rgba(255, 255, 255, 0.4), $background-color); + @include fade(right, $background-color); left: -5px; .fa { @@ -376,6 +364,7 @@ &.sub-nav-scroll { .fade-right { + @include fade(left, $dark-background-color); right: 0; .fa { @@ -384,6 +373,7 @@ } .fade-left { + @include fade(right, $dark-background-color); left: 0; .fa { @@ -400,7 +390,7 @@ @include scrolling-links(); .fade-right { - @include fade(left, rgba(255, 255, 255, 0.4), $white-light); + @include fade(left, $white-light); right: -5px; .fa { @@ -409,7 +399,7 @@ } .fade-left { - @include fade(right, rgba(255, 255, 255, 0.4), $white-light); + @include fade(right, $white-light); left: -5px; .fa { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index b2e22b60440..c75dacf95d9 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -151,7 +151,7 @@ background-position: right 0 bottom 6px; border: 1px solid $input-border; @include border-radius($border-radius-default); - @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { border-color: $input-border-focus; diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 371c1bf17e1..915aa631ef8 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -125,7 +125,7 @@ $panel-inner-border: $border-color; // //## -$well-bg: #f9f9f9; +$well-bg: $gray-light; $well-border: #eee; //== Code diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 06874a993fa..3f8433a0e7f 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -159,25 +159,18 @@ position: relative; a.anchor { - // Setting `display: none` would prevent the anchor being scrolled to, so - // instead we set the height to 0 and it gets updated on hover. - height: 0; + left: -16px; + position: absolute; + text-decoration: none; + + &:after { + content: url('icon_anchor.svg'); + visibility: hidden; + } } - &:hover > a.anchor { - $size: 14px; - position: absolute; - right: 100%; - top: 50%; - margin-top: -11px; - margin-right: 0; - padding-right: 15px; - display: inline-block; - width: $size; - height: $size; - background-image: image-url("icon-link.png"); - background-size: contain; - background-repeat: no-repeat; + &:hover > a.anchor:after { + visibility: visible; } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 5da390118c6..9f563a4de35 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -10,12 +10,78 @@ $sidebar-transition-duration: .15s; $sidebar-breakpoint: 1024px; /* + * Color schema + */ +$white-light: #fff; +$white-normal: #ededed; +$white-dark: #ececec; + +$gray-light: #fafafa; +$gray-normal: #f5f5f5; +$gray-dark: #ededed; +$gray-darkest: #c9c9c9; + +$green-light: #38ae67; +$green-normal: #2faa60; +$green-dark: #2ca05b; + +$blue-light: #2ea8e5; +$blue-normal: #2d9fd8; +$blue-dark: #2897ce; + +$blue-medium-light: #3498cb; +$blue-medium: #2f8ebf; +$blue-medium-dark: #2d86b4; + +$orange-light: #fc8a51; +$orange-normal: #e75e40; +$orange-dark: #ce5237; + +$red-light: #e52c5a; +$red-normal: #d22852; +$red-dark: darken($red-normal, 5%); + +$black: #000; +$black-transparent: rgba(0, 0, 0, 0.3); + +$border-white-light: #f1f2f4; +$border-white-normal: #d6dae2; +$border-white-dark: #c6cacf; + +$border-gray-light: #dcdcdc; +$border-gray-normal: #d7d7d7; +$border-gray-dark: #c6cacf; + +$border-green-light: #2faa60; +$border-green-normal: #2ca05b; +$border-green-dark: #279654; + +$border-blue-light: #2d9fd8; +$border-blue-normal: #2897ce; +$border-blue-dark: #258dc1; + +$border-orange-light: #fc6d26; +$border-orange-normal: #ce5237; +$border-orange-dark: #c14e35; + +$border-red-light: #d22852; +$border-red-normal: #ca264f; +$border-red-dark: darken($border-red-normal, 5%); + +$help-well-bg: $gray-light; +$help-well-border: #e5e5e5; + +$warning-message-bg: #fbf2d9; +$warning-message-color: #9e8e60; +$warning-message-border: #f0e2bb; + +/* * UI elements */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; $table-border-color: #f0f0f0; -$background-color: #fafafa; +$background-color: $gray-light; $dark-background-color: #f5f5f5; $table-text-gray: #8f8f8f; @@ -35,6 +101,7 @@ $gl-icon-color: $gl-placeholder-color; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; +$gl-gray-light: $gl-placeholder-color; $gl-header-color: $gl-title-color; /* @@ -90,73 +157,6 @@ $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; -/* - * Color schema - */ - -$white-light: #fff; -$white-normal: #ededed; -$white-dark: #ececec; - -$gray-light: #faf9f9; -$gray-normal: #f5f5f5; -$gray-dark: #ededed; -$gray-darkest: #c9c9c9; - -$green-light: #38ae67; -$green-normal: #2faa60; -$green-dark: #2ca05b; - -$blue-light: #2ea8e5; -$blue-normal: #2d9fd8; -$blue-dark: #2897ce; - -$blue-medium-light: #3498cb; -$blue-medium: #2f8ebf; -$blue-medium-dark: #2d86b4; - -$orange-light: #fc8a51; -$orange-normal: #e75e40; -$orange-dark: #ce5237; - -$red-light: #e52c5a; -$red-normal: #d22852; -$red-dark: darken($red-normal, 5%); - -$black: #000; -$black-transparent: rgba(0, 0, 0, 0.3); - -$border-white-light: #f1f2f4; -$border-white-normal: #d6dae2; -$border-white-dark: #c6cacf; - -$border-gray-light: #dcdcdc; -$border-gray-normal: #d7d7d7; -$border-gray-dark: #c6cacf; - -$border-green-light: #2faa60; -$border-green-normal: #2ca05b; -$border-green-dark: #279654; - -$border-blue-light: #2d9fd8; -$border-blue-normal: #2897ce; -$border-blue-dark: #258dc1; - -$border-orange-light: #fc6d26; -$border-orange-normal: #ce5237; -$border-orange-dark: #c14e35; - -$border-red-light: #d22852; -$border-red-normal: #ca264f; -$border-red-dark: darken($border-red-normal, 5%); - -$help-well-bg: #fafafa; -$help-well-border: #e5e5e5; - -$warning-message-bg: #fbf2d9; -$warning-message-color: #9e8e60; -$warning-message-border: #f0e2bb; - /* tanuki logo colors */ $tanuki-red: #e24329; $tanuki-orange: #fc6d26; @@ -186,7 +186,7 @@ $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; -$match-line: #fafafa; +$match-line: $gray-light; $table-border-gray: #f0f0f0; $line-target-blue: #eaf3fc; $line-select-yellow: #fcf8e7; @@ -267,7 +267,7 @@ $zen-control-hover-color: #111; $calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); -$calendar-unselectable-bg: #faf9f9; +$calendar-unselectable-bg: $gray-light; /* * Personal Access Tokens diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index c9cdfdcd29c..8f71381f5c4 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -96,6 +96,10 @@ line-height: inherit; } } + + .label-default { + color: $btn-transparent-color; + } } .abuse-reports { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9ac4d801ac4..037278bb083 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -10,7 +10,7 @@ .is-dragging { // Important because plugin sets inline CSS opacity: 1!important; - + * { // !important to make sure no style can override this when dragging cursor: -webkit-grabbing!important; @@ -142,11 +142,6 @@ } } -.board-header-loading-spinner { - margin-right: 10px; - color: $gray-darkest; -} - .board-inner-container { border-bottom: 1px solid $border-color; padding: $gl-padding; @@ -160,40 +155,6 @@ border-bottom: 1px solid $border-color; } -.board-search-container { - position: relative; - background-color: #fff; - - .form-control { - padding-right: 30px; - } -} - -.board-search-icon, -.board-search-clear-btn { - position: absolute; - right: $gl-padding + 10px; - top: 50%; - margin-top: -7px; - font-size: 14px; -} - -.board-search-icon { - color: $gl-placeholder-color; -} - -.board-search-clear-btn { - padding: 0; - line-height: 1; - background: transparent; - border: 0; - outline: 0; - - &:hover { - color: $gl-link-color; - } -} - .board-delete { margin-right: 10px; padding: 0; @@ -304,3 +265,22 @@ margin-right: 8px; font-weight: 500; } + +.issue-boards-search { + width: 335px; + + .form-control { + display: inline-block; + width: 210px; + } +} + +.board-list-count { + padding: 10px 0; + color: $gl-placeholder-color; + font-size: 13px; + + > .fa { + margin-right: 5px; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 8c33e7d9a2e..614405aa5c1 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -36,6 +36,7 @@ &.affix { right: 30px; bottom: 15px; + z-index: 1; @media (min-width: $screen-md-min) { right: 26%; @@ -107,13 +108,23 @@ } .blocks-container { - padding: $gl-padding; + padding: 0 $gl-padding; } .block { width: 100%; } + .js-build-variable { + color: $code-color; + } + + .js-build-value { + padding: 2px 4px; + color: $black; + background-color: $white-light; + } + .build-sidebar-header { padding: 0 $gl-padding $gl-padding; @@ -122,6 +133,13 @@ } } + .retry-link { + color: $gl-link-color; + &:hover { + text-decoration: underline; + } + } + .stage-item { cursor: pointer; @@ -131,7 +149,7 @@ } .build-dropdown { - padding: 0 $gl-padding; + padding: $gl-padding 0; .dropdown-menu-toggle { margin-top: 8px; @@ -145,7 +163,6 @@ } .builds-container { - margin-top: $gl-padding; background-color: $white-light; border-top: 1px solid $border-color; border-bottom: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 6a58b445afa..dc57a837155 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -18,8 +18,7 @@ } .commit-row-title { - line-height: 1; - margin-bottom: 7px; + line-height: 1.35; .notes_count { float: right; @@ -43,6 +42,7 @@ border: 1px solid $border-gray-dark; border-radius: $border-radius-default; margin-left: 5px; + line-height: 1; &:hover { background-color: darken($gray-light, 10%); @@ -113,11 +113,13 @@ .commit-row-description { font-size: 14px; - border-left: 1px solid #eee; + border-left: 1px solid $btn-gray-hover; padding: 10px 15px; margin: 10px 0; - background: #f9f9f9; + background: $gray-light; display: none; + white-space: pre-line; + word-break: normal; pre { border: none; @@ -134,7 +136,7 @@ .commit-row-info { color: $gl-gray; - line-height: 1; + line-height: 1.35; a { color: $gl-gray; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 55f9d4a0011..d01c60ee6ab 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -4,8 +4,9 @@ margin: 0; } - .fa-play { - font-size: 14px; + .icon-play { + height: 13px; + width: 12px; } .dropdown-new { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5c336bb1c7e..1d00da1266c 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -60,7 +60,7 @@ pre { border: none; - background: #f9f9f9; + background: $gray-light; border-radius: 0; color: #777; margin: 0 20px; @@ -92,7 +92,7 @@ border: 1px solid #eee; padding: 5px; @include border-radius(5px); - background: #f9f9f9; + background: $gray-light; margin-left: 10px; top: -6px; img { @@ -115,11 +115,8 @@ } &.commits-stat { - margin-top: 3px; display: block; - padding: 3px; - padding-left: 0; - + padding: 0 3px 0 0; &:hover { background: none; } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index dfe1e3075da..d14224ed00f 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -9,9 +9,13 @@ .issue-check { float: left; - padding-right: 8px; + padding-right: 16px; margin-bottom: 10px; min-width: 15px; + + .selected_issue { + vertical-align: text-top; + } } .issue-labels { @@ -68,12 +72,12 @@ form.edit-issue { } &.closed { - background: #f9f9f9; + background: $gray-light; border-color: #e5e5e5; } &.merged { - background: #f9f9f9; + background: $gray-light; border-color: #e5e5e5; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 606459f82cd..38c7cd98e41 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -7,6 +7,7 @@ display: inline-block; margin-right: 10px; margin-bottom: 10px; + text-decoration: none; } &.suggest-colors-dropdown { diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 1f499897c16..5ec660799e3 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -16,7 +16,7 @@ $colors: ( white_button_origin_chosen : #268ced, white_header_not_chosen : #f0f0f0, - white_line_not_chosen : #f9f9f9, + white_line_not_chosen : $gray-light, dark_header_head_neutral : rgba(#3f3, .2), diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 9bdf0d250bb..2d66ab25da6 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -2,6 +2,7 @@ .stage { max-width: 90px; width: 90px; + text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -146,6 +147,7 @@ } .stage-cell { + text-align: center; svg { height: 18px; @@ -153,10 +155,6 @@ vertical-align: middle; overflow: visible; } - - .light { - width: 3px; - } } .duration, @@ -215,6 +213,13 @@ border-color: $border-white-normal; } } + + .btn { + .icon-play { + height: 13px; + width: 12px; + } + } } } @@ -273,7 +278,7 @@ .stage-column { display: inline-block; vertical-align: top; - margin-right: 50px; + margin-right: 65px; li { list-style: none; @@ -319,6 +324,14 @@ a { color: $layout-link-gray; + text-decoration: none; + + &:hover { + .ci-status-text { + text-decoration: underline; + } + } + } } @@ -334,9 +347,9 @@ content: ''; position: absolute; top: 50%; - right: -54px; + right: -69px; border-top: 2px solid $border-color; - width: 54px; + width: 69px; height: 1px; } } @@ -356,22 +369,25 @@ &::after { right: -20px; border-right: 2px solid $border-color; - border-radius: 0 0 50px; + border-radius: 0 0 15px; } // Left connecting curves &::before { left: -20px; border-left: 2px solid $border-color; - border-radius: 0 0 0 50px; + border-radius: 0 0 0 15px; } } // Connect second build to first build with smaller curved line &:nth-child(2) { &::after, &::before { - height: 45px; - top: -26px; + height: 29px; + top: -10px; + } + .curve { + display: block; } } } @@ -390,6 +406,12 @@ border: none; } } + // Remove opposite curve + .curve { + &::before { + display: none; + } + } } } @@ -401,6 +423,39 @@ border: none; } } + // Remove opposite curve + .curve { + &::after { + display: none; + } + } + } + } + + // Curve first child connecting lines in opposite direction + .curve { + display: none; + + &::before, + &::after { + content: ''; + width: 21px; + height: 25px; + position: absolute; + top: -28.5px; + border-top: 2px solid $border-color; + } + + &::after { + left: -39px; + border-right: 2px solid $border-color; + border-radius: 0 15px; + } + + &::before { + right: -39px; + border-left: 2px solid $border-color; + border-radius: 15px 0 0; } } } @@ -419,11 +474,22 @@ .pipelines.tab-pane { .content-list.pipelines { - overflow: scroll; + overflow: auto; } .stage { - max-width: 60px; - width: 60px; + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } +} + +.ci-status-icon-created { + + svg { + fill: $gray-darkest; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index eaf2d3270b3..f2db373da52 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -311,6 +311,14 @@ a.deploy-project-label { color: $gl-success; } +.lfs-enabled { + color: $gl-success; +} + +.lfs-disabled { + color: $gl-warning; +} + .breadcrumb.repo-breadcrumb { padding: 0; background: transparent; @@ -600,18 +608,25 @@ pre.light-well { } } -.project-show-readme .readme-holder { - padding: $gl-padding 0; - border-top: 0; - - .edit-project-readme { - z-index: 2; - position: relative; +.project-show-readme { + .row-content-block { + background-color: inherit; + border: none; } - .wiki h1 { - border-bottom: none; - padding: 0; + .readme-holder { + padding: $gl-padding 0; + border-top: 0; + + .edit-project-readme { + z-index: 2; + position: relative; + } + + .wiki h1 { + border-bottom: none; + padding: 0; + } } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index c9d436d72ba..436fb00ba2e 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -80,7 +80,7 @@ .search-icon { @extend .fa-search; - @include transition(color .15s); + transition: color 0.15s; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -125,7 +125,7 @@ } .location-badge { - @include transition(all .15s); + transition: all 0.15s; background-color: $location-badge-active-bg; color: $white-light; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 587f2d9f3c1..0ee7ceecae5 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -43,6 +43,15 @@ border-color: $blue-normal; } + &.ci-created { + color: $table-text-gray; + border-color: $table-text-gray; + + svg { + fill: $table-text-gray; + } + } + svg { height: 13px; width: 13px; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 0340526a53a..68a5d1ae06c 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -99,7 +99,7 @@ pre { border: none; - background: #f9f9f9; + background: $gray-light; border-radius: 0; color: #777; margin: 0 20px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 9da40fe2b09..1778c069706 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -11,6 +11,10 @@ } } + .add-to-tree { + vertical-align: top; + } + .tree-table { margin-bottom: 0; @@ -22,6 +26,15 @@ line-height: 21px; } + .last-commit { + @include str-truncated(60%); + } + + .commit-history-link-spacer { + margin: 0 10px; + color: $table-border-color; + } + &:hover { td { background-color: $row-hover; @@ -77,11 +90,17 @@ } } - .tree_commit { - color: $gl-gray; + .tree-time-ago { + min-width: 135px; + color: $gl-gray-light; + } + + .tree-commit { + max-width: 320px; + color: $gl-gray-light; .tree-commit-link { - color: $gl-gray; + color: $gl-gray-light; &:hover { text-decoration: underline; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 8d855ce99b0..c9846103762 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -20,6 +20,9 @@ $l-cyan: #8abeb7; $l-white: $ci-text-color; + .term-bold { + font-weight: bold; + } .term-italic { font-style: italic; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ebc2a4651ba..bd4ba384b29 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception - helper_method :abilities, :can?, :current_application_settings + helper_method :can?, :current_application_settings helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| @@ -97,12 +97,8 @@ class ApplicationController < ActionController::Base current_application_settings.after_sign_out_path.presence || new_user_session_path end - def abilities - Ability.abilities - end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end def access_denied! diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index a69877edfd4..4cb3be41064 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -13,7 +13,7 @@ module ServiceParams # `issue_events` and `merge_request_events` (singular!) # See app/helpers/services_helper.rb for how we # make those event names plural as special case. - :issues_events, :merge_requests_events, + :issues_events, :confidential_issues_events, :merge_requests_events, :notify_only_broken_builds, :notify_only_broken_pipelines, :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 036777c80c1..172d5344b7a 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -8,10 +8,14 @@ module ToggleAwardEmoji def toggle_award_emoji name = params.require(:name) - awardable.toggle_award_emoji(name, current_user) - TodoService.new.new_award_emoji(to_todoable(awardable), current_user) + if awardable.user_can_award?(current_user, name) + awardable.toggle_award_emoji(name, current_user) + TodoService.new.new_award_emoji(to_todoable(awardable), current_user) - render json: { ok: true } + render json: { ok: true } + else + render json: { ok: false } + end end private diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 7e8597a5eb3..256c41e6145 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -1,18 +1,17 @@ class Import::BaseController < ApplicationController private - def get_or_create_namespace + def find_or_create_namespace(name, owner) + return current_user.namespace if name == owner + return current_user.namespace unless current_user.can_create_group? + begin - namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user) + name = params[:target_namespace].presence || name + namespace = Group.create!(name: name, path: name, owner: current_user) namespace.add_owner(current_user) + namespace rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid - namespace = Namespace.find_by_path_or_name(@target_namespace) - unless current_user.can?(:create_projects, namespace) - @already_been_taken = true - return false - end + Namespace.find_by_path_or_name(name) end - - namespace end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 944c73d139a..6ea54744da8 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -35,23 +35,20 @@ class Import::BitbucketController < Import::BaseController end def create - @repo_id = params[:repo_id] || "" - repo = client.project(@repo_id.gsub("___", "/")) - @project_name = repo["slug"] - - repo_owner = repo["owner"] - repo_owner = current_user.username if repo_owner == client.user["user"]["username"] - @target_namespace = params[:new_namespace].presence || repo_owner - - namespace = get_or_create_namespace || (render and return) + @repo_id = params[:repo_id].to_s + repo = client.project(@repo_id.gsub('___', '/')) + @project_name = repo['slug'] + @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username']) unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute - @access_denied = true - render - return + render 'deploy_key' and return end - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute + if current_user.can?(:create_projects, @target_namespace) + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + else + render 'unauthorized' + end end private diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 9c1b0eb20f4..8c6bdd16383 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -41,14 +41,13 @@ class Import::GithubController < Import::BaseController @repo_id = params[:repo_id].to_i repo = client.repo(@repo_id) @project_name = repo.name + @target_namespace = find_or_create_namespace(repo.owner.login, client.user.login) - repo_owner = repo.owner.login - repo_owner = current_user.username if repo_owner == client.user.login - @target_namespace = params[:new_namespace].presence || repo_owner - - namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute + if current_user.can?(:create_projects, @target_namespace) + @project = Gitlab::GithubImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + else + render 'unauthorized' + end end private diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 08130ee8176..73837ffbe67 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -26,15 +26,14 @@ class Import::GitlabController < Import::BaseController def create @repo_id = params[:repo_id].to_i repo = client.project(@repo_id) - @project_name = repo["name"] + @project_name = repo['name'] + @target_namespace = find_or_create_namespace(repo['namespace']['path'], client.user['username']) - repo_owner = repo["namespace"]["path"] - repo_owner = current_user.username if repo_owner == client.user["username"] - @target_namespace = params[:new_namespace].presence || repo_owner - - namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute + if current_user.can?(:create_projects, @target_namespace) + @project = Gitlab::GitlabImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + else + render 'unauthorized' + end end private diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 014b9b43ff2..66ebdcc37a7 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -37,7 +37,7 @@ class JwtController < ApplicationController def authenticate_project(login, password) if login == 'gitlab-ci-token' - Project.find_by(builds_enabled: true, runners_token: password) + Project.with_builds_enabled.find_by(runners_token: password) end end diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index 5a94dcb0dbd..83eec1bf4a2 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -14,7 +14,7 @@ class NamespacesController < ApplicationController if user redirect_to user_path(user) - elsif group && can?(current_user, :read_group, namespace) + elsif group && can?(current_user, :read_group, group) redirect_to group_path(group) elsif current_user.nil? authenticate_user! diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 91315a07deb..b2ff36f6538 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController end def builds_enabled - return render_404 unless @project.builds_enabled? + return render_404 unless @project.feature_available?(:builds, current_user) end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 7241949393b..59222637961 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,22 +1,25 @@ class Projects::ArtifactsController < Projects::ApplicationController + include ExtractsPath + layout 'project' before_action :authorize_read_build! before_action :authorize_update_build!, only: [:keep] + before_action :extract_ref_name_and_path before_action :validate_artifacts! def download - unless artifacts_file.file_storage? - return redirect_to artifacts_file.url + if artifacts_file.file_storage? + send_file artifacts_file.path, disposition: 'attachment' + else + redirect_to artifacts_file.url end - - send_file artifacts_file.path, disposition: 'attachment' end def browse directory = params[:path] ? "#{params[:path]}/" : '' @entry = build.artifacts_metadata_entry(directory) - return render_404 unless @entry.exists? + render_404 unless @entry.exists? end def file @@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController redirect_to namespace_project_build_path(project.namespace, project, build) end + def latest_succeeded + target_path = artifacts_action_path(@path, project, build) + + if target_path + redirect_to(target_path) + else + render_404 + end + end + private + def extract_ref_name_and_path + return unless params[:ref_name_and_path] + + @ref_name, @path = extract_ref(params[:ref_name_and_path]) + end + def validate_artifacts! - render_404 unless build.artifacts? + render_404 unless build && build.artifacts? end def build - @build ||= project.builds.find_by!(id: params[:build_id]) + @build ||= build_from_id || build_from_ref + end + + def build_from_id + project.builds.find_by(id: params[:build_id]) if params[:build_id] + end + + def build_from_ref + return unless @ref_name + + builds = project.latest_successful_builds_for(@ref_name) + builds.find_by(name: params[:job]) end def artifacts_file diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 1a4f6b50e8f..9404612a993 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -8,12 +8,15 @@ module Projects issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute issues = issues.page(params[:page]) - render json: issues.as_json( - only: [:iid, :title, :confidential], - include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } - }) + render json: { + issues: issues.as_json( + only: [:iid, :title, :confidential], + include: { + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + }), + size: issues.total_count + } end def update diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 12195c3cbb8..77934ff9962 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -78,8 +78,8 @@ class Projects::BuildsController < Projects::ApplicationController end def raw - if @build.has_trace? - send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline' + if @build.has_trace_file? + send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline' else render_404 end diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index b2e8733ccb7..d174e1145a7 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController end def module_enabled - render_404 unless @project.merge_requests_enabled + render_404 unless @project.feature_available?(:merge_requests, current_user) end end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index b5624046387..0ae8ff98009 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -59,6 +59,7 @@ class Projects::HooksController < Projects::ApplicationController :pipeline_events, :enable_ssl_verification, :issues_events, + :confidential_issues_events, :merge_requests_events, :note_events, :push_events, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7c03dcd2e64..72d2d361878 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -201,7 +201,7 @@ class Projects::IssuesController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.issues_enabled && @project.default_issues_tracker? + return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? end def redirect_to_external_issue_tracker diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 0ca675623e5..28fa4a5b141 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController protected def module_enabled - unless @project.issues_enabled || @project.merge_requests_enabled + unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) return render_404 end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4f5f3b6aa09..4f9ca0097a1 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -413,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.merge_requests_enabled + return render_404 unless @project.feature_available?(:merge_requests, current_user) end def validates_merge_request diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da2892bfb3f..ff63f22cb5b 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController end def module_enabled - unless @project.issues_enabled || @project.merge_requests_enabled + unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) return render_404 end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 6a227d85f6f..97e6e9471e0 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -20,9 +20,8 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params[:service]) redirect_to( - edit_namespace_project_service_path(@project.namespace, @project, - @service.to_param, notice: - 'Successfully updated.') + edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), + notice: 'Successfully updated.' ) else render 'edit' diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 6d0a7ee1031..17ceefec3b8 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.snippets_enabled + return render_404 unless @project.feature_available?(:snippets, current_user) end def snippet_params diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 8592579abbd..6ea8ee62bc5 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -1,4 +1,6 @@ class Projects::TagsController < Projects::ApplicationController + include SortingHelper + # Authorize before_action :require_non_empty_project before_action :authorize_download_code! @@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_admin_project!, only: [:destroy] def index - @sort = params[:sort] || 'name' - @tags = @repository.tags_sorted_by(@sort) + params[:sort] = params[:sort].presence || 'name' + + @sort = params[:sort] + @tags = TagsFinder.new(@repository, params).execute @tags = Kaminari.paginate_array(@tags).page(params[:page]) @releases = project.releases.where(tag: @tags.map(&:name)) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fc52cd2f367..eaa38fa6c98 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -303,13 +303,23 @@ class ProjectsController < Projects::ApplicationController end def project_params + project_feature_attributes = + { + project_feature_attributes: + [ + :issues_access_level, :builds_access_level, + :wiki_access_level, :merge_requests_access_level, :snippets_access_level + ] + } + params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, + :container_registry_enabled, :issues_tracker_id, :default_branch, - :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, - :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled + :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, + :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, + :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, + :lfs_enabled, project_feature_attributes ) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 33daac0399e..60996b181f2 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -64,7 +64,7 @@ class IssuableFinder if project? @project = Project.find(params[:project_id]) - unless Ability.abilities.allowed?(current_user, :read_project, @project) + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end else diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb new file mode 100644 index 00000000000..b474f0805dc --- /dev/null +++ b/app/finders/tags_finder.rb @@ -0,0 +1,29 @@ +class TagsFinder + def initialize(repository, params) + @repository = repository + @params = params + end + + def execute + tags = @repository.tags_sorted_by(sort) + filter_by_name(tags) + end + + private + + def sort + @params[:sort].presence + end + + def search + @params[:search].presence + end + + def filter_by_name(tags) + if search + tags.select { |tag| tag.name.include?(search) } + else + tags + end + end +end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 06b3e8a9502..a93a63bdb9b 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -83,7 +83,7 @@ class TodosFinder if project? @project = Project.find(params[:project_id]) - unless Ability.abilities.allowed?(current_user, :read_project, @project) + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f3733b01721..5f3765cad0d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,7 +110,7 @@ module ApplicationHelper project = event.project # Skip if project repo is empty or MR disabled - return false unless project && !project.empty_repo? && project.merge_requests_enabled + return false unless project && !project.empty_repo? && project.feature_available?(:merge_requests, current_user) # Skip if user already created appropriate MR return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index bb285a17baf..639deb7c521 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -25,6 +25,11 @@ module CiStatusHelper end end + def ci_status_for_statuseable(subject) + status = subject.try(:status) || 'not found' + status.humanize + end + def ci_icon_for_status(status) icon_name = case status @@ -41,7 +46,7 @@ module CiStatusHelper when 'play' 'icon_play' when 'created' - 'icon_status_pending' + 'icon_status_created' else 'icon_status_cancel' end @@ -66,10 +71,10 @@ module CiStatusHelper Ci::Runner.shared.blank? end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '') + def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body') klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" - data = { toggle: 'tooltip', placement: tooltip_placement } + data = { toggle: 'tooltip', placement: tooltip_placement, container: container } if path link_to ci_icon_for_status(status), path, diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index f1dc906cab4..aa54ee07bdc 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -3,7 +3,7 @@ module CompareHelper from.present? && to.present? && from != to && - project.merge_requests_enabled && + project.feature_available?(:merge_requests, current_user) && project.repository.branch_names.include?(from) && project.repository.branch_names.include?(to) end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 5386ddadc62..a322a90cc4e 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -149,4 +149,20 @@ module GitlabRoutingHelper def resend_invite_group_member_path(group_member, *args) resend_invite_group_group_member_path(group_member.source, group_member) end + + # Artifacts + + def artifacts_action_path(path, project, build) + action, path_params = path.split('/', 2) + args = [project.namespace, project, build, path_params] + + case action + when 'download' + download_namespace_project_build_artifacts_path(*args) + when 'browse' + browse_namespace_project_build_artifacts_path(*args) + when 'file' + file_namespace_project_build_artifacts_path(*args) + end + end end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 109bc1a02d1..021d2b14718 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -1,4 +1,9 @@ module ImportHelper + def import_project_target(owner, name) + namespace = current_user.can_create_group? ? owner : current_user.namespace_path + "#{namespace}/#{name}" + end + def github_project_link(path_with_namespace) link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index b9baeb1d6c4..5c04bba323f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -49,6 +49,19 @@ module IssuablesHelper end end + def project_dropdown_label(project_id, default_label) + return default_label if project_id.nil? + return "Any project" if project_id == "0" + + project = Project.find_by(id: project_id) + + if project + project.name_with_namespace + else + default_label + end + end + def milestone_dropdown_label(milestone_title, default_label = "Milestone") if milestone_title == Milestone::Upcoming.name milestone_title = Milestone::Upcoming.title diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index eb651e3687e..5d82abfca79 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -23,10 +23,14 @@ module LfsHelper end def lfs_download_access? + return false unless project.lfs_enabled? + project.public? || ci? || (user && user.can?(:download_code, project)) end def lfs_upload_access? + return false unless project.lfs_enabled? + user && user.can?(:push_code, project) end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index db6e731c744..a9e175c3f5c 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -98,6 +98,6 @@ module MergeRequestsHelper end def merge_request_button_visibility(merge_request, closed) - return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) + return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 6c1cc6ef072..2b0ff6c0d00 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -25,6 +25,8 @@ module NavHelper current_path?('merge_requests#commits') || current_path?('merge_requests#builds') || current_path?('merge_requests#conflicts') || + current_path?('merge_requests#pipelines') || + current_path?('issues#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 356f27f2d5d..4c685b97c03 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -61,7 +61,9 @@ module ProjectsHelper project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } if current_user - project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) + project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do + icon("chevron-down") + end end full_title = "#{namespace_link} / #{project_link}".html_safe @@ -187,6 +189,18 @@ module ProjectsHelper nav_tabs.flatten end + def project_lfs_status(project) + if project.lfs_enabled? + content_tag(:span, class: 'lfs-enabled') do + 'Enabled' + end + else + content_tag(:span, class: 'lfs-disabled') do + 'Disabled' + end + end + end + def git_user_name if current_user current_user.name @@ -400,4 +414,23 @@ module ProjectsHelper message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end + + def project_feature_options + { + 'Disabled' => ProjectFeature::DISABLED, + 'Only team members' => ProjectFeature::PRIVATE, + 'Everyone with access' => ProjectFeature::ENABLED + } + end + + def project_feature_access_select(field) + # Don't show option "everyone with access" if project is private + options = project_feature_options + level = @project.project_feature.public_send(field) + + options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED + + options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED) + content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index c0195713f4a..4549c2e5bb6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -44,7 +44,7 @@ module SearchHelper def help_autocomplete [ { category: "Help", label: "API Help", url: help_page_path("api/README") }, - { category: "Help", label: "Markdown Help", url: help_page_path("markdown/markdown") }, + { category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") }, { category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") }, { category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") }, { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") }, diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb index f8cccade15b..3d255df66a0 100644 --- a/app/helpers/sentry_helper.rb +++ b/app/helpers/sentry_helper.rb @@ -1,27 +1,9 @@ module SentryHelper def sentry_enabled? - Rails.env.production? && current_application_settings.sentry_enabled? + Gitlab::Sentry.enabled? end def sentry_context - return unless sentry_enabled? - - if current_user - Raven.user_context( - id: current_user.id, - email: current_user.email, - username: current_user.username, - ) - end - - Raven.tags_context(program: sentry_program_context) - end - - def sentry_program_context - if Sidekiq.server? - 'sidekiq' - else - 'rails' - end + Gitlab::Sentry.context(current_user) end end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 2dd0bf5d71e..3d4abf76419 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -8,7 +8,9 @@ module ServicesHelper when "note" "Event will be triggered when someone adds a comment" when "issue" - "Event will be triggered when an issue is created/updated/merged" + "Event will be triggered when an issue is created/updated/closed" + when "confidential_issue" + "Event will be triggered when a confidential issue is created/updated/closed" when "merge_request" "Event will be triggered when a merge request is created/updated/merged" when "build" @@ -19,7 +21,7 @@ module ServicesHelper end def service_event_field_name(event) - event = event.pluralize if %w[merge_request issue].include?(event) + event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index fb85544df2d..c0ec1634cdb 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -3,6 +3,16 @@ module TagsHelper "/tags/#{tag}" end + def filter_tags_path(options = {}) + exist_opts = { + search: params[:search], + sort: params[:sort] + } + + options = exist_opts.merge(options) + namespace_project_tags_path(@project.namespace, @project, @id, options) + end + def tag_list(project) html = '' project.tag_list.each do |tag| diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 0465327060e..1e86f648203 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -78,13 +78,11 @@ module TodosHelper end def todo_actions_options - actions = [ - OpenStruct.new(id: '', title: 'Any Action'), - OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'), - OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned') + [ + { id: '', text: 'Any Action' }, + { id: Todo::ASSIGNED, text: 'Assigned' }, + { id: Todo::MENTIONED, text: 'Mentioned' } ] - - options_from_collection_for_select(actions, 'id', 'title', params[:action_id]) end def todo_projects_options @@ -92,22 +90,28 @@ module TodosHelper projects = projects.includes(:namespace) projects = projects.map do |project| - OpenStruct.new(id: project.id, title: project.name_with_namespace) + { id: project.id, text: project.name_with_namespace } end - projects.unshift(OpenStruct.new(id: '', title: 'Any Project')) - - options_from_collection_for_select(projects, 'id', 'title', params[:project_id]) + projects.unshift({ id: '', text: 'Any Project' }).to_json end def todo_types_options - types = [ - OpenStruct.new(title: 'Any Type', name: ''), - OpenStruct.new(title: 'Issue', name: 'Issue'), - OpenStruct.new(title: 'Merge Request', name: 'MergeRequest') + [ + { id: '', text: 'Any Type' }, + { id: 'Issue', text: 'Issue' }, + { id: 'MergeRequest', text: 'Merge Request' } ] + end + + def todo_actions_dropdown_label(selected_action_id, default_action) + selected_action = todo_actions_options.find { |action| action[:id] == selected_action_id.to_i} + selected_action ? selected_action[:text] : default_action + end - options_from_collection_for_select(types, 'name', 'title', params[:type]) + def todo_types_dropdown_label(selected_type, default_type) + selected_type = todo_types_options.find { |type| type[:id] == selected_type && type[:id] != '' } + selected_type ? selected_type[:text] : default_type end private diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 8b83bbd93b7..61a574d3dc0 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base default reply_to: Proc.new { default_reply_to_address.format } def can? - Ability.abilities.allowed?(current_user, action, subject) + Ability.allowed?(current_user, action, subject) end private diff --git a/app/models/ability.rb b/app/models/ability.rb index 5293be33e5a..fa8f8bc3a5f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,34 +1,5 @@ class Ability class << self - # rubocop: disable Metrics/CyclomaticComplexity - def allowed(user, subject) - return anonymous_abilities(user, subject) if user.nil? - return [] unless user.is_a?(User) - return [] if user.blocked? - - abilities_by_subject_class(user: user, subject: subject) - end - - def abilities_by_subject_class(user:, subject:) - case subject - when CommitStatus then commit_status_abilities(user, subject) - when Project then project_abilities(user, subject) - when Issue then issue_abilities(user, subject) - when Note then note_abilities(user, subject) - when ProjectSnippet then project_snippet_abilities(user, subject) - when PersonalSnippet then personal_snippet_abilities(user, subject) - when MergeRequest then merge_request_abilities(user, subject) - when Group then group_abilities(user, subject) - when Namespace then namespace_abilities(user, subject) - when GroupMember then group_member_abilities(user, subject) - when ProjectMember then project_member_abilities(user, subject) - when User then user_abilities - when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project) - when Ci::Runner then runner_abilities(user, subject) - else [] - end.concat(global_abilities(user)) - end - # Given a list of users and a project this method returns the users that can # read the given project. def users_that_can_read_project(users, project) @@ -61,360 +32,7 @@ class Ability issues.select { |issue| issue.visible_to_user?(user) } end - # List of possible abilities for anonymous user - def anonymous_abilities(user, subject) - if subject.is_a?(PersonalSnippet) - anonymous_personal_snippet_abilities(subject) - elsif subject.is_a?(ProjectSnippet) - anonymous_project_snippet_abilities(subject) - elsif subject.is_a?(CommitStatus) - anonymous_commit_status_abilities(subject) - elsif subject.is_a?(Project) || subject.respond_to?(:project) - anonymous_project_abilities(subject) - elsif subject.is_a?(Group) || subject.respond_to?(:group) - anonymous_group_abilities(subject) - elsif subject.is_a?(User) - anonymous_user_abilities - else - [] - end - end - - def anonymous_project_abilities(subject) - project = if subject.is_a?(Project) - subject - else - subject.project - end - - if project && project.public? - rules = [ - :read_project, - :read_board, - :read_list, - :read_wiki, - :read_label, - :read_milestone, - :read_project_snippet, - :read_project_member, - :read_merge_request, - :read_note, - :read_pipeline, - :read_commit_status, - :read_container_image, - :download_code - ] - - # Allow to read builds by anonymous user if guests are allowed - rules << :read_build if project.public_builds? - - # Allow to read issues by anonymous user if issue is not confidential - rules << :read_issue unless subject.is_a?(Issue) && subject.confidential? - - rules - project_disabled_features_rules(project) - else - [] - end - end - - def anonymous_commit_status_abilities(subject) - rules = anonymous_project_abilities(subject.project) - # If subject is Ci::Build which inherits from CommitStatus filter the abilities - rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) - rules - end - - def anonymous_group_abilities(subject) - rules = [] - - group = if subject.is_a?(Group) - subject - else - subject.group - end - - rules << :read_group if group.public? - - rules - end - - def anonymous_personal_snippet_abilities(snippet) - if snippet.public? - [:read_personal_snippet] - else - [] - end - end - - def anonymous_project_snippet_abilities(snippet) - if snippet.public? - [:read_project_snippet] - else - [] - end - end - - def anonymous_user_abilities - [:read_user] unless restricted_public_level? - end - - def global_abilities(user) - rules = [] - rules << :create_group if user.can_create_group - rules << :read_users_list - rules - end - - def project_abilities(user, project) - key = "/user/#{user.id}/project/#{project.id}" - - if RequestStore.active? - RequestStore.store[key] ||= uncached_project_abilities(user, project) - else - uncached_project_abilities(user, project) - end - end - - def uncached_project_abilities(user, project) - rules = [] - # Push abilities on the users team role - rules.push(*project_team_rules(project.team, user)) - - owner = user.admin? || - project.owner == user || - (project.group && project.group.has_owner?(user)) - - if owner - rules.push(*project_owner_rules) - end - - if project.public? || (project.internal? && !user.external?) - rules.push(*public_project_rules) - - # Allow to read builds for internal projects - rules << :read_build if project.public_builds? - - unless owner || project.team.member?(user) || project_group_member?(project, user) - rules << :request_access if project.request_access_enabled - end - end - - if project.archived? - rules -= project_archived_rules - end - - (rules - project_disabled_features_rules(project)).uniq - end - - def project_team_rules(team, user) - # Rules based on role in project - if team.master?(user) - project_master_rules - elsif team.developer?(user) - project_dev_rules - elsif team.reporter?(user) - project_report_rules - elsif team.guest?(user) - project_guest_rules - else - [] - end - end - - def public_project_rules - @public_project_rules ||= project_guest_rules + [ - :download_code, - :fork_project, - :read_commit_status, - :read_pipeline, - :read_container_image - ] - end - - def project_guest_rules - @project_guest_rules ||= [ - :read_project, - :read_wiki, - :read_issue, - :read_board, - :read_list, - :read_label, - :read_milestone, - :read_project_snippet, - :read_project_member, - :read_merge_request, - :read_note, - :create_project, - :create_issue, - :create_note, - :upload_file - ] - end - - def project_report_rules - @project_report_rules ||= project_guest_rules + [ - :download_code, - :fork_project, - :create_project_snippet, - :update_issue, - :admin_issue, - :admin_label, - :admin_list, - :read_commit_status, - :read_build, - :read_container_image, - :read_pipeline, - :read_environment, - :read_deployment, - :read_cycle_analytics - ] - end - - def project_dev_rules - @project_dev_rules ||= project_report_rules + [ - :admin_merge_request, - :update_merge_request, - :create_commit_status, - :update_commit_status, - :create_build, - :update_build, - :create_pipeline, - :update_pipeline, - :create_merge_request, - :create_wiki, - :push_code, - :resolve_note, - :create_container_image, - :update_container_image, - :create_environment, - :create_deployment - ] - end - - def project_archived_rules - @project_archived_rules ||= [ - :create_merge_request, - :push_code, - :push_code_to_protected_branches, - :update_merge_request, - :admin_merge_request - ] - end - - def project_master_rules - @project_master_rules ||= project_dev_rules + [ - :push_code_to_protected_branches, - :update_project_snippet, - :update_environment, - :update_deployment, - :admin_milestone, - :admin_project_snippet, - :admin_project_member, - :admin_merge_request, - :admin_note, - :admin_wiki, - :admin_project, - :admin_commit_status, - :admin_build, - :admin_container_image, - :admin_pipeline, - :admin_environment, - :admin_deployment - ] - end - - def project_owner_rules - @project_owner_rules ||= project_master_rules + [ - :change_namespace, - :change_visibility_level, - :rename_project, - :remove_project, - :archive_project, - :remove_fork_project, - :destroy_merge_request, - :destroy_issue - ] - end - - def project_disabled_features_rules(project) - rules = [] - - unless project.issues_enabled - rules += named_abilities('issue') - end - - unless project.merge_requests_enabled - rules += named_abilities('merge_request') - end - - unless project.issues_enabled or project.merge_requests_enabled - rules += named_abilities('label') - rules += named_abilities('milestone') - end - - unless project.snippets_enabled - rules += named_abilities('project_snippet') - end - - unless project.wiki_enabled - rules += named_abilities('wiki') - end - - unless project.builds_enabled - rules += named_abilities('build') - rules += named_abilities('pipeline') - rules += named_abilities('environment') - rules += named_abilities('deployment') - end - - unless project.container_registry_enabled - rules += named_abilities('container_image') - end - - rules - end - - def group_abilities(user, group) - rules = [] - rules << :read_group if can_read_group?(user, group) - - owner = user.admin? || group.has_owner?(user) - master = owner || group.has_master?(user) - - # Only group masters and group owners can create new projects - if master - rules += [ - :create_projects, - :admin_milestones - ] - end - - # Only group owner and administrators can admin group - if owner - rules += [ - :admin_group, - :admin_namespace, - :admin_group_member, - :change_visibility_level - ] - end - - if group.public? || (group.internal? && !user.external?) - rules << :request_access if group.request_access_enabled && group.users.exclude?(user) - end - - rules.flatten - end - - def can_read_group?(user, group) - return true if user.admin? - return true if group.public? - return true if group.internal? && !user.external? - return true if group.users.include?(user) - - GroupProjectsFinder.new(group).execute(user).any? - end - + # TODO: make this private and use the actual abilities stuff for this def can_edit_note?(user, note) return false if !note.editable? || !user.present? return true if note.author == user || user.admin? @@ -427,207 +45,23 @@ class Ability end end - def namespace_abilities(user, namespace) - rules = [] - - # Only namespace owner and administrators can admin it - if namespace.owner == user || user.admin? - rules += [ - :create_projects, - :admin_namespace - ] - end - - rules.flatten - end - - [:issue, :merge_request].each do |name| - define_method "#{name}_abilities" do |user, subject| - rules = [] - - if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user) - rules += [ - :"read_#{name}", - :"update_#{name}", - ] - end - - rules += project_abilities(user, subject.project) - rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue) - rules - end - end - - def note_abilities(user, note) - rules = [] - - if note.author == user - rules += [ - :read_note, - :update_note, - :admin_note, - :resolve_note - ] - end - - if note.respond_to?(:project) && note.project - rules += project_abilities(user, note.project) - end - - if note.for_merge_request? && note.noteable.author == user - rules << :resolve_note - end - - rules - end - - def personal_snippet_abilities(user, snippet) - rules = [] - - if snippet.author == user - rules += [ - :read_personal_snippet, - :update_personal_snippet, - :admin_personal_snippet - ] - end - - if snippet.public? || (snippet.internal? && !user.external?) - rules << :read_personal_snippet - end - - rules + def allowed?(user, action, subject) + allowed(user, subject).include?(action) end - def project_snippet_abilities(user, snippet) - rules = [] - - if snippet.author == user || user.admin? - rules += [ - :read_project_snippet, - :update_project_snippet, - :admin_project_snippet - ] - end - - if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user)) - rules << :read_project_snippet - end - - rules - end - - def group_member_abilities(user, subject) - rules = [] - target_user = subject.user - group = subject.group - - unless group.last_owner?(target_user) - can_manage = group_abilities(user, group).include?(:admin_group_member) - - if can_manage - rules << :update_group_member - rules << :destroy_group_member - elsif user == target_user - rules << :destroy_group_member - end - end - - rules - end - - def project_member_abilities(user, subject) - rules = [] - target_user = subject.user - project = subject.project - - unless target_user == project.owner - can_manage = project_abilities(user, project).include?(:admin_project_member) - - if can_manage - rules << :update_project_member - rules << :destroy_project_member - elsif user == target_user - rules << :destroy_project_member - end - end - - rules - end - - def commit_status_abilities(user, subject) - rules = project_abilities(user, subject.project) - # If subject is Ci::Build which inherits from CommitStatus filter the abilities - rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) - rules - end - - def filter_build_abilities(rules) - # If we can't read build we should also not have that - # ability when looking at this in context of commit_status - %w(read create update admin).each do |rule| - rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build") - end - rules - end - - def runner_abilities(user, runner) - if user.is_admin? - [:assign_runner] - elsif runner.is_shared? || runner.locked? - [] - elsif user.ci_authorized_runners.include?(runner) - [:assign_runner] - else - [] - end - end + def allowed(user, subject) + return uncached_allowed(user, subject) unless RequestStore.active? - def user_abilities - [:read_user] - end - - def abilities - @abilities ||= begin - abilities = Six.new - abilities << self - abilities - end + user_key = user ? user.id : 'anonymous' + subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global' + key = "/ability/#{user_key}/#{subject_key}" + RequestStore[key] ||= uncached_allowed(user, subject).freeze end private - def restricted_public_level? - current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) - end - - def named_abilities(name) - [ - :"read_#{name}", - :"create_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end - - def filter_confidential_issues_abilities(user, issue, rules) - return rules if user.admin? || !issue.confidential? - - unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER) - rules.delete(:admin_issue) - rules.delete(:read_issue) - rules.delete(:update_issue) - end - - rules - end - - def project_group_member?(project, user) - project.group && - ( - project.group.members.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) - ) + def uncached_allowed(user, subject) + BasePolicy.class_for(subject).abilities(user, subject) end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 23c8de6f650..61052437318 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -208,22 +208,31 @@ module Ci end end + def has_trace_file? + File.exist?(path_to_trace) || has_old_trace_file? + end + def has_trace? raw_trace.present? end def raw_trace - if File.file?(path_to_trace) - File.read(path_to_trace) - elsif project.ci_id && File.file?(old_path_to_trace) - # Temporary fix for build trace data integrity - File.read(old_path_to_trace) + if File.exist?(trace_file_path) + File.read(trace_file_path) else # backward compatibility read_attribute :trace end end + ## + # Deprecated + # + # This is a hotfix for CI build data integrity, see #4246 + def has_old_trace_file? + project.ci_id && File.exist?(old_path_to_trace) + end + def trace trace = raw_trace if project && trace.present? && project.runners_token.present? @@ -262,6 +271,14 @@ module Ci end end + def trace_file_path + if has_old_trace_file? + old_path_to_trace + else + path_to_trace + end + end + def dir_to_trace File.join( Settings.gitlab_ci.builds_path, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 03812cd195f..bd1737c7587 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -65,8 +65,8 @@ module Ci end # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest_successful_for, ->(ref = default_branch) do - where(ref: ref).success.order(id: :desc).limit(1) + def self.latest_successful_for(ref) + where(ref: ref).order(id: :desc).success.first end def self.truncate_sha(sha) diff --git a/app/models/commit.rb b/app/models/commit.rb index 817d063e4a2..e64fd1e0c1b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -108,15 +108,6 @@ class Commit @diff_line_count end - # Returns a string describing the commit for use in a link title - # - # Example - # - # "Commit: Alex Denisov - Project git clone panel" - def link_title - "Commit: #{author_name} - #{title}" - end - # Returns the commits title. # # Usually, the commit title is the first line of the commit message. diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 630ee9601e0..656a242c265 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -4,12 +4,10 @@ # # range = CommitRange.new('f3f85602...e86e1013', project) # range.exclude_start? # => false -# range.reference_title # => "Commits f3f85602 through e86e1013" # range.to_s # => "f3f85602...e86e1013" # # range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range.exclude_start? # => true -# range.reference_title # => "Commits f3f85602^ through e86e1013" # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_s # => "f3f85602..e86e1013" # @@ -109,11 +107,6 @@ class CommitRange reference end - # Returns a String for use in a link's title attribute - def reference_title - "Commits #{sha_start} through #{sha_to}" - end - # Return a Hash of parameters for passing to a URL helper # # See `namespace_project_compare_url` diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 800a16ab246..d8d4575bb4d 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -2,7 +2,7 @@ module Awardable extend ActiveSupport::Concern included do - has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy + has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy if self < Participable # By default we always load award_emoji user association @@ -59,6 +59,18 @@ module Awardable true end + def awardable_votes?(name) + AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name + end + + def user_can_award?(current_user, name) + if user_authored?(current_user) + !awardable_votes?(normalize_name(name)) + else + true + end + end + def awarded_emoji?(emoji_name, current_user) award_emoji.where(name: emoji_name, user: current_user).exists? end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 8e11d4f57cf..22231b2e0f0 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -196,6 +196,10 @@ module Issuable end end + def user_authored?(user) + user == author + end + def subscribed_without_subscriptions?(user) participants(user).include?(user) end diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index a881fb83b7f..b8dd27a7afe 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -28,4 +28,8 @@ module NoteOnDiff def can_be_award_emoji? false end + + def to_discussion + Discussion.new([self]) + end end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb new file mode 100644 index 00000000000..9216122923e --- /dev/null +++ b/app/models/concerns/project_features_compatibility.rb @@ -0,0 +1,37 @@ +# Makes api V3 compatible with old project features permissions methods +# +# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled +# fields to a new table "project_features", support for the old fields is still needed in the API. + +module ProjectFeaturesCompatibility + extend ActiveSupport::Concern + + def wiki_enabled=(value) + write_feature_attribute(:wiki_access_level, value) + end + + def builds_enabled=(value) + write_feature_attribute(:builds_access_level, value) + end + + def merge_requests_enabled=(value) + write_feature_attribute(:merge_requests_access_level, value) + end + + def issues_enabled=(value) + write_feature_attribute(:issues_access_level, value) + end + + def snippets_enabled=(value) + write_feature_attribute(:snippets_access_level, value) + end + + private + + def write_feature_attribute(field, value) + build_project_feature unless project_feature + + access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + project_feature.update_attribute(field, access_level) + end +end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index df2a9e3e84b..a3ac577cf3e 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -52,11 +52,11 @@ module Taskable end # Return a string that describes the current state of this Taskable's task - # list items, e.g. "20 tasks (12 completed, 8 remaining)" + # list items, e.g. "12 of 20 tasks completed" def task_status return '' if description.blank? sum = tasks.summary - "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)" + "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed" end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index c8320ff87fa..4442cefc7e9 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -107,10 +107,6 @@ class DiffNote < Note self.noteable.find_diff_discussion(self.discussion_id) end - def to_discussion - Discussion.new([self]) - end - private def supported? diff --git a/app/models/event.rb b/app/models/event.rb index fd736d12359..a0b7b0dc2b5 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -65,7 +65,7 @@ class Event < ActiveRecord::Base elsif created_project? true elsif issue? || issue_note? - Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target) + Ability.allowed?(user, :read_issue, note? ? note_target : target) else ((merge_request? || note?) && target.present?) || milestone? end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 836a75b0608..c631e7a7df5 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -2,6 +2,7 @@ class ProjectHook < WebHook belongs_to :project scope :issue_hooks, -> { where(issues_events: true) } + scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) } scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :build_hooks, -> { where(build_events: true) } diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index f365dee3141..595602e80fe 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base default_value_for :push_events, true default_value_for :issues_events, false + default_value_for :confidential_issues_events, false default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 99a9f83cd50..8127c2cdd8d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -94,13 +94,13 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: [:allow_broken, :importing?] + validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches, unless: [:allow_broken, :importing?] - validate :validate_fork + validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] + validate :validate_fork, unless: :closed_without_fork? scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } @@ -243,12 +243,12 @@ class MergeRequest < ActiveRecord::Base def source_branch_head source_branch_ref = @source_branch_sha || source_branch - source_project.repository.commit(source_branch) if source_branch_ref + source_project.repository.commit(source_branch_ref) if source_branch_ref end def target_branch_head target_branch_ref = @target_branch_sha || target_branch - target_project.repository.commit(target_branch) if target_branch_ref + target_project.repository.commit(target_branch_ref) if target_branch_ref end def branch_merge_base_commit @@ -308,19 +308,22 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project + return true if target_project == source_project + return true unless forked_source_project_missing? - if target_project == source_project - true - else - # If source and target projects are different - # we should check if source project is actually a fork of target project - if source_project.forked_from?(target_project) - true - else - errors.add :validate_fork, - 'Source project is not a fork of target project' - end - end + errors.add :validate_fork, + 'Source project is not a fork of the target project' + end + + def closed_without_fork? + closed? && forked_source_project_missing? + end + + def forked_source_project_missing? + return false unless for_fork? + return true unless source_project + + !source_project.forked_from?(target_project) end def ensure_merge_request_diff @@ -411,7 +414,7 @@ class MergeRequest < ActiveRecord::Base def can_remove_source_branch?(current_user) !source_project.protected_branch?(source_branch) && !source_project.root_ref?(source_branch) && - Ability.abilities.allowed?(current_user, :push_code, source_project) && + Ability.allowed?(current_user, :push_code, source_project) && diff_head_commit == source_branch_head end @@ -729,7 +732,9 @@ class MergeRequest < ActiveRecord::Base end def pipeline - @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project + return unless diff_head_sha && source_project + + @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) end def all_pipelines diff --git a/app/models/note.rb b/app/models/note.rb index f2656df028b..b94e3cff2ce 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -223,6 +223,10 @@ class Note < ActiveRecord::Base end end + def user_authored?(user) + user == author + end + def award_emoji? can_be_award_emoji? && contains_emoji_only? end diff --git a/app/models/project.rb b/app/models/project.rb index 0e4fb94f8eb..a6de2c48071 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -11,24 +11,23 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include ProjectFeaturesCompatibility extend Gitlab::ConfigHelper UNKNOWN_IMPORT_URL = 'http://unknown.git' + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true + default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level - default_value_for :issues_enabled, gitlab_config_features.issues - default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests - default_value_for :builds_enabled, gitlab_config_features.builds - default_value_for :wiki_enabled, gitlab_config_features.wiki - default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } after_create :ensure_dir_exist after_save :ensure_dir_exist, if: :namespace_id_changed? + after_initialize :setup_project_feature # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -62,10 +61,10 @@ class Project < ActiveRecord::Base belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' belongs_to :namespace - has_one :board, dependent: :destroy - has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' + has_one :board, dependent: :destroy + # Project services has_many :services has_one :campfire_service, dependent: :destroy @@ -130,6 +129,7 @@ class Project < ActiveRecord::Base has_many :notification_settings, dependent: :destroy, as: :source has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + has_one :project_feature, dependent: :destroy has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id @@ -142,6 +142,7 @@ class Project < ActiveRecord::Base has_many :deployments, dependent: :destroy accepts_nested_attributes_for :variables, allow_destroy: true + accepts_nested_attributes_for :project_feature delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -159,8 +160,6 @@ class Project < ActiveRecord::Base length: { within: 0..255 }, format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } - validates :issues_enabled, :merge_requests_enabled, - :wiki_enabled, inclusion: { in: [true, false] } validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id @@ -196,6 +195,9 @@ class Project < ActiveRecord::Base scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } + scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') } + scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') } + scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } @@ -390,6 +392,13 @@ class Project < ActiveRecord::Base end end + def lfs_enabled? + return false unless Gitlab.config.lfs.enabled + return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil? + + self[:lfs_enabled] + end + def repository_storage_path Gitlab.config.repositories.storages[repository_storage] end @@ -436,7 +445,7 @@ class Project < ActiveRecord::Base # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) - latest_pipeline = pipelines.latest_successful_for(ref).first + latest_pipeline = pipelines.latest_successful_for(ref) if latest_pipeline latest_pipeline.builds.latest.with_artifacts @@ -680,6 +689,10 @@ class Project < ActiveRecord::Base update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) end + def has_wiki? + wiki_enabled? || has_external_wiki? + end + def external_wiki if has_external_wiki.nil? cache_has_external_wiki # Populate @@ -1096,16 +1109,21 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def pipeline(sha, ref) + def pipeline_for(ref, sha = nil) + sha ||= commit(ref).try(:sha) + + return unless sha + pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_pipeline(sha, ref, current_user = nil) - pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user) + def ensure_pipeline(ref, sha, current_user = nil) + pipeline_for(ref, sha) || + pipelines.create(sha: sha, ref: ref, user: current_user) end def enable_ci - self.builds_enabled = true + project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end def any_runners?(&block) @@ -1272,6 +1290,11 @@ class Project < ActiveRecord::Base private + # Prevents the creation of project_feature record for every project + def setup_project_feature + build_project_feature unless project_feature + end + def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb new file mode 100644 index 00000000000..9c602c582bd --- /dev/null +++ b/app/models/project_feature.rb @@ -0,0 +1,63 @@ +class ProjectFeature < ActiveRecord::Base + # == Project features permissions + # + # Grants access level to project tools + # + # Tools can be enabled only for users, everyone or disabled + # Access control is made only for non private projects + # + # levels: + # + # Disabled: not enabled for anyone + # Private: enabled only for team members + # Enabled: enabled for everyone able to access the project + # + + # Permision levels + DISABLED = 0 + PRIVATE = 10 + ENABLED = 20 + + FEATURES = %i(issues merge_requests wiki snippets builds) + + belongs_to :project + + def feature_available?(feature, user) + raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) + + get_permission(user, public_send("#{feature}_access_level")) + end + + def builds_enabled? + return true unless builds_access_level + + builds_access_level > DISABLED + end + + def wiki_enabled? + return true unless wiki_access_level + + wiki_access_level > DISABLED + end + + def merge_requests_enabled? + return true unless merge_requests_access_level + + merge_requests_access_level > DISABLED + end + + private + + def get_permission(user, level) + case level + when DISABLED + false + when PRIVATE + user && (project.team.member?(user) || user.admin?) + when ENABLED + true + else + true + end + end +end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index d7c986c1a91..afebd3b6a12 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -39,7 +39,7 @@ class HipchatService < Service end def supported_events - %w(push issue merge_request note tag_push build) + %w(push issue confidential_issue merge_request note tag_push build) end def execute(data) diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index abbc780dc1a..e6c943db2bf 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -44,7 +44,7 @@ class SlackService < Service end def supported_events - %w(push issue merge_request note tag_push build wiki_page) + %w(push issue confidential_issue merge_request note tag_push build wiki_page) end def execute(data) diff --git a/app/models/repository.rb b/app/models/repository.rb index 91bdafdac99..414b82516bc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -120,8 +120,21 @@ class Repository commits end - def find_branch(name) - raw_repository.branches.find { |branch| branch.name == name } + def find_branch(name, fresh_repo: true) + # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may + # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate + # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc) + # may cause the branch to "disappear" erroneously or have the wrong SHA. + # + # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392 + raw_repo = + if fresh_repo + Gitlab::Git::Repository.new(path_to_repo) + else + raw_repository + end + + raw_repo.find_branch(name) end def find_tag(name) @@ -136,7 +149,7 @@ class Repository return false unless target GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - rugged.branches.create(branch_name, target) + update_ref!(ref, target, oldrev) end after_create_branch @@ -168,7 +181,7 @@ class Repository ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do - rugged.branches.delete(branch_name) + update_ref!(ref, newrev, oldrev) end after_remove_branch @@ -202,6 +215,21 @@ class Repository rugged.references.exist?(ref) end + def update_ref!(name, newrev, oldrev) + # We use 'git update-ref' because libgit2/rugged currently does not + # offer 'compare and swap' ref updates. Without compare-and-swap we can + # (and have!) accidentally reset the ref to an earlier state, clobbering + # commits. See also https://github.com/libgit2/libgit2/issues/1534. + command = %w[git update-ref --stdin -z] + _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| + stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") + end + + return if status.zero? + + raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.") + end + # Makes sure a commit is kept around when Git garbage collection runs. # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for @@ -1001,15 +1029,10 @@ class Repository def commit_with_hooks(current_user, branch) update_autocrlf_option - oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch target_branch = find_branch(branch) was_empty = empty? - if !was_empty && target_branch - oldrev = target_branch.target.id - end - # Make commit newrev = yield(ref) @@ -1017,24 +1040,15 @@ class Repository raise CommitError.new('Failed to create commit') end + oldrev = rugged.lookup(newrev).parent_ids.first || Gitlab::Git::BLANK_SHA + GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do + update_ref!(ref, newrev, oldrev) + if was_empty || !target_branch - # Create branch - rugged.references.create(ref, newrev) - # If repo was empty expire cache after_create if was_empty after_create_branch - else - # Update head - current_head = find_branch(branch).target.id - - # Make sure target branch was not changed during pre-receive hook - if current_head == oldrev - rugged.references.update(ref, newrev) - else - raise CommitError.new('Commit was rejected because branch received new push') - end end end diff --git a/app/models/service.rb b/app/models/service.rb index 09b4717a523..198e7247838 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -7,6 +7,7 @@ class Service < ActiveRecord::Base default_value_for :active, false default_value_for :push_events, true default_value_for :issues_events, true + default_value_for :confidential_issues_events, true default_value_for :merge_requests_events, true default_value_for :tag_push_events, true default_value_for :note_events, true @@ -33,6 +34,7 @@ class Service < ActiveRecord::Base scope :push_hooks, -> { where(push_events: true, active: true) } scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } scope :issue_hooks, -> { where(issues_events: true, active: true) } + scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } @@ -100,7 +102,7 @@ class Service < ActiveRecord::Base end def supported_events - %w(push tag_push issue merge_request wiki_page) + %w(push tag_push issue confidential_issue merge_request wiki_page) end def execute(data) diff --git a/app/models/user.rb b/app/models/user.rb index ad3cfbc03e4..6996740eebd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -433,7 +433,7 @@ class User < ActiveRecord::Base # # This logic is duplicated from `Ability#project_abilities` into a SQL form. def projects_where_can_admin_issues - authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false) + authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled end def is_admin? @@ -460,16 +460,12 @@ class User < ActiveRecord::Base can?(:create_group, nil) end - def abilities - Ability.abilities - end - def can_select_namespace? several_namespaces? || admin end def can?(action, subject) - abilities.allowed?(self, action, subject) + Ability.allowed?(self, action, subject) end def first_name diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb new file mode 100644 index 00000000000..118c100ca11 --- /dev/null +++ b/app/policies/base_policy.rb @@ -0,0 +1,116 @@ +class BasePolicy + class RuleSet + attr_reader :can_set, :cannot_set + def initialize(can_set, cannot_set) + @can_set = can_set + @cannot_set = cannot_set + end + + def size + to_set.size + end + + def self.empty + new(Set.new, Set.new) + end + + def can?(ability) + @can_set.include?(ability) && !@cannot_set.include?(ability) + end + + def include?(ability) + can?(ability) + end + + def to_set + @can_set - @cannot_set + end + + def merge(other) + @can_set.merge(other.can_set) + @cannot_set.merge(other.cannot_set) + end + + def can!(*abilities) + @can_set.merge(abilities) + end + + def cannot!(*abilities) + @cannot_set.merge(abilities) + end + + def freeze + @can_set.freeze + @cannot_set.freeze + super + end + end + + def self.abilities(user, subject) + new(user, subject).abilities + end + + def self.class_for(subject) + return GlobalPolicy if subject.nil? + + subject.class.ancestors.each do |klass| + next unless klass.name + + begin + policy_class = "#{klass.name}Policy".constantize + + # NOTE: the < operator here tests whether policy_class + # inherits from BasePolicy + return policy_class if policy_class < BasePolicy + rescue NameError + nil + end + end + + raise "no policy for #{subject.class.name}" + end + + attr_reader :user, :subject + def initialize(user, subject) + @user = user + @subject = subject + end + + def abilities + return RuleSet.empty if @user && @user.blocked? + return anonymous_abilities if @user.nil? + collect_rules { rules } + end + + def anonymous_abilities + collect_rules { anonymous_rules } + end + + def anonymous_rules + rules + end + + def delegate!(new_subject) + @rule_set.merge(Ability.allowed(@user, new_subject)) + end + + def can?(rule) + @rule_set.can?(rule) + end + + def can!(*rules) + @rule_set.can!(*rules) + end + + def cannot!(*rules) + @rule_set.cannot!(*rules) + end + + private + + def collect_rules(&b) + @rule_set = RuleSet.empty + yield + @rule_set + end +end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb new file mode 100644 index 00000000000..2232e231cf8 --- /dev/null +++ b/app/policies/ci/build_policy.rb @@ -0,0 +1,13 @@ +module Ci + class BuildPolicy < CommitStatusPolicy + def rules + super + + # If we can't read build we should also not have that + # ability when looking at this in context of commit_status + %w(read create update admin).each do |rule| + cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" + end + end + end +end diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb new file mode 100644 index 00000000000..7edd383530d --- /dev/null +++ b/app/policies/ci/runner_policy.rb @@ -0,0 +1,13 @@ +module Ci + class RunnerPolicy < BasePolicy + def rules + return unless @user + + can! :assign_runner if @user.is_admin? + + return if @subject.is_shared? || @subject.locked? + + can! :assign_runner if @user.ci_authorized_runners.include?(@subject) + end + end +end diff --git a/app/policies/commit_status_policy.rb b/app/policies/commit_status_policy.rb new file mode 100644 index 00000000000..593df738328 --- /dev/null +++ b/app/policies/commit_status_policy.rb @@ -0,0 +1,5 @@ +class CommitStatusPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb new file mode 100644 index 00000000000..163d070ff90 --- /dev/null +++ b/app/policies/deployment_policy.rb @@ -0,0 +1,5 @@ +class DeploymentPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb new file mode 100644 index 00000000000..f4219569161 --- /dev/null +++ b/app/policies/environment_policy.rb @@ -0,0 +1,5 @@ +class EnvironmentPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/external_issue_policy.rb b/app/policies/external_issue_policy.rb new file mode 100644 index 00000000000..d9e28bd107a --- /dev/null +++ b/app/policies/external_issue_policy.rb @@ -0,0 +1,5 @@ +class ExternalIssuePolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb new file mode 100644 index 00000000000..3c2fbe6b56b --- /dev/null +++ b/app/policies/global_policy.rb @@ -0,0 +1,8 @@ +class GlobalPolicy < BasePolicy + def rules + return unless @user + + can! :create_group if @user.can_create_group + can! :read_users_list + end +end diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb new file mode 100644 index 00000000000..62335527654 --- /dev/null +++ b/app/policies/group_member_policy.rb @@ -0,0 +1,19 @@ +class GroupMemberPolicy < BasePolicy + def rules + return unless @user + + target_user = @subject.user + group = @subject.group + + return if group.last_owner?(target_user) + + can_manage = Ability.allowed?(@user, :admin_group_member, group) + + if can_manage + can! :update_group_member + can! :destroy_group_member + elsif @user == target_user + can! :destroy_group_member + end + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb new file mode 100644 index 00000000000..97ff6233968 --- /dev/null +++ b/app/policies/group_policy.rb @@ -0,0 +1,45 @@ +class GroupPolicy < BasePolicy + def rules + can! :read_group if @subject.public? + return unless @user + + globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) + member = @subject.users.include?(@user) + owner = @user.admin? || @subject.has_owner?(@user) + master = owner || @subject.has_master?(@user) + + can_read = false + can_read ||= globally_viewable + can_read ||= member + can_read ||= @user.admin? + can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any? + can! :read_group if can_read + + # Only group masters and group owners can create new projects + if master + can! :create_projects + can! :admin_milestones + end + + # Only group owner and administrators can admin group + if owner + can! :admin_group + can! :admin_namespace + can! :admin_group_member + can! :change_visibility_level + end + + if globally_viewable && @subject.request_access_enabled && !member + can! :request_access + end + end + + def can_read_group? + return true if @subject.public? + return true if @user.admin? + return true if @subject.internal? && !@user.external? + return true if @subject.users.include?(@user) + + GroupProjectsFinder.new(@subject).execute(@user).any? + end +end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb new file mode 100644 index 00000000000..c253f9a9399 --- /dev/null +++ b/app/policies/issuable_policy.rb @@ -0,0 +1,14 @@ +class IssuablePolicy < BasePolicy + def action_name + @subject.class.name.underscore + end + + def rules + if @user && (@subject.author == @user || @subject.assignee == @user) + can! :"read_#{action_name}" + can! :"update_#{action_name}" + end + + delegate! @subject.project + end +end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb new file mode 100644 index 00000000000..bd1811a3c54 --- /dev/null +++ b/app/policies/issue_policy.rb @@ -0,0 +1,28 @@ +class IssuePolicy < IssuablePolicy + def issue + @subject + end + + def rules + super + + if @subject.confidential? && !can_read_confidential? + cannot! :read_issue + cannot! :admin_issue + cannot! :update_issue + cannot! :read_issue + end + end + + private + + def can_read_confidential? + return false unless @user + return true if @user.admin? + return true if @subject.author == @user + return true if @subject.assignee == @user + return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER) + + false + end +end diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb new file mode 100644 index 00000000000..bc3afc626fb --- /dev/null +++ b/app/policies/merge_request_policy.rb @@ -0,0 +1,3 @@ +class MergeRequestPolicy < IssuablePolicy + # pass +end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb new file mode 100644 index 00000000000..29bb357e00a --- /dev/null +++ b/app/policies/namespace_policy.rb @@ -0,0 +1,10 @@ +class NamespacePolicy < BasePolicy + def rules + return unless @user + + if @subject.owner == @user || @user.admin? + can! :create_projects + can! :admin_namespace + end + end +end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb new file mode 100644 index 00000000000..83847466ee2 --- /dev/null +++ b/app/policies/note_policy.rb @@ -0,0 +1,19 @@ +class NotePolicy < BasePolicy + def rules + delegate! @subject.project + + return unless @user + + if @subject.author == @user + can! :read_note + can! :update_note + can! :admin_note + can! :resolve_note + end + + if @subject.for_merge_request? && + @subject.noteable.author == @user + can! :resolve_note + end + end +end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb new file mode 100644 index 00000000000..46c5aa1a5be --- /dev/null +++ b/app/policies/personal_snippet_policy.rb @@ -0,0 +1,16 @@ +class PersonalSnippetPolicy < BasePolicy + def rules + can! :read_personal_snippet if @subject.public? + return unless @user + + if @subject.author == @user + can! :read_personal_snippet + can! :update_personal_snippet + can! :admin_personal_snippet + end + + if @subject.internal? && !@user.external? + can! :read_personal_snippet + end + end +end diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb new file mode 100644 index 00000000000..1c038dddd4b --- /dev/null +++ b/app/policies/project_member_policy.rb @@ -0,0 +1,22 @@ +class ProjectMemberPolicy < BasePolicy + def rules + # anonymous users have no abilities here + return unless @user + + target_user = @subject.user + project = @subject.project + + return if target_user == project.owner + + can_manage = Ability.allowed?(@user, :admin_project_member, project) + + if can_manage + can! :update_project_member + can! :destroy_project_member + end + + if @user == target_user + can! :destroy_project_member + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb new file mode 100644 index 00000000000..acf36d422d1 --- /dev/null +++ b/app/policies/project_policy.rb @@ -0,0 +1,224 @@ +class ProjectPolicy < BasePolicy + def rules + team_access!(user) + + owner = user.admin? || + project.owner == user || + (project.group && project.group.has_owner?(user)) + + owner_access! if owner + + if project.public? || (project.internal? && !user.external?) + guest_access! + public_access! + + # Allow to read builds for internal projects + can! :read_build if project.public_builds? + + if project.request_access_enabled && + !(owner || project.team.member?(user) || project_group_member?(user)) + can! :request_access + end + end + + archived_access! if project.archived? + + disabled_features! + end + + def project + @subject + end + + def guest_access! + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_issue + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :create_project + can! :create_issue + can! :create_note + can! :upload_file + end + + def reporter_access! + can! :download_code + can! :fork_project + can! :create_project_snippet + can! :update_issue + can! :admin_issue + can! :admin_label + can! :admin_list + can! :read_commit_status + can! :read_build + can! :read_container_image + can! :read_pipeline + can! :read_environment + can! :read_deployment + end + + def developer_access! + can! :admin_merge_request + can! :update_merge_request + can! :create_commit_status + can! :update_commit_status + can! :create_build + can! :update_build + can! :create_pipeline + can! :update_pipeline + can! :create_merge_request + can! :create_wiki + can! :push_code + can! :resolve_note + can! :create_container_image + can! :update_container_image + can! :create_environment + can! :create_deployment + end + + def master_access! + can! :push_code_to_protected_branches + can! :update_project_snippet + can! :update_environment + can! :update_deployment + can! :admin_milestone + can! :admin_project_snippet + can! :admin_project_member + can! :admin_merge_request + can! :admin_note + can! :admin_wiki + can! :admin_project + can! :admin_commit_status + can! :admin_build + can! :admin_container_image + can! :admin_pipeline + can! :admin_environment + can! :admin_deployment + end + + def public_access! + can! :download_code + can! :fork_project + can! :read_commit_status + can! :read_pipeline + can! :read_container_image + end + + def owner_access! + guest_access! + reporter_access! + developer_access! + master_access! + can! :change_namespace + can! :change_visibility_level + can! :rename_project + can! :remove_project + can! :archive_project + can! :remove_fork_project + can! :destroy_merge_request + can! :destroy_issue + end + + # Push abilities on the users team role + def team_access!(user) + access = project.team.max_member_access(user.id) + + guest_access! if access >= Gitlab::Access::GUEST + reporter_access! if access >= Gitlab::Access::REPORTER + developer_access! if access >= Gitlab::Access::DEVELOPER + master_access! if access >= Gitlab::Access::MASTER + end + + def archived_access! + cannot! :create_merge_request + cannot! :push_code + cannot! :push_code_to_protected_branches + cannot! :update_merge_request + cannot! :admin_merge_request + end + + def disabled_features! + unless project.feature_available?(:issues, user) + cannot!(*named_abilities(:issue)) + end + + unless project.feature_available?(:merge_requests, user) + cannot!(*named_abilities(:merge_request)) + end + + unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user) + cannot!(*named_abilities(:label)) + cannot!(*named_abilities(:milestone)) + end + + unless project.feature_available?(:snippets, user) + cannot!(*named_abilities(:project_snippet)) + end + + unless project.feature_available?(:wiki, user) || project.has_external_wiki? + cannot!(*named_abilities(:wiki)) + end + + unless project.feature_available?(:builds, user) + cannot!(*named_abilities(:build)) + cannot!(*named_abilities(:pipeline)) + cannot!(*named_abilities(:environment)) + cannot!(*named_abilities(:deployment)) + end + + unless project.container_registry_enabled + cannot!(*named_abilities(:container_image)) + end + end + + def anonymous_rules + return unless project.public? + + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :read_pipeline + can! :read_commit_status + can! :read_container_image + can! :download_code + + # NOTE: may be overridden by IssuePolicy + can! :read_issue + + # Allow to read builds by anonymous user if guests are allowed + can! :read_build if project.public_builds? + + disabled_features! + end + + def project_group_member?(user) + project.group && + ( + project.group.members.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) + end + + def named_abilities(name) + [ + :"read_#{name}", + :"create_#{name}", + :"update_#{name}", + :"admin_#{name}" + ] + end +end diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb new file mode 100644 index 00000000000..57acccfafd9 --- /dev/null +++ b/app/policies/project_snippet_policy.rb @@ -0,0 +1,20 @@ +class ProjectSnippetPolicy < BasePolicy + def rules + can! :read_project_snippet if @subject.public? + return unless @user + + if @user && @subject.author == @user || @user.admin? + can! :read_project_snippet + can! :update_project_snippet + can! :admin_project_snippet + end + + if @subject.internal? && !@user.external? + can! :read_project_snippet + end + + if @subject.private? && @subject.project.team.member?(@user) + can! :read_project_snippet + end + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000000..03a2499e263 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,11 @@ +class UserPolicy < BasePolicy + include Gitlab::CurrentSettings + + def rules + can! :read_user if @user || !restricted_public_level? + end + + def restricted_public_level? + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end +end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 0d55ba5a981..0c208150fb8 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -7,12 +7,8 @@ class BaseService @project, @current_user, @params = project, user, params.dup end - def abilities - Ability.abilities - end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end def notification_service diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index 5cb408b9d20..b1887820bd4 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -3,7 +3,10 @@ module Boards class CreateService < Boards::BaseService def execute List.transaction do - create_list_at(next_position) + label = project.labels.find(params[:label_id]) + position = next_position + + create_list(label, position) end end @@ -14,8 +17,8 @@ module Boards max_position.nil? ? 0 : max_position.succ end - def create_list_at(position) - board.lists.create(params.merge(list_type: :label, position: position)) + def create_list(label, position) + board.lists.create(label: label, list_type: :label, position: position) end end end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index f049ed628db..de48a50774e 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -10,13 +10,15 @@ module Ci create_builds! end - new_builds = - stage_indexes_of_created_builds.map do |index| - process_stage(index) - end + @pipeline.with_lock do + new_builds = + stage_indexes_of_created_builds.map do |index| + process_stage(index) + end - # Return a flag if a when builds got enqueued - new_builds.flatten.any? + # Return a flag if a when builds got enqueued + new_builds.flatten.any? + end end private diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 9a187f5d694..6973191b203 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -8,16 +8,18 @@ module Ci builds = if current_runner.shared? builds. - # don't run projects which have not enabled shared runners - joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }). + # don't run projects which have not enabled shared runners and builds + joins(:project).where(projects: { shared_runners_enabled: true }). + joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id'). # this returns builds that are ordered by number of running builds # we prefer projects that don't use shared runners at all joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). + where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') else # do run projects which are only assigned to this runner (FIFO) - builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC') + builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC') end build = builds.find do |build| diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb deleted file mode 100644 index 92e6df442b4..00000000000 --- a/app/services/ci/web_hook_service.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Ci - class WebHookService - def build_end(build) - execute_hooks(build.project, build_data(build)) - end - - def execute_hooks(project, data) - project.web_hooks.each do |web_hook| - async_execute_hook(web_hook, data) - end - end - - def async_execute_hook(hook, data) - Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data) - end - - def build_data(build) - project = build.project - data = {} - data.merge!({ - build_id: build.id, - build_name: build.name, - build_status: build.status, - build_started_at: build.started_at, - build_finished_at: build.finished_at, - project_id: project.id, - project_name: project.name, - gitlab_url: project.gitlab_url, - ref: build.ref, - before_sha: build.before_sha, - sha: build.sha, - }) - end - end -end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e06c37c323e..4c8d93999a7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -45,6 +45,7 @@ class IssuableBaseService < BaseService unless can?(current_user, ability, project) params.delete(:milestone_id) + params.delete(:labels) params.delete(:add_label_ids) params.delete(:remove_label_ids) params.delete(:label_ids) @@ -72,6 +73,7 @@ class IssuableBaseService < BaseService filter_labels_in_param(:add_label_ids) filter_labels_in_param(:remove_label_ids) filter_labels_in_param(:label_ids) + find_or_create_label_ids end def filter_labels_in_param(key) @@ -80,6 +82,17 @@ class IssuableBaseService < BaseService params[key] = project.labels.where(id: params[key]).pluck(:id) end + def find_or_create_label_ids + labels = params.delete(:labels) + return unless labels + + params[:label_ids] = labels.split(",").map do |label_name| + project.labels.create_with(color: Label::DEFAULT_COLOR) + .find_or_create_by(title: label_name.strip) + .id + end + end + def process_label_ids(attributes, existing_label_ids: nil) label_ids = attributes.delete(:label_ids) add_label_ids = attributes.delete(:add_label_ids) @@ -162,7 +175,12 @@ class IssuableBaseService < BaseService if params.present? && update_issuable(issuable, params) issuable.reset_events_cache - handle_common_system_notes(issuable, old_labels: old_labels) + + # We do not touch as it will affect a update on updated_at field + ActiveRecord::Base.no_touching do + handle_common_system_notes(issuable, old_labels: old_labels) + end + handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 089b0f527e2..9ea3ce084ba 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -14,9 +14,10 @@ module Issues end def execute_hooks(issue, action = 'open') - issue_data = hook_data(issue, action) - issue.project.execute_hooks(issue_data, :issue_hooks) - issue.project.execute_services(issue_data, :issue_hooks) + issue_data = hook_data(issue, action) + hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks + issue.project.execute_hooks(issue_data, hooks_scope) + issue.project.execute_services(issue_data, hooks_scope) end end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 290742f1506..e57791f6818 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -83,7 +83,7 @@ module MergeRequests closes_issue = "Closes ##{iid}" if merge_request.description.present? - merge_request.description += closes_issue.prepend("\n") + merge_request.description += closes_issue.prepend("\n\n") else merge_request.description = closes_issue end diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 08c1f72d65a..1262ecbc29a 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -31,7 +31,7 @@ module MergeRequests def get_branches(changes) return [] if project.empty_repo? - return [] unless project.merge_requests_enabled + return [] unless project.merge_requests_enabled? changes_list = Gitlab::ChangesList.new(changes) changes_list.map do |change| diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index adc71b0c2bc..19caa038c44 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,11 +1,14 @@ module MergeRequests class ResolveService < MergeRequests::BaseService - attr_accessor :conflicts, :rugged, :merge_index + attr_accessor :conflicts, :rugged, :merge_index, :merge_request def execute(merge_request) @conflicts = merge_request.conflicts @rugged = project.repository.rugged @merge_index = conflicts.merge_index + @merge_request = merge_request + + fetch_their_commit! conflicts.files.each do |file| write_resolved_file_to_index(file, params[:sections]) @@ -27,5 +30,21 @@ module MergeRequests merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) merge_index.conflict_remove(our_path) end + + # If their commit (in the target project) doesn't exist in the source project, it + # can't be a parent for the merge commit we're about to create. If that's the case, + # fetch the target branch ref into the source project so the commit exists in both. + # + def fetch_their_commit! + return if rugged.include?(conflicts.their_commit.oid) + + random_string = SecureRandom.hex + + project.repository.fetch_ref( + merge_request.target_project.repository.path_to_repo, + "refs/heads/#{merge_request.target_branch}", + "refs/tmp/#{random_string}/head" + ) + end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 30c5f24988c..398ec47f0ea 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,6 +11,10 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + if merge_request.closed_without_fork? + params.except!(:target_branch, :force_remove_source_branch) + end + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) update(merge_request) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 55956be2844..be749ba4a1c 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -7,7 +7,6 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) import_data = params.delete(:import_data) - @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level @@ -81,8 +80,7 @@ module Projects log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") unless @project.gitlab_project_import? - @project.create_wiki if @project.wiki_enabled? - + @project.create_wiki if @project.feature_available?(:wiki, current_user) @project.build_missing_services @project.create_labels diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index de6dc38cc8e..a2de4dccece 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -8,7 +8,6 @@ module Projects name: @project.name, path: @project.path, shared_runners_enabled: @project.shared_runners_enabled, - builds_enabled: @project.builds_enabled, namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } @@ -17,6 +16,9 @@ module Projects end new_project = CreateService.new(current_user, new_params).execute + builds_access_level = @project.project_feature.builds_access_level + new_project.project_feature.update_attributes(builds_access_level: builds_access_level) + new_project end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 92e2dae4842..9175b3d3f96 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -13,7 +13,7 @@ .col-sm-10 = f.text_area :description, class: "form-control", rows: 10 .hint - Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown/markdown'), target: '_blank'}. + Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}. .form-group = f.label :logo, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml index 89d7a40d6b0..107fc25244a 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/background_jobs/_head.html.haml @@ -1,22 +1,24 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - = nav_link(controller: :system_info) do - = link_to admin_system_info_path, title: 'System Info' do - %span - System Info - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %span - Background Jobs - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - %span - Health Check - = nav_link(controller: :requests_profiles) do - = link_to admin_requests_profiles_path, title: 'Requests Profiles' do - %span - Requests Profiles +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index b74da64f82e..c91ab4cb946 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -1,26 +1,28 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - %span - Overview - = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do - %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %span - Groups - = nav_link path: 'builds#index' do - = link_to admin_builds_path, title: 'Builds' do - %span - Builds - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Overview + = nav_link(controller: [:admin, :projects]) do + = link_to admin_namespaces_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'builds#index' do + = link_to admin_builds_path, title: 'Builds' do + %span + Builds + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index b2c607361b3..6c7c3c48604 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -73,6 +73,12 @@ %span.light last commit: %strong = last_commit(@project) + + %li + %span.light Git LFS status: + %strong + = project_lfs_status(@project) + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - else %li %span.light repository: diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 0044d779c31..889086c62b1 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,3 +1,6 @@ +- page_title "CI Lint" +- page_description "Validate your GitLab CI configuration file" + %h2 Check your .gitlab-ci.yml %hr diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index d320d3bcc1e..9d31f31c639 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -28,21 +28,25 @@ .row-content-block.second-block = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do .filter-item.inline - = select_tag('project_id', todo_projects_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Project'}) + - if params[:project_id].present? + = hidden_field_tag(:project_id, params[:project_id]) + = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', + placeholder: 'Search projects', data: { data: todo_projects_options } }) .filter-item.inline - = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) + - if params[:author_id].present? + = hidden_field_tag(:author_id, params[:author_id]) + = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', + placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } }) .filter-item.inline - = select_tag('type', todo_types_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Type'}) + - if params[:type].present? + = hidden_field_tag(:type, params[:type]) + = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', + data: { data: todo_types_options } }) .filter-item.inline.actions-filter - = select_tag('action_id', todo_actions_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Action'}) - + - if params[:action_id].present? + = hidden_field_tag(:action_id, params[:action_id]) + = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', + data: { data: todo_actions_options }}) .pull-right .dropdown.inline.prepend-left-10 %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -66,7 +70,7 @@ - if @todos.any? .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - @todos.group_by(&:project).each do |group| - .panel.panel-default.panel-small.js-todos-list + .panel.panel-default.panel-small - project = group[0] .panel-heading = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) @@ -76,11 +80,3 @@ = paginate @todos, theme: "gitlab" - else .nothing-here-block You're all done! - -:javascript - new UsersSelect(); - - $('form.filter-form').on('submit', function (event) { - event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); - }); diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 5c318cd3b8b..31fdcc5e21b 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,7 +1,7 @@ - if event.visible_to_user?(current_user) .event-item{ class: event_row_class(event) } .event-item-timestamp - #{time_ago_with_tooltip(event.created_at)} + #{time_ago_with_tooltip(event.created_at, skip_js: true)} = cache [event, current_application_settings, "v2.2"] do = author_avatar(event, size: 40) diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index 742f9d7a433..3be7ed8432c 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,3 +1,3 @@ :plain $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); - new MemberExpirationDate(); + new gl.MemberExpirationDate(); diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 85e188d6f8b..d16bd61b779 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -549,4 +549,4 @@ %li wiki page %li help page - You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown/markdown")}. + You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("user/markdown")}. diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index 804ad88468f..8e929538351 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -1,23 +1,4 @@ -- if @already_been_taken - :plain - tr = $("tr#repo_#{@repo_id}") - target_field = tr.find(".import-target") - import_button = tr.find(".btn-import") - origin_target = target_field.text() - project_name = "#{@project_name}" - origin_namespace = "#{@target_namespace}" - target_field.empty() - target_field.append("<p class='alert alert-danger'>This namespace already been taken! Please choose another one</p>") - target_field.append("<input type='text' name='target_namespace' />") - target_field.append("/" + project_name) - target_field.data("project_name", project_name) - target_field.find('input').prop("value", origin_namespace) - import_button.enable().removeClass('is-loading') -- elsif @access_denied - :plain - job = $("tr#repo_#{@repo_id}") - job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") -- elsif @project.persisted? +- if @project.persisted? :plain job = $("tr#repo_#{@repo_id}") job.attr("id", "project_#{@project.id}") diff --git a/app/views/import/base/unauthorized.js.haml b/app/views/import/base/unauthorized.js.haml new file mode 100644 index 00000000000..36f8069c1f7 --- /dev/null +++ b/app/views/import/base/unauthorized.js.haml @@ -0,0 +1,14 @@ +:plain + tr = $("tr#repo_#{@repo_id}") + target_field = tr.find(".import-target") + import_button = tr.find(".btn-import") + origin_target = target_field.text() + project_name = "#{@project_name}" + origin_namespace = "#{@target_namespace.path}" + target_field.empty() + target_field.append("<p class='alert alert-danger'>This namespace has already been taken! Please choose another one.</p>") + target_field.append("<input type='text' name='target_namespace' />") + target_field.append("/" + project_name) + target_field.data("project_name", project_name) + target_field.find('input').prop("value", origin_namespace) + import_button.enable().removeClass('is-loading') diff --git a/app/views/import/bitbucket/deploy_key.js.haml b/app/views/import/bitbucket/deploy_key.js.haml new file mode 100644 index 00000000000..81b34ab5c9d --- /dev/null +++ b/app/views/import/bitbucket/deploy_key.js.haml @@ -0,0 +1,3 @@ +:plain + job = $("tr#repo_#{@repo_id}") + job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 15dd98077c8..f8b4b107513 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -51,7 +51,7 @@ %td = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" %td.import-target - = "#{repo["owner"]}/#{repo["slug"]}" + = import_project_target(repo['owner'], repo['slug']) %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 54ff1d27c67..bd3be20c4f8 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -45,7 +45,7 @@ %td = github_project_link(repo.full_name) %td.import-target - = repo.full_name + = import_project_target(repo.owner.login, repo.name) %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index fcfc6fd37f4..d31fc2e6adb 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -45,7 +45,7 @@ %td = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" %td.import-target - = repo["path_with_namespace"] + = import_project_target(repo['namespace']['path'], repo['name']) %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index d7d36c84b6c..27ac1760166 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,5 +1,5 @@ += render 'layouts/nav/group_settings' .scrolling-tabs-container{ class: nav_control_class } - = render 'layouts/nav/group_settings' .fade-left = icon('angle-left') .fade-right diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index bf9a7ecb786..75275afc0f3 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,22 +1,26 @@ - if current_user + - can_admin_group = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group) - member = @group.members.find_by(user_id: current_user.id) - can_leave = member && can?(current_user, :destroy_group_member, member) - .controls - .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - = nav_link(path: 'groups#projects') do - = link_to 'Projects', projects_group_path(@group), title: 'Projects' - %li.divider - - if can_edit - %li - = link_to 'Edit Group', edit_group_path(@group) - - if can_leave - %li - = link_to polymorphic_path([:leave, @group, :members]), - data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do - Leave Group + - if can_admin_group || can_edit || can_leave + .controls + .dropdown.group-settings-dropdown + %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + - if can_admin_group + = nav_link(path: 'groups#projects') do + = link_to 'Projects', projects_group_path(@group), title: 'Projects' + - if can_edit || can_leave + %li.divider + - if can_edit + %li + = link_to 'Edit Group', edit_group_path(@group) + - if can_leave + %li + = link_to polymorphic_path([:leave, @group, :members]), + data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do + Leave Group diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 52a5bdc1a1b..613b8b7d301 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -26,7 +26,7 @@ %span Protected Branches - - if @project.builds_enabled? + - if @project.feature_available?(:builds, current_user) = nav_link(controller: :runners) do = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do %span diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 19b4249374b..80053dd501b 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,11 +1,14 @@ -%fieldset.builds-feature - %h5.prepend-top-0 - Merge Requests - .form-group - .checkbox - = f.label :only_allow_merge_if_build_succeeds do - = f.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds - .help-block - Builds need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') +.merge-requests-feature + %fieldset.builds-feature + %hr + %h5.prepend-top-0 + Merge Requests + .form-group + .checkbox + = f.label :only_allow_merge_if_build_succeeds do + = f.check_box :only_allow_merge_if_build_succeeds + %strong Only allow merge requests to be merged if the build succeeds + %br + %span.descr + Builds need to be configured to enable this feature. + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index de53a298f84..73066150fb3 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -13,19 +13,13 @@ %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } {{ list.title }} %span.pull-right{ "v-if" => "list.type !== 'blank'" } - {{ list.issues.length }} + {{ list.issuesSize }} - if can?(current_user, :admin_list, @project) %board-delete{ "inline-template" => true, ":list" => "list", "v-if" => "!list.preset && list.id" } %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } = icon("trash") - = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore") - .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" } - %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" } - = icon("search", class: "board-search-icon", "v-show" => "!query") - %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" } - = icon("times", class: "board-search-clear") %board-list{ "inline-template" => true, "v-if" => "list.type !== 'blank'", ":list" => "list", @@ -39,5 +33,11 @@ "v-show" => "!loading", ":data-board" => "list.id" } = render "projects/boards/components/card" + %li.board-list-count.text-center{ "v-if" => "showCount" } + = icon("spinner spin", "v-show" => "list.loadingMore" ) + %span{ "v-if" => "list.issues.length === list.issuesSize" } + Showing all issues + %span{ "v-else" => true } + Showing {{ list.issues.length }} of {{ list.issuesSize }} issues - if can?(current_user, :admin_list, @project) = render "projects/boards/components/blank_state" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 6192ccb710b..808e6b95746 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,6 +27,8 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare + = render 'projects/buttons/download', project: @project, ref: branch.name + - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 5b0b58e087b..56306b05934 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,3 +1,6 @@ +- builds = @build.pipeline.builds.latest.to_a +- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"] + %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default Build @@ -11,40 +14,6 @@ %p.build-detail-row #{@build.coverage}% - - builds = @build.pipeline.builds.latest.to_a - - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"] - - if builds.size > 1 - .dropdown.build-dropdown - .build-light-text Stage - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} - %span.stage-selection More - = icon('caret-down') - %ul.dropdown-menu - - builds.map(&:stage).uniq.each do |stage| - %li - %a.stage-item= stage - - .builds-container - - statuses.each do |build_status| - - builds.select{|build| build.status == build_status}.each do |build| - .build-job{class: ('active' if build == @build), data: {stage: build.stage}} - = link_to namespace_project_build_path(@project.namespace, @project, build) do - = icon('check') - = ci_icon_for_status(build.status) - %span - - if build.name - = build.name - - else - = build.id - - - if @build.retried? - %li.active - %a - Build ##{@build.id} - · - %i.fa.fa-warning - This build was retried. - .blocks-container - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block{ class: ("block-first" if !@build.coverage) } @@ -76,7 +45,7 @@ .title Build details - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post + = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post - if @build.merge_request %p.build-detail-row %span.build-light-text Merge Request: @@ -100,7 +69,7 @@ - elsif @build.runner \##{@build.runner.id} .btn-group.btn-group-justified{ role: :group } - - if @build.has_trace? + - if @build.has_trace_file? = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' - if @build.active? = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post @@ -121,12 +90,13 @@ - if @build.trigger_request.variables %p - %span.build-light-text Variables: + %button.btn.group.btn-group-justified.reveal-variables Reveal Variables - @build.trigger_request.variables.each do |key, value| - %code - #{key}=#{value} + .hide.js-build + .js-build-variable= key + .js-build-value= value .block .title @@ -141,3 +111,35 @@ - @build.tag_list.each do |tag| %span.label.label-primary = tag + + - if builds.size > 1 + .dropdown.build-dropdown + .title Stage + %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %span.stage-selection More + = icon('caret-down') + %ul.dropdown-menu + - builds.map(&:stage).uniq.each do |stage| + %li + %a.stage-item= stage + + .builds-container + - statuses.each do |build_status| + - builds.select{|build| build.status == build_status}.each do |build| + .build-job{class: ('active' if build == @build), data: {stage: build.stage}} + = link_to namespace_project_build_path(@project.namespace, @project, build) do + = icon('check') + = ci_icon_for_status(build.status) + %span + - if build.name + = build.name + - else + = build.id + + - if @build.retried? + %li.active + %a + Build ##{@build.id} + · + %i.fa.fa-warning + This build was retried. diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 58f43ecb5d5..5f5e071eb40 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,4 +1,42 @@ -- unless @project.empty_repo? - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do - = icon('download') +- if !project.empty_repo? && can?(current_user, :download_code, project) + %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'} + .dropdown.inline + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + %span.caret + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar + + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do + %i.fa.fa-download + %span Download '#{job.name}' diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index d78888e9fe4..22db33498f1 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -3,11 +3,11 @@ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do = custom_icon('icon_fork') - Fork + %span Fork - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = custom_icon('icon_fork') - Fork + %span Fork %div.count-with-arrow %span.arrow = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 1fdf32466f2..73de8abe55b 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -89,4 +89,4 @@ = icon('repeat') - elsif build.playable? = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do - = icon('play') + = custom_icon('icon_play') diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 04cbd0c3591..36fb0300aeb 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,14 +1,15 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) %li.build{class: ("playable" if is_playable)} + .curve .build-content - if is_playable = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do = render_status_with_link('build', 'play') - = subject.name + %span.ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do = render_status_with_link('build', subject.status) - = subject.name + %span.ci-status-text= subject.name - else = render_status_with_link('build', subject.status) = ci_icon_for_status(subject.status) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b119f6edf14..bb9493f5158 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -66,13 +66,13 @@ - if actions.any? .btn-group %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = icon("play") + = custom_icon('icon_play') %b.caret %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do - = icon("play") + = custom_icon('icon_play') %span= build.name.humanize - if artifacts.present? .btn-group diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 29f4ef8f49e..f41a11a056d 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -10,7 +10,10 @@ %th Commit - pipelines.stages.each do |stage| %th.stage - %span.has-tooltip{ title: "#{stage.titleize}" } + - if stage.titleize.length > 12 + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize + - else = stage.titleize %th %th diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index fd888f41b1e..389477d0927 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -7,7 +7,7 @@ - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key.push(commit.status) if commit.status -= cache(cache_key) do += cache(cache_key, expires_in: 1.day) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } = author_avatar(commit, size: 36) diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 61152649907..4d1ee1c5318 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,8 +1,5 @@ .scrolling-tabs-container.sub-nav-scroll - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') + = render 'shared/nav_scroll' .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index f7bf3b834ef..16d134eb6b6 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -5,13 +5,13 @@ .inline .dropdown %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = icon("play") + = custom_icon('icon_play') %b.caret %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |action| %li = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do - = icon("play") + = custom_icon('icon_play') %span= action.name.humanize - if local_assigns.fetch(:allow_rollback, false) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b282aa52b25..f6d751a343e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -44,42 +44,56 @@ %hr %fieldset.features.append-bottom-0 %h5.prepend-top-0 - Features - .form-group - .checkbox - = f.label :issues_enabled do - = f.check_box :issues_enabled - %strong Issues - %br - %span.descr Lightweight issue tracking system for this project - .form-group - .checkbox - = f.label :merge_requests_enabled do - = f.check_box :merge_requests_enabled - %strong Merge Requests - %br - %span.descr Submit changes to be merged upstream - .form-group - .checkbox - = f.label :builds_enabled do - = f.check_box :builds_enabled - %strong Builds - %br - %span.descr Test and deploy your changes before merge - .form-group - .checkbox - = f.label :wiki_enabled do - = f.check_box :wiki_enabled - %strong Wiki - %br - %span.descr Pages for project documentation - .form-group - .checkbox - = f.label :snippets_enabled do - = f.check_box :snippets_enabled - %strong Snippets - %br - %span.descr Share code pastes with others out of git repository + Feature Visibility + + = f.fields_for :project_feature do |feature_fields| + .form_group.prepend-top-20 + .row + .col-md-9 + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-3 + = project_feature_access_select(:issues_access_level) + + .row + .col-md-9 + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-3 + = project_feature_access_select(:merge_requests_access_level) + + .row + .col-md-9 + = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + %span.help-block Submit Test and deploy your changes before merge + .col-md-3 + = project_feature_access_select(:builds_access_level) + + .row + .col-md-9 + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-3 + = project_feature_access_select(:wiki_access_level) + + .row + .col-md-9 + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-3 + = project_feature_access_select(:snippets_access_level) + + - if Gitlab.config.lfs.enabled && current_user.admin? + .form-group + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled, checked: @project.lfs_enabled? + %strong LFS + %br + %span.descr + Git Large File Storage + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + - if Gitlab.config.registry.enabled .form-group .checkbox @@ -88,7 +102,7 @@ %strong Container Registry %br %span.descr Enable Container Registry for this repository - %hr + = render 'merge_request_settings', f: f %hr %fieldset.features.append-bottom-default diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index a1d79bdabda..bacc5708e4b 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -32,11 +32,11 @@ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do = custom_icon('icon_fork') - Fork + %span Fork - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do = custom_icon('icon_fork') - Fork + %span Fork = render 'projects', projects: @forks diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 584c0fa18ae..576d0bec51b 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,9 +1,10 @@ %li.build + .curve .build-content - if subject.target_url - link_to subject.target_url do = render_status_with_link('commit status', subject.status) - = subject.name + %span.ci-status-text= subject.name - else = render_status_with_link('commit status', subject.status) - = subject.name + %span.ci-status-text= subject.name diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 45e51389c00..082e2cb4d8c 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,16 +1,18 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } - - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/chart.js') - = page_specific_javascript_tag('graphs/graphs_bundle.js') - = nav_link(action: :show) do - = link_to 'Contributors', namespace_project_graph_path - = nav_link(action: :commits) do - = link_to 'Commits', commits_namespace_project_graph_path - = nav_link(action: :languages) do - = link_to 'Languages', languages_namespace_project_graph_path - - if @project.builds_enabled? - = nav_link(action: :ci) do - = link_to ci_namespace_project_graph_path do - Continuous Integration + - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/chart.js') + = page_specific_javascript_tag('graphs/graphs_bundle.js') + = nav_link(action: :show) do + = link_to 'Contributors', namespace_project_graph_path + = nav_link(action: :commits) do + = link_to 'Commits', commits_namespace_project_graph_path + = nav_link(action: :languages) do + = link_to 'Languages', languages_namespace_project_graph_path + - if @project.feature_available?(:builds, current_user) + = nav_link(action: :ci) do + = link_to ci_namespace_project_graph_path do + Continuous Integration diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml index 3fcf1692e09..ceabe2eab3d 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/hooks/_project_hook.html.haml @@ -3,7 +3,7 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger| + - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray.deploy-project-label= trigger.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index b6cb559afcb..f88b33018d0 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -1,30 +1,32 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) - = nav_link(controller: :issues) do - = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do - %span - Issues +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) + = nav_link(controller: :issues) do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do + %span + Issues - = nav_link(controller: :boards) do - = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do - %span - Board + = nav_link(controller: :boards) do + = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do + %span + Board - - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do - %span - Merge Requests + - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do + %span + Merge Requests - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - %span - Labels + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %span + Labels - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - %span - Milestones + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %span + Milestones
\ No newline at end of file diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 24749699c6d..33556a1a2b3 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,13 +1,12 @@ - if can?(current_user, :push_code, @project) .pull-right #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} + = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do + = icon('spinner spin') + Checking branches = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), - method: :post, class: 'btn btn-new btn-inverted has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do - .checking - = icon('spinner spin') - Checking branches - .available.hide - New branch - .unavailable.hide - = icon('exclamation-triangle') - New branch unavailable + method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do + New branch + = link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do + = icon('exclamation-triangle') + New branch unavailable diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 6ea9f612d13..a8eeab3e55e 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,7 +5,7 @@ - @related_branches.each do |branch| %li - target = @project.repository.find_branch(branch).target - - pipeline = @project.pipeline(target.sha, branch) if target + - pipeline = @project.pipeline_for(branch, target.sha) if target - if pipeline %span.related-branch-ci-status = render_pipeline_status(pipeline) diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index b727efaa6a6..f1d5441f9dd 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -12,7 +12,7 @@ %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve - git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch} + git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch} git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD - else :preserve @@ -47,8 +47,9 @@ Note that pushing to GitLab requires write access to this repository. %p %strong Tip: - You can also checkout merge requests locally by - %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines + = succeed '.' do + You can also checkout merge requests locally by + = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank' :javascript $(function(){ diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 098ce19da21..e35291dff7d 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,3 +1,7 @@ +- if @merge_request.closed_without_fork? + .alert.alert-danger + %p The source project of this merge request has been removed. + .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box{ class: status_box_class(@merge_request) } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 0a1e2bb2cc6..fda0592dd41 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -55,7 +55,7 @@ = render 'bitbucket_import_modal' %div - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do = icon('gitlab', text: 'GitLab.com') - unless gitlab_import_configured? = render 'gitlab_import_modal' diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index d65faf86d4e..f611ddc8f5f 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,19 +1,21 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - %span - Builds + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + %span + Builds - - if project_nav_tab? :environments - = nav_link(controller: %w(environments)) do - = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do - %span - Environments + - if project_nav_tab? :environments + = nav_link(controller: %w(environments)) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 5f466bdbac2..4d957e0d890 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -49,7 +49,10 @@ %th Commit - stages.each do |stage| %th.stage - %span.has-tooltip{ title: "#{stage.titleize}" } + - if stage.titleize.length > 12 + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize + - else = stage.titleize %th %th diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 833954bc039..37e55dc72a3 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,3 @@ :plain $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); - new MemberExpirationDate(); + new gl.MemberExpirationDate(); diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 8ee2aef0e61..1141168f037 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -5,8 +5,8 @@ :plain var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}"); - row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}'); - row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}'); + row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}'); + row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}'); - if @more_log_url :plain diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml deleted file mode 100644 index 24658319060..00000000000 --- a/app/views/projects/repositories/_download_archive.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -- ref = ref || nil -- btn_class = btn_class || '' -- split_button = split_button || false -- if split_button == true - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Archive Format - %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar -- else - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span zip - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span tar.gz diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 340e159c874..9adce776c1c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -72,7 +72,7 @@ = render "projects/buttons/koding" .btn-group.project-repo-btn-group - = render "projects/buttons/download" + = render 'projects/buttons/download', project: @project, ref: @ref = render 'projects/buttons/dropdown' = render 'shared/notifications/button', notification_setting: @notification_setting diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml deleted file mode 100644 index 8a11dbfa9f4..00000000000 --- a/app/views/projects/tags/_download.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%span.btn-group - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do - %span Source code - %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %span Download tar.gz diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 2c11c0e5b21..a156d98bab8 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -11,8 +11,7 @@ = strip_gpg_signature(tag.message) .controls - - if can?(current_user, :download_code, @project) - = render 'projects/tags/download', ref: tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: tag.name - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 368231e73fe..6adbe9351dc 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -8,21 +8,24 @@ Tags give the ability to mark specific points in history as being important .nav-controls - - if can? current_user, :push_code, @project - = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - New tag + = form_tag(filter_tags_path, method: :get) do + = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } - %span.light= @sort.humanize + %span.light + = @sort.humanize %b.caret %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to namespace_project_tags_path(sort: nil) do + = link_to filter_tags_path(sort: nil) do Name - = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do + = link_to filter_tags_path(sort: sort_value_recently_updated) do = sort_title_recently_updated - = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do + = link_to filter_tags_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated + - if can?(current_user, :push_code, @project) + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do + New tag .tags - if @tags.any? diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 395d7af6cbb..4dd7439b2d0 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -12,8 +12,7 @@ = icon('files-o') = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do = icon('history') - - if can? current_user, :download_code, @project - = render 'projects/tags/download', ref: @tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .pull-right = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index a3a4dba3fa4..ee417b58cbf 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -4,6 +4,6 @@ - file_name = blob_item.name = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do %span.str-truncated= file_name - %td.tree_time_ago.cgray - = render 'projects/tree/spinner' - %td.hidden-xs.tree_commit + %td.hidden-xs.tree-commit + %td.tree-time-ago.cgray.text-right + = render 'projects/tree/spinner'
\ No newline at end of file diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 558e6146ae9..0f7d629ab98 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -4,7 +4,6 @@ %thead %tr %th Name - %th Last Update %th.hidden-xs .pull-left Last Commit .last-commit.hidden-sm.pull-left @@ -14,9 +13,11 @@ %small.light = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" – - = truncate(@commit.title, length: 50) - = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - + = time_ago_with_tooltip(@commit.committed_date) + = @commit.full_title + %small.commit-history-link-spacer | + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'commit-history-link' + %th.text-right Last Update - if @path.present? %tr.tree-item %td.tree-item-file-name diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 9577696fc0d..1ccef6d52ab 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -4,6 +4,6 @@ - path = flatten_tree(tree_item) = link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do %span.str-truncated= path - %td.tree_time_ago.cgray - = render 'projects/tree/spinner' - %td.hidden-xs.tree_commit + %td.hidden-xs.tree-commit + %td.tree-time-ago.text-right + = render 'projects/tree/spinner'
\ No newline at end of file diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index bf5360b4dee..37d341212af 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -10,8 +10,7 @@ %div{ class: container_class } .tree-controls = render 'projects/find_file_link' - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true + = render 'projects/buttons/download', project: @project, ref: @ref #tree-holder.tree-holder.clearfix .nav-block diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 7f3de47d7df..f6e0b0a7c8a 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -4,65 +4,89 @@ .col-lg-3 %h4.prepend-top-0 = page_title - %p - Triggers can force a specific branch or tag to rebuild with an API call. + %p.prepend-top-20 + Triggers can force a specific branch or tag to get rebuilt with an API call. + %p.append-bottom-0 + = succeed '.' do + Learn more in the + = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' .col-lg-9 - %h5.prepend-top-0 - Your triggers - - if @triggers.any? - .table-responsive - %table.table - %thead - %th Token - %th Last used - %th - = render partial: 'trigger', collection: @triggers, as: :trigger - - else - %p.settings-message.text-center.append-bottom-default - No triggers have been created yet. Add one using the button below. + .panel.panel-default + .panel-heading + %h4.panel-title + Manage your project's triggers + .panel-body + - if @triggers.any? + .table-responsive + %table.table + %thead + %th + %strong Token + %th + %strong Last used + %th + = render partial: 'trigger', collection: @triggers, as: :trigger + - else + %p.settings-message.text-center.append-bottom-default + No triggers have been created yet. Add one using the button below. - = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| - = f.submit "Add Trigger", class: 'btn btn-success' + = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| + = f.submit "Add trigger", class: 'btn btn-success' - %h5.prepend-top-default - Use CURL + .panel-footer - %p.light - Copy the token above, set your branch or tag name, and that reference will be rebuilt. + %p + In the following examples, you can see the exact API call you need to + make in order to rebuild a specific + %code ref + (branch or tag) with a trigger token. + %p + All you need to do is replace the + %code TOKEN + and + %code REF_NAME + with the trigger token and the branch or tag name respectively. - %pre - :plain - curl -X POST \ - -F token=TOKEN \ - -F ref=REF_NAME \ - #{builds_trigger_url(@project.id)} - %h5.prepend-top-default - Use .gitlab-ci.yml + %h5.prepend-top-default + Use cURL - %p.light - In the - %code .gitlab-ci.yml - of the dependent project, include the following snippet. - The project will rebuild at the end of the build. + %p.light + Copy one of the tokens above, set your branch or tag name, and that + reference will be rebuilt. - %pre - :plain - trigger: - type: deploy - script: - - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" - %h5.prepend-top-default - Pass build variables + %pre + :plain + curl -X POST \ + -F token=TOKEN \ + -F ref=REF_NAME \ + #{builds_trigger_url(@project.id)} + %h5.prepend-top-default + Use .gitlab-ci.yml - %p.light - Add - %code variables[VARIABLE]=VALUE - to an API request. Variable values can be used to distinguish between triggered builds and normal builds. + %p.light + In the + %code .gitlab-ci.yml + of another project, include the following snippet. + The project will be rebuilt at the end of the build. - %pre.append-bottom-0 - :plain - curl -X POST \ - -F token=TOKEN \ - -F "ref=REF_NAME" \ - -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{builds_trigger_url(@project.id)} + %pre + :plain + trigger_build: + stage: deploy + script: + - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" + %h5.prepend-top-default + Pass build variables + + %p.light + Add + %code variables[VARIABLE]=VALUE + to an API request. Variable values can be used to distinguish between triggered builds and normal builds. + + %pre.append-bottom-0 + :plain + curl -X POST \ + -F token=TOKEN \ + -F "ref=REF_NAME" \ + -F "variables[RUN_NIGHTLY_BUILD]=true" \ + #{builds_trigger_url(@project.id)} diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index f8ea479e0b1..551a20c1044 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,13 +1,15 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + = nav_link(path: 'wikis#pages') do + = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access + = nav_link(path: 'wikis#git_access') do + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do + Git Access - = render 'projects/wikis/new' + = render 'projects/wikis/new' diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index b07f1c5603e..9b67422da2c 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,9 +1,9 @@ -<svg width="36" height="36" id="tanuki-logo"> - <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> - <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> - <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> - <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/> - <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/> - <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/> - <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/> +<svg width="36" height="36" class="tanuki-logo"> + <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> + <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> + <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> + <path class="tanuki-shape tanuki-left-eye" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/> + <path class="tanuki-shape tanuki-right-eye" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/> + <path class="tanuki-shape tanuki-left-cheek" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/> + <path class="tanuki-shape tanuki-right-cheek" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/> </svg> diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml new file mode 100644 index 00000000000..4e3b1b3a571 --- /dev/null +++ b/app/views/shared/_nav_scroll.html.haml @@ -0,0 +1,4 @@ +.fade-left + = icon('angle-left') +.fade-right + = icon('angle-right')
\ No newline at end of file diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg index 80a6d41dbf6..e965afa9a56 100644 --- a/app/views/shared/icons/_icon_play.svg +++ b/app/views/shared/icons/_icon_play.svg @@ -1 +1,3 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"> + <path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/> + </svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg new file mode 100644 index 00000000000..1f5c3b51b03 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 4f8ea7e7cef..0f4f744a71f 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -27,15 +27,18 @@ = render "shared/issuable/label_dropdown" .pull-right - - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project) - .dropdown - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } } - Create new list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } - - if can?(current_user, :admin_label, @project) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading + - if controller.controller_name == 'boards' + #js-boards-seach.issue-boards-search + %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } + - if can?(current_user, :admin_list, @project) + .dropdown.pull-right + %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } } + Create new list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading - else = render 'shared/sort_dropdown' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 22594b46443..3856a4917b4 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -134,7 +134,7 @@ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } = icon('question-circle') -- if issuable.is_a?(MergeRequest) +- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork? %hr - if @merge_request.new_record? .form-group @@ -175,7 +175,7 @@ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else .pull-right - - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) + - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index d34d28f6736..24a1a616919 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -12,7 +12,7 @@ - if params[:label_name].present? - if params[:label_name].respond_to?('any?') - params[:label_name].each do |label| - = hidden_field_tag "label_name[]", label, id: nil + = hidden_field_tag "label_name[]", u(label), id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c1b50e65af5..b13daaf43c9 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -118,7 +118,7 @@ = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } + .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } - if issuable.labels_array.any? - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index d2ec6c3ddef..5d659eb83a9 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -52,6 +52,13 @@ %p.light This URL will be triggered when an issue is created/updated/merged %li + = f.check_box :confidential_issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :confidential_issues_events, class: 'list-label' do + %strong Confidential Issues events + %p.light + This URL will be triggered when a confidential issue is created/updated/merged + %li = f.check_box :merge_requests_events, class: 'pull-left' .prepend-left-20 = f.label :merge_requests_events, class: 'list-label' do diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index 80a3e731e1d..7be4a471579 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -1,7 +1,11 @@ -%ul.content-list - = render partial: 'shared/snippets/snippet', collection: @snippets - - if @snippets.empty? - %li - .nothing-here-block Nothing here. +.snippets-list-holder + %ul.content-list + = render partial: 'shared/snippets/snippet', collection: @snippets + - if @snippets.empty? + %li + .nothing-here-block Nothing here. -= paginate @snippets, theme: 'gitlab' + = paginate @snippets, theme: 'gitlab', remote: true + +:javascript + gl.SnippetsList(); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c7f39868e71..9a052abe40a 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -123,6 +123,6 @@ :javascript var userProfile; - userProfile = new User({ + userProfile = new gl.User({ action: "#{controller.action_name}" }); |