diff --git a/CHANGELOG b/CHANGELOG index 7ea2631f9f4..c6a4fe5f5b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,12 +1,15 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.11.0 (unreleased) + - Use test coverage value from the latest successful pipeline in badge. !5862 - Add test coverage report badge. !5708 - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) + - Add Koding (online IDE) integration - Ability to specify branches for Pivotal Tracker integration (Egor Lynko) - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres) - Add delimiter to project stars and forks count (ClemMakesApps) - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres) + - Fix adding line comments on the initial commit to a repo !5900 - Fix the title of the toggle dropdown button. !5515 (herminiotorres) - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz) - Update to Ruby 2.3.1. !4948 @@ -18,13 +21,17 @@ v 8.11.0 (unreleased) - API: Endpoints for enabling and disabling deploy keys - API: List access requests, request access, approve, and deny access requests to a project or a group. !4833 - Use long options for curl examples in documentation !5703 (winniehell) + - Added tooltip listing label names to the labels value in the collapsed issuable sidebar - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) + - Fix badge count alignment (ClemMakesApps) - GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Allow naming U2F devices !5833 - Ignore URLs starting with // in Markdown links !5677 (winniehell) - Fix CI status icon link underline (ClemMakesApps) - The Repository class is now instrumented + - Fix commit mention font inconsistency (ClemMakesApps) + - Do not escape URI when extracting path !5878 (winniehell) - Fix filter label tooltip HTML rendering (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Expand commit message width in repo view (ClemMakesApps) @@ -34,9 +41,11 @@ v 8.11.0 (unreleased) - API: Add deployment endpoints - API: Add Play endpoint on Builds - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' + - Show wall clock time when showing a pipeline. !5734 - Show member roles to all users on members page - Project.visible_to_user is instrumented again - Fix awardable button mutuality loading spinners (ClemMakesApps) + - Sort todos by date and priority - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes - Send notification emails to users newly mentioned in issue and MR edits !5800 @@ -55,6 +64,7 @@ v 8.11.0 (unreleased) - Enforce 2FA restrictions on API authentication endpoints !5820 - Limit git rev-list output count to one in forced push check - Show deployment status on merge requests with external URLs + - Fix branch title trailing space on hover (ClemMakesApps) - Clean up unused routes (Josef Strzibny) - Fix issue on empty project to allow developers to only push to protected branches if given permission - API: Add enpoints for pipelines @@ -71,6 +81,7 @@ v 8.11.0 (unreleased) - Fix devise deprecation warnings. - Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764 - Update version_sorter and use new interface for faster tag sorting + - Load branches asynchronously in Cherry Pick and Revert dialogs. - Optimize checking if a user has read access to a list of issues !5370 - Store all DB secrets in secrets.yml, under descriptive names !5274 - Fix syntax highlighting in file editor @@ -79,7 +90,6 @@ v 8.11.0 (unreleased) - Add archived badge to project list !5798 - Add simple identifier to public SSH keys (muteor) - Admin page now references docs instead of a specific file !5600 (AnAverageHuman) - - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Fix filter input alignment (ClemMakesApps) - Include old revision in merge request update hooks (Ben Boeckel) - Add build event color in HipChat messages (David Eisner) @@ -105,12 +115,14 @@ v 8.11.0 (unreleased) - Fix search for notes which belongs to deleted objects - Allow Akismet to be trained by submitting issues as spam or ham !5538 - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) + - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps) - Allow branch names ending with .json for graph and network page !5579 (winniehell) - Add the `sprockets-es6` gem - Improve OAuth2 client documentation (muteor) - Fix diff comments inverted toggle bug (ClemMakesApps) - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed + - Fix button missing type (ClemMakesApps) - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab. - Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible - Add commit stats in commit api. !5517 (dixpac) @@ -119,14 +131,17 @@ v 8.11.0 (unreleased) - edit_blob_link will use blob passed onto the options parameter - Make error pages responsive (Takuya Noguchi) - The performance of the project dropdown used for moving issues has been improved + - Move to project dropdown with infinite scroll for better performance - Fix skip_repo parameter being ignored when destroying a namespace - Add all builds into stage/job dropdowns on builds page - Change requests_profiles resource constraint to catch virtually any file - Bump gitlab_git to lazy load compare commits - Reduce number of queries made for merge_requests/:id/diffs + - Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski) - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) - Fix bug where destroying a namespace would not always destroy projects - Fix RequestProfiler::Middleware error when code is reloaded in development + - Allow horizontal scrolling of code blocks in issue body - Catch what warden might throw when profiling requests to re-throw it - Avoid commit lookup on diff_helper passing existing local variable to the helper method - Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac) @@ -141,6 +156,7 @@ v 8.11.0 (unreleased) - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0 - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska) - Add pipelines tab to merge requests + - Fix notification_service argument error of declined invitation emails - Fix a memory leak caused by Banzai::Filter::SanitizationFilter - Speed up todos queries by limiting the projects set we join with - Ensure file editing in UI does not overwrite commited changes without warning user @@ -148,6 +164,10 @@ v 8.11.0 (unreleased) - Update gitlab_git gem to 10.4.7 - Simplify SQL queries of marking a todo as done +v 8.10.7 + - Upgrade Hamlit to 2.6.1. !5873 + - Upgrade Doorkeeper to 4.2.0. !5881 + v 8.10.6 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 - Restore "Largest repository" sort option on Admin > Projects page. !5797 @@ -371,6 +391,9 @@ v 8.10.0 - Fix migration corrupting import data for old version upgrades - Show tooltip on GitLab export link in new project page +v 8.9.8 + - Upgrade Doorkeeper to 4.2.0. !5881 + v 8.9.7 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 - Require administrator privileges to perform a project import. @@ -640,6 +663,9 @@ v 8.9.0 - Add tooltip to pin/unpin navbar - Add new sub nav style to Wiki and Graphs sub navigation +v 8.8.9 + - Upgrade Doorkeeper to 4.2.0. !5881 + v 8.8.8 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbc8e15bebf..d8093a61b4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -387,7 +387,8 @@ description area. Copy-paste it to retain the markdown format. 1. The change is as small as possible 1. Include proper tests and make all tests pass (unless it contains a test - exposing a bug in existing code) + exposing a bug in existing code). Every new class should have corresponding + unit tests, even if the class is exercised at a higher level, such as a feature test. 1. If you suspect a failing CI build is unrelated to your contribution, you may try and restart the failing CI job or ask a developer to fix the aforementioned failing test diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 619b5376684..18091983f59 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.3.3 +3.4.0 diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg new file mode 100644 index 00000000000..ad89d684d94 --- /dev/null +++ b/app/assets/images/koding-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index a122fa2d637..fc354dfd677 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -26,8 +26,6 @@ /*= require bootstrap/tooltip */ /*= require bootstrap/popover */ /*= require select2 */ -/*= require ace-rails-ap */ -/*= require ace/ext-searchbox */ /*= require underscore */ /*= require dropzone */ /*= require mousetrap */ @@ -153,7 +151,9 @@ }); }); $('.remove-row').bind('ajax:success', function() { - return $(this).closest('li').fadeOut(); + $(this).tooltip('destroy') + .closest('li') + .fadeOut(); }); $('.js-remove-tr').bind('ajax:before', function() { return $(this).hide(); diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js new file mode 100644 index 00000000000..2afef43f3d6 --- /dev/null +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -0,0 +1,12 @@ +/*= require_tree . */ + +(function() { + $(function() { + var url = $(".js-edit-blob-form").data("relative-url-root"); + url += $(".js-edit-blob-form").data("assets-prefix"); + + var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language')); + new NewCommitForm($('.js-edit-blob-form')); + }); + +}).call(this); diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js similarity index 100% rename from app/assets/javascripts/blob/edit_blob.js rename to app/assets/javascripts/blob_edit/edit_blob.js diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 2c65d4427be..a612cf0f1ae 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -38,7 +38,7 @@ $(() => { ready () { Store.disabled = this.disabled; gl.boardService.all() - .then((resp) => { + .then((resp) => { resp.json().forEach((board) => { const list = Store.addList(board); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index e17784e7948..d7f4107cb02 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -55,7 +55,7 @@ draggable: '.is-draggable', handle: '.js-board-handle', onEnd: (e) => { - document.body.classList.remove('is-dragging'); + gl.issueBoards.onEnd(); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { const order = this.sortable.toArray(), @@ -72,10 +72,6 @@ } }); - if (bp.getBreakpointSize() === 'xs') { - options.handle = '.js-board-drag-handle'; - } - this.sortable = Sortable.create(this.$el.parentNode, options); }, beforeDestroy () { diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 1503d14c508..a6644e9eb8c 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -63,6 +63,8 @@ Store.moving.issue = card.issue; Store.moving.list = card.list; + + gl.issueBoards.onStart(); }, onAdd: (e) => { gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); @@ -72,10 +74,6 @@ } }); - if (bp.getBreakpointSize() === 'xs') { - options.handle = '.js-card-drag-handle'; - } - this.sortable = Sortable.create(this.$els.list, options); // Scroll event on list to load more diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index b7afe4897b6..44addb3ea98 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -2,6 +2,19 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; + gl.issueBoards.onStart = () => { + $('.has-tooltip').tooltip('hide') + .tooltip('disable'); + document.body.classList.add('is-dragging'); + }; + + gl.issueBoards.onEnd = () => { + $('.has-tooltip').tooltip('enable'); + document.body.classList.remove('is-dragging'); + }; + + gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; + gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { let defaultSortOptions = { forceFallback: true, @@ -9,14 +22,11 @@ fallbackOnBody: true, ghostClass: 'is-ghost', filter: '.has-tooltip', - scrollSensitivity: 100, + delay: gl.issueBoards.touchEnabled ? 100 : 0, + scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, - onStart () { - document.body.classList.add('is-dragging'); - }, - onEnd () { - document.body.classList.remove('is-dragging'); - } + onStart: gl.issueBoards.onStart, + onEnd: gl.issueBoards.onEnd } Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 index e81e91fe972..583829552cd 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -3,6 +3,7 @@ class ListLabel { this.id = obj.id; this.title = obj.title; this.color = obj.color; + this.textColor = obj.text_color; this.description = obj.description; this.priority = (obj.priority !== null) ? obj.priority : Infinity; } diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js old mode 100755 new mode 100644 diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 74c4ab563f9..ba64d2bcf0b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -20,6 +20,9 @@ path = page.split(':'); shortcut_handler = null; switch (page) { + case 'projects:boards:show': + shortcut_handler = new ShortcutsNavigation(); + break; case 'projects:issues:index': Issuable.init(); new IssuableBulkActions(); @@ -126,10 +129,12 @@ new NotificationsDropdown(); break; case 'groups:group_members:index': + new gl.MemberExpirationDate(); new GroupMembers(); new UsersSelect(); break; case 'projects:project_members:index': + new gl.MemberExpirationDate(); new ProjectMembers(); new UsersSelect(); break; @@ -171,6 +176,7 @@ new BuildArtifacts(); break; case 'projects:group_links:index': + new gl.MemberExpirationDate(); new GroupsSelect(); break; case 'search:show': diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d3394fae3f9..24abea0d30d 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -31,9 +31,8 @@ this.input .on('keydown', function (e) { var keyCode = e.which; - if (keyCode === 13) { - e.preventDefault() + e.preventDefault(); } }) .on('keyup', function(e) { @@ -111,9 +110,9 @@ matches = fuzzaldrinPlus.match($el.text().trim(), search_text); if (!$el.is('.dropdown-header')) { if (matches.length) { - return $el.show(); + return $el.show().removeClass('option-hidden'); } else { - return $el.hide(); + return $el.hide().addClass('option-hidden'); } } }); @@ -179,7 +178,7 @@ })(); GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex; + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex; LOADING_CLASS = "is-loading"; @@ -191,6 +190,12 @@ currentIndex = -1; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden'; + + SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")"; + + CURSOR_SELECT_SCROLL_PADDING = 5 + FILTER_INPUT = '.dropdown-input .dropdown-input-field'; function GitLabDropdown(el1, options) { @@ -213,6 +218,7 @@ if (this.options.data) { if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { this.fullData = this.options.data; + currentIndex = -1; this.parseData(this.options.data); } else { this.remote = new GitLabDropdownRemote(this.options.data, { @@ -240,7 +246,7 @@ keys: searchFields, elements: (function(_this) { return function() { - selector = '.dropdown-content li:not(.divider)'; + selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; if (_this.dropdown.find('.dropdown-toggle-page').length) { selector = ".dropdown-page-one " + selector; } @@ -256,7 +262,7 @@ return function(data) { _this.parseData(data); if (_this.filterInput.val() !== '') { - selector = '.dropdown-content li:not(.divider):visible'; + selector = SELECTABLE_CLASSES; if (_this.dropdown.find('.dropdown-toggle-page').length) { selector = ".dropdown-page-one " + selector; } @@ -376,7 +382,7 @@ var $target; if (this.options.multiSelect) { $target = $(e.target); - if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) { + if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) { e.stopPropagation(); return false; } else { @@ -387,7 +393,7 @@ GitLabDropdown.prototype.opened = function() { var contentHtml; - currentIndex = -1; + this.resetRows(); this.addArrowKeyEvent(); if (this.options.setIndeterminateIds) { this.options.setIndeterminateIds.call(this); @@ -410,6 +416,7 @@ GitLabDropdown.prototype.hidden = function(e) { var $input; + this.resetRows(); this.removeArrayKeyEvent(); $input = this.dropdown.find(".dropdown-input-field"); if (this.options.filterable) { @@ -463,14 +470,15 @@ return "
  • "; } if (data.header != null) { - return ""; + return _.template('')({ header: data.header }); } if (this.options.renderRow) { html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { value = this.options.id ? this.options.id(data) : data.id; - fieldName = this.options.fieldName; + fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName; + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); if (field.length) { selected = true; @@ -494,11 +502,16 @@ text = this.highlightTextMatches(text, this.filterInput.val()); } if (group) { - groupAttrs = "data-group='" + group + "' data-index='" + index + "'"; + groupAttrs = 'data-group=' + group + ' data-index=' + index; } else { groupAttrs = ''; } - html = "
  • " + text + "
  • "; + html = _.template('
  • class="<%- cssClass %>"><%= text %>
  • ')({ + url: url, + groupAttrs: groupAttrs, + cssClass: cssClass, + text: text + }); } return html; }; @@ -520,20 +533,8 @@ return html = ""; }; - GitLabDropdown.prototype.highlightRow = function(index) { - var selector; - if (this.filterInput.val() !== "") { - selector = '.dropdown-content li:first-child a'; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content li:first-child a"; - } - return this.getElement(selector).addClass('is-focused'); - } - }; - GitLabDropdown.prototype.rowClicked = function(el) { var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; - fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { groupName = el.data('group'); @@ -545,6 +546,7 @@ selectedObject = this.renderedData[selectedIndex]; } } + fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName; value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; if (isInput) { field = $(this.el); @@ -559,10 +561,9 @@ field.remove(); } if (this.options.toggleLabel) { - return this.updateLabel(selectedObject, el, this); - } else { - return selectedObject; + this.updateLabel(selectedObject, el, this); } + return selectedObject; } else if (el.hasClass(INDETERMINATE_CLASS)) { el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); @@ -570,7 +571,7 @@ field.remove(); } if (!field.length && fieldName) { - this.addInput(fieldName, value); + this.addInput(fieldName, value, selectedObject); } return selectedObject; } else { @@ -589,7 +590,7 @@ } if (value != null) { if (!field.length && fieldName) { - this.addInput(fieldName, value); + this.addInput(fieldName, value, selectedObject); } else { field.val(value).trigger('change'); } @@ -598,24 +599,29 @@ } }; - GitLabDropdown.prototype.addInput = function(fieldName, value) { + GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { var $input; $input = $('').attr('type', 'hidden').attr('name', fieldName).val(value); if (this.options.inputId != null) { $input.attr('id', this.options.inputId); } + if (selectedObject && selectedObject.type) { + $input.attr('data-type', selectedObject.type); + } return this.dropdown.before($input); }; GitLabDropdown.prototype.selectRowAtIndex = function(index) { var $el, selector; - selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a"; + selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; if (this.dropdown.find(".dropdown-toggle-page").length) { selector = ".dropdown-page-one " + selector; } $el = $(selector, this.dropdown); if ($el.length) { - return $el.first().trigger('click'); + $el.first().trigger('click'); + var href = $el.attr('href'); + if (href && href !== '#') Turbolinks.visit(href); } }; @@ -623,7 +629,7 @@ var $input, ARROW_KEY_CODES, selector; ARROW_KEY_CODES = [38, 40]; $input = this.dropdown.find(".dropdown-input-field"); - selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible'; + selector = SELECTABLE_CLASSES; if (this.dropdown.find(".dropdown-toggle-page").length) { selector = ".dropdown-page-one " + selector; } @@ -651,7 +657,7 @@ return false; } if (currentKeyCode === 13 && currentIndex !== -1) { - return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1); + return _this.selectRowAtIndex(currentIndex); } }; })(this)); @@ -661,6 +667,11 @@ return $('body').off('keydown'); }; + GitLabDropdown.prototype.resetRows = function resetRows() { + currentIndex = -1; + $('.is-focused', this.dropdown).removeClass('is-focused'); + }; + GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; $('.is-focused', this.dropdown).removeClass('is-focused'); @@ -674,10 +685,14 @@ listItemHeight = $listItem.outerHeight(); listItemTop = $listItem.prop('offsetTop'); listItemBottom = listItemTop + listItemHeight; - if (listItemBottom > dropdownContentBottom + dropdownScrollTop) { - return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom); - } else if (listItemTop < dropdownContentTop + dropdownScrollTop) { - return $dropdownContent.scrollTop(listItemTop - dropdownContentTop); + if (!index) { + $dropdownContent.scrollTop(0) + } else if (index === ($listItems.length - 1)) { + $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); + } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { + $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); + } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { + return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); } }; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 297d4f029f0..b7f92ae9883 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -102,20 +102,34 @@ }; IssuableForm.prototype.initMoveDropdown = function() { - var $moveDropdown; + var $moveDropdown, pageSize; $moveDropdown = $('.js-move-dropdown'); if ($moveDropdown.length) { + pageSize = $moveDropdown.data('page-size'); return $('.js-move-dropdown').select2({ ajax: { url: $moveDropdown.data('projects-url'), - results: function(data) { + quietMillis: 125, + data: function(term, page, context) { return { - results: data + search: term, + offset_id: context }; }, - data: function(query) { + results: function(data) { + var context, + more; + + if (data.length >= pageSize) + more = true; + + if (data[data.length - 1]) + context = data[data.length - 1].id; + return { - search: query + results: data, + more: more, + context: context }; } }, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 0526430989f..565dbeacdb3 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,7 +4,7 @@ var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); labelUrl = $dropdown.data('labels'); @@ -21,6 +21,7 @@ $block = $selectbox.closest('.block'); $form = $dropdown.closest('form'); $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); + $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); if (issueUpdateURL != null) { @@ -31,7 +32,11 @@ labelNoneHTMLTemplate = 'None'; } - new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId); + $sidebarLabelTooltip.tooltip(); + + if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { + new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId); + } saveLabelData = function() { var data, selected; @@ -52,7 +57,7 @@ dataType: 'JSON', data: data }).done(function(data) { - var labelCount, template; + var labelCount, template, labelTooltipTitle, labelTitles; $loading.fadeOut(); $dropdown.trigger('loaded.gl.dropdown'); $selectbox.hide(); @@ -66,6 +71,27 @@ } $value.removeAttr('style').html(template); $sidebarCollapsedValue.text(labelCount); + + if (data.labels.length) { + labelTitles = data.labels.map(function(label) { + return label.title; + }); + + if (labelTitles.length > 5) { + labelTitles = labelTitles.slice(0, 5); + labelTitles.push('and ' + (data.labels.length - 5) + ' more'); + } + + labelTooltipTitle = labelTitles.join(', '); + } else { + labelTooltipTitle = ''; + $sidebarLabelTooltip.tooltip('destroy'); + } + + $sidebarLabelTooltip + .attr('title', labelTooltipTitle) + .tooltip('fixTitle'); + $('.has-tooltip', $value).tooltip({ container: 'body' }); diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js new file mode 100644 index 00000000000..4cdf99cae72 --- /dev/null +++ b/app/assets/javascripts/lib/ace.js @@ -0,0 +1,2 @@ +/*= require ace-rails-ap */ +/*= require ace/ext-searchbox */ diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js new file mode 100644 index 00000000000..1935af491f7 --- /dev/null +++ b/app/assets/javascripts/member_expiration_date.js @@ -0,0 +1,32 @@ +(function() { + // Add datepickers to all `js-access-expiration-date` elements. If those elements are + // children of an element with the `clearable-input` class, and have a sibling + // `js-clear-input` element, then show that element when there is a value in the + // datepicker, and make clicking on that element clear the field. + // + gl.MemberExpirationDate = function() { + function toggleClearInput() { + $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); + } + + var inputs = $('.js-access-expiration-date'); + + inputs.datepicker({ + dateFormat: 'yy-mm-dd', + minDate: 1, + onSelect: toggleClearInput + }); + + inputs.next('.js-clear-input').on('click', function(event) { + event.preventDefault(); + + var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); + input.datepicker('setDate', null); + toggleClearInput.call(input); + }); + + inputs.on('blur', toggleClearInput); + + inputs.each(toggleClearInput); + }; +}).call(this); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index b97f6d22715..4e1de4dfb72 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -65,7 +65,8 @@ url: $dropdown.data('refs-url'), data: { ref: $dropdown.data('ref') - } + }, + dataType: "json" }).done(function(refs) { return callback(refs); }); @@ -73,7 +74,7 @@ selectable: true, filterable: true, filterByText: true, - fieldName: 'ref', + fieldName: $dropdown.data('field-name'), renderRow: function(ref) { var link; if (ref.header != null) { diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js index f6a796b325a..78f7b48bc7d 100644 --- a/app/assets/javascripts/project_members.js +++ b/app/assets/javascripts/project_members.js @@ -5,9 +5,6 @@ return $(this).fadeOut(); }); } - return ProjectMembers; - })(); - }).call(this); diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 index 2fbb088fa04..7aeb5f92514 100644 --- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 @@ -10,8 +10,12 @@ selectable: true, inputId: $dropdown.data('input-id'), fieldName: $dropdown.data('field-name'), - toggleLabel(item) { - return item.text; + toggleLabel(item, el) { + if (el.is('.is-active')) { + return item.text; + } else { + return 'Select'; + } }, clicked(item, $el, e) { e.preventDefault(); diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index 2efca2414dc..46beca469b9 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -47,9 +47,7 @@ const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); - if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ - this.$form.find('input[type="submit"]').removeAttr('disabled'); - } + this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); } } diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 index a59fcbfa082..40bc4adb71b 100644 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -31,6 +31,9 @@ const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + // Do not update if one dropdown has not selected any option + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + $.ajax({ type: 'POST', url: this.$wrap.data('url'), diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 990f6536eb2..227e8c696b4 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -7,7 +7,9 @@ KEYCODE = { ESCAPE: 27, BACKSPACE: 8, - ENTER: 13 + ENTER: 13, + UP: 38, + DOWN: 40 }; function SearchAutocomplete(opts) { @@ -223,6 +225,12 @@ case KEYCODE.ESCAPE: this.restoreOriginalState(); break; + case KEYCODE.ENTER: + this.disableAutocomplete(); + break; + case KEYCODE.UP: + case KEYCODE.DOWN: + return; default: if (this.searchInput.val() === '') { this.disableAutocomplete(); @@ -319,9 +327,11 @@ }; SearchAutocomplete.prototype.disableAutocomplete = function() { - this.searchInput.addClass('disabled'); - this.dropdown.removeClass('open'); - return this.restoreMenu(); + if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { + this.searchInput.addClass('disabled'); + this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); + this.restoreMenu(); + } }; SearchAutocomplete.prototype.restoreMenu = function() { diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js new file mode 100644 index 00000000000..855e97eb301 --- /dev/null +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -0,0 +1,12 @@ +/*= require_tree . */ + +(function() { + $(function() { + var editor = ace.edit("editor") + + $(".snippet-form-holder form").on('submit', function() { + $(".snippet-file-content").val(editor.getValue()); + }); + }); + +}).call(this); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8846e08f390..be5c64c56d3 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -90,6 +90,15 @@ width: 100%; } } + + // Allows dynamic-width text in the dropdown toggle. + // Resizes to allow long text without overflowing the container. + &.dynamic { + width: auto; + min-width: 160px; + max-width: 100%; + padding-right: 25px; + } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 407f1873431..d3e3fc50736 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -63,9 +63,10 @@ &.image_file { background: #eee; text-align: center; + img { - padding: 100px; - max-width: 50%; + padding: 20px; + max-width: 80%; } } diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index f4d35c4b4b1..c0de09f3968 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -2,7 +2,7 @@ * Styles that apply to all GFM related forms. */ -.gfm-commit, .gfm-commit_range { +.gfm-commit_range { font-family: $monospace_font; font-size: 90%; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 26ad2870aa0..8374f30d0b2 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,6 +1,5 @@ .modal-body { position: relative; - overflow-y: auto; padding: 15px; .form-actions { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 7852fc9a424..9e924f99e9c 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -72,6 +72,7 @@ font-weight: normal; background-color: #eee; color: #78a; + vertical-align: baseline; } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 21d87cc9d34..b2e22b60440 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -45,7 +45,8 @@ min-width: 175px; } -.select2-results .select2-result-label { +.select2-results .select2-result-label, +.select2-more-results { padding: 10px 15px; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 8659604cb8b..06874a993fa 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -14,12 +14,20 @@ margin-top: 0; } + // Single code lines should wrap code { font-family: $monospace_font; - white-space: pre; + white-space: pre-wrap; word-wrap: normal; } + // Multi-line code blocks should scroll horizontally + pre { + code { + white-space: pre; + } + } + kbd { display: inline-block; padding: 3px 5px; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ad4b2d6496f..9ac4d801ac4 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -8,9 +8,13 @@ } .is-dragging { + // Important because plugin sets inline CSS + opacity: 1!important; + * { - cursor: -webkit-grabbing; - cursor: grabbing; + // !important to make sure no style can override this when dragging + cursor: -webkit-grabbing!important; + cursor: grabbing!important; } } @@ -101,8 +105,8 @@ .board { display: -webkit-flex; display: flex; - min-width: calc(100vw - 15px); - max-width: calc(100vw - 15px); + min-width: calc(85vw - 15px); + max-width: calc(85vw - 15px); margin-bottom: 25px; padding-right: ($gl-padding / 2); padding-left: ($gl-padding / 2); @@ -154,14 +158,6 @@ padding: $gl-padding; font-size: 1em; border-bottom: 1px solid $border-color; - - .board-mobile-handle { - position: relative; - left: 0; - top: 1px; - margin-top: 0; - margin-right: 5px; - } } .board-search-container { @@ -254,11 +250,6 @@ opacity: 0.3; } -.is-dragging { - // Important because plugin sets inline CSS - opacity: 1!important; -} - .card { position: relative; width: 100%; @@ -269,11 +260,7 @@ list-style: none; &.user-can-drag { - padding-left: ($gl-padding * 2); - - @media (min-width: $screen-sm-min) { - padding-left: $gl-padding; - } + padding-left: $gl-padding; } &:not(:last-child) { @@ -294,17 +281,6 @@ } } -.board-mobile-handle { - position: absolute; - left: 10px; - top: 50%; - margin-top: (-15px / 2); - - @media (min-width: $screen-sm-min) { - display: none; - } -} - .card-title { margin: 0; font-size: 1em; @@ -316,6 +292,7 @@ .card-footer { margin-top: 5px; + line-height: 25px; .label { margin-right: 4px; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 81fce55853c..c1bb250b42d 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -168,7 +168,6 @@ text-overflow: ellipsis; &:hover { - background-color: $row-hover; color: $gl-text-color; } } @@ -190,6 +189,10 @@ display: block; } } + + &:hover { + background-color: $row-hover; + } } } } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index bbe0c6c5f1f..53ec0002afe 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -66,6 +66,15 @@ margin-left: 8px; } } + + .ci-status-link { + + svg { + position: relative; + top: 2px; + margin: 0 2px 0 3px; + } + } } .ci-status-link { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 1b389d83525..4d9c73c6840 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -34,11 +34,4 @@ } } } - - .wiki { - code { - white-space: pre-wrap; - word-break: keep-all; - } - } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index abe8414e5e0..fcdcc837298 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -384,3 +384,10 @@ color: $gl-dark-link-color; } } + +.merge-request-details { + + .title { + margin-bottom: 20px; + } +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ce1c424624f..6fa097e3bf1 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -300,6 +300,17 @@ &.playable { background-color: $gray-light; + + svg { + height: 12px; + width: 12px; + position: relative; + top: 1px; + + path { + fill: $layout-link-gray; + } + } } .build-content { @@ -319,10 +330,6 @@ margin-right: 5px; } - .fa { - font-size: 13px; - } - // Connect first build in each stage with right horizontal line &:first-child { &::after { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 27dc2b2a1fa..eaf2d3270b3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -719,3 +719,29 @@ pre.light-well { width: 300px; } } + +.clearable-input { + position: relative; + + .clear-icon { + @extend .fa-times; + display: none; + position: absolute; + right: 7px; + top: 7px; + color: $location-icon-color; + + &:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + } + } + + &.has-value { + .clear-icon { + cursor: pointer; + display: block; + } + } +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 9e1dc15de84..6ef7cf0bae6 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -109,6 +109,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :sentry_dsn, :akismet_enabled, :akismet_api_key, + :koding_enabled, + :koding_url, :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 4ce18321649..cdfa8d91a28 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController end def members_update - @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) + @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user) redirect_to [:admin, @group], notice: 'Users were successfully added.' end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index c8390af3b36..d425d0f9014 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -2,6 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController before_action :find_todos, only: [:index, :destroy_all] def index + @sort = params[:sort] @todos = @todos.page(params[:page]) end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 9fc41a12536..272164cd0cc 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController end def create - @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) + @group.add_users( + params[:user_ids].split(','), + params[:access_level], + current_user: current_user, + expires_at: params[:expires_at] + ) redirect_to group_group_members_path(@group), notice: 'Users were successfully added.' end @@ -63,7 +68,7 @@ class Groups::GroupMembersController < Groups::ApplicationController protected def member_params - params.require(:group_member).permit(:access_level, :user_id) + params.require(:group_member).permit(:access_level, :user_id, :expires_at) end # MembershipActions concern diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb new file mode 100644 index 00000000000..bb89f3090f9 --- /dev/null +++ b/app/controllers/koding_controller.rb @@ -0,0 +1,15 @@ +class KodingController < ApplicationController + before_action :check_integration!, :authenticate_user!, :reject_blocked! + layout 'koding' + + def index + path = File.join(Rails.root, 'doc/integration/koding-usage.md') + @markdown = File.read(path) + end + + private + + def check_integration! + render_404 unless current_application_settings.koding_enabled? + end +end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 2d894b3dd4a..1a4f6b50e8f 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -12,7 +12,7 @@ module Projects only: [:iid, :title, :confidential], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority] } + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } }) end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 48fe81b0d74..2de8ada3e29 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,6 +15,13 @@ class Projects::BranchesController < Projects::ApplicationController diverging_commit_counts = repository.diverging_commit_counts(branch) [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max end + + respond_to do |format| + format.html + format.json do + render json: @repository.branch_names + end + end end def recent diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 606552fa853..d0c4550733c 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController return render_404 unless can?(current_user, :read_group, group) project.project_group_links.create( - group: group, group_access: params[:link_group_access] + group: group, + group_access: params[:link_group_access], + expires_at: params[:expires_at] ) redirect_to namespace_project_group_links_path(project.namespace, project) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 3435a118964..42a7e5a2c30 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def create - @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) + @project.team.add_users( + params[:user_ids].split(','), + params[:access_level], + expires_at: params[:expires_at], + current_user: current_user + ) redirect_to namespace_project_project_members_path(@project.namespace, @project) end @@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController protected def member_params - params.require(:project_member).permit(:user_id, :access_level) + params.require(:project_member).permit(:user_id, :access_level, :expires_at) end # MembershipActions concern diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb index 3334b8556df..79eb45568be 100644 --- a/app/finders/move_to_project_finder.rb +++ b/app/finders/move_to_project_finder.rb @@ -1,4 +1,6 @@ class MoveToProjectFinder + PAGE_SIZE = 50 + def initialize(user) @user = user end @@ -8,6 +10,10 @@ class MoveToProjectFinder projects = projects.search(search) if search.present? projects = projects.excluding_project(from_project) + # infinite scroll using offset + projects = projects.where('projects.id < ?', offset_id) if offset_id.present? + projects = projects.limit(PAGE_SIZE) + # to ask for Project#name_with_namespace projects.includes(namespace: :owner) end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 37bad596a16..06b3e8a9502 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -33,7 +33,7 @@ class TodosFinder # the project IDs yielded by the todos query thus far items = by_project(items) - items.reorder(id: :desc) + sort(items) end private @@ -106,6 +106,10 @@ class TodosFinder params[:type] end + def sort(items) + params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc) + end + def by_action(items) if action? items = items.where(action: to_action_id) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 78c0b79d2bd..6de25bea654 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -31,6 +31,10 @@ module ApplicationSettingsHelper current_application_settings.akismet_enabled? end + def koding_enabled? + current_application_settings.koding_enabled? + end + def allowed_protocols_present? current_application_settings.enabled_git_access_protocol.present? end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 9ea03720c1e..e13b7cdd707 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -217,4 +217,12 @@ module BlobHelper def gitlab_ci_ymls @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names end + + def blob_editor_paths + { + 'relative-url-root' => Rails.application.config.relative_url_root, + 'assets-prefix' => Gitlab::Application.config.assets.prefix, + 'blob-language' => @blob && @blob.language.try(:ace_mode) + } + end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 94df7d131ca..bb285a17baf 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -39,7 +39,7 @@ module CiStatusHelper when 'running' 'icon_status_running' when 'play' - return icon('play fw') + 'icon_play' when 'created' 'icon_status_pending' else diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 47d174361db..b9baeb1d6c4 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -72,6 +72,15 @@ module IssuablesHelper end end + def issuable_labels_tooltip(labels, limit: 5) + first, last = labels.partition.with_index{ |_, i| i < limit } + + label_names = first.collect(&:name) + label_names << "and #{last.size} more" unless last.empty? + + label_names.join(', ') + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 505545fbabb..249d18c4486 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -236,6 +236,60 @@ module ProjectsHelper ) end + def add_koding_stack_path(project) + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch || 'master', + file_name: '.koding.yml', + commit_message: "Add Koding stack script", + content: <<-CONTENT.strip_heredoc + provider: + aws: + access_key: '${var.aws_access_key}' + secret_key: '${var.aws_secret_key}' + resource: + aws_instance: + #{project.path}-vm: + instance_type: t2.nano + user_data: |- + + # Created by GitLab UI for :> + + echo _KD_NOTIFY_@Installing Base packages...@ + + apt-get update -y + apt-get install git -y + + echo _KD_NOTIFY_@Cloning #{project.name}...@ + + export KODING_USER=${var.koding_user_username} + export REPO_URL=#{root_url}${var.koding_queryString_repo}.git + export BRANCH=${var.koding_queryString_branch} + + sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH + + echo _KD_NOTIFY_@#{project.name} cloned.@ + CONTENT + ) + end + + def koding_project_url(project = nil, branch = nil, sha = nil) + if project + import_path = "/Home/Stacks/import" + + repo = project.path_with_namespace + branch ||= project.default_branch + sha ||= project.commit.short_id + + path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}" + + return URI.join(current_application_settings.koding_url, path).to_s + end + + current_application_settings.koding_url + end + def contribution_guide_path(project) if project && contribution_guide = project.repository.contribution_guide namespace_project_blob_path( diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 790001222f1..271e839692a 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -15,20 +15,9 @@ module TimeHelper "#{from.to_s(:short)} - #{to.to_s(:short)}" end - def duration_in_numbers(finished_at, started_at) - interval = interval_in_seconds(started_at, finished_at) - time_format = interval < 1.hour ? "%M:%S" : "%H:%M:%S" + def duration_in_numbers(duration) + time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S" - Time.at(interval).utc.strftime(time_format) - end - - private - - def interval_in_seconds(started_at, finished_at = nil) - if started_at && finished_at - finished_at.to_i - started_at.to_i - elsif started_at - Time.now.to_i - started_at.to_i - end + Time.at(duration).utc.strftime(time_format) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 07f703f205d..a49dd703926 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -166,40 +166,46 @@ class Ability end def project_abilities(user, project) - rules = [] key = "/user/#{user.id}/project/#{project.id}" - RequestStore.store[key] ||= begin - # 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) + 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) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 8c19d9dc9c8..f0bcb2d7cda 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -55,6 +55,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :akismet_enabled + validates :koding_url, + presence: true, + if: :koding_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -149,6 +153,8 @@ class ApplicationSetting < ActiveRecord::Base two_factor_grace_period: 48, recaptcha_enabled: false, akismet_enabled: false, + koding_enabled: false, + koding_url: nil, repository_checks_enabled: true, disabled_oauth_sign_in_sources: [], send_user_confirmation_email: false, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ed056a07a49..096b3b801af 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -62,6 +62,7 @@ module Ci status_event: 'enqueue' ) MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) + build.pipeline.mark_as_processable_after_stage(build.stage_idx) new_build end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index c360a6ff729..087abe4cbb1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -78,6 +78,10 @@ module Ci CommitStatus.where(pipeline: pluck(:id)).stages end + def self.total_duration + where.not(duration: nil).sum(:duration) + end + def stages_with_latest_statuses statuses.latest.order(:stage_idx).group_by(&:stage) end @@ -146,6 +150,10 @@ module Ci end end + def mark_as_processable_after_stage(stage_idx) + builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process) + end + def latest? return false unless ref commit = project.commit(ref) @@ -250,7 +258,7 @@ module Ci end def update_duration - self.duration = statuses.latest.duration + self.duration = calculate_duration end def execute_hooks diff --git a/app/models/commit.rb b/app/models/commit.rb index cc413448ce8..817d063e4a2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -229,7 +229,7 @@ class Commit def diff_refs Gitlab::Diff::DiffRefs.new( - base_sha: self.parent_id || self.sha, + base_sha: self.parent_id || Gitlab::Git::BLANK_SHA, head_sha: self.sha ) end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 703ca90edb6..84ceeac7d3e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -21,6 +21,7 @@ class CommitStatus < ActiveRecord::Base where(id: max_id.group(:name, :commit_id)) end + scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } @@ -30,6 +31,10 @@ class CommitStatus < ActiveRecord::Base transition [:created, :skipped] => :pending end + event :process do + transition skipped: :created + end + event :run do transition pending: :running end @@ -107,13 +112,7 @@ class CommitStatus < ActiveRecord::Base end def duration - duration = - if started_at && finished_at - finished_at - started_at - elsif started_at - Time.now - started_at - end - duration + calculate_duration end def stuck? diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb new file mode 100644 index 00000000000..be93435453b --- /dev/null +++ b/app/models/concerns/expirable.rb @@ -0,0 +1,15 @@ +module Expirable + extend ActiveSupport::Concern + + included do + scope :expired, -> { where('expires_at <= ?', Time.current) } + end + + def expires? + expires_at.present? + end + + def expires_soon? + expires_at < 7.days.from_now + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index cbae1cd439b..afb5ce37c06 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -131,7 +131,10 @@ module Issuable end def order_labels_priority(excluded_labels: []) - select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority"). + condition_field = "#{table_name}.id" + highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql + + select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). group(arel_table[:id]). reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end @@ -159,20 +162,6 @@ module Issuable grouping_columns end - - private - - def highest_label_priority(excluded_labels) - query = Label.select(Label.arel_table[:priority].minimum). - joins(:label_links). - where(label_links: { target_type: name }). - where("label_links.target_id = #{table_name}.id"). - reorder(nil) - - query.where.not(title: excluded_labels) if excluded_labels.present? - - query - end end def today? diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index 4be6a2f621b..a881fb83b7f 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -17,6 +17,10 @@ module NoteOnDiff raise NotImplementedError end + def original_line_code + raise NotImplementedError + end + def diff_attributes raise NotImplementedError end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 8b47b9e0abd..1ebecd86af9 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -35,5 +35,19 @@ module Sortable all end end + + private + + def highest_label_priority(object_types, condition_field, excluded_labels: []) + query = Label.select(Label.arel_table[:priority].minimum). + joins(:label_links). + where(label_links: { target_type: object_types }). + where("label_links.target_id = #{condition_field}"). + reorder(nil) + + query.where.not(title: excluded_labels) if excluded_labels.present? + + query + end end end diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb index 5d4b0a86899..750f937b724 100644 --- a/app/models/concerns/statuseable.rb +++ b/app/models/concerns/statuseable.rb @@ -35,11 +35,6 @@ module Statuseable all.pluck(self.status_sql).first end - def duration - duration_array = all.map(&:duration).compact - duration_array.reduce(:+) - end - def started_at all.minimum(:started_at) end @@ -85,4 +80,14 @@ module Statuseable def complete? COMPLETED_STATUSES.include?(status) end + + private + + def calculate_duration + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 52215f6e2ae..0c23c1c1934 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -16,6 +16,9 @@ class DiffNote < Note after_initialize :ensure_original_discussion_id before_validation :set_original_position, :update_position, on: :create before_validation :set_line_code, :set_original_discussion_id + # We need to do this again, because it's already in `Note`, but is affected by + # `update_position` and needs to run after that. + before_validation :set_discussion_id after_save :keep_around_commits class << self @@ -57,6 +60,10 @@ class DiffNote < Note diff_file.position(line) == self.original_position end + def original_line_code + self.diff_file.line_code(self.diff_line) + end + def active?(diff_refs = nil) return false unless supported? return true if for_commit? diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 3fddc084af2..9676bc03470 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -12,6 +12,7 @@ class Discussion :for_merge_request?, :line_code, + :original_line_code, :diff_file, :for_line?, :active?, diff --git a/app/models/group.rb b/app/models/group.rb index 37631b99701..c48869ae465 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -95,34 +95,40 @@ class Group < Namespace end end - def add_users(user_ids, access_level, current_user = nil) + def add_users(user_ids, access_level, current_user: nil, expires_at: nil) user_ids.each do |user_id| - Member.add_user(self.group_members, user_id, access_level, current_user) + Member.add_user( + self.group_members, + user_id, + access_level, + current_user: current_user, + expires_at: expires_at + ) end end - def add_user(user, access_level, current_user = nil) - add_users([user], access_level, current_user) + def add_user(user, access_level, current_user: nil, expires_at: nil) + add_users([user], access_level, current_user: current_user, expires_at: expires_at) end def add_guest(user, current_user = nil) - add_user(user, Gitlab::Access::GUEST, current_user) + add_user(user, Gitlab::Access::GUEST, current_user: current_user) end def add_reporter(user, current_user = nil) - add_user(user, Gitlab::Access::REPORTER, current_user) + add_user(user, Gitlab::Access::REPORTER, current_user: current_user) end def add_developer(user, current_user = nil) - add_user(user, Gitlab::Access::DEVELOPER, current_user) + add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user) end def add_master(user, current_user = nil) - add_user(user, Gitlab::Access::MASTER, current_user) + add_user(user, Gitlab::Access::MASTER, current_user: current_user) end def add_owner(user, current_user = nil) - add_user(user, Gitlab::Access::OWNER, current_user) + add_user(user, Gitlab::Access::OWNER, current_user: current_user) end def has_owner?(user) diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 8e26cbe9835..40277a9b139 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -49,6 +49,10 @@ class LegacyDiffNote < Note !line.meta? && diff_file.line_code(line) == self.line_code end + def original_line_code + self.line_code + end + # Check if this note is part of an "active" discussion # # This will always return true for anything except MergeRequest noteables, diff --git a/app/models/member.rb b/app/models/member.rb index 24ab1276ee9..64e0d33fb20 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,6 +1,7 @@ class Member < ActiveRecord::Base include Sortable include Importable + include Expirable include Gitlab::Access attr_accessor :raw_invite_token @@ -73,7 +74,7 @@ class Member < ActiveRecord::Base user end - def add_user(members, user_id, access_level, current_user = nil) + def add_user(members, user_id, access_level, current_user: nil, expires_at: nil) user = user_for_id(user_id) # `user` can be either a User object or an email to be invited @@ -87,6 +88,7 @@ class Member < ActiveRecord::Base if can_update_member?(current_user, member) || project_creator?(member, access_level) member.created_by ||= current_user member.access_level = access_level + member.expires_at = expires_at member.save end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 18e97c969d7..ec2d40eb11c 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -34,7 +34,7 @@ class ProjectMember < Member # :master # ) # - def add_users_to_projects(project_ids, user_ids, access, current_user = nil) + def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil) access_level = if roles_hash.has_key?(access) roles_hash[access] elsif roles_hash.values.include?(access.to_i) @@ -50,7 +50,13 @@ class ProjectMember < Member project = Project.find(project_id) users.each do |user| - Member.add_user(project.project_members, user, access_level, current_user) + Member.add_user( + project.project_members, + user, + access_level, + current_user: current_user, + expires_at: expires_at + ) end end end diff --git a/app/models/note.rb b/app/models/note.rb index 3bbf5db0b70..f2656df028b 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -259,6 +259,8 @@ class Note < ActiveRecord::Base def ensure_discussion_id return unless self.persisted? + # Needed in case the SELECT statement doesn't ask for `discussion_id` + return unless self.has_attribute?(:discussion_id) return if self.discussion_id set_discussion_id diff --git a/app/models/project.rb b/app/models/project.rb index 043da030468..1855760e694 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -611,7 +611,10 @@ class Project < ActiveRecord::Base end def new_issue_address(author) - if Gitlab::IncomingEmail.enabled? && author + # This feature is disabled for the time being. + return nil + + if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode Gitlab::IncomingEmail.reply_address( "#{path_with_namespace}+#{author.authentication_token}") end @@ -1003,8 +1006,8 @@ class Project < ActiveRecord::Base project_members.find_by(user_id: user) end - def add_user(user, access_level, current_user = nil) - team.add_user(user, access_level, current_user) + def add_user(user, access_level, current_user: nil, expires_at: nil) + team.add_user(user, access_level, current_user: current_user, expires_at: expires_at) end def default_branch diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index e52a6bd7c84..7613cbdea93 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -1,4 +1,6 @@ class ProjectGroupLink < ActiveRecord::Base + include Expirable + GUEST = 10 REPORTER = 20 DEVELOPER = 30 @@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base self.class.access_options.key(self.group_access) end - private + private def different_group if self.group && self.project && self.project.group == self.group diff --git a/app/models/project_team.rb b/app/models/project_team.rb index d0a714cd6fc..ab6ea2aae36 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -15,9 +15,9 @@ class ProjectTeam users, access, current_user = *args if users.respond_to?(:each) - add_users(users, access, current_user) + add_users(users, access, current_user: current_user) else - add_user(users, access, current_user) + add_user(users, access, current_user: current_user) end end @@ -33,17 +33,18 @@ class ProjectTeam member end - def add_users(users, access, current_user = nil) + def add_users(users, access, current_user: nil, expires_at: nil) ProjectMember.add_users_to_projects( [project.id], users, access, - current_user + current_user: current_user, + expires_at: expires_at ) end - def add_user(user, access, current_user = nil) - add_users([user], access, current_user) + def add_user(user, access, current_user: nil, expires_at: nil) + add_users([user], access, current_user: current_user, expires_at: expires_at) end # Remove all users from project team diff --git a/app/models/repository.rb b/app/models/repository.rb index 2494c266cd2..bdc3b9d1c1c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -277,7 +277,7 @@ class Repository def cache_keys %i(size commit_count readme version contribution_guide changelog - license_blob license_key gitignore) + license_blob license_key gitignore koding_yml) end # Keys for data on branch/tag operations. @@ -553,6 +553,14 @@ class Repository end end + def koding_yml + return nil unless head_exists? + + cache.fetch(:koding_yml) do + file_on_head(/\A\.koding\.yml\z/) + end + end + def gitlab_ci_yml return nil unless head_exists? diff --git a/app/models/todo.rb b/app/models/todo.rb index 8d7a5965aa1..6ae9956ade5 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,4 +1,6 @@ class Todo < ActiveRecord::Base + include Sortable + ASSIGNED = 1 MENTIONED = 2 BUILD_FAILED = 3 @@ -41,6 +43,23 @@ class Todo < ActiveRecord::Base after_save :keep_around_commit + class << self + def sort(method) + method == "priority" ? order_by_labels_priority : order_by(method) + end + + # Order by priority depending on which issue/merge request the Todo belongs to + # Todos with highest priority first then oldest todos + # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue" + def order_by_labels_priority + highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql + + select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). + order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). + order('todos.created_at') + end + end + def build_failed? action == BUILD_FAILED end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb new file mode 100644 index 00000000000..ca9db59cac7 --- /dev/null +++ b/app/services/members/authorized_destroy_service.rb @@ -0,0 +1,19 @@ +module Members + class AuthorizedDestroyService < BaseService + attr_accessor :member, :user + + def initialize(member, user = nil) + @member, @user = member, user + end + + def execute + return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) + + member.destroy + + if member.request? && member.user != user + notification_service.decline_access_request(member) + end + end + end +end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 9e3f6af628d..9a2bf82ef51 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -11,12 +11,7 @@ module Members unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) raise Gitlab::Access::AccessDeniedError end - - member.destroy - - if member.request? && member.user != current_user - notification_service.decline_access_request(member) - end + AuthorizedDestroyService.new(member, current_user).execute end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 66a838b3d13..6139ed56e25 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -242,7 +242,6 @@ class NotificationService project_member.real_source_type, project_member.project.id, project_member.invite_email, - project_member.access_level, project_member.created_by_id ).deliver_later end @@ -269,7 +268,6 @@ class NotificationService group_member.real_source_type, group_member.group.id, group_member.invite_email, - group_member.access_level, group_member.created_by_id ).deliver_later end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index c7fd344eea2..e0878512e62 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -388,6 +388,25 @@ .help-block If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. + %fieldset + %legend Koding + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :koding_enabled do + = f.check_box :koding_enabled + Enable Koding + .form-group + = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' + .help-block + Koding has integration enabled out of the box for the + %strong gitlab + team, and you need to provide that team's URL here. Learn more in the + = succeed "." do + = link_to "Koding integration documentation", help_page_path("integration/koding") + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 352adbedee4..f29d9c94441 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -51,7 +51,7 @@ - if build.duration %p.duration = custom_icon("icon_timer") - = duration_in_numbers(build.finished_at, build.started_at) + = duration_in_numbers(build.duration) - if build.finished_at %p.finished-at diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 4e340b6ec16..d320d3bcc1e 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -43,6 +43,25 @@ class: 'select2 trigger-submit', include_blank: true, data: {placeholder: 'Action'}) + .pull-right + .dropdown.inline.prepend-left-10 + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %li + = link_to todos_filter_path(sort: sort_value_priority) do + = sort_title_priority + = link_to todos_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to todos_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + + .prepend-top-default - if @todos.any? .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index b2e55f7647a..3a95a652810 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -7,7 +7,7 @@ .diff-content.code.js-syntax-highlight %table - - discussions = { discussion.line_code => discussion } + - discussions = { discussion.original_line_code => discussion } = render partial: "projects/diffs/line", collection: discussion.truncated_diff_lines, as: :line, diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index 9bb9f962177..2fb3190ab11 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -14,5 +14,14 @@ Read more about role permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .form-group + = f.label :expires_at, 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + %i.clear-icon.js-clear-input + .help-block + On this date, the user(s) will automatically lose access to this group and all of its projects. + .form-actions = f.submit 'Add users to group', class: "btn btn-create" diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index da71de4cd1e..742f9d7a433 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,2 +1,3 @@ :plain $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); + new MemberExpirationDate(); diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml new file mode 100644 index 00000000000..111cc67336c --- /dev/null +++ b/app/views/koding/index.html.haml @@ -0,0 +1,9 @@ +.row-content-block.second-block.center + %p + = icon('circle', class: 'cgreen') + Integration is active for + = link_to koding_project_url, target: '_blank' do + #{current_application_settings.koding_url} + +.documentation.wiki + = markdown @markdown diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml new file mode 100644 index 00000000000..22319bba745 --- /dev/null +++ b/app/views/layouts/koding.html.haml @@ -0,0 +1,5 @@ +- page_title "Koding" +- page_description "Koding Dashboard" +- header_title "Koding", koding_path + += render template: "layouts/application" diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 3a14751ea8e..67f558c854b 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -12,6 +12,11 @@ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do %span Activity + - if koding_enabled? + = nav_link(controller: :koding) do + = link_to koding_path, title: 'Koding' do + %span + Koding = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do %span diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 1d3b8fc3683..f7012595a5a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -65,7 +65,7 @@ Graphs - if project_nav_tab? :issues - = nav_link(controller: [:issues, :labels, :milestones]) do + = nav_link(controller: [:issues, :labels, :milestones, :boards]) do = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do %span Issues diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 7b0621f9401..680e95ac6b5 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,4 +1,7 @@ - page_title "Edit", @blob.path, @ref +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') + = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') - if @conflict .alert.alert-danger @@ -16,14 +19,10 @@ = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do = editing_preview_title(@blob.name) - = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit_sha', @last_commit_sha = hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) - -:javascript - blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") - new NewCommitForm($('.js-edit-blob-form')) diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index c952bc7e5db..b6ed9518c48 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,17 +1,16 @@ - page_title "New File", @path.presence, @ref +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') + = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') %h3.page-title New File .file-editor - = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do + = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref = render 'shared/new_commit_form', placeholder: "Add new file" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) - -:javascript - blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}") - new NewCommitForm($('.js-new-blob-form')) diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index f8ebf397ee2..de53a298f84 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -11,7 +11,6 @@ .board-inner %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } - = icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)") {{ list.title }} %span.pull-right{ "v-if" => "list.type !== 'blank'" } {{ list.issues.length }} diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index b20c23f6b8e..e8b60b54d80 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -9,7 +9,6 @@ "track-by" => "id" } %li.card{ ":class" => "{ 'user-can-drag': !disabled }", ":index" => "index" } - = icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled") %h4.card-title = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") %a{ ":href" => "issueLinkBase + '/' + issue.id", diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 4bd85061240..6192ccb710b 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -5,8 +5,8 @@ - number_commits_ahead = diverging_commit_counts[:ahead] %li(class="js-branch-#{branch.name}") %div - = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do - %span.item-title.str-truncated= branch.name + = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do + = branch.name   - if branch.name == @repository.root_ref %span.label.label-primary default diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml new file mode 100644 index 00000000000..fdc80d44253 --- /dev/null +++ b/app/views/projects/buttons/_koding.html.haml @@ -0,0 +1,7 @@ +- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch) + - if @repository.koding_yml + = link_to koding_project_url(@project), class: 'btn', target: '_blank' do + Run in IDE (Koding) + - else + = link_to add_koding_stack_path(@project), class: 'btn' do + Set Up Koding diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 91081435220..1fdf32466f2 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -63,7 +63,7 @@ - if build.duration %p.duration = custom_icon("icon_timer") - = duration_in_numbers(build.finished_at, build.started_at) + = duration_in_numbers(build.duration) - if build.finished_at %p.finished-at = icon("calendar") diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index be387201f8d..9a672b23341 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -48,10 +48,10 @@ \- %td - - if pipeline.started_at && pipeline.finished_at + - if pipeline.duration %p.duration = custom_icon("icon_timer") - = duration_in_numbers(pipeline.finished_at, pipeline.started_at) + = duration_in_numbers(pipeline.duration) - if pipeline.finished_at %p.finished-at = icon("calendar") diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index d9b800a4ded..e4cd55b9f7a 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -17,7 +17,9 @@ .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 - = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch" + = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch' + = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }}) + - if can?(current_user, :push_code, @project) .js-create-merge-request-container .checkbox diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 3ad866bb2f1..29d767e7769 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -56,10 +56,10 @@ = pluralize(@commit.pipelines.count, 'pipeline') = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do = ci_icon_for_status(@commit.status) - = ci_label_for_status(@commit.status) - - if @commit.pipelines.duration - in - = time_interval_in_words @commit.pipelines.duration + %span.ci-status-label + = ci_label_for_status(@commit.status) + in + = time_interval_in_words @commit.pipelines.total_duration .commit-box.content-block %h3.commit-title diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml index 2b904544f28..ca700cb3a3b 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/index.html.haml @@ -17,6 +17,13 @@ .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" %span.caret + .form-group + = label_tag :expires_at, 'Access expiration date', class: 'label-light' + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + %i.clear-icon.js-clear-input + .help-block + On this date, all users in the group will automatically lose access to this project. = submit_tag "Share", class: "btn btn-create" .col-lg-9.col-lg-offset-3 %hr @@ -35,6 +42,10 @@ = group.name %br up to #{group_link.human_access} + - if group_link.expires? + · + %span{ class: ('text-warning' if group_link.expires_soon?) } + expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .pull-right = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do %span.sr-only disable sharing diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 9f1a046ea74..3fb4191c60e 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -22,7 +22,7 @@ - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue) .issuable-actions .clearfix.issue-btn-group.dropdown - %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } } + %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } %span.caret Options .dropdown-menu.dropdown-menu-align-right.hidden-lg diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f8025fc1dbe..9d8b4cc56be 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -16,6 +16,9 @@ - if @merge_request.open? .pull-right - if @merge_request.source_branch_exists? + - if koding_enabled? && @repository.koding_yml + = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do + Run in IDE (Koding) = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do Check out branch 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 b24bdf22ceb..098ce19da21 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -14,7 +14,7 @@ - if can?(current_user, :update_merge_request, @merge_request) .issuable-actions .clearfix.issue-btn-group.dropdown - %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } } + %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } %span.caret Options .dropdown-menu.dropdown-menu-align-right.hidden-lg diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 8289aefcde7..063e83a407a 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -9,7 +9,7 @@ = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" - if @pipeline.duration in - = time_interval_in_words @pipeline.duration + = time_interval_in_words(@pipeline.duration) .pull-right = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 978c4dfc5ec..fa8cbf71733 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -14,5 +14,14 @@ Read more about role permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .form-group + = f.label :expires_at, 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + %i.clear-icon.js-clear-input + .help-block + On this date, the user(s) will automatically lose access to this project. + .form-actions = f.submit 'Add users to project', class: "btn btn-create" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9031f01b496..9d063b3081f 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,6 +1,6 @@ - page_title "Members" -.project-members-page.prepend-top-default +.project-members-page.js-project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) .panel.panel-default .panel-heading diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 45f8ef89060..833954bc039 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,2 +1,3 @@ :plain $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); + new MemberExpirationDate(); diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index d4c6fa24768..e95a3b1b4c3 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -22,16 +22,20 @@ %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } Allowed to merge: .col-md-10 - = dropdown_tag('Select', - options: { toggle_class: 'js-allowed-to-merge wide', - data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }}) + .merge_access_levels-container + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-merge wide', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }}) .form-group %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } Allowed to push: .col-md-10 - = dropdown_tag('Select', - options: { toggle_class: 'js-allowed-to-push wide', - data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) + .push_access_levels-container + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-push wide', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) .panel-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 835398b6f98..33d5cbff420 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -1,18 +1,20 @@ +- @no_container = true - page_title "Edit", @tag.name, "Tags" = render "projects/commits/head" -.row-content-block - .oneline - .title - Release notes for tag - %strong #{@tag.name} +%div{ class: container_class } + .sub-header-block.no-bottom-space + .oneline + .title + Release notes for tag + %strong #{@tag.name} + -.prepend-top-default = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' .error-alert - .form-actions.prepend-top-default + .prepend-top-default = f.submit 'Save changes', class: 'btn btn-save' = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index a666d07e9eb..340e159c874 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -64,10 +64,12 @@ %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do Set Up CI + %li.project-repo-buttons-right .project-repo-buttons.project-right-buttons - if current_user = render 'shared/members/access_request_buttons', source: @project + = render "projects/buttons/koding" .btn-group.project-repo-btn-group = render "projects/buttons/download" @@ -86,4 +88,4 @@ Archived project! Repository is read-only %div{class: "project-show-#{default_project_view}"} - = render default_project_view \ No newline at end of file + = render default_project_view diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index ea7162d4d63..9a8252ab087 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -6,7 +6,7 @@ - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } = dropdown_title "Switch branch/tag" = dropdown_filter "Search branches and tags" diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg new file mode 100644 index 00000000000..80a6d41dbf6 --- /dev/null +++ b/app/views/shared/icons/_icon_play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index d717c3d92ee..544ed6203aa 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -121,7 +121,7 @@ = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 .issuable-form-select-holder - = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) } + = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }   %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', 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.' } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 8e2fcbdfab8..c1b50e65af5 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -109,7 +109,7 @@ - if issuable.project.labels.any? .block.labels - .sidebar-collapsed-icon + .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } = icon('tags') %span = issuable.labels_array.size diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index fc6e206d082..5f20e4bd42a 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -16,7 +16,7 @@ = button_tag icon('pencil'), type: 'button', class: 'btn inline js-toggle-button', - title: 'Edit access level' + title: 'Edit' - if member.request? = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), @@ -59,6 +59,10 @@ = time_ago_with_tooltip(member.requested_at) - else Joined #{time_ago_with_tooltip(member.created_at)} + - if member.expires? + · + %span{ class: ('text-warning' if member.expires_soon?) } + Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else = image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: '' @@ -73,8 +77,16 @@ - if show_roles .edit-member.hide.js-toggle-content %br - = form_for member, remote: true do |f| - .prepend-top-10 - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control' + = form_for member, remote: true, html: { class: 'form-horizontal' } do |f| + .form-group + = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label' + .col-sm-10 + = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}" + .form-group + = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}" + %i.clear-icon.js-clear-input .prepend-top-10 = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 47ec09f62c6..0c788032020 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,3 +1,7 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') + = page_specific_javascript_tag('snippet/snippet_bundle.js') + .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| = form_errors(@snippet) @@ -31,8 +35,3 @@ - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" -:javascript - var editor = ace.edit("editor"); - $(".snippet-form-holder form").submit(function(){ - $(".snippet-file-content").val(editor.getValue()); - }); diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index c6a5af2809a..1dc7e0adef7 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,13 +33,13 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - compare = CompareService.new.execute(project, before_sha, project, after_sha) + compare = CompareService.new.execute(project, after_sha, project, before_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = CompareService.new.execute(project, after_sha, project, before_sha) + compare = CompareService.new.execute(project, before_sha, project, after_sha) diff_refs = compare.diff_refs reverse_compare = true diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb new file mode 100644 index 00000000000..246c8b6650a --- /dev/null +++ b/app/workers/remove_expired_group_links_worker.rb @@ -0,0 +1,7 @@ +class RemoveExpiredGroupLinksWorker + include Sidekiq::Worker + + def perform + ProjectGroupLink.expired.destroy_all + end +end diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb new file mode 100644 index 00000000000..cf765af97ce --- /dev/null +++ b/app/workers/remove_expired_members_worker.rb @@ -0,0 +1,13 @@ +class RemoveExpiredMembersWorker + include Sidekiq::Worker + + def perform + Member.expired.find_each do |member| + begin + Members::AuthorizedDestroyService.new(member).execute + rescue => ex + logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}") + end + end + end +end diff --git a/config/application.rb b/config/application.rb index 6b80f8ddafa..4792f6670a8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -88,6 +88,8 @@ module Gitlab config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" + config.assets.precompile << "blob_edit/blob_edit_bundle.js" + config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index deac3b0f0f9..7a9376def02 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -293,6 +293,12 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker' +Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *' +Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker' +Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' +Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker' # # GitLab Shell diff --git a/config/routes.rb b/config/routes.rb index 66f77aee06a..e93b640fbc0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,6 +90,11 @@ Rails.application.routes.draw do get 'help/ui' => 'help#ui' get 'help/*path' => 'help#show', as: :help_page + # + # Koding route + # + get 'koding' => 'koding#index' + # # Global snippets # diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_pipelines.rb similarity index 51% rename from db/fixtures/development/14_builds.rb rename to db/fixtures/development/14_pipelines.rb index 069d9dd6226..49e6e2361b1 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -1,4 +1,4 @@ -class Gitlab::Seeder::Builds +class Gitlab::Seeder::Pipelines STAGES = %w[build test deploy notify] BUILDS = [ { name: 'build:linux', stage: 'build', status: :success }, @@ -7,11 +7,12 @@ class Gitlab::Seeder::Builds { name: 'rspec:windows', stage: 'test', status: :success }, { name: 'rspec:windows', stage: 'test', status: :success }, { name: 'rspec:osx', stage: 'test', status_event: :success }, - { name: 'spinach:linux', stage: 'test', status: :pending }, - { name: 'spinach:osx', stage: 'test', status: :canceled }, - { name: 'cucumber:linux', stage: 'test', status: :running }, - { name: 'cucumber:osx', stage: 'test', status: :failed }, - { name: 'staging', stage: 'deploy', environment: 'staging', status: :success }, + { name: 'spinach:linux', stage: 'test', status: :success }, + { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true}, + { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending }, + { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running }, + { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled }, + { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success }, { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped }, { name: 'slack', stage: 'notify', when: 'manual', status: :created }, ] @@ -34,72 +35,86 @@ class Gitlab::Seeder::Builds end end + private + def pipelines - master_pipelines + merge_request_pipelines + create_master_pipelines + create_merge_request_pipelines end - def master_pipelines - create_pipelines_for(@project, 'master') - rescue - [] - end - - def merge_request_pipelines - @project.merge_requests.last(5).map do |merge_request| - create_pipelines(merge_request.source_project, merge_request.source_branch, merge_request.commits.last(5)) - end.flatten - rescue - [] - end - - def create_pipelines_for(project, ref) - commits = project.repository.commits(ref, limit: 5) - create_pipelines(project, ref, commits) - end - - def create_pipelines(project, ref, commits) - commits.map do |commit| - project.pipelines.create(sha: commit.id, ref: ref) + def create_master_pipelines + @project.repository.commits('master', limit: 4).map do |commit| + create_pipeline!(@project, 'master', commit) end + rescue + [] + end + + def create_merge_request_pipelines + pipelines = @project.merge_requests.first(3).map do |merge_request| + project = merge_request.source_project + branch = merge_request.source_branch + + merge_request.commits.last(4).map do |commit| + create_pipeline!(project, branch, commit) + end + end + + pipelines.flatten + rescue + [] + end + + + def create_pipeline!(project, ref, commit) + project.pipelines.create(sha: commit.id, ref: ref) end def build_create!(pipeline, opts = {}) - attributes = build_attributes_for(pipeline, opts) + attributes = job_attributes(pipeline, opts) + .merge(commands: '$ build command') - Ci::Build.create!(attributes) do |build| - if opts[:name].start_with?('build') - artifacts_cache_file(artifacts_archive_path) do |file| - build.artifacts_file = file - end + Ci::Build.create!(attributes).tap do |build| + # We need to set build trace and artifacts after saving a build + # (id required), that is why we need `#tap` method instead of passing + # block directly to `Ci::Build#create!`. - artifacts_cache_file(artifacts_metadata_path) do |file| - build.artifacts_metadata = file - end - end + setup_artifacts(build) + setup_build_log(build) + build.save + end + end - if %w(running success failed).include?(build.status) - # We need to set build trace after saving a build (id required) - build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") - end + def setup_artifacts(build) + return unless %w[build test].include?(build.stage) + + artifacts_cache_file(artifacts_archive_path) do |file| + build.artifacts_file = file + end + + artifacts_cache_file(artifacts_metadata_path) do |file| + build.artifacts_metadata = file + end + end + + def setup_build_log(build) + if %w(running success failed).include?(build.status) + build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") end end def commit_status_create!(pipeline, opts = {}) - attributes = commit_status_attributes_for(pipeline, opts) + attributes = job_attributes(pipeline, opts) + GenericCommitStatus.create!(attributes) end - def commit_status_attributes_for(pipeline, opts) + def job_attributes(pipeline, opts) { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline, created_at: Time.now, updated_at: Time.now }.merge(opts) end - def build_attributes_for(pipeline, opts) - commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command') - end - def build_user @project.team.users.sample end @@ -131,8 +146,8 @@ class Gitlab::Seeder::Builds end Gitlab::Seeder.quiet do - Project.all.sample(10).each do |project| - project_builds = Gitlab::Seeder::Builds.new(project) + Project.all.sample(5).each do |project| + project_builds = Gitlab::Seeder::Pipelines.new(project) project_builds.seed! end end diff --git a/db/migrate/20160801163421_add_expires_at_to_member.rb b/db/migrate/20160801163421_add_expires_at_to_member.rb new file mode 100644 index 00000000000..8db0fc60c4b --- /dev/null +++ b/db/migrate/20160801163421_add_expires_at_to_member.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddExpiresAtToMember < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :members, :expires_at, :date + end +end diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb new file mode 100644 index 00000000000..915d3d78e40 --- /dev/null +++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb @@ -0,0 +1,10 @@ +class AddKodingToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :koding_enabled, :boolean + add_column :application_settings, :koding_url, :string + end +end diff --git a/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb new file mode 100644 index 00000000000..0ed538b0df8 --- /dev/null +++ b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddExpiresAtToProjectGroupLinks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :project_group_links, :expires_at, :date + end +end diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb new file mode 100644 index 00000000000..b6e8bb18e7b --- /dev/null +++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToNoteDiscussionId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :notes, :discussion_id + end +end diff --git a/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb new file mode 100644 index 00000000000..0c68cf01900 --- /dev/null +++ b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ResetDiffNoteDiscussionIdBecauseItWasCalculatedWrongly < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute "UPDATE notes SET discussion_id = NULL WHERE discussion_id IS NOT NULL AND type = 'DiffNote'" + end +end diff --git a/db/schema.rb b/db/schema.rb index c74d4688a7d..4947745b232 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160817154936) do +ActiveRecord::Schema.define(version: 20160819221833) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -90,6 +90,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.string "enabled_git_access_protocol" t.boolean "domain_blacklist_enabled", default: false t.text "domain_blacklist" + t.boolean "koding_enabled" + t.string "koding_url" end create_table "audit_events", force: :cascade do |t| @@ -569,6 +571,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.string "invite_token" t.datetime "invite_accepted_at" t.datetime "requested_at" + t.date "expires_at" end add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree @@ -701,6 +704,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree + add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree @@ -785,6 +789,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.datetime "created_at" t.datetime "updated_at" t.integer "group_access", default: 30, null: false + t.date "expires_at" end create_table "project_import_data", force: :cascade do |t| @@ -1151,4 +1156,4 @@ ActiveRecord::Schema.define(version: 20160817154936) do add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" add_foreign_key "u2f_registrations", "users" -end +end \ No newline at end of file diff --git a/doc/api/members.md b/doc/api/members.md index d002e6eaf89..fd6d728dad2 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -86,7 +86,8 @@ Example response: "name": "Raymond Smith", "state": "active", "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 + "access_level": 30, + "expires_at": null } ``` @@ -106,6 +107,7 @@ POST /projects/:id/members | `id` | integer/string | yes | The group/project ID or path | | `user_id` | integer | yes | The user ID of the new member | | `access_level` | integer | yes | A valid access level | +| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30 @@ -141,6 +143,7 @@ PUT /projects/:id/members/:user_id | `id` | integer/string | yes | The group/project ID or path | | `user_id` | integer | yes | The user ID of the member | | `access_level` | integer | yes | A valid access level | +| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40 diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index d90d7aca4fd..20cd88c8d20 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -67,6 +67,8 @@ use following Markdown code to embed the est coverage report into `README.md`: ![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage) ``` +The latest successful pipeline will be used to read the test coverage value. + [builds]: #builds [jobs]: yaml/README.md#jobs [stages]: yaml/README.md#stages diff --git a/doc/integration/README.md b/doc/integration/README.md index ddbd570ac6c..70895abbcad 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services. - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users - [Akismet](akismet.md) Configure Akismet to stop spam +- [Koding](koding.md) Configure Koding to use IDE integration GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/doc/integration/img/koding_build-in-progress.png b/doc/integration/img/koding_build-in-progress.png new file mode 100644 index 00000000000..f8cc81834c4 Binary files /dev/null and b/doc/integration/img/koding_build-in-progress.png differ diff --git a/doc/integration/img/koding_build-logs.png b/doc/integration/img/koding_build-logs.png new file mode 100644 index 00000000000..a04cd5aff99 Binary files /dev/null and b/doc/integration/img/koding_build-logs.png differ diff --git a/doc/integration/img/koding_build-success.png b/doc/integration/img/koding_build-success.png new file mode 100644 index 00000000000..2a0dd296480 Binary files /dev/null and b/doc/integration/img/koding_build-success.png differ diff --git a/doc/integration/img/koding_commit-koding.yml.png b/doc/integration/img/koding_commit-koding.yml.png new file mode 100644 index 00000000000..3e133c50327 Binary files /dev/null and b/doc/integration/img/koding_commit-koding.yml.png differ diff --git a/doc/integration/img/koding_different-stack-on-mr-try.png b/doc/integration/img/koding_different-stack-on-mr-try.png new file mode 100644 index 00000000000..fd25e32f648 Binary files /dev/null and b/doc/integration/img/koding_different-stack-on-mr-try.png differ diff --git a/doc/integration/img/koding_edit-on-ide.png b/doc/integration/img/koding_edit-on-ide.png new file mode 100644 index 00000000000..fd5aaff75f5 Binary files /dev/null and b/doc/integration/img/koding_edit-on-ide.png differ diff --git a/doc/integration/img/koding_enable-koding.png b/doc/integration/img/koding_enable-koding.png new file mode 100644 index 00000000000..c0ae0ee9918 Binary files /dev/null and b/doc/integration/img/koding_enable-koding.png differ diff --git a/doc/integration/img/koding_landing.png b/doc/integration/img/koding_landing.png new file mode 100644 index 00000000000..7c629d9b05e Binary files /dev/null and b/doc/integration/img/koding_landing.png differ diff --git a/doc/integration/img/koding_open-gitlab-from-koding.png b/doc/integration/img/koding_open-gitlab-from-koding.png new file mode 100644 index 00000000000..c958cf8f224 Binary files /dev/null and b/doc/integration/img/koding_open-gitlab-from-koding.png differ diff --git a/doc/integration/img/koding_run-in-ide.png b/doc/integration/img/koding_run-in-ide.png new file mode 100644 index 00000000000..f91ee0f74cc Binary files /dev/null and b/doc/integration/img/koding_run-in-ide.png differ diff --git a/doc/integration/img/koding_run-mr-in-ide.png b/doc/integration/img/koding_run-mr-in-ide.png new file mode 100644 index 00000000000..502817a2a46 Binary files /dev/null and b/doc/integration/img/koding_run-mr-in-ide.png differ diff --git a/doc/integration/img/koding_set-up-ide.png b/doc/integration/img/koding_set-up-ide.png new file mode 100644 index 00000000000..7f408c980b5 Binary files /dev/null and b/doc/integration/img/koding_set-up-ide.png differ diff --git a/doc/integration/img/koding_stack-import.png b/doc/integration/img/koding_stack-import.png new file mode 100644 index 00000000000..2a4e3c87fc8 Binary files /dev/null and b/doc/integration/img/koding_stack-import.png differ diff --git a/doc/integration/img/koding_start-build.png b/doc/integration/img/koding_start-build.png new file mode 100644 index 00000000000..52159440f62 Binary files /dev/null and b/doc/integration/img/koding_start-build.png differ diff --git a/doc/integration/koding-usage.md b/doc/integration/koding-usage.md new file mode 100644 index 00000000000..bb74badce66 --- /dev/null +++ b/doc/integration/koding-usage.md @@ -0,0 +1,122 @@ +# Koding & GitLab + +This document will guide you through using Koding integration on GitLab in +detail. For configuring and installing please follow [this](koding.md) guide. + +You can use Koding integration to run and develop your projects on GitLab. This +will allow you and the users to test your project without leaving the browser. +Koding handles projects as stacks which are basic recipes to define your +environment for your project. With this integration you can automatically +create a proper stack template for your projects. Currently auto-generated +stack templates are designed to work with AWS which requires a valid AWS +credential to be able to use these stacks. You can find more information about +stacks and the other providers that you can use on Koding from +[here](https://www.koding.com/docs). + + +# Enable Integration + +You can enable Koding integration by providing the running Koding instance URL +in Application Settings; + + - Open **Admin area > Settings** (`/admin/application_settings`). + +![Enable Koding](help/integration/img/koding_enable-koding.png) + +Once enabled you will see `Koding` link on your sidebar which leads you to +Koding Landing page + +![Koding Landing](help/integration/img/koding_landing.png) + +You can navigate to running Koding instance from here. For more information and +details about configuring integration please follow [this](koding.md) guide. + + +# Set up Koding on Projects + +Once it's enabled, you will see some integration buttons on Project pages, +Merge Requests etc. To get started working on a specific project you first need +to create a `.koding.yml` file under your project root. You can easily do that +by using `Set Up Koding` button which will be visible on every project's +landing page; + +![Set Up Koding](help/integration/img/koding_set-up-ide.png) + +Once you click this will open a New File page on GitLab with auto-generated +`.koding.yml` content based on your server and repository configuration. + +![Commit .koding.yml](help/integration/img/koding_commit-koding.yml.png) + + +# Run a project on Koding + +If there is `.koding.yml` exists in your project root, you will see +`Run in IDE (Koding)` button in your project landing page. You can initiate the +process from here. + +![Run on Koding](help/integration/img/koding_run-in-ide.png) + +This will open Koding defined in the settings in a new window and will start +importing the project's stack file; + +![Import Stack](help/integration/img/koding_stack-import.png) + +You should see the details of your repository imported into your Koding +instance. Once it's completed it will lead you to the Stack Editor and from +there you can start using your new stack integrated with your project on your +GitLab instance. For details about what's next you can follow +[this](https://www.koding.com/docs/creating-an-aws-stack) guide from 8. step. + +Once stack initialized you will see the `README.md` content from your project +in `Stack Build` wizard, this wizard will let you to build the stack and import +your project into it. **Once it's completed it will automatically open the +related vm instead of importing from scratch** + +![Stack Building](help/integration/img/koding_start-build.png) + +This will take time depending on the required environment. + +![Stack Building in Progress](help/integration/img/koding_build-in-progress.png) + +It usually takes ~4 min. to make it ready with a `t2.nano` instance on given +AWS region. (`t2.nano` is default vm type on auto-generated stack template +which can be manually changed) + +![Stack Building Success](help/integration/img/koding_build-success.png) + +You can check out the `Build Logs` from this success modal as well; + +![Stack Build Logs](help/integration/img/koding_build-logs.png) + +You can now `Start Coding`! + +![Edit On IDE](help/integration/img/koding_edit-on-ide.png) + + +# Try a Merge Request on IDE + +It's also possible to try a change on IDE before merging it. This flow only +enabled if the target project has `.koding.yml` in it's target branch. You +should see the alternative version of `Run in IDE (Koding)` button in merge +request pages as well; + +![Run in IDE on MR](help/integration/img/koding_run-mr-in-ide.png) + +This will again take you to Koding with proper arguments passed, which will +allow Koding to modify the stack template provided by target branch. You can +see the difference; + +![Different Branch for MR](help/integration/img/koding_different-stack-on-mr-try.png) + +The flow for the branch stack is also same with the regular project flow. + + +# Open GitLab from Koding + +Since stacks generated with import flow defined in previous steps, they have +information about the repository they are belonging to. By using this +information you can access to related GitLab page from stacks on your sidebar +on Koding. + +![Open GitLab from Koding](help/integration/img/koding_open-gitlab-from-koding.png) + diff --git a/doc/integration/koding.md b/doc/integration/koding.md new file mode 100644 index 00000000000..53450b6d048 --- /dev/null +++ b/doc/integration/koding.md @@ -0,0 +1,239 @@ +# Koding & GitLab + +This document will guide you through installing and configuring Koding with +GitLab. + +First of all, to be able to use Koding and GitLab together you will need public +access to your server. This allows you to use single sign-on from GitLab to +Koding and using vms from cloud providers like AWS. Koding has a registry for +VMs, called Kontrol and it runs on the same server as Koding itself, VMs from +cloud providers register themselves to Kontrol via the agent that we put into +provisioned VMs. This agent is called Klient and it provides Koding to access +and manage the target machine. + +Kontrol and Klient are based on another technology called +[Kite](github.com/koding/kite), that we have written at Koding. Which is a +microservice framework that allows you to develop microservices easily. + + +## Requirements + +### Hardware + +Minimum requirements are; + + - 2 cores CPU + - 3G RAM + - 10G Storage + +If you plan to use AWS to install Koding it is recommended that you use at +least a `c3.xlarge` instance. + +### Software + + - [git](https://git-scm.com) + - [docker](https://www.docker.com) + - [docker-compose](https://www.docker.com/products/docker-compose) + +Koding can run on most of the UNIX based operating systems, since it's shipped +as containerized with Docker support, it can work on any operating system that +supports Docker. + +Required services are; + + - PostgreSQL # Kontrol and Service DB provider + - MongoDB # Main DB provider the application + - Redis # In memory DB used by both application and services + - RabbitMQ # Message Queue for both application and services + +which are also provided as a Docker container by Koding. + + +## Getting Started with Development Versions + + +### Koding + +You can run `docker-compose` environment for developing koding by +executing commands in the following snippet. + +```bash +git clone https://github.com/koding/koding.git +cd koding +docker-compose up +``` + +This should start koding on `localhost:8090`. + +By default there is no team exists in Koding DB. You'll need to create a team +called `gitlab` which is the default team name for GitLab integration in the +configuration. To make things in order it's recommended to create the `gitlab` +team first thing after setting up Koding. + + +### GitLab + +To install GitLab to your environment for development purposes it's recommended +to use GitLab Development Kit which you can get it from +[here](https://gitlab.com/gitlab-org/gitlab-development-kit). + +After all those steps, gitlab should be running on `localhost:3000` + + +## Integration + +Integration includes following components; + + - Single Sign On with OAuth from GitLab to Koding + - System Hook integration for handling GitLab events on Koding + (`project_created`, `user_joined` etc.) + - Service endpoints for importing/executing stacks from GitLab to Koding + (`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs) + +As it's pointed out before, you will need public access to this machine that +you've installed Koding and GitLab on. Better to use a domain but a static IP +is also fine. + +For IP based installation you can use [xip.io](https://xip.io) service which is +free and provides DNS resolution to IP based requests like following; + + - 127.0.0.1.xip.io -> resolves to 127.0.0.1 + - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1 + - and so on... + +As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for +a running koding instance on `127.0.0.1` server will be handled as `foo` team +requests. + + +### GitLab Side + +You need to enable Koding integration from Settings under Admin Area. To do +that login with an Admin account and do followings; + + - open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings) + - scroll to bottom of the page until Koding section + - check `Enable Koding` checkbox + - provide GitLab team page for running Koding instance as `Koding URL`* + +* For `Koding URL` you need to provide the gitlab integration enabled team on +your Koding installation. Team called `gitlab` has integration on Koding out +of the box, so if you didn't change anything your team on Koding should be +`gitlab`. + +So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs +to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host +with your Koding installation here. + + +#### Registering Koding for OAuth integration + +We need `Application ID` and `Secret` to enable login to Koding via GitLab +feature and to do that you need to register running Koding as a new application +to your running GitLab application. Follow +[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to +enable this integration. + +Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback` +which again you need to _replace `127.0.0.1` with your instance public IP._ + +Take a copy of `Application ID` and `Secret` that is generated by the GitLab +application, we will need those on _Koding Part_ of this guide. + + +#### Registering system hooks to Koding (optional) + +Koding can take actions based on the events generated by GitLab application. +This feature is still in progress and only following events are processed by +Koding at the moment; + + - user_create + - user_destroy + +All system events are handled but not implemented on Koding side. + +To enable this feature you need to provide a `URL` and a `Secret Token` to your +GitLab application. Open your admin area on your GitLab app from +[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks) +and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the +endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and +keep a copy of it, we will need it on _Koding Part_ of this guide. + +_(replace `127.0.0.1` with your instance public IP)_ + + +### Koding Part + +If you followed the steps in GitLab part we should have followings to enable +Koding part integrations; + + - `Application ID` and `Secret` for OAuth integration + - `Secret Token` for system hook integration + - Public address of running GitLab instance + + +#### Start Koding with GitLab URL + +Now we need to configure Koding with all this information to get things ready. +If it's already running please stop koding first. + +##### From command-line + +Replace followings with the ones you got from GitLab part of this guide; + +```bash +cd koding +docker-compose run \ + --service-ports backend \ + /opt/koding/scripts/bootstrap-container build \ + --host=**YOUR_IP**.xip.io \ + --gitlabHost=**GITLAB_IP** \ + --gitlabPort=**GITLAB_PORT** \ + --gitlabToken=**SECRET_TOKEN** \ + --gitlabAppId=**APPLICATION_ID** \ + --gitlabAppSecret=**SECRET** +``` + +##### By updating configuration + +Alternatively you can update `gitlab` section on +`config/credentials.default.coffee` like following; + +``` +gitlab = + host: '**GITLAB_IP**' + port: '**GITLAB_PORT**' + applicationId: '**APPLICATION_ID**' + applicationSecret: '**SECRET**' + team: 'gitlab' + redirectUri: '' + systemHookToken: '**SECRET_TOKEN**' + hooksEnabled: yes +``` + +and start by only providing the `host`; + +```bash +cd koding +docker-compose run \ + --service-ports backend \ + /opt/koding/scripts/bootstrap-container build \ + --host=**YOUR_IP**.xip.io \ +``` + +#### Enable Single Sign On + +Once you restarted your Koding and logged in with your username and password +you need to activate oauth authentication for your user. To do that + + - Navigate to Dashboard on Koding from; + `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account` + - Scroll down to Integrations section + - Click on toggle to turn On integration in GitLab integration section + +This will redirect you to your GitLab instance and will ask your permission ( +if you are not logged in to GitLab at this point you will be redirected after +login) once you accept you will be redirected to your Koding instance. + +From now on you can login by using `SIGN IN WITH GITLAB` button on your Login +screen in your Koding instance. diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index 84c624cbcb7..253fff50bdd 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-11-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.3.3 +sudo -u git -H git checkout v3.4.0 ``` ### 5. Update gitlab-workhorse diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md index 4c59f59c587..8e50cb03e63 100644 --- a/doc/workflow/share_projects_with_other_groups.md +++ b/doc/workflow/share_projects_with_other_groups.md @@ -1,22 +1,24 @@ # Share Projects with other Groups -In GitLab Enterprise Edition you can share projects with other groups. -This makes it possible to add a group of users to a project with a single action. +You can share projects with other groups. This makes it possible to add a group of users +to a project with a single action. ## Groups as collections of users -In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md). -In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members. +Groups are used primarily to [create collections of projects](groups.md), but you can also +take advantage of the fact that groups define collections of _users_, namely the group +members. ## Sharing a project with a group of users -The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'. -But what if 'Project Acme' already belongs to another group, say 'Open Source'? -This is where the (Enterprise Edition only) group sharing feature can be of use. +The primary mechanism to give a group of users, say 'Engineering', access to a project, +say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project +Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'? +This is where the group sharing feature can be of use. To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section. -![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png) +![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png) Now you can add the 'Engineering' group with the maximum access level of your choice. After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard. diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb index dfa2fa75def..e9b45823c67 100644 --- a/features/steps/group/members.rb +++ b/features/steps/group/members.rb @@ -116,8 +116,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps member = mary_jane_member page.within "#group_member_#{member.id}" do - click_button "Edit access level" - select 'Developer', from: 'group_member_access_level' + click_button 'Edit' + select 'Developer', from: "member_access_level_#{member.id}" click_on 'Save' end end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 841d191d55b..bb79424ee08 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see its content with new lines preserved at end of file' do - expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n" + expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n" end step 'I click link "Raw"' do @@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I can edit code' do set_new_content - expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content + expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content end step 'I edit code' do @@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I edit code with new lines at end of file' do - execute_script('blob.editor.setValue("Sample\n\n\n")') + execute_script('ace.edit("editor").setValue("Sample\n\n\n")') end step 'I fill the new file name' do @@ -378,7 +378,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps private def set_new_content - execute_script("blob.editor.setValue('#{new_gitignore_content}')") + execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')") end # Content of the gitignore file on the seed repository. diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index f32576d2cb1..e920f5a706b 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -65,8 +65,8 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps user = User.find_by(name: 'Dmitriy') project_member = project.project_members.find_by(user_id: user.id) page.within "#project_member_#{project_member.id}" do - click_button "Edit access level" - select "Reporter", from: "project_member_access_level" + click_button 'Edit' + select "Reporter", from: "member_access_level_#{project_member.id}" click_button "Save" end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index fcb0b12c191..3f050a8fd81 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -97,6 +97,10 @@ module API member = options[:member] || options[:members].find { |m| m.user_id == user.id } member.access_level end + expose :expires_at do |user, options| + member = options[:member] || options[:members].find { |m| m.user_id == user.id } + member.expires_at + end end class AccessRequester < UserBasic diff --git a/lib/api/members.rb b/lib/api/members.rb index 2fae83f60b2..94c16710d9a 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -49,6 +49,7 @@ module API # id (required) - The group/project ID # user_id (required) - The user ID of the new member # access_level (required) - A valid access level + # expires_at (optional) - Date string in the format YEAR-MONTH-DAY # # Example Request: # POST /groups/:id/members @@ -72,7 +73,7 @@ module API conflict!('Member already exists') if source_type == 'group' && member unless member - source.add_user(params[:user_id], params[:access_level], current_user) + source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) member = source.members.find_by(user_id: params[:user_id]) end @@ -81,7 +82,7 @@ module API else # Since `source.add_user` doesn't return a member object, we have to # build a new one and populate its errors in order to render them. - member = source.members.build(attributes_for_keys([:user_id, :access_level])) + member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at])) member.valid? # populate the errors # This is to ensure back-compatibility but 400 behavior should be used @@ -97,6 +98,7 @@ module API # id (required) - The group/project ID # user_id (required) - The user ID of the member # access_level (required) - A valid access level + # expires_at (optional) - Date string in the format YEAR-MONTH-DAY # # Example Request: # PUT /groups/:id/members/:user_id @@ -107,8 +109,9 @@ module API required_attributes! [:user_id, :access_level] member = source.members.find_by!(user_id: params[:user_id]) + attrs = attributes_for_keys [:access_level, :expires_at] - if member.update_attributes(access_level: params[:access_level]) + if member.update_attributes(attrs) present member.user, with: Entities::Member, member: member else # This is to ensure back-compatibility but 400 behavior should be used diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 84688f6646e..a293fa2752f 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -94,7 +94,9 @@ module ExtractsPath @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } @options = HashWithIndifferentAccess.new(@options) - @id = Addressable::URI.normalize_component(get_id) + @id = params[:id] || params[:ref] + @id += "/" + params[:path] unless params[:path].blank? + @ref, @path = extract_ref(@id) @repo = @project.repository if @options[:extended_sha1].blank? @@ -116,12 +118,4 @@ module ExtractsPath def tree @tree ||= @repo.tree(@commit.id, @path) end - - private - - def get_id - id = params[:id] || params[:ref] - id += "/" + params[:path] unless params[:path].blank? - id - end end diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb index 3d56ea3e47a..95d925dc7f3 100644 --- a/lib/gitlab/badge/coverage/report.rb +++ b/lib/gitlab/badge/coverage/report.rb @@ -13,8 +13,7 @@ module Gitlab @job = job @pipeline = @project.pipelines - .where(ref: @ref) - .where(sha: @project.commit(@ref).try(:sha)) + .latest_successful_for(@ref) .first end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 735331df66c..27acd817e51 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -30,6 +30,7 @@ module Gitlab signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], + koding_enabled: false, sign_in_text: nil, after_sign_up_text: nil, help_page_text: nil, diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index 2fdcf8d7838..ecf62dead35 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -139,13 +139,19 @@ module Gitlab private def find_diff_file(repository) - diffs = Gitlab::Git::Compare.new( - repository.raw_repository, - start_sha, - head_sha - ).diffs(paths: paths) + # We're at the initial commit, so just get that as we can't compare to anything. + if Gitlab::Git.blank_ref?(start_sha) + compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha) + else + compare = Gitlab::Git::Compare.new( + repository.raw_repository, + start_sha, + head_sha + ) + end + + diff = compare.diffs(paths: paths).first - diff = diffs.first return unless diff Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs) diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb index bd3267e2a80..5cf9d5ebe28 100644 --- a/lib/gitlab/email/handler.rb +++ b/lib/gitlab/email/handler.rb @@ -4,7 +4,8 @@ require 'gitlab/email/handler/create_issue_handler' module Gitlab module Email module Handler - HANDLERS = [CreateNoteHandler, CreateIssueHandler] + # The `CreateIssueHandler` feature is disabled for the time being. + HANDLERS = [CreateNoteHandler] def self.for(mail, mail_key) HANDLERS.find do |klass| diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index d13fe0ef8a9..e59ead5d76c 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -7,7 +7,7 @@ module Gitlab # @param cmd [Array] # @return [Boolean] def system_silent(cmd) - Popen::popen(cmd).last.zero? + Popen.popen(cmd).last.zero? end def force_utf8(str) diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 44128a43362..a121cb2fc97 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -237,6 +237,56 @@ describe AutocompleteController do end end + context 'authorized projects apply limit' do + before do + authorized_project2 = create(:project) + authorized_project3 = create(:project) + + authorized_project.team << [user, :master] + authorized_project2.team << [user, :master] + authorized_project3.team << [user, :master] + + stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + end + + describe 'GET #projects with project ID' do + before do + get(:projects, project_id: project.id) + end + + let(:body) { JSON.parse(response.body) } + + it do + expect(body).to be_kind_of(Array) + expect(body.size).to eq 3 # Of a total of 4 + end + end + end + + context 'authorized projects with offset' do + before do + authorized_project2 = create(:project) + authorized_project3 = create(:project) + + authorized_project.team << [user, :master] + authorized_project2.team << [user, :master] + authorized_project3.team << [user, :master] + end + + describe 'GET #projects with project ID and offset_id' do + before do + get(:projects, project_id: project.id, offset_id: authorized_project.id) + end + + let(:body) { JSON.parse(response.body) } + + it do + expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there + expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either + end + end + end + context 'authorized projects without admin_issue ability' do before(:each) do authorized_project.team << [user, :guest] diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 8910c50c294..5d777895542 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -572,6 +572,18 @@ describe 'Issue Boards', feature: true, js: true do end end + context 'keyboard shortcuts' do + before do + visit namespace_project_board_path(project.namespace, project) + wait_for_vue_resource + end + + it 'allows user to use keyboard shortcuts' do + find('.boards-list').native.send_keys('i') + expect(page).to have_content('New Issue') + end + end + context 'signed out user' do before do logout diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index cb445e22af0..2e595959f04 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -525,7 +525,7 @@ describe 'Issues', feature: true do end end - describe 'new issue by email' do + xdescribe 'new issue by email' do shared_examples 'show the email in the modal' do before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb index af86d3c338a..5972e7f31c2 100644 --- a/spec/features/projects/badges/coverage_spec.rb +++ b/spec/features/projects/badges/coverage_spec.rb @@ -4,12 +4,6 @@ feature 'test coverage badge' do given!(:user) { create(:user) } given!(:project) { create(:project, :private) } - given!(:pipeline) do - create(:ci_pipeline, project: project, - ref: 'master', - sha: project.commit.id) - end - context 'when user has access to view badge' do background do project.team << [user, :developer] @@ -17,8 +11,10 @@ feature 'test coverage badge' do end scenario 'user requests coverage badge image for pipeline' do - create_job(coverage: 100, name: 'test:1') - create_job(coverage: 90, name: 'test:2') + create_pipeline do |pipeline| + create_build(pipeline, coverage: 100, name: 'test:1') + create_build(pipeline, coverage: 90, name: 'test:2') + end show_test_coverage_badge @@ -26,9 +22,11 @@ feature 'test coverage badge' do end scenario 'user requests coverage badge for specific job' do - create_job(coverage: 50, name: 'test:1') - create_job(coverage: 50, name: 'test:2') - create_job(coverage: 85, name: 'coverage') + create_pipeline do |pipeline| + create_build(pipeline, coverage: 50, name: 'test:1') + create_build(pipeline, coverage: 50, name: 'test:2') + create_build(pipeline, coverage: 85, name: 'coverage') + end show_test_coverage_badge(job: 'coverage') @@ -36,7 +34,9 @@ feature 'test coverage badge' do end scenario 'user requests coverage badge for pipeline without coverage' do - create_job(coverage: nil, name: 'test') + create_pipeline do |pipeline| + create_build(pipeline, coverage: nil, name: 'test') + end show_test_coverage_badge @@ -54,10 +54,19 @@ feature 'test coverage badge' do end end - def create_job(coverage:, name:) - create(:ci_build, name: name, - coverage: coverage, - pipeline: pipeline) + def create_pipeline + opts = { project: project, ref: 'master', sha: project.commit.id } + + create(:ci_pipeline, opts).tap do |pipeline| + yield pipeline + pipeline.build_updated + end + end + + def create_build(pipeline, coverage:, name:) + opts = { pipeline: pipeline, coverage: coverage, name: name } + + create(:ci_build, :success, opts) end def show_test_coverage_badge(job: nil) diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb new file mode 100644 index 00000000000..63878c55421 --- /dev/null +++ b/spec/features/projects/branches/delete_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +feature 'Delete branch', feature: true, js: true do + include WaitForAjax + + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as user + visit namespace_project_branches_path(project.namespace, project) + end + + it 'destroys tooltip' do + first('.remove-row').hover + expect(page).to have_selector('.tooltip') + + first('.remove-row').click + wait_for_ajax + + expect(page).not_to have_selector('.tooltip') + end +end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 79abba21854..1b14945bf0a 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -20,7 +20,7 @@ describe 'Branches', feature: true do describe 'Find branches' do it 'shows filtered branches', js: true do - visit namespace_project_branches_path(project.namespace, project, project.id) + visit namespace_project_branches_path(project.namespace, project) fill_in 'branch-search', with: 'fix' find('#branch-search').native.send_keys(:enter) diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb index 1b4ff6b6f1b..e45e3a36d01 100644 --- a/spec/features/projects/commits/cherry_pick_spec.rb +++ b/spec/features/projects/commits/cherry_pick_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +include WaitForAjax describe 'Cherry-pick Commits' do let(:project) { create(:project) } @@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do before do login_as :user project.team << [@user, :master] - visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 }) + visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) end context "I cherry-pick a commit" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) find("a[href='#modal-cherry-pick-commit']").click expect(page).not_to have_content('v1.0.0') # Only branches, not tags page.within('#modal-cherry-pick-commit') do @@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do context "I cherry-pick a merge commit" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id) find("a[href='#modal-cherry-pick-commit']").click page.within('#modal-cherry-pick-commit') do uncheck 'create_merge_request' @@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do context "I cherry-pick a commit that was previously cherry-picked" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) find("a[href='#modal-cherry-pick-commit']").click page.within('#modal-cherry-pick-commit') do uncheck 'create_merge_request' @@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do context "I cherry-pick a commit in a new merge request" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) find("a[href='#modal-cherry-pick-commit']").click page.within('#modal-cherry-pick-commit') do click_button 'Cherry-pick' @@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.') end end + + context "I cherry-pick a commit from a different branch", js: true do + it do + find('.commit-action-buttons a.dropdown-toggle').click + find(:css, "a[href='#modal-cherry-pick-commit']").click + + page.within('#modal-cherry-pick-commit') do + click_button 'master' + end + + wait_for_ajax + + page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do + click_link 'feature' + end + + page.within('#modal-cherry-pick-commit') do + uncheck 'create_merge_request' + click_button 'Cherry-pick' + end + + expect(page).to have_content('The commit has been successfully cherry-picked.') + end + end end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb new file mode 100644 index 00000000000..1a71a03fbd9 --- /dev/null +++ b/spec/features/projects/group_links_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +feature 'Project group links', feature: true, js: true do + include Select2Helper + + let(:master) { create(:user) } + let(:project) { create(:project) } + let!(:group) { create(:group) } + + background do + project.team << [master, :master] + login_as(master) + end + + context 'setting an expiration date for a group link' do + before do + visit namespace_project_group_links_path(project.namespace, project) + + select2 group.id, from: '#link_group_id' + fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') + page.find('body').click + click_on 'Share' + end + + it 'shows the expiration time with a warning class' do + page.within('.enabled-groups') do + expect(page).to have_content('expires in 4 days') + expect(page).to have_selector('.text-warning') + end + end + end +end diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb new file mode 100644 index 00000000000..430c384ac2e --- /dev/null +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do + include Select2Helper + include ActiveSupport::Testing::TimeHelpers + + let(:master) { create(:user) } + let(:project) { create(:project) } + let!(:new_member) { create(:user) } + + background do + project.team << [master, :master] + login_as(master) + end + + scenario 'expiration date is displayed in the members list' do + travel_to Time.zone.parse('2016-08-06 08:00') do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.users-project-form' do + select2(new_member.id, from: '#user_ids', multiple: true) + fill_in 'expires_at', with: '2016-08-10' + click_on 'Add users to project' + end + + page.within '.project_member:first-child' do + expect(page).to have_content('Expires in 4 days') + end + end + end + + scenario 'change expiration date' do + travel_to Time.zone.parse('2016-08-06 08:00') do + project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06') + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.project_member:first-child' do + click_on 'Edit' + fill_in 'Access expiration date', with: '2016-08-09' + click_on 'Save' + expect(page).to have_content('Expires in 3 days') + end + end + end +end diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb new file mode 100644 index 00000000000..395c61a4743 --- /dev/null +++ b/spec/features/protected_branches/access_control_ce_spec.rb @@ -0,0 +1,71 @@ +RSpec.shared_examples "protected branches > access control > CE" do + ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can push to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + allowed_to_push_button = find(".js-allowed-to-push") + + unless allowed_to_push_button.text == access_type_name + allowed_to_push_button.click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) + end + + it "allows updating protected branches so that #{access_type_name} can push to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".js-allowed-to-push").click + within('.js-allowed-to-push-container') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) + end + end + + ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can merge to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + allowed_to_merge_button = find(".js-allowed-to-merge") + + unless allowed_to_merge_button.text == access_type_name + allowed_to_merge_button.click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) + end + + it "allows updating protected branches so that #{access_type_name} can merge to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".js-allowed-to-merge").click + within('.js-allowed-to-merge-container') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) + end + end +end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index a0ee6cab7ec..1a3f7b970f6 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f } feature 'Projected Branches', feature: true, js: true do include WaitForAjax @@ -88,74 +89,6 @@ feature 'Projected Branches', feature: true, js: true do end describe "access control" do - ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| - it "allows creating protected branches that #{access_type_name} can push to" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - within('.new_protected_branch') do - allowed_to_push_button = find(".js-allowed-to-push") - - unless allowed_to_push_button.text == access_type_name - allowed_to_push_button.click - within(".dropdown.open .dropdown-menu") { click_on access_type_name } - end - end - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) - end - - it "allows updating protected branches so that #{access_type_name} can push to them" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - - within(".protected-branches-list") do - find(".js-allowed-to-push").click - within('.js-allowed-to-push-container') { click_on access_type_name } - end - - wait_for_ajax - expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) - end - end - - ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| - it "allows creating protected branches that #{access_type_name} can merge to" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - within('.new_protected_branch') do - allowed_to_merge_button = find(".js-allowed-to-merge") - - unless allowed_to_merge_button.text == access_type_name - allowed_to_merge_button.click - within(".dropdown.open .dropdown-menu") { click_on access_type_name } - end - end - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) - end - - it "allows updating protected branches so that #{access_type_name} can merge to them" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - - within(".protected-branches-list") do - find(".js-allowed-to-merge").click - within('.js-allowed-to-merge-container') { click_on access_type_name } - end - - wait_for_ajax - expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) - end - end + include_examples "protected branches > access control > CE" end end diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index 788581a26cb..40f773956d1 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do it { is_expected.to be_allowed_for :visitor } end + describe "GET /koding" do + subject { koding_path } + + context 'with Koding enabled' do + before do + stub_application_setting(koding_enabled?: true) + end + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + describe "GET /projects/new" do it { expect(new_project_path).to be_allowed_for :admin } it { expect(new_project_path).to be_allowed_for :user } diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb new file mode 100644 index 00000000000..e74a51acede --- /dev/null +++ b/spec/features/todos/todos_sorting_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe "Dashboard > User sorts todos", feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } + let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } + + let(:issue_1) { create(:issue, title: 'issue_1', project: project) } + let(:issue_2) { create(:issue, title: 'issue_2', project: project) } + let(:issue_3) { create(:issue, title: 'issue_3', project: project) } + let(:issue_4) { create(:issue, title: 'issue_4', project: project) } + + let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") } + + before do + create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago) + create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago) + create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago) + create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago) + create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) + + merge_request_1.labels << label_1 + issue_3.labels << label_1 + issue_2.labels << label_3 + issue_1.labels << label_2 + + project.team << [user, :developer] + login_as(user) + visit dashboard_todos_path + end + + it "sorts with oldest created todos first" do + click_link "Last created" + + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("merge_request_1") + expect(results_list.all('p')[1]).to have_content("issue_1") + expect(results_list.all('p')[2]).to have_content("issue_3") + expect(results_list.all('p')[3]).to have_content("issue_2") + expect(results_list.all('p')[4]).to have_content("issue_4") + end + + it "sorts with newest created todos first" do + click_link "Oldest created" + + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_4") + expect(results_list.all('p')[1]).to have_content("issue_2") + expect(results_list.all('p')[2]).to have_content("issue_3") + expect(results_list.all('p')[3]).to have_content("issue_1") + expect(results_list.all('p')[4]).to have_content("merge_request_1") + end + + it "sorts by priority" do + click_link "Priority" + + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_3") + expect(results_list.all('p')[1]).to have_content("merge_request_1") + expect(results_list.all('p')[2]).to have_content("issue_1") + expect(results_list.all('p')[3]).to have_content("issue_2") + expect(results_list.all('p')[4]).to have_content("issue_4") + end +end diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb index 4f3304f7b6d..fdce4e714ff 100644 --- a/spec/finders/move_to_project_finder_spec.rb +++ b/spec/finders/move_to_project_finder_spec.rb @@ -51,6 +51,28 @@ describe MoveToProjectFinder do expect(subject.execute(project).to_a).to eq([other_reporter_project]) end + + it 'returns a page of projects ordered by id in descending order' do + stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + + reporter_project.team << [user, :reporter] + developer_project.team << [user, :developer] + master_project.team << [user, :master] + + expect(subject.execute(project).to_a).to eq([master_project, developer_project]) + end + + it 'returns projects after the given offset id' do + stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + + reporter_project.team << [user, :reporter] + developer_project.team << [user, :developer] + master_project.team << [user, :master] + + expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project]) + expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project]) + expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty + end end context 'search' do diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb new file mode 100644 index 00000000000..f7e7e733cf7 --- /dev/null +++ b/spec/finders/todos_finder_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe TodosFinder do + describe '#execute' do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:finder) { described_class } + + before { project.team << [user, :developer] } + + describe '#sort' do + context 'by date' do + let!(:todo1) { create(:todo, user: user, project: project) } + let!(:todo2) { create(:todo, user: user, project: project) } + let!(:todo3) { create(:todo, user: user, project: project) } + + it 'sorts with oldest created first' do + todos = finder.new(user, { sort: 'id_asc' }).execute + + expect(todos.first).to eq(todo1) + expect(todos.second).to eq(todo2) + expect(todos.third).to eq(todo3) + end + + it 'sorts with newest created first' do + todos = finder.new(user, { sort: 'id_desc' }).execute + + expect(todos.first).to eq(todo3) + expect(todos.second).to eq(todo2) + expect(todos.third).to eq(todo1) + end + end + + it "sorts by priority" do + label_1 = create(:label, title: 'label_1', project: project, priority: 1) + label_2 = create(:label, title: 'label_2', project: project, priority: 2) + label_3 = create(:label, title: 'label_3', project: project, priority: 3) + + issue_1 = create(:issue, title: 'issue_1', project: project) + issue_2 = create(:issue, title: 'issue_2', project: project) + issue_3 = create(:issue, title: 'issue_3', project: project) + issue_4 = create(:issue, title: 'issue_4', project: project) + merge_request_1 = create(:merge_request, source_project: project) + + merge_request_1.labels << label_1 + + # Covers the case where Todo has more than one label + issue_3.labels << label_1 + issue_3.labels << label_3 + + issue_2.labels << label_3 + issue_1.labels << label_2 + + todo_1 = create(:todo, user: user, project: project, target: issue_4) + todo_2 = create(:todo, user: user, project: project, target: issue_2) + todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago) + todo_4 = create(:todo, user: user, project: project, target: issue_1) + todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) + + todos = finder.new(user, { sort: 'priority' }).execute + + expect(todos.first).to eq(todo_3) + expect(todos.second).to eq(todo_5) + expect(todos.third).to eq(todo_4) + expect(todos.fourth).to eq(todo_2) + expect(todos.fifth).to eq(todo_1) + end + end + end +end diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 299e4675d6f..532ebb9640e 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -10,23 +10,31 @@ "title": { "type": "string" }, "confidential": { "type": "boolean" }, "labels": { - "type": ["array"], - "required": [ - "id", - "color", - "description", - "title", - "priority" - ], - "properties": { - "id": { "type": "integer" }, - "color": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "color", + "description", + "title", + "priority" + ], + "properties": { + "id": { "type": "integer" }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "description": { "type": ["string", "null"] }, + "text_color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "title": { "type": "string" }, + "priority": { "type": ["integer", "null"] } }, - "description": { "type": ["string", "null"] }, - "title": { "type": "string" }, - "priority": { "type": ["integer", "null"] } + "additionalProperties": false } }, "assignee": { diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb new file mode 100644 index 00000000000..2dd2eab0524 --- /dev/null +++ b/spec/helpers/issuables_helper_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe IssuablesHelper do + let(:label) { build_stubbed(:label) } + let(:label2) { build_stubbed(:label) } + + context 'label tooltip' do + it 'returns label text' do + expect(issuable_labels_tooltip([label])).to eq(label.title) + end + + it 'returns label text' do + expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") + end + end +end diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb index bf3ed5c094c..21f35585367 100644 --- a/spec/helpers/time_helper_spec.rb +++ b/spec/helpers/time_helper_spec.rb @@ -19,16 +19,16 @@ describe TimeHelper do describe "#duration_in_numbers" do it "returns minutes and seconds" do - duration_in_numbers = { - [100, 0] => "01:40", - [121, 0] => "02:01", - [3721, 0] => "01:02:01", - [0, 0] => "00:00", - [nil, Time.now.to_i - 42] => "00:42" + durations_and_expectations = { + 100 => "01:40", + 121 => "02:01", + 3721 => "01:02:01", + 0 => "00:00", + 42 => "00:42" } - duration_in_numbers.each do |interval, expectation| - expect(duration_in_numbers(*interval)).to eq(expectation) + durations_and_expectations.each do |duration, expectation| + expect(duration_in_numbers(duration)).to eq(expectation) end end end diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml new file mode 100644 index 00000000000..a20390c08ee --- /dev/null +++ b/spec/javascripts/fixtures/gl_dropdown.html.haml @@ -0,0 +1,16 @@ +%div + .dropdown.inline + %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + Projects + %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle + .dropdown-menu.dropdown-select.dropdown-menu-selectable + .dropdown-title + %span Go to project + %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}} + %i.fa.fa-times.dropdown-menu-close-icon + .dropdown-input + %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'} + %i.fa.fa-search.dropdown-input-search + .dropdown-content + .dropdown-loading + %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml new file mode 100644 index 00000000000..397bdc85c67 --- /dev/null +++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml @@ -0,0 +1,16 @@ +.block.labels + .sidebar-collapsed-icon.js-sidebar-labels-tooltip + .title.hide-collapsed + %a.edit-link.pull-right{ href: "#" } + Edit + .selectbox.hide-collapsed{ style: "display: none;" } + .dropdown + %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } } + %span.dropdown-toggle-text + Label + %i.fa.fa-chevron-down + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + .dropdown-content + .dropdown-loading + %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 new file mode 100644 index 00000000000..b529ea6458d --- /dev/null +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -0,0 +1,119 @@ +/*= require jquery */ +/*= require gl_dropdown */ +/*= require turbolinks */ +/*= require lib/utils/common_utils */ +/*= require lib/utils/type_utility */ + +(() => { + const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; + const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; + const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`; + + const ARROW_KEYS = { + DOWN: 40, + UP: 38, + ENTER: 13, + ESC: 27 + }; + + let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) { + i = i || 0; + if (!i) direction = direction.toUpperCase(); + $('body').trigger({ + type: 'keydown', + which: ARROW_KEYS[direction], + keyCode: ARROW_KEYS[direction] + }); + i++; + if (i <= steps) { + navigateWithKeys(direction, steps, cb, i); + } else { + cb(); + } + }; + + describe('Dropdown', function describeDropdown() { + fixture.preload('gl_dropdown.html'); + fixture.preload('projects.json'); + + beforeEach(() => { + fixture.load('gl_dropdown.html'); + this.dropdownContainerElement = $('.dropdown.inline'); + this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); + this.projectsData = fixture.load('projects.json')[0]; + this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ + selectable: true, + data: this.projectsData, + text: (project) => { + (project.name_with_namespace || project.name); + }, + id: (project) => { + project.id; + } + }); + }); + + afterEach(() => { + $('body').unbind('keydown'); + this.dropdownContainerElement.unbind('keyup'); + }); + + it('should open on click', () => { + expect(this.dropdownContainerElement).not.toHaveClass('open'); + this.dropdownButtonElement.click(); + expect(this.dropdownContainerElement).toHaveClass('open'); + }); + + describe('that is open', () => { + beforeEach(() => { + this.dropdownButtonElement.click(); + }); + + it('should select a following item on DOWN keypress', () => { + expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0); + let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); + navigateWithKeys('down', randomIndex, () => { + expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1); + expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused'); + }); + }); + + it('should select a previous item on UP keypress', () => { + expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0); + navigateWithKeys('down', (this.projectsData.length - 1), () => { + expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1); + let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); + navigateWithKeys('up', randomIndex, () => { + expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1); + expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused'); + }); + }); + }); + + it('should click the selected item on ENTER keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open') + let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0 + navigateWithKeys('down', randomIndex, () => { + spyOn(Turbolinks, 'visit').and.stub(); + navigateWithKeys('enter', null, () => { + expect(this.dropdownContainerElement).not.toHaveClass('open'); + let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement); + expect(link).toHaveClass('is-active'); + let linkedLocation = link.attr('href'); + if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation); + }); + }); + }); + + it('should close on ESC keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open'); + this.dropdownContainerElement.trigger({ + type: 'keyup', + which: ARROW_KEYS.ESC, + keyCode: ARROW_KEYS.ESC + }); + expect(this.dropdownContainerElement).not.toHaveClass('open'); + }); + }); + }); +})(); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 new file mode 100644 index 00000000000..840c7b6d015 --- /dev/null +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -0,0 +1,89 @@ +//= require lib/utils/type_utility +//= require jquery +//= require bootstrap +//= require gl_dropdown +//= require select2 +//= require jquery.nicescroll +//= require api +//= require create_label +//= require issuable_context +//= require users_select +//= require labels_select + +(() => { + let saveLabelCount = 0; + describe('Issue dropdown sidebar', () => { + fixture.preload('issue_sidebar_label.html'); + + beforeEach(() => { + fixture.load('issue_sidebar_label.html'); + new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); + new LabelsSelect(); + + spyOn(jQuery, 'ajax').and.callFake((req) => { + const d = $.Deferred(); + let LABELS_DATA = [] + + if (req.url === '/root/test/labels.json') { + for (let i = 0; i < 10; i++) { + LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'}); + } + } else if (req.url === '/root/test/issues/2.json') { + let tmp = [] + for (let i = 0; i < saveLabelCount; i++) { + tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'}); + } + LABELS_DATA = {labels: tmp}; + } + + d.resolve(LABELS_DATA); + return d.promise(); + }); + }); + + it('changes collapsed tooltip when changing labels when less than 5', (done) => { + saveLabelCount = 5; + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.dropdown-content a').length).toBe(10); + + $('.dropdow-content a').each((i, $link) => { + if (i < 5) { + $link.get(0).click(); + } + }); + + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4'); + done(); + }, 0); + }, 0); + }); + + it('changes collapsed tooltip when changing labels when more than 5', (done) => { + saveLabelCount = 6; + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.dropdown-content a').length).toBe(10); + + $('.dropdow-content a').each((i, $link) => { + if (i < 5) { + $link.get(0).click(); + } + }); + + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more'); + done(); + }, 0); + }, 0); + }); + }); +})(); + diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 68d64483d67..324f5152780 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -105,13 +105,13 @@ a3 = "a[href='" + mrsAssignedToMeLink + "']"; a4 = "a[href='" + mrsIHaveCreatedLink + "']"; expect(list.find(a1).length).toBe(1); - expect(list.find(a1).text()).toBe(' Issues assigned to me '); + expect(list.find(a1).text()).toBe('Issues assigned to me'); expect(list.find(a2).length).toBe(1); - expect(list.find(a2).text()).toBe(" Issues I've created "); + expect(list.find(a2).text()).toBe("Issues I've created"); expect(list.find(a3).length).toBe(1); - expect(list.find(a3).text()).toBe(' Merge requests assigned to me '); + expect(list.find(a3).text()).toBe('Merge requests assigned to me'); expect(list.find(a4).length).toBe(1); - return expect(list.find(a4).text()).toBe(" Merge requests I've created "); + return expect(list.find(a4).text()).toBe("Merge requests I've created"); }; describe('Search autocomplete dropdown', function() { diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 36c77206a3f..86d04ecfa36 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -30,17 +30,6 @@ describe ExtractsPath, lib: true do expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") end - context 'escaped slash character in ref' do - let(:ref) { 'improve%2Fawesome' } - - it 'has no escape sequences in @ref or @logs_path' do - assign_ref_vars - - expect(@ref).to eq('improve/awesome') - expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") - end - end - context 'ref contains %20' do let(:ref) { 'foo%20bar' } @@ -52,6 +41,16 @@ describe ExtractsPath, lib: true do expect(@id).to start_with('foo%20bar/') end end + + context 'path contains space' do + let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } } + + it 'is not converted to %20 in @path' do + assign_ref_vars + + expect(@path).to eq(params[:path]) + end + end end describe '#extract_ref' do diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb index 1ff49602486..ab0cce6e091 100644 --- a/spec/lib/gitlab/badge/coverage/report_spec.rb +++ b/spec/lib/gitlab/badge/coverage/report_spec.rb @@ -44,45 +44,49 @@ describe Gitlab::Badge::Coverage::Report do end end - context 'pipeline exists' do - let!(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.id, - ref: 'master') - end - - context 'builds exist' do - before do - create(:ci_build, name: 'first', pipeline: pipeline, coverage: 40) - create(:ci_build, pipeline: pipeline, coverage: 60) + context 'when latest successful pipeline exists' do + before do + create_pipeline do |pipeline| + create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40) + create(:ci_build, :success, pipeline: pipeline, coverage: 60) end - context 'particular job specified' do - let(:job_name) { 'first' } - - it 'returns coverage for the particular job' do - expect(badge.status).to eq 40 - end - end - - context 'particular job not specified' do - let(:job_name) { '' } - - it 'returns arithemetic mean for the pipeline' do - expect(badge.status).to eq 50 - end + create_pipeline do |pipeline| + create(:ci_build, :failed, pipeline: pipeline, coverage: 10) end end - context 'builds do not exist' do - it_behaves_like 'unknown coverage report' + context 'when particular job specified' do + let(:job_name) { 'first' } - context 'particular job specified' do - let(:job_name) { 'nonexistent' } + it 'returns coverage for the particular job' do + expect(badge.status).to eq 40 + end + end - it 'retruns nil' do - expect(badge.status).to be_nil - end + context 'when particular job not specified' do + let(:job_name) { '' } + + it 'returns arithemetic mean for the pipeline' do + expect(badge.status).to eq 50 + end + end + end + + context 'when only failed pipeline exists' do + before do + create_pipeline do |pipeline| + create(:ci_build, :failed, pipeline: pipeline, coverage: 10) + end + end + + it_behaves_like 'unknown coverage report' + + context 'particular job specified' do + let(:job_name) { 'nonexistent' } + + it 'retruns nil' do + expect(badge.status).to be_nil end end end @@ -90,4 +94,13 @@ describe Gitlab::Badge::Coverage::Report do context 'pipeline does not exist' do it_behaves_like 'unknown coverage report' end + + def create_pipeline + opts = { project: project, sha: project.commit.id, ref: 'master' } + + create(:ci_pipeline, opts).tap do |pipeline| + yield pipeline + pipeline.build_updated + end + end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 10537bea008..6e8fff6f516 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -339,6 +339,48 @@ describe Gitlab::Diff::Position, lib: true do end end + describe "position for a file in the initial commit" do + let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") } + + subject do + described_class.new( + old_path: "README.md", + new_path: "README.md", + old_line: nil, + new_line: 1, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.new_file).to be true + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+testme") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + describe "#to_json" do let(:hash) do { diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index e1153154778..a5cc7b02936 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require_relative '../email_shared_blocks' -describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do +xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do include_context :email_shared_context it_behaves_like :email_shared_examples diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index fa241867858..eae9c060c38 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -493,7 +493,12 @@ describe Notify do end def invite_to_project(project:, email:, inviter:) - ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter) + Member.add_user( + project.project_members, + 'toto@example.com', + Gitlab::Access::DEVELOPER, + current_user: inviter + ) project.project_members.invite.last end @@ -740,7 +745,12 @@ describe Notify do end def invite_to_group(group:, email:, inviter:) - GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter) + Member.add_user( + group.group_members, + 'toto@example.com', + Gitlab::Access::DEVELOPER, + current_user: inviter + ) group.group_members.invite.last end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 853f6943cef..aa3b2bbf471 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -171,6 +171,70 @@ describe Ability, lib: true do end end + shared_examples_for ".project_abilities" do |enable_request_store| + before do + RequestStore.begin! if enable_request_store + end + + after do + if enable_request_store + RequestStore.end! + RequestStore.clear! + end + end + + describe '.project_abilities' do + let!(:project) { create(:empty_project, :public) } + let!(:user) { create(:user) } + + it 'returns permissions for admin user' do + admin = create(:admin) + + results = described_class.project_abilities(admin, project) + + expect(results.count).to eq(68) + end + + it 'returns permissions for an owner' do + results = described_class.project_abilities(project.owner, project) + + expect(results.count).to eq(68) + end + + it 'returns permissions for a master' do + project.team << [user, :master] + + results = described_class.project_abilities(user, project) + + expect(results.count).to eq(60) + end + + it 'returns permissions for a developer' do + project.team << [user, :developer] + + results = described_class.project_abilities(user, project) + + expect(results.count).to eq(44) + end + + it 'returns permissions for a guest' do + project.team << [user, :guest] + + results = described_class.project_abilities(user, project) + + expect(results.count).to eq(21) + end + end + end + + describe '.project_abilities with RequestStore' do + it_behaves_like ".project_abilities", true + end + + describe '.project_abilities without RequestStore' do + it_behaves_like ".project_abilities", false + end + describe '.issues_readable_by_user' do context 'with an admin user' do it 'returns all given issues' do diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 72688137f08..02d6263094a 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe BroadcastMessage, models: true do - include ActiveSupport::Testing::TimeHelpers - subject { create(:broadcast_message) } it { is_expected.to be_valid } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 8137e9f8f71..721b20e0cb2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -124,17 +124,21 @@ describe Ci::Pipeline, models: true do describe 'state machine' do let(:current) { Time.now.change(usec: 0) } - let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current } - let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current } + let(:build) { create :ci_build, name: 'build1', pipeline: pipeline } describe '#duration' do before do - build.skip - build2.skip + travel_to(current - 120) do + pipeline.run + end + + travel_to(current) do + pipeline.succeed + end end it 'matches sum of builds duration' do - expect(pipeline.reload.duration).to eq(build.duration + build2.duration) + expect(pipeline.reload.duration).to eq(120) end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 2277f4e13bf..fef90d9b5cb 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -65,11 +65,21 @@ describe Member, models: true do @master_user = create(:user).tap { |u| project.team << [u, :master] } @master = project.members.find_by(user_id: @master_user.id) - ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user) + Member.add_user( + project.members, + 'toto1@example.com', + Gitlab::Access::DEVELOPER, + current_user: @master_user + ) @invited_member = project.members.invite.find_by_invite_email('toto1@example.com') accepted_invite_user = build(:user) - ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user) + Member.add_user( + project.members, + 'toto2@example.com', + Gitlab::Access::DEVELOPER, + current_user: @master_user + ) @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) } requested_user = create(:user).tap { |u| project.request_access(u) } diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb new file mode 100644 index 00000000000..b76513d2a3c --- /dev/null +++ b/spec/models/network/graph_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Network::Graph, models: true do + let(:project) { create(:project) } + let!(:note_on_commit) { create(:note_on_commit, project: project) } + + it '#initialize' do + graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil) + + expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } ) + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d1f3a815290..9a3660012f9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -247,7 +247,7 @@ describe Project, models: true do end end - describe "#new_issue_address" do + xdescribe "#new_issue_address" do let(:project) { create(:empty_project, path: "somewhere") } let(:user) { create(:user) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f7dbfd712cc..1fea50ad42c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -719,6 +719,14 @@ describe Repository, models: true do expect(merge_commit).to be_present expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present end + + it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do + merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) + merge_commit_id = repository.merge(user, merge_request, commit_options) + repository.commit(merge_commit_id) + + expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) + end end describe '#revert' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index a56ee30f7b1..1e365bf353a 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -122,12 +122,13 @@ describe API::Members, api: true do it 'creates a new member' do expect do post api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id, access_level: Member::DEVELOPER + user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' expect(response).to have_http_status(201) end.to change { source.members.count }.by(1) expect(json_response['id']).to eq(stranger.id) expect(json_response['access_level']).to eq(Member::DEVELOPER) + expect(json_response['expires_at']).to eq('2016-08-05') end end @@ -183,11 +184,12 @@ describe API::Members, api: true do context 'when authenticated as a master/owner' do it 'updates the member' do put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), - access_level: Member::MASTER + access_level: Member::MASTER, expires_at: '2016-08-05' expect(response).to have_http_status(200) expect(json_response['id']).to eq(developer.id) expect(json_response['access_level']).to eq(Member::MASTER) + expect(json_response['expires_at']).to eq('2016-08-05') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1d4df9197f6..d65648dd0b2 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -116,12 +116,19 @@ describe HelpController, "routing" do expect(get(path)).to route_to('help#show', path: 'workflow/protected_branches/protected_branches1', format: 'png') - + path = '/help/ui' expect(get(path)).to route_to('help#ui') end end +# koding GET /koding(.:format) koding#index +describe KodingController, "routing" do + it "to #index" do + expect(get("/koding")).to route_to('koding#index') + end +end + # profile_account GET /profile/account(.:format) profile#account # profile_history GET /profile/history(.:format) profile#history # profile_password PUT /profile/password(.:format) profile#password_update diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index ad8c2485888..8326e5cd313 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' describe Ci::ProcessPipelineService, services: true do let(:pipeline) { create(:ci_pipeline, ref: 'master') } let(:user) { create(:user) } - let(:all_builds) { pipeline.builds } - let(:builds) { all_builds.where.not(status: [:created, :skipped]) } let(:config) { nil } before do @@ -12,6 +10,14 @@ describe Ci::ProcessPipelineService, services: true do end describe '#execute' do + def all_builds + pipeline.builds + end + + def builds + all_builds.where.not(status: [:created, :skipped]) + end + def create_builds described_class.new(pipeline.project, user).execute(pipeline) end @@ -48,7 +54,7 @@ describe Ci::ProcessPipelineService, services: true do it 'does not process pipeline if existing stage is running' do expect(create_builds).to be_truthy expect(builds.pending.count).to eq(2) - + expect(create_builds).to be_falsey expect(builds.pending.count).to eq(2) end @@ -224,6 +230,40 @@ describe Ci::ProcessPipelineService, services: true do end end + context 'when failed build in the middle stage is retried' do + context 'when failed build is the only unsuccessful build in the stage' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2) + end + + it 'does trigger builds in the next stage' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2') + + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)) + .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + + pipeline.builds.find_by(name: 'test:1').success + pipeline.builds.find_by(name: 'test:2').drop + + expect(builds.pluck(:name)) + .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success + + expect(builds.pluck(:name)).to contain_exactly( + 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') + end + end + end + context 'creates a builds from .gitlab-ci.yml' do let(:config) do YAML.dump({ diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 18da3b1b453..f81a58899fd 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1113,6 +1113,46 @@ describe NotificationService, services: true do end end + describe 'GroupMember' do + describe '#decline_group_invite' do + let(:creator) { create(:user) } + let(:group) { create(:group) } + let(:member) { create(:user) } + + before(:each) do + group.add_owner(creator) + group.add_developer(member, creator) + end + + it do + group_member = group.members.first + + expect do + notification.decline_group_invite(group_member) + end.to change { ActionMailer::Base.deliveries.size }.by(1) + end + end + end + + describe 'ProjectMember' do + describe '#decline_group_invite' do + let(:project) { create(:project) } + let(:member) { create(:user) } + + before(:each) do + project.team << [member, :developer, project.owner] + end + + it do + project_member = project.members.first + + expect do + notification.decline_project_invite(project_member) + end.to change { ActionMailer::Base.deliveries.size }.by(1) + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2e2aa7c4fc0..c144cd85487 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,6 +33,7 @@ RSpec.configure do |config| config.include EmailHelpers config.include TestEnv config.include ActiveJob::TestHelper + config.include ActiveSupport::Testing::TimeHelpers config.include StubGitlabCalls config.include StubGitlabData diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index eecc32875a5..7ca2c29da1c 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -2,19 +2,19 @@ require 'spec_helper' describe EmailsOnPushWorker do include RepoHelpers + include EmailSpec::Matchers let(:project) { create(:project) } let(:user) { create(:user) } let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } let(:recipients) { user.email } let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) } + let(:email) { ActionMailer::Base.deliveries.last } subject { EmailsOnPushWorker.new } describe "#perform" do context "when push is a new branch" do - let(:email) { ActionMailer::Base.deliveries.last } - before do data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA) @@ -31,8 +31,6 @@ describe EmailsOnPushWorker do end context "when push is a deleted branch" do - let(:email) { ActionMailer::Base.deliveries.last } - before do data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA) @@ -48,15 +46,40 @@ describe EmailsOnPushWorker do end end - context "when there are no errors in sending" do - let(:email) { ActionMailer::Base.deliveries.last } + context "when push is a force push to delete commits" do + before do + data_force_push = data.stringify_keys.merge( + "after" => data[:before], + "before" => data[:after] + ) + subject.perform(project.id, recipients, data_force_push) + end + + it "sends a mail with the correct subject" do + expect(email.subject).to include('Change some files') + end + + it "mentions force pushing in the body" do + expect(email).to have_body_text("force push") + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end + end + + context "when there are no errors in sending" do before { perform } it "sends a mail with the correct subject" do expect(email.subject).to include('Change some files') end + it "does not mention force pushing in the body" do + expect(email).not_to have_body_text("force push") + end + it "sends the mail to the correct recipient" do expect(email.to).to eq([user.email]) end @@ -66,6 +89,7 @@ describe EmailsOnPushWorker do before do ActionMailer::Base.deliveries.clear allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) + allow(subject).to receive_message_chain(:logger, :info) perform end diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb new file mode 100644 index 00000000000..689bc3d27b4 --- /dev/null +++ b/spec/workers/remove_expired_group_links_worker_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe RemoveExpiredGroupLinksWorker do + describe '#perform' do + let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) } + let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) } + let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) } + + it 'removes expired group links' do + expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1) + expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil + end + + it 'leaves group links that expire in the future' do + subject.perform + expect(project_group_link_expiring_in_future.reload).to be_present + end + + it 'leaves group links that do not expire at all' do + subject.perform + expect(non_expiring_project_group_link.reload).to be_present + end + end +end diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb new file mode 100644 index 00000000000..402aa1e714e --- /dev/null +++ b/spec/workers/remove_expired_members_worker_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe RemoveExpiredMembersWorker do + let(:worker) { RemoveExpiredMembersWorker.new } + + describe '#perform' do + context 'project members' do + let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) } + let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) } + let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) } + + it 'removes expired members' do + expect { worker.perform }.to change { Member.count }.by(-1) + expect(Member.find_by(id: expired_project_member.id)).to be_nil + end + + it 'leaves members that expire in the future' do + worker.perform + expect(project_member_expiring_in_future.reload).to be_present + end + + it 'leaves members that do not expire at all' do + worker.perform + expect(non_expiring_project_member.reload).to be_present + end + end + + context 'group members' do + let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) } + let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) } + let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) } + + it 'removes expired members' do + expect { worker.perform }.to change { Member.count }.by(-1) + expect(Member.find_by(id: expired_group_member.id)).to be_nil + end + + it 'leaves members that expire in the future' do + worker.perform + expect(group_member_expiring_in_future.reload).to be_present + end + + it 'leaves members that do not expire at all' do + worker.perform + expect(non_expiring_group_member.reload).to be_present + end + end + + context 'when the last group owner expires' do + let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) } + + it 'does not delete the owner' do + worker.perform + expect(expired_group_owner.reload).to be_present + end + end + end +end diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js old mode 100755 new mode 100644 diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js old mode 100755 new mode 100644 diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js old mode 100755 new mode 100644