diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md new file mode 100644 index 0000000000000000000000000000000000000000..d2a1eb564237e00473a2e5ec7d1a38d346f5d7a8 --- /dev/null +++ b/.gitlab/merge_request_templates/Documentation.md @@ -0,0 +1,14 @@ +See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html. + +## What does this MR do? + +(briefly describe what this MR is about) + +## Moving docs to a new location? + +See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location + +- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location. +- [ ] Make sure internal links pointing to the document in question are not broken. +- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory. +- [ ] If working on CE, submit an MR to EE with the changes as well. diff --git a/.rubocop.yml b/.rubocop.yml index 282f4539f03168608f9383108077623039ebe940..5bd31ccf32915feed8f1e6628f0f7ac5ae4e1142 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,8 +5,8 @@ require: inherit_from: .rubocop_todo.yml AllCops: - TargetRubyVersion: 2.1 - # Cop names are not displayed in offense messages by default. Change behavior + TargetRubyVersion: 2.3 + # Cop names are not d§splayed in offense messages by default. Change behavior # by overriding DisplayCopNames, or by giving the -D/--display-cop-names # option. DisplayCopNames: true @@ -192,6 +192,9 @@ Style/FlipFlop: Style/For: Enabled: true +# Checks if there is a magic comment to enforce string literals +Style/FrozenStringLiteralComment: + Enabled: false # Do not introduce global variables. Style/GlobalVars: Enabled: true diff --git a/CHANGELOG b/CHANGELOG index a6cd8f4c7e1b6c98ce51d6ee2e1f5b98a4e1d056..934dabe743a6bd070061c1b0a51f8e79073c00cd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,31 +1,57 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.12.0 (unreleased) + - Filter tags by name !6121 - Make push events have equal vertical spacing. - Add two-factor recovery endpoint to internal API !5510 + - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) - Add font color contrast to external label in admin area (ClemMakesApps) - Change logo animation to CSS (ClemMakesApps) + - Instructions for enabling Git packfile bitmaps !6104 - Change merge_error column from string to text type - Reduce contributions calendar data payload (ClemMakesApps) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) + - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) + - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps) + - Center build stage columns in pipeline overview (ClemMakesApps) + - Shorten task status phrase (ClemMakesApps) - Add hover color to emoji icon (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps) + - Add white background for no readme container (ClemMakesApps) + - API: Expose issue confidentiality flag. (Robert Schilling) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Remove Gitorious import + - Fix inconsistent background color for filter input field (ClemMakesApps) + - Remove prefixes from transition CSS property (ClemMakesApps) - Add Sentry logging to API calls + - Add BroadcastMessage API - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) - Remove unused mixins (ClemMakesApps) + - Add search to all issue board lists - Fix groups sort dropdown alignment (ClemMakesApps) - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps) + - Use JavaScript tooltips for mentions !5301 (winniehell) - Fix markdown help references (ClemMakesApps) + - Add last commit time to repo view (ClemMakesApps) + - Fix accessibility and visibility of project list dropdown button !6140 + - Added project specific enable/disable setting for LFS !5997 + - Don't expose a user's token in the `/api/v3/user` API (!6047) + - Remove redundant js-timeago-pending from user activity log (ClemMakesApps) + - Ability to manage project issues, snippets, wiki, merge requests and builds access level + - Remove inconsistent font weight for sidebar's labels (ClemMakesApps) - Added tests for diff notes + - Add a button to download latest successful artifacts for branches and tags !5142 + - Remove redundant pipeline tooltips (ClemMakesApps) + - Expire commit info views after one day, instead of two weeks, to allow for user email updates - Add delimiter to project stars and forks count (ClemMakesApps) - Fix badge count alignment (ClemMakesApps) + - Fix repo title alignment (ClemMakesApps) - Fix branch title trailing space on hover (ClemMakesApps) - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison) - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison) + - Order award emoji tooltips in order they were added (EspadaV8) - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps) - Update merge_requests.md with a simpler way to check out a merge request. !5944 - Fix button missing type (ClemMakesApps) @@ -34,20 +60,46 @@ v 8.12.0 (unreleased) - Load branches asynchronously in Cherry Pick and Revert dialogs. - Add merge request versions !5467 - Change using size to use count and caching it for number of group members. !5935 + - Replace play icon font with svg (ClemMakesApps) - Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck) - Reduce number of database queries on builds tab - Capitalize mentioned issue timeline notes (ClemMakesApps) + - Fix inconsistent checkbox alignment (ClemMakesApps) - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) - Adds response mime type to transaction metric action when it's not HTML - Fix hover leading space bug in pipeline graph !5980 - -v 8.11.3 (unreleased) + - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 + - Fixed invisible scroll controls on build page on iPhone + +v 8.11.5 (unreleased) + - Optimize branch lookups and force a repository reload for Repository#find_branch + - Fix suggested colors options for new labels in the admin area. !6138 + +v 8.11.4 + - Fix resolving conflicts on forks. !6082 + - Fix diff commenting on merge requests created prior to 8.10. !6029 + - Fix pipelines tab layout regression. !5952 + - Fix "Wiki" link not appearing in navigation for projects with external wiki. !6057 + - Do not enforce using hash with hidden key in CI configuration. !6079 + - Fix hover leading space bug in pipeline graph !5980 + - Fix sorting issues by "last updated" doesn't work after import from GitHub + - GitHub importer use default project visibility for non-private projects + - Creating an issue through our API now emails label subscribers !5720 + - Block concurrent updates for Pipeline + - Don't create groups for unallowed users when importing projects + - Fix issue boards leak private label names and descriptions + - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner) + - Remove gitorious. !5866 + +v 8.11.3 - Allow system info page to handle case where info is unavailable - Label list shows all issues (opened or closed) with that label - Don't show resolve conflicts link before MR status is updated - - Fix IE11 fork button bug !598 + - Fix IE11 fork button bug !5982 - Don't prevent viewing the MR when git refs for conflicts can't be found on disk - Fix external issue tracker "Issues" link leading to 404s + - Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters + - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) v 8.11.2 - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c457af2ae6f3be806cdd2ee9e1a8352a99e38c29..c77dcd96a7d1b15e9e267a52b66449adf4d2a8f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -287,6 +287,8 @@ request is as follows: migrations on a fresh database before the MR is reviewed. If the review leads to large changes in the MR, do this again once the review is complete. 1. For more complex migrations, write tests. +1. Merge requests **must** adhere to the [merge request performance + guidelines](doc/development/merge_request_performance_guidelines.md). The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. This is the best time to submit an MR and get diff --git a/Gemfile b/Gemfile index 194379dd687b668529eea386ee1ed5d0980d26a0..620338e59970253c5e69c8781b208ed730d71df2 100644 --- a/Gemfile +++ b/Gemfile @@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.4.7' +gem 'gitlab_git', '~> 10.5' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -97,9 +97,6 @@ gem 'fog-rackspace', '~> 0.1.1' # for aws storage gem 'unf', '~> 0.1.4' -# Authorization -gem 'six', '~> 0.2.0' - # Seed data gem 'seed-fu', '~> 2.3.5' diff --git a/Gemfile.lock b/Gemfile.lock index 0c28975060cfea0762871c5d1b89ae14919dfb88..28ede86b3ba09803d273bda641c734d1de041773 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,7 +279,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.4.7) + gitlab_git (10.5.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -683,7 +683,6 @@ GEM rack (~> 1.5) rack-protection (~> 1.4) tilt (>= 1.3, < 3) - six (0.2.0) slack-notifier (1.2.1) slop (3.6.0) spinach (0.8.10) @@ -859,7 +858,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.4.7) + gitlab_git (~> 10.5) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) @@ -954,7 +953,6 @@ DEPENDENCIES sidekiq-cron (~> 0.4.0) simplecov (= 0.12.0) sinatra (~> 1.4.4) - six (~> 0.2.0) slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) diff --git a/PROCESS.md b/PROCESS.md index 8e1a3f7360f6508b35cf76ea8703dad873c72b32..8af660fbdd156c163f0cbc06e76cc8bba6619e24 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -50,7 +50,7 @@ etc.). The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help -on those issue. Please select someone with relevant experience from +on those issues. Please select someone with relevant experience from [GitLab core team][core-team]. If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 5ea6086ab779d5026eeccf662a304a06ebb530f9..d5e11e22be539f8956ad9512b1a4938725850e21 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -12,7 +12,7 @@ } Activities.prototype.updateTooltips = function() { - return gl.utils.localTimeAgo($('.js-timeago', '#activity')); + return gl.utils.localTimeAgo($('.js-timeago', '.content_list')); }; Activities.prototype.reloadActivities = function() { diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 8ac1ba7665e352fdeb4b3c8067a21aa81895b66a..5467e3edc69278fb5e8d89b1e2c145df027a0a32 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,6 +1,6 @@ (function(w) { $(function() { - $('.js-toggle-button').on('click', function(e) { + $('body').on('click', '.js-toggle-button', function(e) { e.preventDefault(); $(this) .find('.fa') diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index a612cf0f1aed57eac5776581db40626bfb5e0918..91c12570e09889987c80985c342311f184e86d81 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -54,4 +54,11 @@ $(() => { }); } }); + + gl.IssueBoardsSearch = new Vue({ + el: '#js-boards-seach', + data: { + filters: Store.state.filters + } + }); }); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index d7f4107cb021d1971aef227c1ebb4b99762af8e4..7e86f001f44e81a2526d58612ee3411a68b51e50 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -21,15 +21,10 @@ }, data () { return { - query: '', filters: Store.state.filters }; }, watch: { - query () { - this.list.filters = this.getFilterData(); - this.list.getIssues(true); - }, filters: { handler () { this.list.page = 1; @@ -38,16 +33,6 @@ deep: true } }, - methods: { - getFilterData () { - const filters = this.filters; - let queryData = { search: this.query }; - - Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; }); - - return queryData; - } - }, ready () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index a6644e9eb8c4b0d342b7d250bd125cbedf4d3f32..50fc11d77374f76cdbeaa92f324985ddedecb8ab 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -20,7 +20,8 @@ data () { return { scrollOffset: 250, - filters: Store.state.filters + filters: Store.state.filters, + showCount: false }; }, watch: { @@ -30,6 +31,15 @@ this.$els.list.scrollTop = 0; }, deep: true + }, + issues () { + this.$nextTick(() => { + if (this.scrollHeight() > this.listHeight()) { + this.showCount = true; + } else { + this.showCount = false; + } + }); } }, methods: { @@ -58,6 +68,7 @@ group: 'issues', sort: false, disabled: this.disabled, + filter: '.board-list-count', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index be2b8c568a83c5dec6e0ccbdb6842d27dd38de6a..91fd620fdb3215ef39831a63c3f484c8e2230045 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -11,6 +11,7 @@ class List { this.loading = true; this.loadingMore = false; this.issues = []; + this.issuesSize = 0; if (obj.label) { this.label = new ListLabel(obj.label); @@ -51,17 +52,13 @@ class List { } nextPage () { - if (Math.floor(this.issues.length / 20) === this.page) { + if (this.issuesSize > this.issues.length) { this.page++; return this.getIssues(false); } } - canSearch () { - return this.type === 'backlog'; - } - getIssues (emptyIssues = true) { const filters = this.filters; let data = { page: this.page }; @@ -80,12 +77,13 @@ class List { .then((resp) => { const data = resp.json(); this.loading = false; + this.issuesSize = data.size; if (emptyIssues) { this.issues = []; } - this.createIssues(data); + this.createIssues(data.issues); }); } @@ -96,14 +94,20 @@ class List { } addIssue (issue, listFrom) { - this.issues.push(issue); + if (!this.findIssue(issue.id)) { + this.issues.push(issue); - if (this.label) { - issue.addLabel(this.label); - } + if (this.label) { + issue.addLabel(this.label); + } - if (listFrom) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id); + if (listFrom) { + this.issuesSize++; + gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + .then(() => { + listFrom.getIssues(false); + }); + } } } @@ -116,6 +120,7 @@ class List { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { + this.issuesSize--; issue.removeLabel(this.label); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 18f26a1f911177e0828af1b640dd96e8a5ee250f..bd07ee0c161e50f79fe469a043398df1b0d7dcf7 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -15,7 +15,8 @@ author_id: gl.utils.getParameterValues('author_id')[0], assignee_id: gl.utils.getParameterValues('assignee_id')[0], milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]') + label_name: gl.utils.getParameterValues('label_name[]'), + search: '' }; }, addList (listObj) { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 25e0569f85f0abb0f171750efb1f3abb964c4eec..a72beb426461da8302ebb2b2ff3228a198c0f155 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -203,6 +203,7 @@ break; case 'labels': switch (path[2]) { + case 'new': case 'edit': new Labels(); } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 5a2a8523d9fb3ec9a30149d07dc30fd756ca1014..77b2082cba07e6fcead94cab49b169cdbf840e89 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -556,7 +556,7 @@ if (isInput) { field = $(this.el); } else { - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']"); } if (el.hasClass(ACTIVE_CLASS)) { el.removeClass(ACTIVE_CLASS); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index a81aae2cee3e73fd8c5dcd05b0d5785b0ee8859e..9d40a303a8f88e0821d99cf1737c6072793d9fb3 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -171,7 +171,7 @@ instance.addInput(this.fieldName, label.id); } } - if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value=\"" + (this.id(label)) + "\"]").length) { + if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) { selectedClass.push('is-active'); } if ($dropdown.hasClass('js-multiselect') && removesAll) { diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index e5d4fd44c96d5d1bd408f95a19be0bcfcacc8b6c..7d8eef1b49574b9e4475ab08db028aca5795182e 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,16 +1,12 @@ (function() { Turbolinks.enableProgressBar(); - start = function() { + $(document).on('page:fetch', function() { $('.tanuki-logo').addClass('animate'); - }; + }); - stop = function() { + $(document).on('page:change', function() { $('.tanuki-logo').removeClass('animate'); - }; - - $(document).on('page:fetch', start); - - $(document).on('page:change', stop); + }); }).call(this); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 6e677fa8cc6907a6349d58d6571d5f777f3a13cd..23eda7d44ca5ecb2b96d9baa20ed7f14a1fc5566 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -13,6 +13,7 @@ this.perPage = this.el.data('perPage'); this.clearListeners(); this.initBtnListeners(); + this.initFilters(); } Todos.prototype.clearListeners = function() { @@ -27,6 +28,31 @@ return $('.todo').on('click', this.goToTodoUrl); }; + Todos.prototype.initFilters = function() { + new UsersSelect(); + this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); + this.initFilterDropdown($('.js-type-search'), 'type'); + this.initFilterDropdown($('.js-action-search'), 'action_id'); + + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); + }; + + Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { + $dropdown.glDropdown({ + selectable: true, + filterable: searchFields ? true : false, + fieldName: fieldName, + search: { fields: searchFields }, + data: $dropdown.data('data'), + clicked: function() { + return $dropdown.closest('form.filter-form').submit(); + } + }) + }; + Todos.prototype.doneClicked = function(e) { var $this; e.preventDefault(); @@ -66,7 +92,7 @@ success: (function(_this) { return function(data) { $this.remove(); - $('.js-todos-list').remove(); + $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); return _this.updateBadges(data); }; })(this) diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js deleted file mode 100644 index 6c4d88cf407b5f6fed01eacc96441f832e2e675a..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/user.js +++ /dev/null @@ -1,29 +0,0 @@ -(function() { - this.User = (function() { - function User(opts) { - this.opts = opts; - $('.profile-groups-avatars').tooltip({ - "placement": "top" - }); - this.initTabs(); - $('.hide-project-limit-message').on('click', function(e) { - $.cookie('hide_project_limit_message', 'false', { - path: gon.relative_url_root || '/' - }); - $(this).parents('.project-limit-message').remove(); - return e.preventDefault(); - }); - } - - User.prototype.initTabs = function() { - return new UserTabs({ - parentEl: '.user-profile', - action: this.opts.action - }); - }; - - return User; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..6889d3a7491add3eb8aabb2feea60ad7786661cc --- /dev/null +++ b/app/assets/javascripts/user.js.es6 @@ -0,0 +1,34 @@ +(global => { + global.User = class { + constructor(opts) { + this.opts = opts; + this.placeProfileAvatarsToTop(); + this.initTabs(); + this.hideProjectLimitMessage(); + } + + placeProfileAvatarsToTop() { + $('.profile-groups-avatars').tooltip({ + placement: 'top' + }); + } + + initTabs() { + return new UserTabs({ + parentEl: '.user-profile', + action: this.opts.action + }); + } + + hideProjectLimitMessage() { + $('.hide-project-limit-message').on('click', e => { + e.preventDefault(); + const path = gon.relative_url_root || '/'; + $.cookie('hide_project_limit_message', 'false', { + path: path + }); + $(this).parents('.project-limit-message').remove(); + }); + } + } +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8f68eab58ad2e2e282d3eb81edf7246c323d31c8..4a87a73a68a7edb634c3e2931d6d4e0509158d7f 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -183,6 +183,13 @@ &.dropdown-menu-user-link { line-height: 16px; } + + .icon-play { + fill: $table-text-gray; + margin-right: 6px; + height: 12px; + width: 11px; + } } .dropdown-header { @@ -195,6 +202,12 @@ .separator + .dropdown-header { padding-top: 2px; } + + .unclickable { + cursor: not-allowed; + padding: 5px 8px; + color: $dropdown-header-color; + } } .dropdown-menu-large { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index afe4a276ae55e2e8bb61dc6607f89996b03af9d9..1036219172e33df0c2c463ada7a16673e9501b93 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -84,7 +84,7 @@ header { .side-nav-toggle { position: absolute; left: -10px; - margin: 6px 0; + margin: 7px 0; font-size: 18px; padding: 6px 10px; border: none; @@ -136,6 +136,8 @@ header { } .title { + position: relative; + padding-right: 20px; margin: 0; font-size: 19px; max-width: 400px; @@ -148,7 +150,11 @@ header { vertical-align: top; white-space: nowrap; - @media (max-width: $screen-sm-max) { + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + max-width: 300px; + } + + @media (max-width: $screen-xs-max) { max-width: 190px; } @@ -160,11 +166,15 @@ header { } .dropdown-toggle-caret { - position: relative; - top: -2px; + color: $gl-text-color; + border: transparent; + background: transparent; + position: absolute; + right: 3px; width: 12px; - line-height: 12px; - margin-left: 5px; + line-height: 19px; + margin-top: (($header-height - 19) / 2); + padding: 0; font-size: 10px; text-align: center; cursor: pointer; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 396a37bab6e00896972a04e38d18dd0a64d85d8a..62dc2cb3fdc6923d3b314a7e9c3cdc50c91ae4d4 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -9,14 +9,6 @@ border-radius: $radius; } -@mixin transition($transition) { - -webkit-transition: $transition; - -moz-transition: $transition; - -ms-transition: $transition; - -o-transition: $transition; - transition: $transition; -} - /** * Prefilled mixins * Mixins with fixed values diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ef2fe844f94b78219ea5276f3fb845ae6c049d0b..9efbaf54e906ea01fafc453baa19718bec955481 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -8,10 +8,7 @@ height: 30px; transition-duration: .3s; -webkit-transform: translateZ(0); - background: -webkit-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); - background: -o-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); - background: -moz-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); - background: linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); + background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4)); &.scrolling { visibility: visible; @@ -211,12 +208,6 @@ } } - .project-filter-form { - input { - background-color: $background-color; - } - } - @media (max-width: $screen-xs-max) { padding-bottom: 0; width: 100%; diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index b2e22b60440e4130185db46e4c141f7755d73f69..c75dacf95d9a80e856c4523a3d55950de1f1be9e 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -151,7 +151,7 @@ background-position: right 0 bottom 6px; border: 1px solid $input-border; @include border-radius($border-radius-default); - @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { border-color: $input-border-focus; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9ac4d801ac426fbe8a745eaeff31ab6b913e0cba..037278bb083a38064b6aa602d66df98216c8fff7 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -10,7 +10,7 @@ .is-dragging { // Important because plugin sets inline CSS opacity: 1!important; - + * { // !important to make sure no style can override this when dragging cursor: -webkit-grabbing!important; @@ -142,11 +142,6 @@ } } -.board-header-loading-spinner { - margin-right: 10px; - color: $gray-darkest; -} - .board-inner-container { border-bottom: 1px solid $border-color; padding: $gl-padding; @@ -160,40 +155,6 @@ border-bottom: 1px solid $border-color; } -.board-search-container { - position: relative; - background-color: #fff; - - .form-control { - padding-right: 30px; - } -} - -.board-search-icon, -.board-search-clear-btn { - position: absolute; - right: $gl-padding + 10px; - top: 50%; - margin-top: -7px; - font-size: 14px; -} - -.board-search-icon { - color: $gl-placeholder-color; -} - -.board-search-clear-btn { - padding: 0; - line-height: 1; - background: transparent; - border: 0; - outline: 0; - - &:hover { - color: $gl-link-color; - } -} - .board-delete { margin-right: 10px; padding: 0; @@ -304,3 +265,22 @@ margin-right: 8px; font-weight: 500; } + +.issue-boards-search { + width: 335px; + + .form-control { + display: inline-block; + width: 210px; + } +} + +.board-list-count { + padding: 10px 0; + color: $gl-placeholder-color; + font-size: 13px; + + > .fa { + margin-right: 5px; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 8c33e7d9a2e977978eeb14a730eb138b6ddf96e9..cee198691c2686f8d96c26cc805fa04558b05fdb 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -36,6 +36,7 @@ &.affix { right: 30px; bottom: 15px; + z-index: 1; @media (min-width: $screen-md-min) { right: 26%; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 55f9d4a001123f9549b31aace1063f1e85e2bdf3..d01c60ee6abb5bb15d8596f1abd7f757ea9558d6 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -4,8 +4,9 @@ margin: 0; } - .fa-play { - font-size: 14px; + .icon-play { + height: 13px; + width: 12px; } .dropdown-new { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index dfe1e3075dadd45b2404d2db6f0bdbee4d006445..3a3bb10feac2a8ab8b689541e6252a73c9019043 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -12,6 +12,10 @@ padding-right: 8px; margin-bottom: 10px; min-width: 15px; + + .selected_issue { + vertical-align: text-top; + } } .issue-labels { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 0dcf61dd2ddd82b2fcf31257613d3be60fb0c903..ee5d9de66d8d2cda9b9658617f2556a6e8270644 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -2,6 +2,7 @@ .stage { max-width: 90px; width: 90px; + text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -146,6 +147,7 @@ } .stage-cell { + text-align: center; svg { height: 18px; @@ -153,10 +155,6 @@ vertical-align: middle; overflow: visible; } - - .light { - width: 3px; - } } .duration, @@ -215,6 +213,13 @@ border-color: $border-white-normal; } } + + .btn { + .icon-play { + height: 13px; + width: 12px; + } + } } } @@ -469,11 +474,22 @@ .pipelines.tab-pane { .content-list.pipelines { - overflow: scroll; + overflow: auto; } .stage { - max-width: 60px; - width: 60px; + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } +} + +.ci-status-icon-created { + + svg { + fill: $table-text-gray; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index eaf2d3270b30379eab2a803c1899fde3aaef2124..f2db373da52736bf60a3b6a88850414d100c5d55 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -311,6 +311,14 @@ a.deploy-project-label { color: $gl-success; } +.lfs-enabled { + color: $gl-success; +} + +.lfs-disabled { + color: $gl-warning; +} + .breadcrumb.repo-breadcrumb { padding: 0; background: transparent; @@ -600,18 +608,25 @@ pre.light-well { } } -.project-show-readme .readme-holder { - padding: $gl-padding 0; - border-top: 0; - - .edit-project-readme { - z-index: 2; - position: relative; +.project-show-readme { + .row-content-block { + background-color: inherit; + border: none; } - .wiki h1 { - border-bottom: none; - padding: 0; + .readme-holder { + padding: $gl-padding 0; + border-top: 0; + + .edit-project-readme { + z-index: 2; + position: relative; + } + + .wiki h1 { + border-bottom: none; + padding: 0; + } } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index c9d436d72ba87cbb89f654b012413fd0c331b676..436fb00ba2e4322f33779d881c081f48812da172 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -80,7 +80,7 @@ .search-icon { @extend .fa-search; - @include transition(color .15s); + transition: color 0.15s; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -125,7 +125,7 @@ } .location-badge { - @include transition(all .15s); + transition: all 0.15s; background-color: $location-badge-active-bg; color: $white-light; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 587f2d9f3c13735d1692b23561b0b0e507403f05..0ee7ceecae5b9073bd85606452d8ae043dda3532 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -43,6 +43,15 @@ border-color: $blue-normal; } + &.ci-created { + color: $table-text-gray; + border-color: $table-text-gray; + + svg { + fill: $table-text-gray; + } + } + svg { height: 13px; width: 13px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 9da40fe2b09d5a6fac7b167defc56af175ee18ea..538f211c65beef779025569a0c52c0e1c5dc693e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -11,6 +11,16 @@ } } + .last-commit { + max-width: 506px; + + .last-commit-content { + @include str-truncated; + width: calc(100% - 140px); + margin-left: 3px; + } + } + .tree-table { margin-bottom: 0; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ebc2a4651ba5959b3d351b1f06d63316cc2bd643..bd4ba384b2937dffdf70c4fe6972024712328bb9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception - helper_method :abilities, :can?, :current_application_settings + helper_method :can?, :current_application_settings helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| @@ -97,12 +97,8 @@ class ApplicationController < ActionController::Base current_application_settings.after_sign_out_path.presence || new_user_session_path end - def abilities - Ability.abilities - end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end def access_denied! diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 036777c80c19f1edf6a29e19b4d53f91b6d6262b..172d5344b7a1594bd096b6291616f2d728451eb9 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -8,10 +8,14 @@ module ToggleAwardEmoji def toggle_award_emoji name = params.require(:name) - awardable.toggle_award_emoji(name, current_user) - TodoService.new.new_award_emoji(to_todoable(awardable), current_user) + if awardable.user_can_award?(current_user, name) + awardable.toggle_award_emoji(name, current_user) + TodoService.new.new_award_emoji(to_todoable(awardable), current_user) - render json: { ok: true } + render json: { ok: true } + else + render json: { ok: false } + end end private diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 014b9b43ff26f955f969a883defd154817555721..66ebdcc37a79d32cae70621f86040934088acd46 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -37,7 +37,7 @@ class JwtController < ApplicationController def authenticate_project(login, password) if login == 'gitlab-ci-token' - Project.find_by(builds_enabled: true, runners_token: password) + Project.with_builds_enabled.find_by(runners_token: password) end end diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index 5a94dcb0dbda623f540ddf128c0e1d25c9829bf9..83eec1bf4a2645ea9aa5987ca9379681b52e6b4b 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -14,7 +14,7 @@ class NamespacesController < ApplicationController if user redirect_to user_path(user) - elsif group && can?(current_user, :read_group, namespace) + elsif group && can?(current_user, :read_group, group) redirect_to group_path(group) elsif current_user.nil? authenticate_user! diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 91315a07debcff03bba0db8f7e0578847fac0dd0..b2ff36f65380395359a788b04c66d0f468ee4e1b 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController end def builds_enabled - return render_404 unless @project.builds_enabled? + return render_404 unless @project.feature_available?(:builds, current_user) end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 7241949393b5dfc18024e62d78d1aa6b622a0fdb..5922263796145f46ce6330ae68d540e8a55ddd29 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,22 +1,25 @@ class Projects::ArtifactsController < Projects::ApplicationController + include ExtractsPath + layout 'project' before_action :authorize_read_build! before_action :authorize_update_build!, only: [:keep] + before_action :extract_ref_name_and_path before_action :validate_artifacts! def download - unless artifacts_file.file_storage? - return redirect_to artifacts_file.url + if artifacts_file.file_storage? + send_file artifacts_file.path, disposition: 'attachment' + else + redirect_to artifacts_file.url end - - send_file artifacts_file.path, disposition: 'attachment' end def browse directory = params[:path] ? "#{params[:path]}/" : '' @entry = build.artifacts_metadata_entry(directory) - return render_404 unless @entry.exists? + render_404 unless @entry.exists? end def file @@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController redirect_to namespace_project_build_path(project.namespace, project, build) end + def latest_succeeded + target_path = artifacts_action_path(@path, project, build) + + if target_path + redirect_to(target_path) + else + render_404 + end + end + private + def extract_ref_name_and_path + return unless params[:ref_name_and_path] + + @ref_name, @path = extract_ref(params[:ref_name_and_path]) + end + def validate_artifacts! - render_404 unless build.artifacts? + render_404 unless build && build.artifacts? end def build - @build ||= project.builds.find_by!(id: params[:build_id]) + @build ||= build_from_id || build_from_ref + end + + def build_from_id + project.builds.find_by(id: params[:build_id]) if params[:build_id] + end + + def build_from_ref + return unless @ref_name + + builds = project.latest_successful_builds_for(@ref_name) + builds.find_by(name: params[:job]) end def artifacts_file diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 1a4f6b50e8f4b2fe72f2354d65c47be8711acb0a..9404612a993b3c6f27d610b7679d5792f639d9ed 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -8,12 +8,15 @@ module Projects issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute issues = issues.page(params[:page]) - render json: issues.as_json( - only: [:iid, :title, :confidential], - include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } - }) + render json: { + issues: issues.as_json( + only: [:iid, :title, :confidential], + include: { + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + }), + size: issues.total_count + } end def update diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index b2e8733ccb741c1411d8a424238c06285b594993..d174e1145a783a6026ef1295b9e706046f8f2c9f 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController end def module_enabled - render_404 unless @project.merge_requests_enabled + render_404 unless @project.feature_available?(:merge_requests, current_user) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7c03dcd2e64cc36f685b09143ac1acc8877a8f8a..72d2d3618784313af1eda1092fff9cbb0c2a7a5c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -201,7 +201,7 @@ class Projects::IssuesController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.issues_enabled && @project.default_issues_tracker? + return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? end def redirect_to_external_issue_tracker diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 0ca675623e55d42ee72800f16a46bc3ba569db50..28fa4a5b141a16e4abc665216c2803a5c9ce33c4 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController protected def module_enabled - unless @project.issues_enabled || @project.merge_requests_enabled + unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) return render_404 end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4f5f3b6aa09d5a650ec3fb55fe4c2d826260b549..4f9ca0097a1478e3783132ce24345c78c1e1b2b5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -413,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.merge_requests_enabled + return render_404 unless @project.feature_available?(:merge_requests, current_user) end def validates_merge_request diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da2892bfb3f5e908833bec8f0069aa05e771d138..ff63f22cb5b047dd3dd4d242e0b96ff0d52821f9 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController end def module_enabled - unless @project.issues_enabled || @project.merge_requests_enabled + unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) return render_404 end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 6d0a7ee10317da6f66b3a24018f237fee8b0d1d6..17ceefec3b86ff45b2e663ae5698cb845b84a534 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.snippets_enabled + return render_404 unless @project.feature_available?(:snippets, current_user) end def snippet_params diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 8592579abbd18a6174f6401e637fc6c135fabd23..6ea8ee62bc59e57fc1248347dbf0938b69272556 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -1,4 +1,6 @@ class Projects::TagsController < Projects::ApplicationController + include SortingHelper + # Authorize before_action :require_non_empty_project before_action :authorize_download_code! @@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_admin_project!, only: [:destroy] def index - @sort = params[:sort] || 'name' - @tags = @repository.tags_sorted_by(@sort) + params[:sort] = params[:sort].presence || 'name' + + @sort = params[:sort] + @tags = TagsFinder.new(@repository, params).execute @tags = Kaminari.paginate_array(@tags).page(params[:page]) @releases = project.releases.where(tag: @tags.map(&:name)) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fc52cd2f367f2e629d97985882799c3f9b748d4a..eaa38fa6c98322bb4392aaaad679c5ba9f6f3e12 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -303,13 +303,23 @@ class ProjectsController < Projects::ApplicationController end def project_params + project_feature_attributes = + { + project_feature_attributes: + [ + :issues_access_level, :builds_access_level, + :wiki_access_level, :merge_requests_access_level, :snippets_access_level + ] + } + params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, + :container_registry_enabled, :issues_tracker_id, :default_branch, - :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, - :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled + :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, + :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, + :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, + :lfs_enabled, project_feature_attributes ) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 33daac0399e29551b63af478c056d0a2e6d7d2b8..60996b181f22d005ae7215fe307639767aaf26b2 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -64,7 +64,7 @@ class IssuableFinder if project? @project = Project.find(params[:project_id]) - unless Ability.abilities.allowed?(current_user, :read_project, @project) + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end else diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..b474f0805dcdddd9c15f060819e0b8dfbabcfeff --- /dev/null +++ b/app/finders/tags_finder.rb @@ -0,0 +1,29 @@ +class TagsFinder + def initialize(repository, params) + @repository = repository + @params = params + end + + def execute + tags = @repository.tags_sorted_by(sort) + filter_by_name(tags) + end + + private + + def sort + @params[:sort].presence + end + + def search + @params[:search].presence + end + + def filter_by_name(tags) + if search + tags.select { |tag| tag.name.include?(search) } + else + tags + end + end +end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 06b3e8a9502372e44dfc1728c2b9c64cab65ed9e..a93a63bdb9b7892650063caf1763ae1360bfe817 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -83,7 +83,7 @@ class TodosFinder if project? @project = Project.find(params[:project_id]) - unless Ability.abilities.allowed?(current_user, :read_project, @project) + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f3733b0172145ab9e1e252bb8392d0f8a22186c9..5f3765cad0d7a7961e872964bd722a5f9ab2f47f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,7 +110,7 @@ module ApplicationHelper project = event.project # Skip if project repo is empty or MR disabled - return false unless project && !project.empty_repo? && project.merge_requests_enabled + return false unless project && !project.empty_repo? && project.feature_available?(:merge_requests, current_user) # Skip if user already created appropriate MR return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 0327b476d18bc24f7881cf7735b6fc06212fca07..639deb7c521f60617f029b369f49d8fb2d916115 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -25,6 +25,11 @@ module CiStatusHelper end end + def ci_status_for_statuseable(subject) + status = subject.try(:status) || 'not found' + status.humanize + end + def ci_icon_for_status(status) icon_name = case status @@ -41,7 +46,7 @@ module CiStatusHelper when 'play' 'icon_play' when 'created' - 'icon_status_pending' + 'icon_status_created' else 'icon_status_cancel' end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index f1dc906cab462204754cbe163b810881e2925541..aa54ee07bdccaf3b8ef34f2a7ef946e044724760 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -3,7 +3,7 @@ module CompareHelper from.present? && to.present? && from != to && - project.merge_requests_enabled && + project.feature_available?(:merge_requests, current_user) && project.repository.branch_names.include?(from) && project.repository.branch_names.include?(to) end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 5386ddadc62bfe7efd8f20ad2a17f089fcdd8639..a322a90cc4e037aeb3f90b4e36c6eea3f30ea6c4 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -149,4 +149,20 @@ module GitlabRoutingHelper def resend_invite_group_member_path(group_member, *args) resend_invite_group_group_member_path(group_member.source, group_member) end + + # Artifacts + + def artifacts_action_path(path, project, build) + action, path_params = path.split('/', 2) + args = [project.namespace, project, build, path_params] + + case action + when 'download' + download_namespace_project_build_artifacts_path(*args) + when 'browse' + browse_namespace_project_build_artifacts_path(*args) + when 'file' + file_namespace_project_build_artifacts_path(*args) + end + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 3a3fda2bb925daa9248708d82f3efa70b0c4943d..d1e432e41aef16f2faec6e1b4c882f60c70df828 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -43,6 +43,19 @@ module IssuablesHelper end end + def project_dropdown_label(project_id, default_label) + return default_label if project_id.nil? + return "Any project" if project_id == "0" + + project = Project.find_by(id: project_id) + + if project + project.name_with_namespace + else + default_label + end + end + def milestone_dropdown_label(milestone_title, default_label = "Milestone") if milestone_title == Milestone::Upcoming.name milestone_title = Milestone::Upcoming.title diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index eb651e3687eb64eb8d0eaf66e43faf7ad6e78128..5d82abfca79078df050e04a9132fef69774d4048 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -23,10 +23,14 @@ module LfsHelper end def lfs_download_access? + return false unless project.lfs_enabled? + project.public? || ci? || (user && user.can?(:download_code, project)) end def lfs_upload_access? + return false unless project.lfs_enabled? + user && user.can?(:push_code, project) end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index db6e731c7446dde8a884c0b4bfe9d3eafad1deb8..a9e175c3f5ce542dff5f6efae876e28a092a74cf 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -98,6 +98,6 @@ module MergeRequestsHelper end def merge_request_button_visibility(merge_request, closed) - return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) + return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 6c1cc6ef072df5a99f6e156ab2dd350db1b1e0e4..2b0ff6c0d00bc721d15e46c72fea0fae86033393 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -25,6 +25,8 @@ module NavHelper current_path?('merge_requests#commits') || current_path?('merge_requests#builds') || current_path?('merge_requests#conflicts') || + current_path?('merge_requests#pipelines') || + current_path?('issues#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 356f27f2d5dad79bbc5011261f4afb0431223a6b..d6efa60322316305241cdb981368a88ac7f9d51c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -61,7 +61,9 @@ module ProjectsHelper project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } if current_user - project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) + project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do + icon("chevron-down") + end end full_title = "#{namespace_link} / #{project_link}".html_safe @@ -187,6 +189,18 @@ module ProjectsHelper nav_tabs.flatten end + def project_lfs_status(project) + if project.lfs_enabled? + content_tag(:span, class: 'lfs-enabled') do + 'Enabled' + end + else + content_tag(:span, class: 'lfs-disabled') do + 'Disabled' + end + end + end + def git_user_name if current_user current_user.name @@ -400,4 +414,23 @@ module ProjectsHelper message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end + + def project_feature_options + { + 'Disabled' => ProjectFeature::DISABLED, + 'Only team members' => ProjectFeature::PRIVATE, + 'Everyone with access' => ProjectFeature::ENABLED + } + end + + def project_feature_access_select(field) + # Don't show option "everyone with access" if project is private + options = project_feature_options + level = @project.project_feature.public_send(field) + + options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED + + options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED) + content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control").html_safe + end end diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb index f8cccade15b5520ffb8405d393fab7fbc34f0ea9..3d255df66a0c39214ea4ea9d2b66e5d36b6a44ed 100644 --- a/app/helpers/sentry_helper.rb +++ b/app/helpers/sentry_helper.rb @@ -1,27 +1,9 @@ module SentryHelper def sentry_enabled? - Rails.env.production? && current_application_settings.sentry_enabled? + Gitlab::Sentry.enabled? end def sentry_context - return unless sentry_enabled? - - if current_user - Raven.user_context( - id: current_user.id, - email: current_user.email, - username: current_user.username, - ) - end - - Raven.tags_context(program: sentry_program_context) - end - - def sentry_program_context - if Sidekiq.server? - 'sidekiq' - else - 'rails' - end + Gitlab::Sentry.context(current_user) end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index fb85544df2d7a9da130de651631bf962562cfd6a..c0ec1634cdb83b5cb004afcd8e06886892e430e1 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -3,6 +3,16 @@ module TagsHelper "/tags/#{tag}" end + def filter_tags_path(options = {}) + exist_opts = { + search: params[:search], + sort: params[:sort] + } + + options = exist_opts.merge(options) + namespace_project_tags_path(@project.namespace, @project, @id, options) + end + def tag_list(project) html = '' project.tag_list.each do |tag| diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 0465327060ee1ee1a32c13bcc4e72e8532aa73db..1e86f648203de99eb67aa8693eca29361a0213dd 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -78,13 +78,11 @@ module TodosHelper end def todo_actions_options - actions = [ - OpenStruct.new(id: '', title: 'Any Action'), - OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'), - OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned') + [ + { id: '', text: 'Any Action' }, + { id: Todo::ASSIGNED, text: 'Assigned' }, + { id: Todo::MENTIONED, text: 'Mentioned' } ] - - options_from_collection_for_select(actions, 'id', 'title', params[:action_id]) end def todo_projects_options @@ -92,22 +90,28 @@ module TodosHelper projects = projects.includes(:namespace) projects = projects.map do |project| - OpenStruct.new(id: project.id, title: project.name_with_namespace) + { id: project.id, text: project.name_with_namespace } end - projects.unshift(OpenStruct.new(id: '', title: 'Any Project')) - - options_from_collection_for_select(projects, 'id', 'title', params[:project_id]) + projects.unshift({ id: '', text: 'Any Project' }).to_json end def todo_types_options - types = [ - OpenStruct.new(title: 'Any Type', name: ''), - OpenStruct.new(title: 'Issue', name: 'Issue'), - OpenStruct.new(title: 'Merge Request', name: 'MergeRequest') + [ + { id: '', text: 'Any Type' }, + { id: 'Issue', text: 'Issue' }, + { id: 'MergeRequest', text: 'Merge Request' } ] + end + + def todo_actions_dropdown_label(selected_action_id, default_action) + selected_action = todo_actions_options.find { |action| action[:id] == selected_action_id.to_i} + selected_action ? selected_action[:text] : default_action + end - options_from_collection_for_select(types, 'name', 'title', params[:type]) + def todo_types_dropdown_label(selected_type, default_type) + selected_type = todo_types_options.find { |type| type[:id] == selected_type && type[:id] != '' } + selected_type ? selected_type[:text] : default_type end private diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 8b83bbd93b74fc42d04bf4470f0d4d39586486e0..61a574d3dc0d1a9ec23360d3cefd03cde1bea350 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base default reply_to: Proc.new { default_reply_to_address.format } def can? - Ability.abilities.allowed?(current_user, action, subject) + Ability.allowed?(current_user, action, subject) end private diff --git a/app/models/ability.rb b/app/models/ability.rb index a49dd7039262df49d81f84d6a7f97837c1458728..fa8f8bc3a5f8dca53e3340e53f2a90c54a51a3d6 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,34 +1,5 @@ class Ability class << self - # rubocop: disable Metrics/CyclomaticComplexity - def allowed(user, subject) - return anonymous_abilities(user, subject) if user.nil? - return [] unless user.is_a?(User) - return [] if user.blocked? - - abilities_by_subject_class(user: user, subject: subject) - end - - def abilities_by_subject_class(user:, subject:) - case subject - when CommitStatus then commit_status_abilities(user, subject) - when Project then project_abilities(user, subject) - when Issue then issue_abilities(user, subject) - when Note then note_abilities(user, subject) - when ProjectSnippet then project_snippet_abilities(user, subject) - when PersonalSnippet then personal_snippet_abilities(user, subject) - when MergeRequest then merge_request_abilities(user, subject) - when Group then group_abilities(user, subject) - when Namespace then namespace_abilities(user, subject) - when GroupMember then group_member_abilities(user, subject) - when ProjectMember then project_member_abilities(user, subject) - when User then user_abilities - when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project) - when Ci::Runner then runner_abilities(user, subject) - else [] - end.concat(global_abilities(user)) - end - # Given a list of users and a project this method returns the users that can # read the given project. def users_that_can_read_project(users, project) @@ -61,359 +32,7 @@ class Ability issues.select { |issue| issue.visible_to_user?(user) } end - # List of possible abilities for anonymous user - def anonymous_abilities(user, subject) - if subject.is_a?(PersonalSnippet) - anonymous_personal_snippet_abilities(subject) - elsif subject.is_a?(ProjectSnippet) - anonymous_project_snippet_abilities(subject) - elsif subject.is_a?(CommitStatus) - anonymous_commit_status_abilities(subject) - elsif subject.is_a?(Project) || subject.respond_to?(:project) - anonymous_project_abilities(subject) - elsif subject.is_a?(Group) || subject.respond_to?(:group) - anonymous_group_abilities(subject) - elsif subject.is_a?(User) - anonymous_user_abilities - else - [] - end - end - - def anonymous_project_abilities(subject) - project = if subject.is_a?(Project) - subject - else - subject.project - end - - if project && project.public? - rules = [ - :read_project, - :read_board, - :read_list, - :read_wiki, - :read_label, - :read_milestone, - :read_project_snippet, - :read_project_member, - :read_merge_request, - :read_note, - :read_pipeline, - :read_commit_status, - :read_container_image, - :download_code - ] - - # Allow to read builds by anonymous user if guests are allowed - rules << :read_build if project.public_builds? - - # Allow to read issues by anonymous user if issue is not confidential - rules << :read_issue unless subject.is_a?(Issue) && subject.confidential? - - rules - project_disabled_features_rules(project) - else - [] - end - end - - def anonymous_commit_status_abilities(subject) - rules = anonymous_project_abilities(subject.project) - # If subject is Ci::Build which inherits from CommitStatus filter the abilities - rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) - rules - end - - def anonymous_group_abilities(subject) - rules = [] - - group = if subject.is_a?(Group) - subject - else - subject.group - end - - rules << :read_group if group.public? - - rules - end - - def anonymous_personal_snippet_abilities(snippet) - if snippet.public? - [:read_personal_snippet] - else - [] - end - end - - def anonymous_project_snippet_abilities(snippet) - if snippet.public? - [:read_project_snippet] - else - [] - end - end - - def anonymous_user_abilities - [:read_user] unless restricted_public_level? - end - - def global_abilities(user) - rules = [] - rules << :create_group if user.can_create_group - rules << :read_users_list - rules - end - - def project_abilities(user, project) - key = "/user/#{user.id}/project/#{project.id}" - - if RequestStore.active? - RequestStore.store[key] ||= uncached_project_abilities(user, project) - else - uncached_project_abilities(user, project) - end - end - - def uncached_project_abilities(user, project) - rules = [] - # Push abilities on the users team role - rules.push(*project_team_rules(project.team, user)) - - owner = user.admin? || - project.owner == user || - (project.group && project.group.has_owner?(user)) - - if owner - rules.push(*project_owner_rules) - end - - if project.public? || (project.internal? && !user.external?) - rules.push(*public_project_rules) - - # Allow to read builds for internal projects - rules << :read_build if project.public_builds? - - unless owner || project.team.member?(user) || project_group_member?(project, user) - rules << :request_access if project.request_access_enabled - end - end - - if project.archived? - rules -= project_archived_rules - end - - (rules - project_disabled_features_rules(project)).uniq - end - - def project_team_rules(team, user) - # Rules based on role in project - if team.master?(user) - project_master_rules - elsif team.developer?(user) - project_dev_rules - elsif team.reporter?(user) - project_report_rules - elsif team.guest?(user) - project_guest_rules - else - [] - end - end - - def public_project_rules - @public_project_rules ||= project_guest_rules + [ - :download_code, - :fork_project, - :read_commit_status, - :read_pipeline, - :read_container_image - ] - end - - def project_guest_rules - @project_guest_rules ||= [ - :read_project, - :read_wiki, - :read_issue, - :read_board, - :read_list, - :read_label, - :read_milestone, - :read_project_snippet, - :read_project_member, - :read_merge_request, - :read_note, - :create_project, - :create_issue, - :create_note, - :upload_file - ] - end - - def project_report_rules - @project_report_rules ||= project_guest_rules + [ - :download_code, - :fork_project, - :create_project_snippet, - :update_issue, - :admin_issue, - :admin_label, - :admin_list, - :read_commit_status, - :read_build, - :read_container_image, - :read_pipeline, - :read_environment, - :read_deployment - ] - end - - def project_dev_rules - @project_dev_rules ||= project_report_rules + [ - :admin_merge_request, - :update_merge_request, - :create_commit_status, - :update_commit_status, - :create_build, - :update_build, - :create_pipeline, - :update_pipeline, - :create_merge_request, - :create_wiki, - :push_code, - :resolve_note, - :create_container_image, - :update_container_image, - :create_environment, - :create_deployment - ] - end - - def project_archived_rules - @project_archived_rules ||= [ - :create_merge_request, - :push_code, - :push_code_to_protected_branches, - :update_merge_request, - :admin_merge_request - ] - end - - def project_master_rules - @project_master_rules ||= project_dev_rules + [ - :push_code_to_protected_branches, - :update_project_snippet, - :update_environment, - :update_deployment, - :admin_milestone, - :admin_project_snippet, - :admin_project_member, - :admin_merge_request, - :admin_note, - :admin_wiki, - :admin_project, - :admin_commit_status, - :admin_build, - :admin_container_image, - :admin_pipeline, - :admin_environment, - :admin_deployment - ] - end - - def project_owner_rules - @project_owner_rules ||= project_master_rules + [ - :change_namespace, - :change_visibility_level, - :rename_project, - :remove_project, - :archive_project, - :remove_fork_project, - :destroy_merge_request, - :destroy_issue - ] - end - - def project_disabled_features_rules(project) - rules = [] - - unless project.issues_enabled - rules += named_abilities('issue') - end - - unless project.merge_requests_enabled - rules += named_abilities('merge_request') - end - - unless project.issues_enabled or project.merge_requests_enabled - rules += named_abilities('label') - rules += named_abilities('milestone') - end - - unless project.snippets_enabled - rules += named_abilities('project_snippet') - end - - unless project.wiki_enabled - rules += named_abilities('wiki') - end - - unless project.builds_enabled - rules += named_abilities('build') - rules += named_abilities('pipeline') - rules += named_abilities('environment') - rules += named_abilities('deployment') - end - - unless project.container_registry_enabled - rules += named_abilities('container_image') - end - - rules - end - - def group_abilities(user, group) - rules = [] - rules << :read_group if can_read_group?(user, group) - - owner = user.admin? || group.has_owner?(user) - master = owner || group.has_master?(user) - - # Only group masters and group owners can create new projects - if master - rules += [ - :create_projects, - :admin_milestones - ] - end - - # Only group owner and administrators can admin group - if owner - rules += [ - :admin_group, - :admin_namespace, - :admin_group_member, - :change_visibility_level - ] - end - - if group.public? || (group.internal? && !user.external?) - rules << :request_access if group.request_access_enabled && group.users.exclude?(user) - end - - rules.flatten - end - - def can_read_group?(user, group) - return true if user.admin? - return true if group.public? - return true if group.internal? && !user.external? - return true if group.users.include?(user) - - GroupProjectsFinder.new(group).execute(user).any? - end - + # TODO: make this private and use the actual abilities stuff for this def can_edit_note?(user, note) return false if !note.editable? || !user.present? return true if note.author == user || user.admin? @@ -426,207 +45,23 @@ class Ability end end - def namespace_abilities(user, namespace) - rules = [] - - # Only namespace owner and administrators can admin it - if namespace.owner == user || user.admin? - rules += [ - :create_projects, - :admin_namespace - ] - end - - rules.flatten - end - - [:issue, :merge_request].each do |name| - define_method "#{name}_abilities" do |user, subject| - rules = [] - - if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user) - rules += [ - :"read_#{name}", - :"update_#{name}", - ] - end - - rules += project_abilities(user, subject.project) - rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue) - rules - end - end - - def note_abilities(user, note) - rules = [] - - if note.author == user - rules += [ - :read_note, - :update_note, - :admin_note, - :resolve_note - ] - end - - if note.respond_to?(:project) && note.project - rules += project_abilities(user, note.project) - end - - if note.for_merge_request? && note.noteable.author == user - rules << :resolve_note - end - - rules - end - - def personal_snippet_abilities(user, snippet) - rules = [] - - if snippet.author == user - rules += [ - :read_personal_snippet, - :update_personal_snippet, - :admin_personal_snippet - ] - end - - if snippet.public? || (snippet.internal? && !user.external?) - rules << :read_personal_snippet - end - - rules + def allowed?(user, action, subject) + allowed(user, subject).include?(action) end - def project_snippet_abilities(user, snippet) - rules = [] - - if snippet.author == user || user.admin? - rules += [ - :read_project_snippet, - :update_project_snippet, - :admin_project_snippet - ] - end - - if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user)) - rules << :read_project_snippet - end - - rules - end - - def group_member_abilities(user, subject) - rules = [] - target_user = subject.user - group = subject.group - - unless group.last_owner?(target_user) - can_manage = group_abilities(user, group).include?(:admin_group_member) - - if can_manage - rules << :update_group_member - rules << :destroy_group_member - elsif user == target_user - rules << :destroy_group_member - end - end - - rules - end - - def project_member_abilities(user, subject) - rules = [] - target_user = subject.user - project = subject.project - - unless target_user == project.owner - can_manage = project_abilities(user, project).include?(:admin_project_member) - - if can_manage - rules << :update_project_member - rules << :destroy_project_member - elsif user == target_user - rules << :destroy_project_member - end - end - - rules - end - - def commit_status_abilities(user, subject) - rules = project_abilities(user, subject.project) - # If subject is Ci::Build which inherits from CommitStatus filter the abilities - rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) - rules - end - - def filter_build_abilities(rules) - # If we can't read build we should also not have that - # ability when looking at this in context of commit_status - %w(read create update admin).each do |rule| - rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build") - end - rules - end - - def runner_abilities(user, runner) - if user.is_admin? - [:assign_runner] - elsif runner.is_shared? || runner.locked? - [] - elsif user.ci_authorized_runners.include?(runner) - [:assign_runner] - else - [] - end - end + def allowed(user, subject) + return uncached_allowed(user, subject) unless RequestStore.active? - def user_abilities - [:read_user] - end - - def abilities - @abilities ||= begin - abilities = Six.new - abilities << self - abilities - end + user_key = user ? user.id : 'anonymous' + subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global' + key = "/ability/#{user_key}/#{subject_key}" + RequestStore[key] ||= uncached_allowed(user, subject).freeze end private - def restricted_public_level? - current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) - end - - def named_abilities(name) - [ - :"read_#{name}", - :"create_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end - - def filter_confidential_issues_abilities(user, issue, rules) - return rules if user.admin? || !issue.confidential? - - unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER) - rules.delete(:admin_issue) - rules.delete(:read_issue) - rules.delete(:update_issue) - end - - rules - end - - def project_group_member?(project, user) - project.group && - ( - project.group.members.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) - ) + def uncached_allowed(user, subject) + BasePolicy.class_for(subject).abilities(user, subject) end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 03812cd195f8f2adab3bbee66986c875f7df91e4..bd1737c7587ce6fdf31c9a70d2689870a3af7365 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -65,8 +65,8 @@ module Ci end # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest_successful_for, ->(ref = default_branch) do - where(ref: ref).success.order(id: :desc).limit(1) + def self.latest_successful_for(ref) + where(ref: ref).order(id: :desc).success.first end def self.truncate_sha(sha) diff --git a/app/models/commit.rb b/app/models/commit.rb index 817d063e4a2ac88f5eff91cc0112b31ec7a1fe21..e64fd1e0c1b381e86b30e16c4acafec3ee9102c2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -108,15 +108,6 @@ class Commit @diff_line_count end - # Returns a string describing the commit for use in a link title - # - # Example - # - # "Commit: Alex Denisov - Project git clone panel" - def link_title - "Commit: #{author_name} - #{title}" - end - # Returns the commits title. # # Usually, the commit title is the first line of the commit message. diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 630ee9601e0212e418e6363b6f7d0c0694b137cc..656a242c265a4b3849adfbf72bedfe91bc8147f0 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -4,12 +4,10 @@ # # range = CommitRange.new('f3f85602...e86e1013', project) # range.exclude_start? # => false -# range.reference_title # => "Commits f3f85602 through e86e1013" # range.to_s # => "f3f85602...e86e1013" # # range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range.exclude_start? # => true -# range.reference_title # => "Commits f3f85602^ through e86e1013" # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_s # => "f3f85602..e86e1013" # @@ -109,11 +107,6 @@ class CommitRange reference end - # Returns a String for use in a link's title attribute - def reference_title - "Commits #{sha_start} through #{sha_to}" - end - # Return a Hash of parameters for passing to a URL helper # # See `namespace_project_compare_url` diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 800a16ab246c83db03d993b6b45154bab80ab987..d8d4575bb4dff230aa4259b99df580084b68dc35 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -2,7 +2,7 @@ module Awardable extend ActiveSupport::Concern included do - has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy + has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy if self < Participable # By default we always load award_emoji user association @@ -59,6 +59,18 @@ module Awardable true end + def awardable_votes?(name) + AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name + end + + def user_can_award?(current_user, name) + if user_authored?(current_user) + !awardable_votes?(normalize_name(name)) + else + true + end + end + def awarded_emoji?(emoji_name, current_user) award_emoji.where(name: emoji_name, user: current_user).exists? end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 8e11d4f57cf41c6e81bdf793bfa12978bed7745d..22231b2e0f03a20750862f2880d64d4fae6c63a5 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -196,6 +196,10 @@ module Issuable end end + def user_authored?(user) + user == author + end + def subscribed_without_subscriptions?(user) participants(user).include?(user) end diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index a881fb83b7f6fee492c63895e008e02942222711..b8dd27a7afe5d0bfffeb50d7bbac37613733339a 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -28,4 +28,8 @@ module NoteOnDiff def can_be_award_emoji? false end + + def to_discussion + Discussion.new([self]) + end end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb new file mode 100644 index 0000000000000000000000000000000000000000..9216122923e4bf22ab79d03f38a1ef1c870962b4 --- /dev/null +++ b/app/models/concerns/project_features_compatibility.rb @@ -0,0 +1,37 @@ +# Makes api V3 compatible with old project features permissions methods +# +# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled +# fields to a new table "project_features", support for the old fields is still needed in the API. + +module ProjectFeaturesCompatibility + extend ActiveSupport::Concern + + def wiki_enabled=(value) + write_feature_attribute(:wiki_access_level, value) + end + + def builds_enabled=(value) + write_feature_attribute(:builds_access_level, value) + end + + def merge_requests_enabled=(value) + write_feature_attribute(:merge_requests_access_level, value) + end + + def issues_enabled=(value) + write_feature_attribute(:issues_access_level, value) + end + + def snippets_enabled=(value) + write_feature_attribute(:snippets_access_level, value) + end + + private + + def write_feature_attribute(field, value) + build_project_feature unless project_feature + + access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + project_feature.update_attribute(field, access_level) + end +end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index df2a9e3e84be06a8d7c8a8f01b3de00cb5382d4b..a3ac577cf3e8d329dcf35e3104739a089812b903 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -52,11 +52,11 @@ module Taskable end # Return a string that describes the current state of this Taskable's task - # list items, e.g. "20 tasks (12 completed, 8 remaining)" + # list items, e.g. "12 of 20 tasks completed" def task_status return '' if description.blank? sum = tasks.summary - "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)" + "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed" end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index c8320ff87fa735cef0e79ea80f91e0242753928b..4442cefc7e955c255c921e8ecd02afa7a33e6ab3 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -107,10 +107,6 @@ class DiffNote < Note self.noteable.find_diff_discussion(self.discussion_id) end - def to_discussion - Discussion.new([self]) - end - private def supported? diff --git a/app/models/event.rb b/app/models/event.rb index fd736d123593b66b386c552b006a8146f2756d9d..a0b7b0dc2b597016dfc0854abfc350ce20b7f634 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -65,7 +65,7 @@ class Event < ActiveRecord::Base elsif created_project? true elsif issue? || issue_note? - Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target) + Ability.allowed?(user, :read_issue, note? ? note_target : target) else ((merge_request? || note?) && target.present?) || milestone? end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1d05e4a85d1bbb2baf8ef2d7e353826368d43c99..b0b1313f94ace26e7ac4c469a4a8035f83bd3dc6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -91,13 +91,13 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: [:allow_broken, :importing?] + validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches, unless: [:allow_broken, :importing?] - validate :validate_fork + validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] + validate :validate_fork, unless: :closed_without_fork? scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } @@ -240,12 +240,12 @@ class MergeRequest < ActiveRecord::Base def source_branch_head source_branch_ref = @source_branch_sha || source_branch - source_project.repository.commit(source_branch) if source_branch_ref + source_project.repository.commit(source_branch_ref) if source_branch_ref end def target_branch_head target_branch_ref = @target_branch_sha || target_branch - target_project.repository.commit(target_branch) if target_branch_ref + target_project.repository.commit(target_branch_ref) if target_branch_ref end def branch_merge_base_commit @@ -305,19 +305,22 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project + return true if target_project == source_project + return true unless forked_source_project_missing? - if target_project == source_project - true - else - # If source and target projects are different - # we should check if source project is actually a fork of target project - if source_project.forked_from?(target_project) - true - else - errors.add :validate_fork, - 'Source project is not a fork of target project' - end - end + errors.add :validate_fork, + 'Source project is not a fork of the target project' + end + + def closed_without_fork? + closed? && forked_source_project_missing? + end + + def forked_source_project_missing? + return false unless for_fork? + return true unless source_project + + !source_project.forked_from?(target_project) end def ensure_merge_request_diff @@ -408,7 +411,7 @@ class MergeRequest < ActiveRecord::Base def can_remove_source_branch?(current_user) !source_project.protected_branch?(source_branch) && !source_project.root_ref?(source_branch) && - Ability.abilities.allowed?(current_user, :push_code, source_project) && + Ability.allowed?(current_user, :push_code, source_project) && diff_head_commit == source_branch_head end @@ -726,7 +729,9 @@ class MergeRequest < ActiveRecord::Base end def pipeline - @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project + return unless diff_head_sha && source_project + + @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) end def all_pipelines diff --git a/app/models/note.rb b/app/models/note.rb index f2656df028b2a282573f6b884714a37a7104ae1b..b94e3cff2cec8fe9232fb550fe2e932117a1f286 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -223,6 +223,10 @@ class Note < ActiveRecord::Base end end + def user_authored?(user) + user == author + end + def award_emoji? can_be_award_emoji? && contains_emoji_only? end diff --git a/app/models/project.rb b/app/models/project.rb index 0e4fb94f8eb5ddf56b07b25b6e07a1be23e04ba3..a6de2c4807164c1456b39ee240162dc6f2242041 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -11,24 +11,23 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include ProjectFeaturesCompatibility extend Gitlab::ConfigHelper UNKNOWN_IMPORT_URL = 'http://unknown.git' + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true + default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level - default_value_for :issues_enabled, gitlab_config_features.issues - default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests - default_value_for :builds_enabled, gitlab_config_features.builds - default_value_for :wiki_enabled, gitlab_config_features.wiki - default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } after_create :ensure_dir_exist after_save :ensure_dir_exist, if: :namespace_id_changed? + after_initialize :setup_project_feature # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -62,10 +61,10 @@ class Project < ActiveRecord::Base belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' belongs_to :namespace - has_one :board, dependent: :destroy - has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' + has_one :board, dependent: :destroy + # Project services has_many :services has_one :campfire_service, dependent: :destroy @@ -130,6 +129,7 @@ class Project < ActiveRecord::Base has_many :notification_settings, dependent: :destroy, as: :source has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + has_one :project_feature, dependent: :destroy has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id @@ -142,6 +142,7 @@ class Project < ActiveRecord::Base has_many :deployments, dependent: :destroy accepts_nested_attributes_for :variables, allow_destroy: true + accepts_nested_attributes_for :project_feature delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -159,8 +160,6 @@ class Project < ActiveRecord::Base length: { within: 0..255 }, format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } - validates :issues_enabled, :merge_requests_enabled, - :wiki_enabled, inclusion: { in: [true, false] } validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id @@ -196,6 +195,9 @@ class Project < ActiveRecord::Base scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } + scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') } + scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') } + scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } @@ -390,6 +392,13 @@ class Project < ActiveRecord::Base end end + def lfs_enabled? + return false unless Gitlab.config.lfs.enabled + return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil? + + self[:lfs_enabled] + end + def repository_storage_path Gitlab.config.repositories.storages[repository_storage] end @@ -436,7 +445,7 @@ class Project < ActiveRecord::Base # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) - latest_pipeline = pipelines.latest_successful_for(ref).first + latest_pipeline = pipelines.latest_successful_for(ref) if latest_pipeline latest_pipeline.builds.latest.with_artifacts @@ -680,6 +689,10 @@ class Project < ActiveRecord::Base update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) end + def has_wiki? + wiki_enabled? || has_external_wiki? + end + def external_wiki if has_external_wiki.nil? cache_has_external_wiki # Populate @@ -1096,16 +1109,21 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def pipeline(sha, ref) + def pipeline_for(ref, sha = nil) + sha ||= commit(ref).try(:sha) + + return unless sha + pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_pipeline(sha, ref, current_user = nil) - pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user) + def ensure_pipeline(ref, sha, current_user = nil) + pipeline_for(ref, sha) || + pipelines.create(sha: sha, ref: ref, user: current_user) end def enable_ci - self.builds_enabled = true + project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end def any_runners?(&block) @@ -1272,6 +1290,11 @@ class Project < ActiveRecord::Base private + # Prevents the creation of project_feature record for every project + def setup_project_feature + build_project_feature unless project_feature + end + def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb new file mode 100644 index 0000000000000000000000000000000000000000..9c602c582bd24b039ee4d66cbce5fb6ebd6d091f --- /dev/null +++ b/app/models/project_feature.rb @@ -0,0 +1,63 @@ +class ProjectFeature < ActiveRecord::Base + # == Project features permissions + # + # Grants access level to project tools + # + # Tools can be enabled only for users, everyone or disabled + # Access control is made only for non private projects + # + # levels: + # + # Disabled: not enabled for anyone + # Private: enabled only for team members + # Enabled: enabled for everyone able to access the project + # + + # Permision levels + DISABLED = 0 + PRIVATE = 10 + ENABLED = 20 + + FEATURES = %i(issues merge_requests wiki snippets builds) + + belongs_to :project + + def feature_available?(feature, user) + raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) + + get_permission(user, public_send("#{feature}_access_level")) + end + + def builds_enabled? + return true unless builds_access_level + + builds_access_level > DISABLED + end + + def wiki_enabled? + return true unless wiki_access_level + + wiki_access_level > DISABLED + end + + def merge_requests_enabled? + return true unless merge_requests_access_level + + merge_requests_access_level > DISABLED + end + + private + + def get_permission(user, level) + case level + when DISABLED + false + when PRIVATE + user && (project.team.member?(user) || user.admin?) + when ENABLED + true + else + true + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 91bdafdac99b9f7e394f062f92a4594daf267c49..f891e8374d2f3844bdf9db5e535a7fe736f6257f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -120,8 +120,21 @@ class Repository commits end - def find_branch(name) - raw_repository.branches.find { |branch| branch.name == name } + def find_branch(name, fresh_repo: true) + # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may + # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate + # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc) + # may cause the branch to "disappear" erroneously or have the wrong SHA. + # + # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392 + raw_repo = + if fresh_repo + Gitlab::Git::Repository.new(path_to_repo) + else + raw_repository + end + + raw_repo.find_branch(name) end def find_tag(name) diff --git a/app/models/user.rb b/app/models/user.rb index ad3cfbc03e4c6a5cfefc31194107164b4fb34b4e..6996740eebd6e5031dbb934e53d97e00d5d03a74 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -433,7 +433,7 @@ class User < ActiveRecord::Base # # This logic is duplicated from `Ability#project_abilities` into a SQL form. def projects_where_can_admin_issues - authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false) + authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled end def is_admin? @@ -460,16 +460,12 @@ class User < ActiveRecord::Base can?(:create_group, nil) end - def abilities - Ability.abilities - end - def can_select_namespace? several_namespaces? || admin end def can?(action, subject) - abilities.allowed?(self, action, subject) + Ability.allowed?(self, action, subject) end def first_name diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..118c100ca11e3dd6c0f656c373a8b2d64e9a67cd --- /dev/null +++ b/app/policies/base_policy.rb @@ -0,0 +1,116 @@ +class BasePolicy + class RuleSet + attr_reader :can_set, :cannot_set + def initialize(can_set, cannot_set) + @can_set = can_set + @cannot_set = cannot_set + end + + def size + to_set.size + end + + def self.empty + new(Set.new, Set.new) + end + + def can?(ability) + @can_set.include?(ability) && !@cannot_set.include?(ability) + end + + def include?(ability) + can?(ability) + end + + def to_set + @can_set - @cannot_set + end + + def merge(other) + @can_set.merge(other.can_set) + @cannot_set.merge(other.cannot_set) + end + + def can!(*abilities) + @can_set.merge(abilities) + end + + def cannot!(*abilities) + @cannot_set.merge(abilities) + end + + def freeze + @can_set.freeze + @cannot_set.freeze + super + end + end + + def self.abilities(user, subject) + new(user, subject).abilities + end + + def self.class_for(subject) + return GlobalPolicy if subject.nil? + + subject.class.ancestors.each do |klass| + next unless klass.name + + begin + policy_class = "#{klass.name}Policy".constantize + + # NOTE: the < operator here tests whether policy_class + # inherits from BasePolicy + return policy_class if policy_class < BasePolicy + rescue NameError + nil + end + end + + raise "no policy for #{subject.class.name}" + end + + attr_reader :user, :subject + def initialize(user, subject) + @user = user + @subject = subject + end + + def abilities + return RuleSet.empty if @user && @user.blocked? + return anonymous_abilities if @user.nil? + collect_rules { rules } + end + + def anonymous_abilities + collect_rules { anonymous_rules } + end + + def anonymous_rules + rules + end + + def delegate!(new_subject) + @rule_set.merge(Ability.allowed(@user, new_subject)) + end + + def can?(rule) + @rule_set.can?(rule) + end + + def can!(*rules) + @rule_set.can!(*rules) + end + + def cannot!(*rules) + @rule_set.cannot!(*rules) + end + + private + + def collect_rules(&b) + @rule_set = RuleSet.empty + yield + @rule_set + end +end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..2232e231cf84f0b9c37505c3a522185db3a3040e --- /dev/null +++ b/app/policies/ci/build_policy.rb @@ -0,0 +1,13 @@ +module Ci + class BuildPolicy < CommitStatusPolicy + def rules + super + + # If we can't read build we should also not have that + # ability when looking at this in context of commit_status + %w(read create update admin).each do |rule| + cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" + end + end + end +end diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..7edd383530d794a5ccf66ebba4dd1f271a976c9b --- /dev/null +++ b/app/policies/ci/runner_policy.rb @@ -0,0 +1,13 @@ +module Ci + class RunnerPolicy < BasePolicy + def rules + return unless @user + + can! :assign_runner if @user.is_admin? + + return if @subject.is_shared? || @subject.locked? + + can! :assign_runner if @user.ci_authorized_runners.include?(@subject) + end + end +end diff --git a/app/policies/commit_status_policy.rb b/app/policies/commit_status_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..593df738328045f390787de45c8ff54fc2b3d9a2 --- /dev/null +++ b/app/policies/commit_status_policy.rb @@ -0,0 +1,5 @@ +class CommitStatusPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..163d070ff903447ac5929a95131d9d892bc8a3f3 --- /dev/null +++ b/app/policies/deployment_policy.rb @@ -0,0 +1,5 @@ +class DeploymentPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..f4219569161e92d36d054a70237e8271c4cb35ba --- /dev/null +++ b/app/policies/environment_policy.rb @@ -0,0 +1,5 @@ +class EnvironmentPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/external_issue_policy.rb b/app/policies/external_issue_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9e28bd107a25699ac8c7eb69b23bda25bb9c0ff --- /dev/null +++ b/app/policies/external_issue_policy.rb @@ -0,0 +1,5 @@ +class ExternalIssuePolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..3c2fbe6b56baa58386384ef32b957684a5a203ec --- /dev/null +++ b/app/policies/global_policy.rb @@ -0,0 +1,8 @@ +class GlobalPolicy < BasePolicy + def rules + return unless @user + + can! :create_group if @user.can_create_group + can! :read_users_list + end +end diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..62335527654cd230a2947a5f52ff97807f0f02fc --- /dev/null +++ b/app/policies/group_member_policy.rb @@ -0,0 +1,19 @@ +class GroupMemberPolicy < BasePolicy + def rules + return unless @user + + target_user = @subject.user + group = @subject.group + + return if group.last_owner?(target_user) + + can_manage = Ability.allowed?(@user, :admin_group_member, group) + + if can_manage + can! :update_group_member + can! :destroy_group_member + elsif @user == target_user + can! :destroy_group_member + end + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..97ff62339683be16508b4fb8361da693167c7b03 --- /dev/null +++ b/app/policies/group_policy.rb @@ -0,0 +1,45 @@ +class GroupPolicy < BasePolicy + def rules + can! :read_group if @subject.public? + return unless @user + + globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) + member = @subject.users.include?(@user) + owner = @user.admin? || @subject.has_owner?(@user) + master = owner || @subject.has_master?(@user) + + can_read = false + can_read ||= globally_viewable + can_read ||= member + can_read ||= @user.admin? + can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any? + can! :read_group if can_read + + # Only group masters and group owners can create new projects + if master + can! :create_projects + can! :admin_milestones + end + + # Only group owner and administrators can admin group + if owner + can! :admin_group + can! :admin_namespace + can! :admin_group_member + can! :change_visibility_level + end + + if globally_viewable && @subject.request_access_enabled && !member + can! :request_access + end + end + + def can_read_group? + return true if @subject.public? + return true if @user.admin? + return true if @subject.internal? && !@user.external? + return true if @subject.users.include?(@user) + + GroupProjectsFinder.new(@subject).execute(@user).any? + end +end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..c253f9a93995ebcfb1b688f037f90c1414e3f577 --- /dev/null +++ b/app/policies/issuable_policy.rb @@ -0,0 +1,14 @@ +class IssuablePolicy < BasePolicy + def action_name + @subject.class.name.underscore + end + + def rules + if @user && (@subject.author == @user || @subject.assignee == @user) + can! :"read_#{action_name}" + can! :"update_#{action_name}" + end + + delegate! @subject.project + end +end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..bd1811a3c54d51e31b75687ee67534d8c6b22af3 --- /dev/null +++ b/app/policies/issue_policy.rb @@ -0,0 +1,28 @@ +class IssuePolicy < IssuablePolicy + def issue + @subject + end + + def rules + super + + if @subject.confidential? && !can_read_confidential? + cannot! :read_issue + cannot! :admin_issue + cannot! :update_issue + cannot! :read_issue + end + end + + private + + def can_read_confidential? + return false unless @user + return true if @user.admin? + return true if @subject.author == @user + return true if @subject.assignee == @user + return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER) + + false + end +end diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..bc3afc626fb7bebb3c4e241a3a20aee857437246 --- /dev/null +++ b/app/policies/merge_request_policy.rb @@ -0,0 +1,3 @@ +class MergeRequestPolicy < IssuablePolicy + # pass +end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..29bb357e00a8c63b53267d160d91974ebb62bb1b --- /dev/null +++ b/app/policies/namespace_policy.rb @@ -0,0 +1,10 @@ +class NamespacePolicy < BasePolicy + def rules + return unless @user + + if @subject.owner == @user || @user.admin? + can! :create_projects + can! :admin_namespace + end + end +end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..83847466ee23ef3e35ce1b79a31f400961c8efd0 --- /dev/null +++ b/app/policies/note_policy.rb @@ -0,0 +1,19 @@ +class NotePolicy < BasePolicy + def rules + delegate! @subject.project + + return unless @user + + if @subject.author == @user + can! :read_note + can! :update_note + can! :admin_note + can! :resolve_note + end + + if @subject.for_merge_request? && + @subject.noteable.author == @user + can! :resolve_note + end + end +end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..46c5aa1a5be4849b8498d856873707af84b6b8b7 --- /dev/null +++ b/app/policies/personal_snippet_policy.rb @@ -0,0 +1,16 @@ +class PersonalSnippetPolicy < BasePolicy + def rules + can! :read_personal_snippet if @subject.public? + return unless @user + + if @subject.author == @user + can! :read_personal_snippet + can! :update_personal_snippet + can! :admin_personal_snippet + end + + if @subject.internal? && !@user.external? + can! :read_personal_snippet + end + end +end diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..1c038dddd4bdd0b991aed4f55921747e24a9c741 --- /dev/null +++ b/app/policies/project_member_policy.rb @@ -0,0 +1,22 @@ +class ProjectMemberPolicy < BasePolicy + def rules + # anonymous users have no abilities here + return unless @user + + target_user = @subject.user + project = @subject.project + + return if target_user == project.owner + + can_manage = Ability.allowed?(@user, :admin_project_member, project) + + if can_manage + can! :update_project_member + can! :destroy_project_member + end + + if @user == target_user + can! :destroy_project_member + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..acf36d422d1df3047fe782e40853c0d6b41bb493 --- /dev/null +++ b/app/policies/project_policy.rb @@ -0,0 +1,224 @@ +class ProjectPolicy < BasePolicy + def rules + team_access!(user) + + owner = user.admin? || + project.owner == user || + (project.group && project.group.has_owner?(user)) + + owner_access! if owner + + if project.public? || (project.internal? && !user.external?) + guest_access! + public_access! + + # Allow to read builds for internal projects + can! :read_build if project.public_builds? + + if project.request_access_enabled && + !(owner || project.team.member?(user) || project_group_member?(user)) + can! :request_access + end + end + + archived_access! if project.archived? + + disabled_features! + end + + def project + @subject + end + + def guest_access! + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_issue + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :create_project + can! :create_issue + can! :create_note + can! :upload_file + end + + def reporter_access! + can! :download_code + can! :fork_project + can! :create_project_snippet + can! :update_issue + can! :admin_issue + can! :admin_label + can! :admin_list + can! :read_commit_status + can! :read_build + can! :read_container_image + can! :read_pipeline + can! :read_environment + can! :read_deployment + end + + def developer_access! + can! :admin_merge_request + can! :update_merge_request + can! :create_commit_status + can! :update_commit_status + can! :create_build + can! :update_build + can! :create_pipeline + can! :update_pipeline + can! :create_merge_request + can! :create_wiki + can! :push_code + can! :resolve_note + can! :create_container_image + can! :update_container_image + can! :create_environment + can! :create_deployment + end + + def master_access! + can! :push_code_to_protected_branches + can! :update_project_snippet + can! :update_environment + can! :update_deployment + can! :admin_milestone + can! :admin_project_snippet + can! :admin_project_member + can! :admin_merge_request + can! :admin_note + can! :admin_wiki + can! :admin_project + can! :admin_commit_status + can! :admin_build + can! :admin_container_image + can! :admin_pipeline + can! :admin_environment + can! :admin_deployment + end + + def public_access! + can! :download_code + can! :fork_project + can! :read_commit_status + can! :read_pipeline + can! :read_container_image + end + + def owner_access! + guest_access! + reporter_access! + developer_access! + master_access! + can! :change_namespace + can! :change_visibility_level + can! :rename_project + can! :remove_project + can! :archive_project + can! :remove_fork_project + can! :destroy_merge_request + can! :destroy_issue + end + + # Push abilities on the users team role + def team_access!(user) + access = project.team.max_member_access(user.id) + + guest_access! if access >= Gitlab::Access::GUEST + reporter_access! if access >= Gitlab::Access::REPORTER + developer_access! if access >= Gitlab::Access::DEVELOPER + master_access! if access >= Gitlab::Access::MASTER + end + + def archived_access! + cannot! :create_merge_request + cannot! :push_code + cannot! :push_code_to_protected_branches + cannot! :update_merge_request + cannot! :admin_merge_request + end + + def disabled_features! + unless project.feature_available?(:issues, user) + cannot!(*named_abilities(:issue)) + end + + unless project.feature_available?(:merge_requests, user) + cannot!(*named_abilities(:merge_request)) + end + + unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user) + cannot!(*named_abilities(:label)) + cannot!(*named_abilities(:milestone)) + end + + unless project.feature_available?(:snippets, user) + cannot!(*named_abilities(:project_snippet)) + end + + unless project.feature_available?(:wiki, user) || project.has_external_wiki? + cannot!(*named_abilities(:wiki)) + end + + unless project.feature_available?(:builds, user) + cannot!(*named_abilities(:build)) + cannot!(*named_abilities(:pipeline)) + cannot!(*named_abilities(:environment)) + cannot!(*named_abilities(:deployment)) + end + + unless project.container_registry_enabled + cannot!(*named_abilities(:container_image)) + end + end + + def anonymous_rules + return unless project.public? + + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :read_pipeline + can! :read_commit_status + can! :read_container_image + can! :download_code + + # NOTE: may be overridden by IssuePolicy + can! :read_issue + + # Allow to read builds by anonymous user if guests are allowed + can! :read_build if project.public_builds? + + disabled_features! + end + + def project_group_member?(user) + project.group && + ( + project.group.members.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) + end + + def named_abilities(name) + [ + :"read_#{name}", + :"create_#{name}", + :"update_#{name}", + :"admin_#{name}" + ] + end +end diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..57acccfafd95c67b661fa923bb676e34d8124361 --- /dev/null +++ b/app/policies/project_snippet_policy.rb @@ -0,0 +1,20 @@ +class ProjectSnippetPolicy < BasePolicy + def rules + can! :read_project_snippet if @subject.public? + return unless @user + + if @user && @subject.author == @user || @user.admin? + can! :read_project_snippet + can! :update_project_snippet + can! :admin_project_snippet + end + + if @subject.internal? && !@user.external? + can! :read_project_snippet + end + + if @subject.private? && @subject.project.team.member?(@user) + can! :read_project_snippet + end + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..03a2499e2638b2e46401b1f3c8a152c67ebac6ea --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,11 @@ +class UserPolicy < BasePolicy + include Gitlab::CurrentSettings + + def rules + can! :read_user if @user || !restricted_public_level? + end + + def restricted_public_level? + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end +end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 0d55ba5a9816a944123058c034de268b9bd88f7b..0c208150fb86f3932010e3ef21b26017e94b9236 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -7,12 +7,8 @@ class BaseService @project, @current_user, @params = project, user, params.dup end - def abilities - Ability.abilities - end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end def notification_service diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index 5cb408b9d2021ae369ae63b897daa42c2cc6919c..b1887820bd4f2625ae2517993e7ed12316985870 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -3,7 +3,10 @@ module Boards class CreateService < Boards::BaseService def execute List.transaction do - create_list_at(next_position) + label = project.labels.find(params[:label_id]) + position = next_position + + create_list(label, position) end end @@ -14,8 +17,8 @@ module Boards max_position.nil? ? 0 : max_position.succ end - def create_list_at(position) - board.lists.create(params.merge(list_type: :label, position: position)) + def create_list(label, position) + board.lists.create(label: label, list_type: :label, position: position) end end end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index f049ed628db19863bcb6c7f6e321780f6c9fb6d8..de48a50774e2c1d931ee1d6812a1e5a991247021 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -10,13 +10,15 @@ module Ci create_builds! end - new_builds = - stage_indexes_of_created_builds.map do |index| - process_stage(index) - end + @pipeline.with_lock do + new_builds = + stage_indexes_of_created_builds.map do |index| + process_stage(index) + end - # Return a flag if a when builds got enqueued - new_builds.flatten.any? + # Return a flag if a when builds got enqueued + new_builds.flatten.any? + end end private diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 9a187f5d6945e63c76d0beeb73f75829278ca9a4..6973191b2030ca7ee6f8c9d695b7f6c8db9cfb3a 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -8,16 +8,18 @@ module Ci builds = if current_runner.shared? builds. - # don't run projects which have not enabled shared runners - joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }). + # don't run projects which have not enabled shared runners and builds + joins(:project).where(projects: { shared_runners_enabled: true }). + joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id'). # this returns builds that are ordered by number of running builds # we prefer projects that don't use shared runners at all joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). + where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') else # do run projects which are only assigned to this runner (FIFO) - builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC') + builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC') end build = builds.find do |build| diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb deleted file mode 100644 index 92e6df442b4f3f9ee929077a254c81684ed3571c..0000000000000000000000000000000000000000 --- a/app/services/ci/web_hook_service.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Ci - class WebHookService - def build_end(build) - execute_hooks(build.project, build_data(build)) - end - - def execute_hooks(project, data) - project.web_hooks.each do |web_hook| - async_execute_hook(web_hook, data) - end - end - - def async_execute_hook(hook, data) - Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data) - end - - def build_data(build) - project = build.project - data = {} - data.merge!({ - build_id: build.id, - build_name: build.name, - build_status: build.status, - build_started_at: build.started_at, - build_finished_at: build.finished_at, - project_id: project.id, - project_name: project.name, - gitlab_url: project.gitlab_url, - ref: build.ref, - before_sha: build.before_sha, - sha: build.sha, - }) - end - end -end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e06c37c323ed3bc4e47a67cf64e028f5945e9948..4c8d93999a7049f218ea3a273a4a7dd47b03e041 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -45,6 +45,7 @@ class IssuableBaseService < BaseService unless can?(current_user, ability, project) params.delete(:milestone_id) + params.delete(:labels) params.delete(:add_label_ids) params.delete(:remove_label_ids) params.delete(:label_ids) @@ -72,6 +73,7 @@ class IssuableBaseService < BaseService filter_labels_in_param(:add_label_ids) filter_labels_in_param(:remove_label_ids) filter_labels_in_param(:label_ids) + find_or_create_label_ids end def filter_labels_in_param(key) @@ -80,6 +82,17 @@ class IssuableBaseService < BaseService params[key] = project.labels.where(id: params[key]).pluck(:id) end + def find_or_create_label_ids + labels = params.delete(:labels) + return unless labels + + params[:label_ids] = labels.split(",").map do |label_name| + project.labels.create_with(color: Label::DEFAULT_COLOR) + .find_or_create_by(title: label_name.strip) + .id + end + end + def process_label_ids(attributes, existing_label_ids: nil) label_ids = attributes.delete(:label_ids) add_label_ids = attributes.delete(:add_label_ids) @@ -162,7 +175,12 @@ class IssuableBaseService < BaseService if params.present? && update_issuable(issuable, params) issuable.reset_events_cache - handle_common_system_notes(issuable, old_labels: old_labels) + + # We do not touch as it will affect a update on updated_at field + ActiveRecord::Base.no_touching do + handle_common_system_notes(issuable, old_labels: old_labels) + end + handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 08c1f72d65a1190525a31f14dd49ef0421256b74..1262ecbc29aa00a9ace6ac63244ba2846e7ee693 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -31,7 +31,7 @@ module MergeRequests def get_branches(changes) return [] if project.empty_repo? - return [] unless project.merge_requests_enabled + return [] unless project.merge_requests_enabled? changes_list = Gitlab::ChangesList.new(changes) changes_list.map do |change| diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index adc71b0c2bcfb47ff7e5cda058af6197a06e2ace..19caa038c4415f3198b565e367173f5bd9832173 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,11 +1,14 @@ module MergeRequests class ResolveService < MergeRequests::BaseService - attr_accessor :conflicts, :rugged, :merge_index + attr_accessor :conflicts, :rugged, :merge_index, :merge_request def execute(merge_request) @conflicts = merge_request.conflicts @rugged = project.repository.rugged @merge_index = conflicts.merge_index + @merge_request = merge_request + + fetch_their_commit! conflicts.files.each do |file| write_resolved_file_to_index(file, params[:sections]) @@ -27,5 +30,21 @@ module MergeRequests merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) merge_index.conflict_remove(our_path) end + + # If their commit (in the target project) doesn't exist in the source project, it + # can't be a parent for the merge commit we're about to create. If that's the case, + # fetch the target branch ref into the source project so the commit exists in both. + # + def fetch_their_commit! + return if rugged.include?(conflicts.their_commit.oid) + + random_string = SecureRandom.hex + + project.repository.fetch_ref( + merge_request.target_project.repository.path_to_repo, + "refs/heads/#{merge_request.target_branch}", + "refs/tmp/#{random_string}/head" + ) + end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 30c5f24988c8d379c25e03b8a953ebd33f78ffb4..398ec47f0ea22f35a8e130e7e9a04a26051203c8 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,6 +11,10 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + if merge_request.closed_without_fork? + params.except!(:target_branch, :force_remove_source_branch) + end + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) update(merge_request) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 55956be28445b5b8da27b54d8b1e809ffecd0250..be749ba4a1c3162621c59daba1a277dca9e6cc6c 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -7,7 +7,6 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) import_data = params.delete(:import_data) - @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level @@ -81,8 +80,7 @@ module Projects log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") unless @project.gitlab_project_import? - @project.create_wiki if @project.wiki_enabled? - + @project.create_wiki if @project.feature_available?(:wiki, current_user) @project.build_missing_services @project.create_labels diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index de6dc38cc8e345f87c22445f4622a2a11086f349..a2de4dcceceae096df5cdebf1a8545e13499d2ef 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -8,7 +8,6 @@ module Projects name: @project.name, path: @project.path, shared_runners_enabled: @project.shared_runners_enabled, - builds_enabled: @project.builds_enabled, namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } @@ -17,6 +16,9 @@ module Projects end new_project = CreateService.new(current_user, new_params).execute + builds_access_level = @project.project_feature.builds_access_level + new_project.project_feature.update_attributes(builds_access_level: builds_access_level) + new_project end diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index b2c607361b3d417e9b9755efc11893abcda1a69d..6c7c3c48604aa7a26baf05d8d9cc0d079ee2bacf 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -73,6 +73,12 @@ %span.light last commit: %strong = last_commit(@project) + + %li + %span.light Git LFS status: + %strong + = project_lfs_status(@project) + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - else %li %span.light repository: diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 0044d779c316728fabe30495b6dfb74c0a590038..889086c62b166e79a83945f5d80bcaa84f948394 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,3 +1,6 @@ +- page_title "CI Lint" +- page_description "Validate your GitLab CI configuration file" + %h2 Check your .gitlab-ci.yml %hr diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index d320d3bcc1e31ffec36a9f09b11733ed93de2d84..9d31f31c639d98a385e2530442187105b13bfb66 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -28,21 +28,25 @@ .row-content-block.second-block = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do .filter-item.inline - = select_tag('project_id', todo_projects_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Project'}) + - if params[:project_id].present? + = hidden_field_tag(:project_id, params[:project_id]) + = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', + placeholder: 'Search projects', data: { data: todo_projects_options } }) .filter-item.inline - = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) + - if params[:author_id].present? + = hidden_field_tag(:author_id, params[:author_id]) + = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', + placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } }) .filter-item.inline - = select_tag('type', todo_types_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Type'}) + - if params[:type].present? + = hidden_field_tag(:type, params[:type]) + = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', + data: { data: todo_types_options } }) .filter-item.inline.actions-filter - = select_tag('action_id', todo_actions_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Action'}) - + - if params[:action_id].present? + = hidden_field_tag(:action_id, params[:action_id]) + = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', + data: { data: todo_actions_options }}) .pull-right .dropdown.inline.prepend-left-10 %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -66,7 +70,7 @@ - if @todos.any? .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - @todos.group_by(&:project).each do |group| - .panel.panel-default.panel-small.js-todos-list + .panel.panel-default.panel-small - project = group[0] .panel-heading = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) @@ -76,11 +80,3 @@ = paginate @todos, theme: "gitlab" - else .nothing-here-block You're all done! - -:javascript - new UsersSelect(); - - $('form.filter-form').on('submit', function (event) { - event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); - }); diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 5c318cd3b8bdcff0f5e7212f654e9c1cf62ef205..31fdcc5e21bc404c71be3ab22c7323f19a2dd527 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,7 +1,7 @@ - if event.visible_to_user?(current_user) .event-item{ class: event_row_class(event) } .event-item-timestamp - #{time_ago_with_tooltip(event.created_at)} + #{time_ago_with_tooltip(event.created_at, skip_js: true)} = cache [event, current_application_settings, "v2.2"] do = author_avatar(event, size: 40) diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index d7d36c84b6caa9dc647e823af93cb427f2f1ea9d..27ac1760166fe75cbfb8e0e68196d4ef0f28fa09 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,5 +1,5 @@ += render 'layouts/nav/group_settings' .scrolling-tabs-container{ class: nav_control_class } - = render 'layouts/nav/group_settings' .fade-left = icon('angle-left') .fade-right diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index bf9a7ecb78679cfa46ee9708bc38f91d71ee6f0d..75275afc0f3058f85d0bbb0bbb1c07bfde805b1e 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,22 +1,26 @@ - if current_user + - can_admin_group = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group) - member = @group.members.find_by(user_id: current_user.id) - can_leave = member && can?(current_user, :destroy_group_member, member) - .controls - .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - = nav_link(path: 'groups#projects') do - = link_to 'Projects', projects_group_path(@group), title: 'Projects' - %li.divider - - if can_edit - %li - = link_to 'Edit Group', edit_group_path(@group) - - if can_leave - %li - = link_to polymorphic_path([:leave, @group, :members]), - data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do - Leave Group + - if can_admin_group || can_edit || can_leave + .controls + .dropdown.group-settings-dropdown + %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + - if can_admin_group + = nav_link(path: 'groups#projects') do + = link_to 'Projects', projects_group_path(@group), title: 'Projects' + - if can_edit || can_leave + %li.divider + - if can_edit + %li + = link_to 'Edit Group', edit_group_path(@group) + - if can_leave + %li + = link_to polymorphic_path([:leave, @group, :members]), + data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do + Leave Group diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 52a5bdc1a1b1093f98ed10ff6bf7f42f51c24d6d..613b8b7d3013d1b3c30e5156ef20a0f519e9898f 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -26,7 +26,7 @@ %span Protected Branches - - if @project.builds_enabled? + - if @project.feature_available?(:builds, current_user) = nav_link(controller: :runners) do = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do %span diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index de53a298f8480f1b98aa074d85ea3e0f9b9487f2..73066150fb3dfc9155828e458dd1c5596111c21c 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -13,19 +13,13 @@ %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } {{ list.title }} %span.pull-right{ "v-if" => "list.type !== 'blank'" } - {{ list.issues.length }} + {{ list.issuesSize }} - if can?(current_user, :admin_list, @project) %board-delete{ "inline-template" => true, ":list" => "list", "v-if" => "!list.preset && list.id" } %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } = icon("trash") - = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore") - .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" } - %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" } - = icon("search", class: "board-search-icon", "v-show" => "!query") - %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" } - = icon("times", class: "board-search-clear") %board-list{ "inline-template" => true, "v-if" => "list.type !== 'blank'", ":list" => "list", @@ -39,5 +33,11 @@ "v-show" => "!loading", ":data-board" => "list.id" } = render "projects/boards/components/card" + %li.board-list-count.text-center{ "v-if" => "showCount" } + = icon("spinner spin", "v-show" => "list.loadingMore" ) + %span{ "v-if" => "list.issues.length === list.issuesSize" } + Showing all issues + %span{ "v-else" => true } + Showing {{ list.issues.length }} of {{ list.issuesSize }} issues - if can?(current_user, :admin_list, @project) = render "projects/boards/components/blank_state" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 6192ccb710b3d99b3ba4c9a53a745ae1b3c5a422..808e6b957462e6e7fcfda794340703d8b74b3cad 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,6 +27,8 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare + = render 'projects/buttons/download', project: @project, ref: branch.name + - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 58f43ecb5d5f2341d08bdc45110e394b67438511..5f5e071eb40e318cd085ae005103ecae2f25d414 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,4 +1,42 @@ -- unless @project.empty_repo? - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do - = icon('download') +- if !project.empty_repo? && can?(current_user, :download_code, project) + %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'} + .dropdown.inline + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + %span.caret + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar + + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do + %i.fa.fa-download + %span Download '#{job.name}' diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 1fdf32466f234ac7c99e032935fa185d0be9b59a..73de8abe55b906275ad669f60ccf7925aa937b78 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -89,4 +89,4 @@ = icon('repeat') - elsif build.playable? = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do - = icon('play') + = custom_icon('icon_play') diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b119f6edf14909ca5711c58e230453fc91a2fadb..bb9493f51583cf8169e36212249345022fb3dbfe 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -66,13 +66,13 @@ - if actions.any? .btn-group %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = icon("play") + = custom_icon('icon_play') %b.caret %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do - = icon("play") + = custom_icon('icon_play') %span= build.name.humanize - if artifacts.present? .btn-group diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 29f4ef8f49e10edf3be8f17959eeafd62a300590..f41a11a056d2fe8a2bcfd5233680540e88090a50 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -10,7 +10,10 @@ %th Commit - pipelines.stages.each do |stage| %th.stage - %span.has-tooltip{ title: "#{stage.titleize}" } + - if stage.titleize.length > 12 + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize + - else = stage.titleize %th %th diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index fd888f41b1ed86ac1db7abb4480383ef535091c8..389477d09277d490b7bbadb7bf1d8fcdc1241721 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -7,7 +7,7 @@ - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key.push(commit.status) if commit.status -= cache(cache_key) do += cache(cache_key, expires_in: 1.day) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } = author_avatar(commit, size: 36) diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index f7bf3b834ef469da187f4297d8c0a0ae2b54ca14..16d134eb6b6694108cb38b26c76aba03f32349a0 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -5,13 +5,13 @@ .inline .dropdown %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = icon("play") + = custom_icon('icon_play') %b.caret %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |action| %li = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do - = icon("play") + = custom_icon('icon_play') %span= action.name.humanize - if local_assigns.fetch(:allow_rollback, false) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b282aa52b25d0a6eadcbd7b1ac1d05062f859f5c..f6d751a343e735a8a76d0af37e6d59c263f510f0 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -44,42 +44,56 @@ %hr %fieldset.features.append-bottom-0 %h5.prepend-top-0 - Features - .form-group - .checkbox - = f.label :issues_enabled do - = f.check_box :issues_enabled - %strong Issues - %br - %span.descr Lightweight issue tracking system for this project - .form-group - .checkbox - = f.label :merge_requests_enabled do - = f.check_box :merge_requests_enabled - %strong Merge Requests - %br - %span.descr Submit changes to be merged upstream - .form-group - .checkbox - = f.label :builds_enabled do - = f.check_box :builds_enabled - %strong Builds - %br - %span.descr Test and deploy your changes before merge - .form-group - .checkbox - = f.label :wiki_enabled do - = f.check_box :wiki_enabled - %strong Wiki - %br - %span.descr Pages for project documentation - .form-group - .checkbox - = f.label :snippets_enabled do - = f.check_box :snippets_enabled - %strong Snippets - %br - %span.descr Share code pastes with others out of git repository + Feature Visibility + + = f.fields_for :project_feature do |feature_fields| + .form_group.prepend-top-20 + .row + .col-md-9 + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-3 + = project_feature_access_select(:issues_access_level) + + .row + .col-md-9 + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-3 + = project_feature_access_select(:merge_requests_access_level) + + .row + .col-md-9 + = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + %span.help-block Submit Test and deploy your changes before merge + .col-md-3 + = project_feature_access_select(:builds_access_level) + + .row + .col-md-9 + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-3 + = project_feature_access_select(:wiki_access_level) + + .row + .col-md-9 + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-3 + = project_feature_access_select(:snippets_access_level) + + - if Gitlab.config.lfs.enabled && current_user.admin? + .form-group + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled, checked: @project.lfs_enabled? + %strong LFS + %br + %span.descr + Git Large File Storage + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + - if Gitlab.config.registry.enabled .form-group .checkbox @@ -88,7 +102,7 @@ %strong Container Registry %br %span.descr Enable Container Registry for this repository - %hr + = render 'merge_request_settings', f: f %hr %fieldset.features.append-bottom-default diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 31d40f6ad032b6100a87054cb8bacf7add03a89e..576d0bec51bb334a744edb024d56468bcea8b2ff 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,4 +1,5 @@ %li.build + .curve .build-content - if subject.target_url - link_to subject.target_url do diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index a231d68455965f7879c89648534aab5eecc4e129..082e2cb4d8cddaa5e641ccee523fd095bd01a2b5 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -12,7 +12,7 @@ = link_to 'Commits', commits_namespace_project_graph_path = nav_link(action: :languages) do = link_to 'Languages', languages_namespace_project_graph_path - - if @project.builds_enabled? + - if @project.feature_available?(:builds, current_user) = nav_link(action: :ci) do = link_to ci_namespace_project_graph_path do Continuous Integration diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 6ea9f612d13abe4f6560553acc610fbbf539e4b2..a8eeab3e55e416871021e6b8997f00a968a45a6d 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,7 +5,7 @@ - @related_branches.each do |branch| %li - target = @project.repository.find_branch(branch).target - - pipeline = @project.pipeline(target.sha, branch) if target + - pipeline = @project.pipeline_for(branch, target.sha) if target - if pipeline %span.related-branch-ci-status = render_pipeline_status(pipeline) diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 098ce19da21bc95ddfb5972870483cf7e7e687b8..e35291dff7de65afb21ee1140a172501bca63d22 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,3 +1,7 @@ +- if @merge_request.closed_without_fork? + .alert.alert-danger + %p The source project of this merge request has been removed. + .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box{ class: status_box_class(@merge_request) } diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 5f466bdbac2c89882907ffcce380028984590142..4d957e0d890c4e4ccf5d723f4de7c7c82674952f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -49,7 +49,10 @@ %th Commit - stages.each do |stage| %th.stage - %span.has-tooltip{ title: "#{stage.titleize}" } + - if stage.titleize.length > 12 + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize + - else = stage.titleize %th %th diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml deleted file mode 100644 index 24658319060b7c5f4fd4eb2bc786b3bb2e260eef..0000000000000000000000000000000000000000 --- a/app/views/projects/repositories/_download_archive.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -- ref = ref || nil -- btn_class = btn_class || '' -- split_button = split_button || false -- if split_button == true - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Archive Format - %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar -- else - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span zip - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span tar.gz diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 340e159c87412d2330cb728889e653798a8d30f9..9adce776c1cd68ef039b7cbd5fe1ae2412d5187b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -72,7 +72,7 @@ = render "projects/buttons/koding" .btn-group.project-repo-btn-group - = render "projects/buttons/download" + = render 'projects/buttons/download', project: @project, ref: @ref = render 'projects/buttons/dropdown' = render 'shared/notifications/button', notification_setting: @notification_setting diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml deleted file mode 100644 index 8a11dbfa9f4adfc4f4a9da1229ee8f31c5db155c..0000000000000000000000000000000000000000 --- a/app/views/projects/tags/_download.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%span.btn-group - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do - %span Source code - %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %span Download tar.gz diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 2c11c0e5b21801fbccc291df013bf22252a98f0a..a156d98bab8fa9baae973dcaf44971b797527901 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -11,8 +11,7 @@ = strip_gpg_signature(tag.message) .controls - - if can?(current_user, :download_code, @project) - = render 'projects/tags/download', ref: tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: tag.name - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 368231e73fe6cae1daa6c85b64badebad0b2aa99..6adbe9351dcec5d3c7b3734c08c990d7139dd98e 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -8,21 +8,24 @@ Tags give the ability to mark specific points in history as being important .nav-controls - - if can? current_user, :push_code, @project - = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - New tag + = form_tag(filter_tags_path, method: :get) do + = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } - %span.light= @sort.humanize + %span.light + = @sort.humanize %b.caret %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to namespace_project_tags_path(sort: nil) do + = link_to filter_tags_path(sort: nil) do Name - = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do + = link_to filter_tags_path(sort: sort_value_recently_updated) do = sort_title_recently_updated - = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do + = link_to filter_tags_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated + - if can?(current_user, :push_code, @project) + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do + New tag .tags - if @tags.any? diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 395d7af6cbb4d033ea539e5c2fabfc0fe35fce79..4dd7439b2d0153a5f64f19adda79fdb49a1d9682 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -12,8 +12,7 @@ = icon('files-o') = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do = icon('history') - - if can? current_user, :download_code, @project - = render 'projects/tags/download', ref: @tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .pull-right = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 558e6146ae993f7c860061ce3223fa21f3dd9149..ca5d2d7722a9f0fba1744912f39b862257b6410e 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -5,16 +5,17 @@ %tr %th Name %th Last Update - %th.hidden-xs - .pull-left Last Commit - .last-commit.hidden-sm.pull-left - + %th.hidden-xs.last-commit + Last Commit + .last-commit-content.hidden-sm %i.fa.fa-angle-right %small.light = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" – - = truncate(@commit.title, length: 50) + = time_ago_with_tooltip(@commit.committed_date) + – + = @commit.full_title = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - if @path.present? diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index bf5360b4deee9195834eca9859419a845d3ad686..37d341212af1ce77f454d362f4e424f2a42913da 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -10,8 +10,7 @@ %div{ class: container_class } .tree-controls = render 'projects/find_file_link' - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true + = render 'projects/buttons/download', project: @project, ref: @ref #tree-holder.tree-holder.clearfix .nav-block diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg index 80a6d41dbf651c6295f1e62e5e44220e7e6d457f..e965afa9a56100d1f044c8c3b429215f3b0c111d 100644 --- a/app/views/shared/icons/_icon_play.svg +++ b/app/views/shared/icons/_icon_play.svg @@ -1 +1,3 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"> + <path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/> + </svg> \ No newline at end of file diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg new file mode 100644 index 0000000000000000000000000000000000000000..1f5c3b51b0386aaf4fecf79893bf38714b37169b --- /dev/null +++ b/app/views/shared/icons/_icon_status_created.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b83e0bbfda56aed19e4c4a9d046b09ea7db9536f..a3bad0e8326252ab7187e4f922aee620ee478eb0 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -28,15 +28,18 @@ = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } .pull-right - - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project) - .dropdown - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } } - Create new list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } - - if can?(current_user, :admin_label, @project) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading + - if controller.controller_name == 'boards' + #js-boards-seach.issue-boards-search + %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } + - if can?(current_user, :admin_list, @project) + .dropdown.pull-right + %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } } + Create new list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading - else = render 'shared/sort_dropdown' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index a54704e9257b8abc6ae0aff351cbf3dd7018cad8..ac593d8937a9d32dbda08761f98879493c67b610 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -119,7 +119,7 @@ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } = icon('question-circle') -- if issuable.is_a?(MergeRequest) +- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork? %hr - if @merge_request.new_record? .form-group @@ -160,7 +160,7 @@ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else .pull-right - - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) + - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index a14ba0a2d9f452a88faaae15bc1490ec92d6367f..6d5bdbe435e983236aafb11171c4c5a43aad1c2e 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -15,7 +15,7 @@ - if selected - selected.each do |label| - = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil + = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : u(label.try(:title)), id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b8043138a6baa9e10b1bff938d7882148b41b091..4e3bb8a82858d0eab5b8223c8707faaf0aabb9f5 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -119,7 +119,7 @@ = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } + .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label| = link_to_label(label, type: issuable.to_ability_name) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c7f39868e71f5487fd052d89bfbc3ab2c93215d2..9a052abe40a320c0820e912c884e7b93e7d18165 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -123,6 +123,6 @@ :javascript var userProfile; - userProfile = new User({ + userProfile = new gl.User({ action: "#{controller.action_name}" }); diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 74fef7cadfe24904b5d1425ce7506df5d95856d8..5892c1de024190331922c7eeded4b78152acfdd7 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -18,6 +18,7 @@ if Rails.env.production? # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) + config.tags = { program: Gitlab::Sentry.program_context } end end end diff --git a/config/routes.rb b/config/routes.rb index 24f9b44a53a4e59fb8c31daf4d3524b43ec84776..262a174437a095b12c4e1958cb9a63fee92dbe61 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -782,6 +782,14 @@ Rails.application.routes.draw do resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all + + resources :artifacts, only: [] do + collection do + get :latest_succeeded, + path: '*ref_name_and_path', + format: false + end + end end member do diff --git a/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb new file mode 100644 index 0000000000000000000000000000000000000000..c169084e976977171b21bf994dc0330558ea1c87 --- /dev/null +++ b/db/migrate/20160823213309_add_lfs_enabled_to_projects.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 AddLfsEnabledToProjects < 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 :projects, :lfs_enabled, :boolean + end +end diff --git a/db/migrate/20160824103857_drop_unused_ci_tables.rb b/db/migrate/20160824103857_drop_unused_ci_tables.rb new file mode 100644 index 0000000000000000000000000000000000000000..65cf46308d95b73ffd7cedd709de650b22791e38 --- /dev/null +++ b/db/migrate/20160824103857_drop_unused_ci_tables.rb @@ -0,0 +1,11 @@ +class DropUnusedCiTables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + drop_table(:ci_services) + drop_table(:ci_web_hooks) + end +end diff --git a/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c55bc23cf2fecf8f419d26594d95b127bfbc183 --- /dev/null +++ b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb @@ -0,0 +1,16 @@ +class EnsureLockVersionHasNoDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_default :issues, :lock_version, nil + change_column_default :merge_requests, :lock_version, nil + + execute('UPDATE issues SET lock_version = 1 WHERE lock_version = 0') + execute('UPDATE merge_requests SET lock_version = 1 WHERE lock_version = 0') + end + + def down + end +end diff --git a/db/migrate/20160830232601_change_lock_version_not_null.rb b/db/migrate/20160830232601_change_lock_version_not_null.rb new file mode 100644 index 0000000000000000000000000000000000000000..01c58ed5bdca892342b4b8eaefb2c4133c7ae1c0 --- /dev/null +++ b/db/migrate/20160830232601_change_lock_version_not_null.rb @@ -0,0 +1,13 @@ +class ChangeLockVersionNotNull < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_null :issues, :lock_version, true + change_column_null :merge_requests, :lock_version, true + end + + def down + end +end diff --git a/db/migrate/20160831214002_create_project_features.rb b/db/migrate/20160831214002_create_project_features.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d76a015a08615f3044c988dfde670463036180a --- /dev/null +++ b/db/migrate/20160831214002_create_project_features.rb @@ -0,0 +1,16 @@ +class CreateProjectFeatures < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :project_features do |t| + t.belongs_to :project, index: true + t.integer :merge_requests_access_level + t.integer :issues_access_level + t.integer :wiki_access_level + t.integer :snippets_access_level + t.integer :builds_access_level + + t.timestamps + end + end +end diff --git a/db/migrate/20160831214543_migrate_project_features.rb b/db/migrate/20160831214543_migrate_project_features.rb new file mode 100644 index 0000000000000000000000000000000000000000..93f9821bc76e21fc743d401eb892358884cc0078 --- /dev/null +++ b/db/migrate/20160831214543_migrate_project_features.rb @@ -0,0 +1,44 @@ +class MigrateProjectFeatures < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = + <<-EOT + Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to + a new table called project_features. + EOT + + def up + sql = + %Q{ + INSERT INTO project_features(project_id, issues_access_level, merge_requests_access_level, wiki_access_level, + builds_access_level, snippets_access_level, created_at, updated_at) + SELECT + id AS project_id, + CASE WHEN issues_enabled IS true THEN 20 ELSE 0 END AS issues_access_level, + CASE WHEN merge_requests_enabled IS true THEN 20 ELSE 0 END AS merge_requests_access_level, + CASE WHEN wiki_enabled IS true THEN 20 ELSE 0 END AS wiki_access_level, + CASE WHEN builds_enabled IS true THEN 20 ELSE 0 END AS builds_access_level, + CASE WHEN snippets_enabled IS true THEN 20 ELSE 0 END AS snippets_access_level, + created_at, + updated_at + FROM projects + } + + execute(sql) + end + + def down + sql = %Q{ + UPDATE projects + SET + issues_enabled = COALESCE((SELECT CASE WHEN issues_access_level = 20 THEN true ELSE false END AS issues_enabled FROM project_features WHERE project_features.project_id = projects.id), true), + merge_requests_enabled = COALESCE((SELECT CASE WHEN merge_requests_access_level = 20 THEN true ELSE false END AS merge_requests_enabled FROM project_features WHERE project_features.project_id = projects.id),true), + wiki_enabled = COALESCE((SELECT CASE WHEN wiki_access_level = 20 THEN true ELSE false END AS wiki_enabled FROM project_features WHERE project_features.project_id = projects.id), true), + builds_enabled = COALESCE((SELECT CASE WHEN builds_access_level = 20 THEN true ELSE false END AS builds_enabled FROM project_features WHERE project_features.project_id = projects.id), true), + snippets_enabled = COALESCE((SELECT CASE WHEN snippets_access_level = 20 THEN true ELSE false END AS snippets_enabled FROM project_features WHERE project_features.project_id = projects.id),true) + } + + execute(sql) + end +end diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb new file mode 100644 index 0000000000000000000000000000000000000000..a2c207b49ea7ee9d957add561f46594c15a5c677 --- /dev/null +++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.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 RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + DOWNTIME_REASON = "Removing fields from database requires downtine." + + def up + remove_column :projects, :issues_enabled + remove_column :projects, :merge_requests_enabled + remove_column :projects, :builds_enabled + remove_column :projects, :wiki_enabled + remove_column :projects, :snippets_enabled + end + + # Ugly SQL but the only way i found to make it work on both Postgres and Mysql + # It will be slow but it is ok since it is a revert method + def down + add_column_with_default(:projects, :issues_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :merge_requests_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :builds_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :wiki_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :snippets_enabled, :boolean, default: true, allow_null: false) + end +end diff --git a/db/schema.rb b/db/schema.rb index 5a105a91ad1bf25ee8d4b9e7e50df78a2601caee..af6e74a4e25aa9d16f7d5b6a9a310af3209e8075 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: 20160823081327) do +ActiveRecord::Schema.define(version: 20160831223750) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -295,16 +295,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree - create_table "ci_services", force: :cascade do |t| - t.string "type" - t.string "title" - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false - t.text "properties" - end - create_table "ci_sessions", force: :cascade do |t| t.string "session_id", null: false t.text "data" @@ -360,13 +350,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree - create_table "ci_web_hooks", force: :cascade do |t| - t.string "url", null: false - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false @@ -783,6 +766,19 @@ ActiveRecord::Schema.define(version: 20160823081327) do add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree + create_table "project_features", force: :cascade do |t| + t.integer "project_id" + t.integer "merge_requests_access_level" + t.integer "issues_access_level" + t.integer "wiki_access_level" + t.integer "snippets_access_level" + t.integer "builds_access_level" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree + create_table "project_group_links", force: :cascade do |t| t.integer "project_id", null: false t.integer "group_id", null: false @@ -807,11 +803,7 @@ ActiveRecord::Schema.define(version: 20160823081327) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" t.integer "visibility_level", default: 0, null: false @@ -825,7 +817,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do t.integer "commit_count", default: 0 t.text "import_error" t.integer "ci_id" - t.boolean "builds_enabled", default: true, null: false t.boolean "shared_runners_enabled", default: true, null: false t.string "runners_token" t.string "build_coverage_regex" @@ -842,6 +833,7 @@ ActiveRecord::Schema.define(version: 20160823081327) do t.string "repository_storage", default: "default", null: false t.boolean "request_access_enabled", default: true, null: false t.boolean "has_external_wiki" + t.boolean "lfs_enabled" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md new file mode 100644 index 0000000000000000000000000000000000000000..c3a9207a3ae51ecec296abe3162830cc8f19eee5 --- /dev/null +++ b/doc/api/broadcast_messages.md @@ -0,0 +1,158 @@ +# Broadcast Messages + +> **Note:** This feature was introduced in GitLab 8.12. + +The broadcast message API is only accessible to administrators. All requests by +guests will respond with `401 Unauthorized`, and all requests by normal users +will respond with `403 Forbidden`. + +## Get all broadcast messages + +``` +GET /broadcast_messages +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages +``` + +Example response: + +```json +[ + { + "message":"Example broadcast message", + "starts_at":"2016-08-24T23:21:16.078Z", + "ends_at":"2016-08-26T23:21:16.080Z", + "color":"#E75E40", + "font":"#FFFFFF", + "id":1, + "active": false + } +] +``` + +## Get a specific broadcast message + +``` +GET /broadcast_messages/:id +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ------------------------- | +| `id` | integer | yes | Broadcast message ID | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +``` + +Example response: + +```json +{ + "message":"Deploy in progress", + "starts_at":"2016-08-24T23:21:16.078Z", + "ends_at":"2016-08-26T23:21:16.080Z", + "color":"#cecece", + "font":"#FFFFFF", + "id":1, + "active":false +} +``` + +## Create a broadcast message + +Responds with `400 Bad request` when the `message` parameter is missing or the +`color` or `font` values are invalid, and `201 Created` when the broadcast +message was successfully created. + +``` +POST /broadcast_messages +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ---------------------------------------------------- | +| `message` | string | yes | Message to display | +| `starts_at` | datetime | no | Starting time (defaults to current time) | +| `ends_at` | datetime | no | Ending time (defaults to one hour from current time) | +| `color` | string | no | Background color hex code | +| `font` | string | no | Foreground color hex code | + +```bash +curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages +``` + +Example response: + +```json +{ + "message":"Deploy in progress", + "starts_at":"2016-08-26T00:41:35.060Z", + "ends_at":"2016-08-26T01:41:35.060Z", + "color":"#cecece", + "font":"#FFFFFF", + "id":1, + "active": true +} +``` + +## Update a broadcast message + +``` +PUT /broadcast_messages/:id +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ------------------------- | +| `id` | integer | yes | Broadcast message ID | +| `message` | string | no | Message to display | +| `starts_at` | datetime | no | Starting time | +| `ends_at` | datetime | no | Ending time | +| `color` | string | no | Background color hex code | +| `font` | string | no | Foreground color hex code | + +```bash +curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +``` + +Example response: + +```json +{ + "message":"Update message", + "starts_at":"2016-08-26T00:41:35.060Z", + "ends_at":"2016-08-26T01:41:35.060Z", + "color":"#000", + "font":"#FFFFFF", + "id":1, + "active": true +} +``` + +## Delete a broadcast message + +``` +DELETE /broadcast_messages/:id +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ------------------------- | +| `id` | integer | yes | Broadcast message ID | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +``` + +Example response: + +```json +{ + "message":"Update message", + "starts_at":"2016-08-26T00:41:35.060Z", + "ends_at":"2016-08-26T01:41:35.060Z", + "color":"#000", + "font":"#FFFFFF", + "id":1, + "active": true +} +``` diff --git a/doc/api/commits.md b/doc/api/commits.md index 5c98c5d7565b5820a72af26bc05751b39152f900..682151d4b1ddd19c073b321b39a1bedf4ca72f13 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -10,7 +10,7 @@ GET /projects/:id/repository/commits | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | | `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | @@ -58,7 +58,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash @@ -102,7 +102,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash @@ -138,7 +138,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash @@ -187,7 +187,7 @@ POST /projects/:id/repository/commits/:sha/comments | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA or name of a repository branch or tag | | `note` | string | yes | The text of the comment | | `path` | string | no | The file path relative to the repository | @@ -232,7 +232,7 @@ GET /projects/:id/repository/commits/:sha/statuses | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA | `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch | `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test` @@ -306,7 +306,7 @@ POST /projects/:id/statuses/:sha | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project +| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA | `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled` | `ref` | string | no | The `ref` (branch or tag) to which the status refers diff --git a/doc/api/issues.md b/doc/api/issues.md index b194799ccbf93215d087c17294066d0939f6b02d..eed0d2fce5172a62aaa983a21847880e34036068 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -80,7 +80,8 @@ Example response: "subscribed" : false, "user_notes_count": 1, "due_date": "2016-07-22", - "web_url": "http://example.com/example/example/issues/6" + "web_url": "http://example.com/example/example/issues/6", + "confidential": false } ] ``` @@ -158,7 +159,8 @@ Example response: "subscribed" : false, "user_notes_count": 1, "due_date": null, - "web_url": "http://example.com/example/example/issues/1" + "web_url": "http://example.com/example/example/issues/1", + "confidential": false } ] ``` @@ -238,7 +240,8 @@ Example response: "subscribed" : false, "user_notes_count": 1, "due_date": "2016-07-22", - "web_url": "http://example.com/example/example/issues/1" + "web_url": "http://example.com/example/example/issues/1", + "confidential": false } ] ``` @@ -303,7 +306,8 @@ Example response: "subscribed": false, "user_notes_count": 1, "due_date": null, - "web_url": "http://example.com/example/example/issues/1" + "web_url": "http://example.com/example/example/issues/1", + "confidential": false } ``` @@ -324,6 +328,7 @@ POST /projects/:id/issues | `id` | integer | yes | The ID of a project | | `title` | string | yes | The title of an issue | | `description` | string | no | The description of an issue | +| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | | `assignee_id` | integer | no | The ID of a user to assign issue | | `milestone_id` | integer | no | The ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | @@ -362,7 +367,8 @@ Example response: "subscribed" : true, "user_notes_count": 0, "due_date": null, - "web_url": "http://example.com/example/example/issues/14" + "web_url": "http://example.com/example/example/issues/14", + "confidential": false } ``` @@ -385,6 +391,7 @@ PUT /projects/:id/issues/:issue_id | `issue_id` | integer | yes | The ID of a project's issue | | `title` | string | no | The title of an issue | | `description` | string | no | The description of an issue | +| `confidential` | boolean | no | Updates an issue to be confidential | | `assignee_id` | integer | no | The ID of a user to assign the issue to | | `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `labels` | string | no | Comma-separated label names for an issue | @@ -424,7 +431,8 @@ Example response: "subscribed" : true, "user_notes_count": 0, "due_date": "2016-07-22", - "web_url": "http://example.com/example/example/issues/15" + "web_url": "http://example.com/example/example/issues/15", + "confidential": false } ``` @@ -503,7 +511,8 @@ Example response: "web_url": "https://gitlab.example.com/u/solon.cremin" }, "due_date": null, - "web_url": "http://example.com/example/example/issues/11" + "web_url": "http://example.com/example/example/issues/11", + "confidential": false } ``` @@ -559,7 +568,8 @@ Example response: "web_url": "https://gitlab.example.com/u/solon.cremin" }, "due_date": null, - "web_url": "http://example.com/example/example/issues/11" + "web_url": "http://example.com/example/example/issues/11", + "confidential": false } ``` @@ -616,7 +626,8 @@ Example response: }, "subscribed": false, "due_date": null, - "web_url": "http://example.com/example/example/issues/12" + "web_url": "http://example.com/example/example/issues/12", + "confidential": false } ``` @@ -704,7 +715,8 @@ Example response: "upvotes": 0, "downvotes": 0, "due_date": null, - "web_url": "http://example.com/example/example/issues/110" + "web_url": "http://example.com/example/example/issues/110", + "confidential": false }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10", "body": "Vel voluptas atque dicta mollitia adipisci qui at.", diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index f4760ceac7c7152f6e96afe7cd8066baf70a1b86..494040a1ce83e75c5a0f3eb24c31e93103157e3d 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -68,6 +68,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : false, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -135,6 +137,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": "9999999999999999999999999999999999999999", "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -238,6 +242,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -322,6 +328,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 0, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -397,6 +405,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -499,6 +509,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": "9999999999999999999999999999999999999999", "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -569,6 +581,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -724,7 +738,9 @@ Example response: }, "merge_when_build_succeeds": false, "merge_status": "cannot_be_merged", - "subscribed": true + "subscribed": true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null } ``` @@ -798,7 +814,9 @@ Example response: }, "merge_when_build_succeeds": false, "merge_status": "cannot_be_merged", - "subscribed": false + "subscribed": false, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null } ``` @@ -891,6 +909,8 @@ Example response: "merge_when_build_succeeds": false, "merge_status": "unchecked", "subscribed": true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 7, "should_remove_source_branch": true, "force_remove_source_branch": false, diff --git a/doc/api/projects.md b/doc/api/projects.md index 0e4806e31c51810229e99704dc33640bf27b6de0..a62aaee14d79061fe112705a18914c8419b2384a 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -452,6 +452,7 @@ Parameters: - `import_url` (optional) - `public_builds` (optional) - `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) ### Create project for user @@ -478,6 +479,7 @@ Parameters: - `import_url` (optional) - `public_builds` (optional) - `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) ### Edit project @@ -489,7 +491,7 @@ PUT /projects/:id Parameters: -- `id` (required) - The ID of a project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project - `name` (optional) - project name - `path` (optional) - repository name for project - `description` (optional) - short project description @@ -505,6 +507,7 @@ Parameters: - `visibility_level` (optional) - `public_builds` (optional) - `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) On success, method returns 200 with the updated project. If parameters are invalid, 400 is returned. @@ -519,7 +522,7 @@ POST /projects/fork/:id Parameters: -- `id` (required) - The ID of the project to be forked +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked ### Star a project @@ -532,7 +535,7 @@ POST /projects/:id/star | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" @@ -599,7 +602,7 @@ DELETE /projects/:id/star | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" @@ -670,7 +673,7 @@ POST /projects/:id/archive | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" @@ -757,7 +760,7 @@ POST /projects/:id/unarchive | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" @@ -839,7 +842,7 @@ DELETE /projects/:id Parameters: -- `id` (required) - The ID of a project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked ## Uploads @@ -853,7 +856,7 @@ POST /projects/:id/uploads Parameters: -- `id` (required) - The ID of the project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `file` (required) - The file to be uploaded ```json @@ -882,7 +885,7 @@ POST /projects/:id/share Parameters: -- `id` (required) - The ID of a project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `group_id` (required) - The ID of a group - `group_access` (required) - Level of permissions for sharing @@ -998,6 +1001,8 @@ is available before it is returned in the JSON response or an empty response is ## Branches +For more information please consult the [Branches](branches.md) documentation. + ### List branches Lists all branches of a project. @@ -1016,56 +1021,46 @@ Parameters: "name": "async", "commit": { "id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca", - "parents": [ - { - "id": "3f94fc7c85061973edc9906ae170cc269b07ca55" - } + "parent_ids": [ + "3f94fc7c85061973edc9906ae170cc269b07ca55" ], - "tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee", "message": "give Caolan credit where it's due (up top)", - "author": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, - "committer": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, + "author_name": "Jeremy Ashkenas", + "author_email": "jashkenas@example.com", "authored_date": "2010-12-08T21:28:50+00:00", + "committer_name": "Jeremy Ashkenas", + "committer_email": "jashkenas@example.com", "committed_date": "2010-12-08T21:28:50+00:00" }, - "protected": false + "protected": false, + "developers_can_push": false, + "developers_can_merge": false }, { "name": "gh-pages", "commit": { "id": "101c10a60019fe870d21868835f65c25d64968fc", - "parents": [ - { - "id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa" - } + "parent_ids": [ + "9c15d2e26945a665131af5d7b6d30a06ba338aaa" ], - "tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a", "message": "Underscore.js 1.5.2", - "author": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, - "committer": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, + "author_name": "Jeremy Ashkenas", + "author_email": "jashkenas@example.com", "authored_date": "2013-09-07T12:58:21+00:00", + "committer_name": "Jeremy Ashkenas", + "committer_email": "jashkenas@example.com", "committed_date": "2013-09-07T12:58:21+00:00" }, - "protected": false + "protected": false, + "developers_can_push": false, + "developers_can_merge": false } ] ``` -### List single branch +### Single branch -Lists a specific branch of a project. +A specific branch of a project. ``` GET /projects/:id/repository/branches/:branch @@ -1075,6 +1070,8 @@ Parameters: - `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project - `branch` (required) - The name of the branch. +- `developers_can_push` - Flag if developers can push to the branch. +- `developers_can_merge` - Flag if developers can merge to the branch. ### Protect single branch @@ -1114,7 +1111,7 @@ POST /projects/:id/fork/:forked_from_id Parameters: -- `id` (required) - The ID of the project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `forked_from_id:` (required) - The ID of the project that was forked from ### Delete an existing forked from relationship @@ -1125,7 +1122,7 @@ DELETE /projects/:id/fork Parameter: -- `id` (required) - The ID of the project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked ## Search for projects by name diff --git a/doc/api/users.md b/doc/api/users.md index 7e848586dbd02e11c2b3da7b716e4c249ebc9954..54f7a2a2aceaa4653ee07c8c5605da9233422052 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -310,8 +310,7 @@ GET /user "can_create_group": true, "can_create_project": true, "two_factor_enabled": true, - "external": false, - "private_token": "dd34asd13as" + "external": false } ``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e7850aa2c9d372d12df27e5bd50a15633f5499a0..58d5306f12af263dbf300567021a90ade582f4de 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -39,7 +39,7 @@ If you want a quick introduction to GitLab CI, follow our - [before_script and after_script](#before_script-and-after_script) - [Git Strategy](#git-strategy) - [Shallow cloning](#shallow-cloning) -- [Hidden jobs](#hidden-jobs) +- [Hidden keys](#hidden-keys) - [Special YAML features](#special-yaml-features) - [Anchors](#anchors) - [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) @@ -934,24 +934,27 @@ variables: GIT_DEPTH: "3" ``` -## Hidden jobs +## Hidden keys >**Note:** Introduced in GitLab 8.6 and GitLab Runner v1.1.1. -Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can +Keys that start with a dot (`.`) will be not processed by GitLab CI. You can use this feature to ignore jobs, or use the -[special YAML features](#special-yaml-features) and transform the hidden jobs +[special YAML features](#special-yaml-features) and transform the hidden keys into templates. -In the following example, `.job_name` will be ignored: +In the following example, `.key_name` will be ignored: ```yaml -.job_name: +.key_name: script: - rake spec ``` +Hidden keys can be hashes like normal CI jobs, but you are also allowed to use +different types of structures to leverage special YAML features. + ## Special YAML features It's possible to use special YAML features like anchors (`&`), aliases (`*`) @@ -967,7 +970,7 @@ Introduced in GitLab 8.6 and GitLab Runner v1.1.1. YAML also has a handy feature called 'anchors', which let you easily duplicate content across your document. Anchors can be used to duplicate/inherit -properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs) +properties, and is a perfect example to be used with [hidden keys](#hidden-keys) to provide templates for your jobs. The following example uses anchors and map merging. It will create two jobs, @@ -975,7 +978,7 @@ The following example uses anchors and map merging. It will create two jobs, having their own custom `script` defined: ```yaml -.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition' +.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition' image: ruby:2.1 services: - postgres @@ -1081,7 +1084,7 @@ test:mysql: - ruby ``` -You can see that the hidden jobs are conveniently used as templates. +You can see that the hidden keys are conveniently used as templates. ## Validate the .gitlab-ci.yml diff --git a/doc/development/README.md b/doc/development/README.md index 57f37da6f809307e262e39dc16da2d6c9a55247b..58c00f618fa804240d4ecfd03631f564909fa6e1 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -18,6 +18,8 @@ ## Process - [Code review guidelines](code_review.md) for reviewing code and having code reviewed. +- [Merge request performance guidelines](merge_request_performance_guidelines.md) + for ensuring merge requests do not negatively impact GitLab performance ## Backend howtos diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 37bb59e112c318f4a88ff5808c3a157911e295c8..39b801f761d653f103a4ad088e0fc4c5eb19d17c 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -155,15 +155,30 @@ Inside the document: - Every piece of documentation that comes with a new feature should declare the GitLab version that feature got introduced. Right below the heading add a - note: `> Introduced in GitLab 8.3.`. + note: + + ``` + > Introduced in GitLab 8.3. + ``` + - If possible every feature should have a link to the MR that introduced it. The above note would be then transformed to: - `> [Introduced][ce-1242] in GitLab 8.3.`, where - the [link identifier](#links) is named after the repository (CE) and the MR - number. -- If the feature is only in GitLab EE, don't forget to mention it, like: - `> Introduced in GitLab EE 8.3.`. Otherwise, leave - this mention out. + + ``` + > [Introduced][ce-1242] in GitLab 8.3. + ``` + + , where the [link identifier](#links) is named after the repository (CE) and + the MR number. + +- If the feature is only in GitLab Enterprise Edition, don't forget to mention + it, like: + + ``` + > Introduced in GitLab Enterprise Edition 8.3. + ``` + + Otherwise, leave this mention out. ## References diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md new file mode 100644 index 0000000000000000000000000000000000000000..0363bf8c1d54b1f5c3d377e6e38e0b5343ae4dfd --- /dev/null +++ b/doc/development/merge_request_performance_guidelines.md @@ -0,0 +1,171 @@ +# Merge Request Performance Guidelines + +To ensure a merge request does not negatively impact performance of GitLab +_every_ merge request **must** adhere to the guidelines outlined in this +document. There are no exceptions to this rule unless specifically discussed +with and agreed upon by merge request endbosses and performance specialists. + +To measure the impact of a merge request you can use +[Sherlock](profiling.md#sherlock). It's also highly recommended that you read +the following guides: + +* [Performance Guidelines](performance.md) +* [What requires downtime?](what_requires_downtime.md) + +## Impact Analysis + +**Summary:** think about the impact your merge request may have on performance +and those maintaining a GitLab setup. + +Any change submitted can have an impact not only on the application itself but +also those maintaining it and those keeping it up and running (e.g. production +engineers). As a result you should think carefully about the impact of your +merge request on not only the application but also on the people keeping it up +and running. + +Can the queries used potentially take down any critical services and result in +engineers being woken up in the night? Can a malicious user abuse the code to +take down a GitLab instance? Will my changes simply make loading a certain page +slower? Will execution time grow exponentially given enough load or data in the +database? + +These are all questions one should ask themselves before submitting a merge +request. It may sometimes be difficult to assess the impact, in which case you +should ask a performance specialist to review your code. See the "Reviewing" +section below for more information. + +## Performance Review + +**Summary:** ask performance specialists to review your code if you're not sure +about the impact. + +Sometimes it's hard to assess the impact of a merge request. In this case you +should ask one of the merge request (mini) endbosses to review your changes. You +can find a list of these endbosses at <https://about.gitlab.com/team/>. An +endboss in turn can request a performance specialist to review the changes. + +## Query Counts + +**Summary:** a merge request **should not** increase the number of executed SQL +queries unless absolutely necessary. + +The number of queries executed by the code modified or added by a merge request +must not increase unless absolutely necessary. When building features it's +entirely possible you will need some extra queries, but you should try to keep +this at a minimum. + +As an example, say you introduce a feature that updates a number of database +rows with the same value. It may be very tempting (and easy) to write this using +the following pseudo code: + +```ruby +objects_to_update.each do |object| + object.some_field = some_value + object.save +end +``` + +This will end up running one query for every object to update. This code can +easily overload a database given enough rows to update or many instances of this +code running in parallel. This particular problem is known as the +["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations). + +In this particular case the workaround is fairly easy: + +```ruby +objects_to_update.update_all(some_field: some_value) +``` + +This uses ActiveRecord's `update_all` method to update all rows in a single +query. This in turn makes it much harder for this code to overload a database. + +## Executing Queries in Loops + +**Summary:** SQL queries **must not** be executed in a loop unless absolutely +necessary. + +Executing SQL queries in a loop can result in many queries being executed +depending on the number of iterations in a loop. This may work fine for a +development environment with little data, but in a production environment this +can quickly spiral out of control. + +There are some cases where this may be needed. If this is the case this should +be clearly mentioned in the merge request description. + +## Eager Loading + +**Summary:** always eager load associations when retrieving more than one row. + +When retrieving multiple database records for which you need to use any +associations you **must** eager load these associations. For example, if you're +retrieving a list of blog posts and you want to display their authors you +**must** eager load the author associations. + +In other words, instead of this: + +```ruby +Post.all.each do |post| + puts post.author.name +end +``` + +You should use this: + +```ruby +Post.all.includes(:author).each do |post| + puts post.author.name +end +``` + +## Memory Usage + +**Summary:** merge requests **must not** increase memory usage unless absolutely +necessary. + +A merge request must not increase the memory usage of GitLab by more than the +absolute bare minimum required by the code. This means that if you have to parse +some large document (e.g. an HTML document) it's best to parse it as a stream +whenever possible, instead of loading the entire input into memory. Sometimes +this isn't possible, in that case this should be stated explicitly in the merge +request. + +## Lazy Rendering of UI Elements + +**Summary:** only render UI elements when they're actually needed. + +Certain UI elements may not always be needed. For example, when hovering over a +diff line there's a small icon displayed that can be used to create a new +comment. Instead of always rendering these kind of elements they should only be +rendered when actually needed. This ensures we don't spend time generating +Haml/HTML when it's not going to be used. + +## Instrumenting New Code + +**Summary:** always add instrumentation for new classes, modules, and methods. + +Newly added classes, modules, and methods must be instrumented. This ensures +we can track the performance of this code over time. + +For more information see [Instrumentation](instrumentation.md). This guide +describes how to add instrumentation and where to add it. + +## Use of Caching + +**Summary:** cache data in memory or in Redis when it's needed multiple times in +a transaction or has to be kept around for a certain time period. + +Sometimes certain bits of data have to be re-used in different places during a +transaction. In these cases this data should be cached in memory to remove the +need for running complex operations to fetch the data. You should use Redis if +data should be cached for a certain time period instead of the duration of the +transaction. + +For example, say you process multiple snippets of text containiner username +mentions (e.g. `Hello @alice` and `How are you doing @alice?`). By caching the +user objects for every username we can remove the need for running the same +query for every mention of `@alice`. + +Caching data per transaction can be done using +[RequestStore](https://github.com/steveklabnik/request_store). Caching data in +Redis can be done using [Rails' caching +system](http://guides.rubyonrails.org/caching_with_rails.html). diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md index e03adcaadea5d38ca247b9025bcdba0283ee0405..32aac2529a444b6529f3b497d4cdccc4bd1a3d28 100644 --- a/doc/development/newlines_styleguide.md +++ b/doc/development/newlines_styleguide.md @@ -2,7 +2,7 @@ This style guide recommends best practices for newlines in Ruby code. -## Rule: separate code with newlines only when it makes sense from logic perspectice +## Rule: separate code with newlines only to group together related logic ```ruby # bad diff --git a/doc/install/installation.md b/doc/install/installation.md index d4b89fa834562e9c8974bb7bb4e7a29db44a89e5..9522c3e71708da8cfe0932b3ac0ae1cd805c4bf6 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -268,9 +268,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-11-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-12-stable gitlab -**Note:** You can change `8-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -331,6 +331,9 @@ sudo usermod -aG redis git # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed sudo -u git -H git config --global gc.auto 0 + # Enable packfile bitmaps + sudo -u git -H git config --global repack.writeBitmaps true + # Configure Redis connection settings sudo -u git -H cp config/resque.yml.example config/resque.yml diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 2eb6266ebe7952e8235ef09273711b343e567a3f..556d71b8b7643936db8688a2437647c4189cc12f 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -1,111 +1,164 @@ -# Integrate your server with Bitbucket +# Integrate your GitLab server with Bitbucket -Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account. +Import projects from Bitbucket.org and login to your GitLab instance with your +Bitbucket.org account. -To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket. -Bitbucket will generate an application ID and secret key for you to use. +## Overview -1. Sign in to Bitbucket. +You can set up Bitbucket.org as an OAuth provider so that you can use your +credentials to authenticate into GitLab or import your projects from +Bitbucket.org. -1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you. +- To use Bitbucket.org as an OmniAuth provider, follow the [Bitbucket OmniAuth + provider](#bitbucket-omniauth-provider) section. +- To import projects from Bitbucket, follow both the + [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and + [Bitbucket project import](#bitbucket-project-import) sections. -1. Select "OAuth" in the left menu. +## Bitbucket OmniAuth provider -1. Select "Add consumer". +> **Note:** +Make sure to first follow the [Initial OmniAuth configuration][init-oauth] +before proceeding with setting up the Bitbucket integration. -1. Provide the required details. - - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - - Application description: Fill this in if you wish. - - URL: The URL to your GitLab installation. 'https://gitlab.company.com' -1. Select "Save". +To enable the Bitbucket OmniAuth provider you must register your application +with Bitbucket.org. Bitbucket will generate an application ID and secret key for +you to use. -1. You should now see a Key and Secret in the list of OAuth customers. - Keep this page open as you continue configuration. +1. Sign in to [Bitbucket.org](https://bitbucket.org). +1. Navigate to your individual user settings (**Bitbucket settings**) or a team's + settings (**Manage team**), depending on how you want the application registered. + It does not matter if the application is registered as an individual or a + team, that is entirely up to you. +1. Select **OAuth** in the left menu under "Access Management". +1. Select **Add consumer**. +1. Provide the required details: -1. On your GitLab server, open the configuration file. + | Item | Description | + | :--- | :---------- | + | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. | + | **Application description** | Fill this in if you wish. | + | **Callback URL** | Leave blank. | + | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | - For omnibus package: + And grant at least the following permissions: - ```sh - sudo editor /etc/gitlab/gitlab.rb + ``` + Account: Email + Repositories: Read, Admin ``` - For installations from source: + >**Note:** + It may seem a little odd to giving GitLab admin permissions to repositories, + but this is needed in order for GitLab to be able to clone the repositories. - ```sh - cd /home/git/gitlab +  + +1. Select **Save**. +1. Select your newly created OAuth consumer and you should now see a Key and + Secret in the list of OAuth customers. Keep this page open as you continue + the configuration. + +  + +1. On your GitLab server, open the configuration file: - sudo -u git -H editor config/gitlab.yml ``` + # For Omnibus packages + sudo editor /etc/gitlab/gitlab.rb -1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + # For installations from source + sudo -u git -H editor /home/git/gitlab/config/gitlab.yml + ``` -1. Add the provider configuration: +1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) + for initial settings. +1. Add the Bitbucket provider configuration: - For omnibus package: + For Omnibus packages: ```ruby - gitlab_rails['omniauth_providers'] = [ - { - "name" => "bitbucket", - "app_id" => "YOUR_KEY", - "app_secret" => "YOUR_APP_SECRET", - "url" => "https://bitbucket.org/" - } - ] + gitlab_rails['omniauth_providers'] = [ + { + "name" => "bitbucket", + "app_id" => "BITBUCKET_APP_KEY", + "app_secret" => "BITBUCKET_APP_SECRET", + "url" => "https://bitbucket.org/" + } + ] ``` - For installation from source: + For installations from source: - ``` - - { name: 'bitbucket', app_id: 'YOUR_KEY', - app_secret: 'YOUR_APP_SECRET' } + ```yaml + - { name: 'bitbucket', + app_id: 'BITBUCKET_APP_KEY', + app_secret: 'BITBUCKET_APP_SECRET' } ``` -1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7. + --- -1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7. + Where `BITBUCKET_APP_KEY` is the Key and `BITBUCKET_APP_SECRET` the Secret + from the Bitbucket application page. 1. Save the configuration file. +1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you + installed GitLab via Omnibus or from source respectively. -1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```). - -1. Restart GitLab for the changes to take effect. - -On the sign in page there should now be a Bitbucket icon below the regular sign in form. -Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. -If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a Bitbucket icon below the regular sign +in form. Click the icon to begin the authentication process. Bitbucket will ask +the user to sign in and authorize the GitLab application. If everything goes +well, the user will be returned to GitLab and will be signed in. ## Bitbucket project import -To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com. +To allow projects to be imported directly into GitLab, Bitbucket requires two +extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md). -Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. +Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and +instead requires GitLab to use SSH and identify itself using your GitLab +server's SSH key. -### Step 1: Public key +To be able to access repositories on Bitbucket, GitLab will automatically +register your public key with Bitbucket as a deploy key for the repositories to +be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which +translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to +`/home/git/.ssh/bitbucket_rsa.pub` for installations from source. -To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. +--- -If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: +Below are the steps that will allow GitLab to be able to import your projects +from Bitbucket. -1. Create a new SSH key: +1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider). +1. Create a new SSH key with an **empty passphrase**: ```sh sudo -u git -H ssh-keygen ``` - When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. - Make sure to use an **empty passphrase**. + When asked to 'Enter file in which to save the key' enter: + `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or + `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is + important so make sure to get it right. -1. Configure SSH client to use your new key: + > **Warning:** + This key must NOT be associated with ANY existing Bitbucket accounts. If it + is, the import will fail with an `Access denied! Please verify you can add + deploy keys to this repository.` error. - Open the SSH configuration file of the git user. +1. Next, you need to to configure the SSH client to use your new key. Open the + SSH configuration file of the `git` user: - ```sh - sudo editor /home/git/.ssh/config + ``` + # For Omnibus packages + sudo editor /var/opt/gitlab/.ssh/config + + # For installations from source + sudo editor /home/git/.ssh/config ``` - Add a host configuration for `bitbucket.org`. +1. Add a host configuration for `bitbucket.org`: ```sh Host bitbucket.org @@ -113,28 +166,46 @@ If you have that file in place, you're all set and should see the "Import projec User git ``` -### Step 2: Known hosts - -To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: - -1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: +1. Save the file and exit. +1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git` + user that GitLab will use: ```sh sudo -u git -H ssh bitbucket.org ``` -1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): + That step is performed because GitLab needs to connect to Bitbucket over SSH, + in order to add `bitbucket.org` to your GitLab server's known SSH hosts. + +1. Verify the RSA key fingerprint you'll see in the response matches the one + in the [Bitbucket documentation][bitbucket-docs] (the specific IP address + doesn't matter): ```sh - The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. - RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. + The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established. + RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A. Are you sure you want to continue connecting (yes/no)? ``` -1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. +1. If the fingerprint matches, type `yes` to continue connecting and have + `bitbucket.org` be added to your known SSH hosts. After confirming you should + see a permission denied message. If you see an authentication successful + message you have done something wrong. The key you are using has already been + added to a Bitbucket account and will cause the import script to fail. Ensure + the key you are using CANNOT authenticate with Bitbucket. +1. Restart GitLab to allow it to find the new public key. -1. Your GitLab server is now able to connect to Bitbucket over SSH. +Your GitLab server is now able to connect to Bitbucket over SSH. You should be +able to see the "Import projects from Bitbucket" option on the New Project page +enabled. -1. Restart GitLab to allow it to find the new public key. +## Acknowledgemts + +Special thanks to the writer behind the following article: + +- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/ -You should now see the "Import projects from Bitbucket" option on the New Project page enabled. +[init-oauth]: omniauth.md#initial-omniauth-configuration +[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb2f7524a3e24b4727cb4b888a8f884116e7534 Binary files /dev/null and b/doc/integration/img/bitbucket_oauth_keys.png differ diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png new file mode 100644 index 0000000000000000000000000000000000000000..a3047712d8c1f670184e564c23329e2b653445be Binary files /dev/null and b/doc/integration/img/bitbucket_oauth_settings_page.png differ diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 46b260e7033b50e47a6e99ab787dbb63c124a915..8a55fce96fe2f6952abecbabf0f9784d5b0d33e3 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -102,8 +102,8 @@ To change these settings: block_auto_created_users: true ``` -Now we can choose one or more of the Supported Providers listed above to continue -the configuration process. +Now we can choose one or more of the [Supported Providers](#supported-providers) +listed above to continue the configuration process. ## Enable OmniAuth for an Existing User diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index 9872176356628837b3ab146596e26319020be801..b058f8e2a03231eb0729d831ccacb4fb68623a08 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -82,7 +82,7 @@ GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout v0.7.8 +sudo -u git -H git checkout v0.7.11 sudo -u git -H make ``` diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md new file mode 100644 index 0000000000000000000000000000000000000000..c8ca42f97bce74908841461f98094b1cb190ce92 --- /dev/null +++ b/doc/update/8.11-to-8.12.md @@ -0,0 +1,199 @@ +# From 8.11 to 8.12 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz +echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz +cd ruby-2.3.1 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-12-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-12-stable-ee +``` + +### 5. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v3.4.0 +``` + +### 6. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.11 +sudo -u git -H make +``` + +### 7. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production +``` + +### 8. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-11-stable:config/gitlab.yml.example origin/8-12-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Configure Git to generate packfile bitmaps (introduced in Git 2.0) on +the GitLab server during `git gc`. + +```sh +sudo -u git -H git config --global repack.writeBitmaps true +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-11-stable:lib/support/nginx/gitlab-ssl origin/8-12-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-11-stable:lib/support/nginx/gitlab origin/8-12-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-12-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-12-stable/config/initializers/smtp_settings.rb.sample#L13? + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 9. Start application + + sudo service gitlab start + sudo service nginx restart + +### 10. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.11) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.10 to 8.11](8.10-to-8.11.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 665428617816f388698307e91e187df9910e1d50..1498cb361c8718d28768129e705729c74bc2c21a 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -104,6 +104,15 @@ will find the option to flag the user as external. By default new users are not set as external users. This behavior can be changed by an administrator under **Admin > Application Settings**. +## Project features + +Project features like wiki and issues can be hidden from users depending on +which visibility level you select on project settings. + +- Disabled: disabled for everyone +- Only team members: only team members will see even if your project is public or internal +- Everyone with access: everyone can see depending on your project visibility level + ## GitLab CI GitLab CI permissions rely on the role the user has in GitLab. There are four diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md index a523b3facbe317030977849f3e15c18d723309b7..f19e7df8c9aa59fcf4b4c8a04c124ea4ac7db2a9 100644 --- a/doc/workflow/project_features.md +++ b/doc/workflow/project_features.md @@ -32,4 +32,12 @@ Snippets are little bits of code or text. This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control. -For example, a specific config file that is used by > the team that is only valid for the people that work on the code. +For example, a specific config file that is used by the team that is only valid for the people that work on the code. + +## Git LFS + +>**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins. + +Git Large File Storage allows you to easily manage large binary files with Git. +With this setting admins can better control which projects are allowed to use +LFS. diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature index 42f5d6d2af7f5357b9f33677a587a49c5e41580e..0b23bbb7951d822562206cff658b863b30c0b10a 100644 --- a/features/dashboard/todos.feature +++ b/features/dashboard/todos.feature @@ -22,26 +22,6 @@ Feature: Dashboard Todos And I mark all todos as done Then I should see all todos marked as done - @javascript - Scenario: I filter by project - Given I filter by "Enterprise" - Then I should not see todos - - @javascript - Scenario: I filter by author - Given I filter by "John Doe" - Then I should not see todos related to "Mary Jane" in the list - - @javascript - Scenario: I filter by type - Given I filter by "Issue" - Then I should not see todos related to "Merge Requests" in the list - - @javascript - Scenario: I filter by action - Given I filter by "Mentioned" - Then I should not see todos related to "Assignments" in the list - @javascript Scenario: I click on a todo row Given I click on the todo diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 60152d3da55ff49e0babaec939fb0bfcd5af5bd3..344b6fda9a638f941dd44879ca33eab04f20ddf2 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -3,7 +3,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps include SharedPaths include SharedProject include SharedUser - include Select2Helper step '"John Doe" is a developer of project "Shop"' do project.team << [john_doe, :developer] @@ -54,7 +53,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.within('.todos-pending-count') { expect(page).to have_content '0' } expect(page).to have_content 'To do 0' expect(page).to have_content 'Done 4' - expect(page).not_to have_link project.name_with_namespace + expect(page).to have_content "You're all done!" + expect('.prepend-top-default').not_to have_link project.name_with_namespace should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}" should_not_see_todo "John Doe assigned you issue #{issue.to_reference}" @@ -79,19 +79,31 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I filter by "Enterprise"' do - select2(enterprise.id, from: "#project_id") + click_button 'Project' + page.within '.dropdown-menu-project' do + click_link enterprise.name_with_namespace + end end step 'I filter by "John Doe"' do - select2(john_doe.id, from: "#author_id") + click_button 'Author' + page.within '.dropdown-menu-author' do + click_link john_doe.username + end end step 'I filter by "Issue"' do - select2('Issue', from: "#type") + click_button 'Type' + page.within '.dropdown-menu-type' do + click_link 'Issue' + end end step 'I filter by "Mentioned"' do - select2("#{Todo::MENTIONED}", from: '#action_id') + click_button 'Action' + page.within '.dropdown-menu-action' do + click_link 'Mentioned' + end end step 'I should not see todos' do diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 76fefee9254f12cae6041e1aba43654b12f143d4..975c879149e5dae8531480f18acf6191bd7e69b6 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -5,7 +5,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'change project settings' do fill_in 'project_name_edit', with: 'NewName' - uncheck 'project_issues_enabled' + select 'Disabled', from: 'project_project_feature_attributes_issues_access_level' end step 'I save project' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 0b4920883b86248c601477d5acae6f85b34f5fe2..afbd8ef123306da2ff89e4402e98c7ff98dd8cb9 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -15,7 +15,7 @@ module SharedProject # Create a specific project called "Shop" step 'I own project "Shop"' do @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop", namespace: @user.namespace, snippets_enabled: true) + @project ||= create(:project, name: "Shop", namespace: @user.namespace) @project.team << [@user, :master] end @@ -41,6 +41,8 @@ module SharedProject step 'I own project "Forum"' do @project = Project.find_by(name: "Forum") @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project') + @project.build_project_feature + @project.project_feature.save @project.team << [@user, :master] end @@ -95,7 +97,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project) expect(page).to have_content("Project name") - expect(page).to have_content("Features") + expect(page).to have_content("Feature Visibility") end def current_project diff --git a/lib/api/api.rb b/lib/api/api.rb index 4602e627fdb71b39c1f3b5ee3ccd54eb4c17aaf4..e14464c1b0d05847a67c8c22cd1181fb6a9f9e6d 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -31,6 +31,7 @@ module API mount ::API::AccessRequests mount ::API::AwardEmoji mount ::API::Branches + mount ::API::BroadcastMessages mount ::API::Builds mount ::API::CommitStatuses mount ::API::Commits diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 2efe7e3adf3474888034daa4a7233148d190afd5..7c22b17e4e507a9b5397989a05c2e19d668a21d6 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -54,7 +54,7 @@ module API post endpoint do required_attributes! [:name] - not_found!('Award Emoji') unless can_read_awardable? + not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? award = awardable.create_award_emoji(params[:name], current_user) @@ -92,6 +92,10 @@ module API can?(current_user, ability, awardable) end + def can_award_awardable? + awardable.user_can_award?(current_user, params[:name]) + end + def awardable @awardable ||= begin diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb new file mode 100644 index 0000000000000000000000000000000000000000..fb2a41480113d71390b2ca2d5f18a192e541ec4d --- /dev/null +++ b/lib/api/broadcast_messages.rb @@ -0,0 +1,99 @@ +module API + class BroadcastMessages < Grape::API + before { authenticate! } + before { authenticated_as_admin! } + + resource :broadcast_messages do + helpers do + def find_message + BroadcastMessage.find(params[:id]) + end + end + + desc 'Get all broadcast messages' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + optional :page, type: Integer, desc: 'Current page number' + optional :per_page, type: Integer, desc: 'Number of messages per page' + end + get do + messages = BroadcastMessage.all + + present paginate(messages), with: Entities::BroadcastMessage + end + + desc 'Create a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :message, type: String, desc: 'Message to display' + optional :starts_at, type: DateTime, desc: 'Starting time', default: -> { Time.zone.now } + optional :ends_at, type: DateTime, desc: 'Ending time', default: -> { 1.hour.from_now } + optional :color, type: String, desc: 'Background color' + optional :font, type: String, desc: 'Foreground color' + end + post do + create_params = declared(params, include_missing: false).to_h + message = BroadcastMessage.create(create_params) + + if message.persisted? + present message, with: Entities::BroadcastMessage + else + render_validation_error!(message) + end + end + + desc 'Get a specific broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + end + get ':id' do + message = find_message + + present message, with: Entities::BroadcastMessage + end + + desc 'Update a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + optional :message, type: String, desc: 'Message to display' + optional :starts_at, type: DateTime, desc: 'Starting time' + optional :ends_at, type: DateTime, desc: 'Ending time' + optional :color, type: String, desc: 'Background color' + optional :font, type: String, desc: 'Foreground color' + end + put ':id' do + message = find_message + update_params = declared(params, include_missing: false).to_h + + if message.update(update_params) + present message, with: Entities::BroadcastMessage + else + render_validation_error!(message) + end + end + + desc 'Delete a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + end + delete ':id' do + message = find_message + + present message.destroy, with: Entities::BroadcastMessage + end + end + end +end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 4df6ca8333ecc10cf05166dd9263e05b8075f4b8..5e3c9563703ad0c25882cc9dec866328ca1eb208 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -64,7 +64,7 @@ module API ref = branches.first end - pipeline = @project.ensure_pipeline(commit.sha, ref, current_user) + pipeline = @project.ensure_pipeline(ref, commit.sha, current_user) name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref]) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index cbb324dd06d63d48066c28a035978193beecc738..3faba79415b36aa0f2768bf547394dbdf9fb390a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -76,15 +76,23 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled + expose :container_registry_enabled + + # Expose old field names with the new permissions methods to keep API compatible + expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) } + expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) } + expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) } + expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) } + expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) } + expose :created_at, :last_activity_at - expose :shared_runners_enabled + expose :shared_runners_enabled, :lfs_enabled expose :creator_id expose :namespace expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } expose :avatar_url expose :star_count, :forks_count - expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } + expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds expose :shared_with_groups do |project, options| @@ -211,6 +219,7 @@ module API expose :user_notes_count expose :upvotes, :downvotes expose :due_date + expose :confidential expose :web_url do |issue, options| Gitlab::UrlBuilder.build(issue) @@ -232,6 +241,8 @@ module API expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds expose :merge_status + expose :diff_head_sha, as: :sha + expose :merge_commit_sha expose :subscribed do |merge_request, options| merge_request.subscribed?(options[:current_user]) end @@ -574,5 +585,10 @@ module API class Template < Grape::Entity expose :name, :content end + + class BroadcastMessage < Grape::Entity + expose :id, :message, :starts_at, :ends_at, :color, :font + expose :active?, as: :active + end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9d8b8d737a9b06faf7a8d1589b9e57369d53fbda..d2df77238d59adb6f8fa79e356a0a6439a9359e8 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -30,7 +30,7 @@ module API # Example Request: # POST /groups post do - authorize! :create_group, current_user + authorize! :create_group required_attributes! [:name, :path] attrs = attributes_for_keys [:name, :path, :description, :visibility_level] @@ -97,7 +97,7 @@ module API group = find_group(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) projects = paginate projects - present projects, with: Entities::Project + present projects, with: Entities::Project, user: current_user end # Transfer a project to the Group namespace diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index da4b1bf9902c8493fcaa3fb49dd96e79efad0622..6a20ba95a79a199e2ea2dd873bc5f8faa565fd2e 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -129,7 +129,7 @@ module API forbidden! unless current_user.is_admin? end - def authorize!(action, subject) + def authorize!(action, subject = nil) forbidden! unless can?(current_user, action, subject) end @@ -148,7 +148,7 @@ module API end def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end # Checks the occurrences of required attributes, each attribute must be present in the params hash @@ -408,14 +408,6 @@ module API links.join(', ') end - def abilities - @abilities ||= begin - abilities = Six.new - abilities << Ability - abilities - end - end - def secret_token File.read(Gitlab.config.gitlab_shell.secret_file).chomp end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 077258faee19235a01025d992e04fe9e528b6cd5..556684187d87f27c02b05d0ff7638694a4f79c03 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -140,12 +140,13 @@ module API # labels (optional) - The labels of an issue # created_at (optional) - Date time string, ISO 8601 formatted # due_date (optional) - Date time string in the format YEAR-MONTH-DAY + # confidential (optional) - Boolean parameter if the issue should be confidential # Example Request: # POST /projects/:id/issues post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id, :due_date] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) @@ -154,21 +155,19 @@ module API render_api_error!({ labels: errors }, 400) end - project = user_project + attrs[:labels] = params[:labels] if params[:labels] - issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute + # Convert and filter out invalid confidential flags + attrs['confidential'] = to_boolean(attrs['confidential']) + attrs.delete('confidential') if attrs['confidential'].nil? + + issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end if issue.valid? - # Find or create labels and attach to issue. Labels are valid because - # we already checked its name, so there can't be an error here - if params[:labels].present? - issue.add_labels_by_names(params[:labels].split(',')) - end - present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) @@ -188,12 +187,13 @@ module API # state_event (optional) - The state event of an issue (close|reopen) # updated_at (optional) - Date time string, ISO 8601 formatted # due_date (optional) - Date time string in the format YEAR-MONTH-DAY + # confidential (optional) - Boolean parameter if the issue should be confidential # Example Request: # PUT /projects/:id/issues/:issue_id put ':id/issues/:issue_id' do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date] + keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential] keys << :updated_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) @@ -202,17 +202,15 @@ module API render_api_error!({ labels: errors }, 400) end + attrs[:labels] = params[:labels] if params[:labels] + + # Convert and filter out invalid confidential flags + attrs['confidential'] = to_boolean(attrs['confidential']) + attrs.delete('confidential') if attrs['confidential'].nil? + issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) if issue.valid? - # Find or create labels and attach to issue. Labels are valid because - # we already checked its name, so there can't be an error here - if params[:labels] && can?(current_user, :admin_issue, user_project) - issue.remove_labels - # Create and add labels to the new created issue - issue.add_labels_by_names(params[:labels].split(',')) - end - present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 71efd4f33ca6d2c5d0905e8ee58c17c0b1668f79..a1fd598414abcf4d67e36804694f64edfc602caf 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -51,7 +51,7 @@ module API @projects = current_user.viewable_starred_projects @projects = filter_projects(@projects) @projects = paginate @projects - present @projects, with: Entities::Project + present @projects, with: Entities::Project, user: current_user end # Get all projects for admin user @@ -105,6 +105,7 @@ module API # visibility_level (optional) - 0 by default # import_url (optional) # public_builds (optional) + # lfs_enabled (optional) # Example Request # POST /projects post do @@ -124,7 +125,8 @@ module API :visibility_level, :import_url, :public_builds, - :only_allow_merge_if_build_succeeds] + :only_allow_merge_if_build_succeeds, + :lfs_enabled] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(current_user, attrs).execute if @project.saved? @@ -156,6 +158,7 @@ module API # visibility_level (optional) # import_url (optional) # public_builds (optional) + # lfs_enabled (optional) # Example Request # POST /projects/user/:user_id post "user/:user_id" do @@ -174,7 +177,8 @@ module API :visibility_level, :import_url, :public_builds, - :only_allow_merge_if_build_succeeds] + :only_allow_merge_if_build_succeeds, + :lfs_enabled] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(user, attrs).execute if @project.saved? @@ -220,6 +224,7 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project # public_builds (optional) + # lfs_enabled (optional) # Example Request # PUT /projects/:id put ':id' do @@ -237,7 +242,8 @@ module API :public, :visibility_level, :public_builds, - :only_allow_merge_if_build_succeeds] + :only_allow_merge_if_build_succeeds, + :lfs_enabled] attrs = map_public_to_visibility_level(attrs) authorize_admin_project authorize! :rename_project, user_project if attrs[:name].present? diff --git a/lib/api/users.rb b/lib/api/users.rb index 8a376d3c2a322051bb5aa8e70d913f41cda27cf5..c440305ff0fcb227bd1edbb1a6311a8f6b25519f 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -327,7 +327,7 @@ module API # Example Request: # GET /user get do - present @current_user, with: Entities::UserLogin + present @current_user, with: Entities::UserFull end # Get currently authenticated user's keys diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index f117fc3d37def0815360c09e4221dbadea2a9f11..9fcd9a3f9994799fce4855a992653c839fcd3447 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -55,7 +55,7 @@ module Backup bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s) FileUtils.mv(path, bk_repos_path) # This is expected from gitlab:check - FileUtils.mkdir_p(path, mode: 2770) + FileUtils.mkdir_p(path, mode: 02770) end Project.find_each(batch_size: 1000) do |project| diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index d77a5e3ff09099ef5047d2d0517702d75ed595ef..16cd774c81a1197a2a447e2f4d18297cd2817596 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -18,10 +18,6 @@ module Banzai @object_sym ||= object_name.to_sym end - def self.object_class_title - @object_title ||= object_class.name.titleize - end - # Public: Find references in text (like `!123` for merge requests) # # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches| @@ -49,10 +45,6 @@ module Banzai self.class.object_sym end - def object_class_title - self.class.object_class_title - end - def references_in(*args, &block) self.class.references_in(*args, &block) end @@ -198,7 +190,7 @@ module Banzai end def object_link_title(object) - "#{object_class_title}: #{object.title}" + object.title end def object_link_text(object, matches) diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index bbb88c979cc3f3c6e4d8d6895bf5e9262666364a..4358bf45549bc2d762a91823b7383000a197ca3a 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -35,7 +35,7 @@ module Banzai end def object_link_title(range) - range.reference_title + nil end end end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 2ce1816672b93e630244e0048675e4511ff1e56f..a26dd09c25a2f155483fd29666e96c2f0eda9f8d 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -28,10 +28,6 @@ module Banzai only_path: context[:only_path]) end - def object_link_title(commit) - commit.link_title - end - def object_link_text_extras(object, matches) extras = super diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index e258dc8e2bf017d02bd637d0b6e2a4803bcee2cb..8f262ef3d8d55e322b466c8ecba6959bd55893c8 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -70,6 +70,11 @@ module Banzai def unescape_html_entities(text) CGI.unescapeHTML(text.to_s) end + + def object_link_title(object) + # use title of wrapped element instead + nil + end end end end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index ca686c87d97bd9ef628c40b7d525c8cebcefe608..58fff496d003d765149262e89f6f152a2816026c 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -59,6 +59,10 @@ module Banzai html_safe end end + + def object_link_title(object) + nil + end end end end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index bf058241cda706d8896b8ad76ab2103b3cc26375..2d221290f7eb0e08c3cf01145ebb5e7742173890 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -52,7 +52,7 @@ module Banzai end def reference_class(type) - "gfm gfm-#{type}" + "gfm gfm-#{type} has-tooltip" end # Ensure that a :project key exists in context diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index 6cf218aaa0d304022ba1ce91c9f9716bca50add1..e8e03e4a98fefa220090b0d9a4785d7f6181bc8e 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -211,7 +211,7 @@ module Banzai end def can?(user, permission, subject) - Ability.abilities.allowed?(user, permission, subject) + Ability.allowed?(user, permission, subject) end def find_projects_for_hash_keys(hash) diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb index 95d925dc7f3eccb01165f0cd6f1ba2a92b655faa..9a0482306b7ebda075cda06589ddc1699ab55673 100644 --- a/lib/gitlab/badge/coverage/report.rb +++ b/lib/gitlab/badge/coverage/report.rb @@ -12,9 +12,7 @@ module Gitlab @ref = ref @job = job - @pipeline = @project.pipelines - .latest_successful_for(@ref) - .first + @pipeline = @project.pipelines.latest_successful_for(@ref) end def entity diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden.rb similarity index 82% rename from lib/gitlab/ci/config/node/hidden_job.rb rename to lib/gitlab/ci/config/node/hidden.rb index 073044b66f89c66ae593d1042b22214b6d901bfc..fe4ee8a7fc6f33bfa5ff8951238d9e2f2edcd900 100644 --- a/lib/gitlab/ci/config/node/hidden_job.rb +++ b/lib/gitlab/ci/config/node/hidden.rb @@ -5,11 +5,10 @@ module Gitlab ## # Entry that represents a hidden CI/CD job. # - class HiddenJob < Entry + class Hidden < Entry include Validatable validations do - validates :config, type: Hash validates :config, presence: true end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 51683c82ceb7565f742b5063fd2163ac5bc8c654..a1a26d4fd8f3b9baa546952b01b845d6d64382cd 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -30,7 +30,7 @@ module Gitlab def compose! @config.each do |name, config| - node = hidden?(name) ? Node::HiddenJob : Node::Job + node = hidden?(name) ? Node::Hidden : Node::Job factory = Node::Factory.new(node) .value(config || {}) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 02ffb43d89bd42e3c4da8ca049cd02d9e5cc7bfb..4fdc2f46be094aec6d6486d11222c8ec25c27025 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -152,12 +152,14 @@ module Gitlab end def create_comments(issuable, comments) - comments.each do |raw| - begin - comment = CommentFormatter.new(project, raw) - issuable.notes.create!(comment.attributes) - rescue => e - errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + ActiveRecord::Base.no_touching do + comments.each do |raw| + begin + comment = CommentFormatter.new(project, raw) + issuable.notes.create!(comment.attributes) + rescue => e + errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end end end end @@ -166,7 +168,7 @@ module Gitlab unless project.wiki_enabled? wiki = WikiFormatter.new(project) gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) - project.update_attribute(:wiki_enabled, true) + project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) end rescue Gitlab::Shell::Error => e # GitHub error message when the wiki repo has not been created, diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 835ec858b35c537dfd11c8c2146ea89fae2eec8e..07edbe37a1399a9d507d7bb870f3eec16e112913 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -12,7 +12,7 @@ module Gitlab author_id: author_id, assignee_id: assignee_id, created_at: raw_data.created_at, - updated_at: updated_at + updated_at: raw_data.updated_at } end @@ -69,10 +69,6 @@ module Gitlab def state raw_data.state == 'closed' ? 'closed' : 'opened' end - - def updated_at - state == 'closed' ? raw_data.closed_at : raw_data.updated_at - end end end end diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index 53d4b3102d195a40fb6cad8bb68d3e3898458279..b2fa524cf5b41816b9a96583cb10f81298b842c3 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -3,14 +3,14 @@ module Gitlab class MilestoneFormatter < BaseFormatter def attributes { - iid: number, + iid: raw_data.number, project: project, - title: title, - description: description, - due_date: due_date, + title: raw_data.title, + description: raw_data.description, + due_date: raw_data.due_on, state: state, - created_at: created_at, - updated_at: updated_at + created_at: raw_data.created_at, + updated_at: raw_data.updated_at } end @@ -20,33 +20,9 @@ module Gitlab private - def number - raw_data.number - end - - def title - raw_data.title - end - - def description - raw_data.description - end - - def due_date - raw_data.due_on - end - def state raw_data.state == 'closed' ? 'closed' : 'active' end - - def created_at - raw_data.created_at - end - - def updated_at - state == 'closed' ? raw_data.closed_at : raw_data.updated_at - end end end end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index f4221003db5119a171c1a69921cbb4f9a4f15615..e9725880c5eb2e4cc9b712cfd4a1a375f661ba5d 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -11,18 +11,24 @@ module Gitlab end def execute - ::Projects::CreateService.new( + project = ::Projects::CreateService.new( current_user, name: repo.name, path: repo.name, description: repo.description, namespace_id: namespace.id, - visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, + visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility, import_type: "github", import_source: repo.full_name, - import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"), - wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later + import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@") ).execute + + # If repo has wiki we'll import it later + if repo.has_wiki? && project + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + end + + project end end end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 04aa3664f640388dfa0de539512496f60048f594..d9d436d749059267c6b62f59c49a96d0137fa35d 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -20,7 +20,7 @@ module Gitlab author_id: author_id, assignee_id: assignee_id, created_at: raw_data.created_at, - updated_at: updated_at + updated_at: raw_data.updated_at } end @@ -103,15 +103,6 @@ module Gitlab 'opened' end end - - def updated_at - case state - when 'merged' then raw_data.merged_at - when 'closed' then raw_data.closed_at - else - raw_data.updated_at - end - end end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 1da51043611a4d547da37db26133362771e74bb6..c2e8a1ca5dd2e289e58392f143c90b7ea8472243 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -39,15 +39,12 @@ project_tree: - :labels - milestones: - :events + - :project_feature # Only include the following attributes for the models specified. included_attributes: project: - :description - - :issues_enabled - - :merge_requests_enabled - - :wiki_enabled - - :snippets_enabled - :visibility_level - :archived user: @@ -72,4 +69,4 @@ methods: statuses: - :type merge_request_diff: - - :utf8_st_diffs \ No newline at end of file + - :utf8_st_diffs diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb new file mode 100644 index 0000000000000000000000000000000000000000..117fc5081359d70250f09ba27bde84982c775057 --- /dev/null +++ b/lib/gitlab/sentry.rb @@ -0,0 +1,27 @@ +module Gitlab + module Sentry + def self.enabled? + Rails.env.production? && current_application_settings.sentry_enabled? + end + + def self.context(current_user = nil) + return unless self.enabled? + + if current_user + Raven.user_context( + id: current_user.id, + email: current_user.email, + username: current_user.username, + ) + end + end + + def self.program_context + if Sidekiq.server? + 'sidekiq' + else + 'rails' + end + end + end +end diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index d0ad5e26dbd077725cfc8bf28d546a9483191998..2896636db5a4b19bbd00258478188bbaa540795c 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -41,8 +41,8 @@ describe Projects::Boards::IssuesController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false) end it 'returns a successful 403 response' do diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 9496636e3cc4c58d77d4b09aa4882c33bd1f5f71..d687dea3c3b34a29b538c918a85791eeecb4d247 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -35,11 +35,11 @@ describe Projects::Boards::ListsController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false) end - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do read_board_list user: user expect(response).to have_http_status(403) @@ -56,9 +56,9 @@ describe Projects::Boards::ListsController do end describe 'POST create' do - let(:label) { create(:label, project: project, name: 'Development') } - context 'with valid params' do + let(:label) { create(:label, project: project, name: 'Development') } + it 'returns a successful 200 response' do create_board_list user: user, label_id: label.id @@ -73,20 +73,29 @@ describe Projects::Boards::ListsController do end context 'with invalid params' do - it 'returns an error' do - create_board_list user: user, label_id: nil + context 'when label is nil' do + it 'returns a not found 404 response' do + create_board_list user: user, label_id: nil + + expect(response).to have_http_status(404) + end + end - parsed_response = JSON.parse(response.body) + context 'when label that does not belongs to project' do + it 'returns a not found 404 response' do + label = create(:label, name: 'Development') - expect(parsed_response['label']).to contain_exactly "can't be blank" - expect(response).to have_http_status(422) + create_board_list user: user, label_id: label.id + + expect(response).to have_http_status(404) + end end end context 'with unauthorized user' do - let(:label) { create(:label, project: project, name: 'Development') } + it 'returns a forbidden 403 response' do + label = create(:label, project: project, name: 'Development') - it 'returns a successful 403 response' do create_board_list user: guest, label_id: label.id expect(response).to have_http_status(403) @@ -122,7 +131,7 @@ describe Projects::Boards::ListsController do end context 'with invalid position' do - it 'returns a unprocessable entity 422 response' do + it 'returns an unprocessable entity 422 response' do move user: user, list: planning, position: 6 expect(response).to have_http_status(422) @@ -138,7 +147,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do move user: guest, list: planning, position: 6 expect(response).to have_http_status(403) @@ -180,7 +189,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do remove_board_list user: guest, list: planning expect(response).to have_http_status(403) @@ -213,7 +222,7 @@ describe Projects::Boards::ListsController do end context 'when board lists is not empty' do - it 'returns a unprocessable entity 422 response' do + it 'returns an unprocessable entity 422 response' do create(:list, board: board) generate_default_board_lists user: user @@ -223,7 +232,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do generate_default_board_lists user: guest expect(response).to have_http_status(403) diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 75a6d39e82c1a84652e750d76dbdfe858e88d99b..6f6e608e1f3fa4bac30573ae419785bd6391205f 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -23,8 +23,8 @@ describe Projects::BoardsController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) end it 'returns a successful 404 response' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c64c2b075c53c81adac2617da089badcb47953ca..a219400d75fcc7b26d74abbdc216920ff081f85b 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -170,6 +170,35 @@ describe Projects::MergeRequestsController do expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) expect(merge_request.reload.closed?).to be_truthy end + + it 'allows editing of a closed merge request' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + title: 'New title' + } + + expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) + expect(merge_request.reload.title).to eq 'New title' + end + + it 'does not allow to update target branch closed merge request' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + target_branch: 'new_branch' + } + + expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } + end end end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index b8a28f4370716091c782dee31ea1692fcbf6349e..72a3ebf2ebd4cdd2c1517a970a1093267f0822b3 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::SnippetsController do - let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) } + let(:project) { create(:project_empty_repo, :public) } let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f82d68a1816eba8380a1029aeb96c6ca883c6dfa..fb84ba07d254c769b2d9f3b15952f64f9df0d184 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -8,7 +8,6 @@ FactoryGirl.define do path { name.downcase.gsub(/\s/, '_') } namespace creator - snippets_enabled true trait :public do visibility_level Gitlab::VisibilityLevel::PUBLIC @@ -27,6 +26,26 @@ FactoryGirl.define do project.create_repository end end + + # Nest Project Feature attributes + transient do + wiki_access_level ProjectFeature::ENABLED + builds_access_level ProjectFeature::ENABLED + snippets_access_level ProjectFeature::ENABLED + issues_access_level ProjectFeature::ENABLED + merge_requests_access_level ProjectFeature::ENABLED + end + + after(:create) do |project, evaluator| + project.project_feature. + update_attributes( + wiki_access_level: evaluator.wiki_access_level, + builds_access_level: evaluator.builds_access_level, + snippets_access_level: evaluator.snippets_access_level, + issues_access_level: evaluator.issues_access_level, + merge_requests_access_level: evaluator.merge_requests_access_level, + ) + end end # Project with empty repository diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 5d777895542cc40b0dd7ba464024f76aca56a78d..c6c2e2095dfdc93694b103f46ab4a56d53171ddd 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -110,6 +110,45 @@ describe 'Issue Boards', feature: true, js: true do end end + it 'search backlog list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue1.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) + end + + it 'search done list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue8.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) + end + + it 'search list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue5.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) + end + it 'allows user to delete board' do page.within(find('.board:nth-child(2)')) do find('.board-delete').click @@ -143,14 +182,21 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('20') + expect(page.find('.board-header')).to have_content('56') expect(page).to have_selector('.card', count: 20) + expect(page).to have_content('Showing 20 of 56 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") wait_for_vue_resource(spinner: false) - expect(page.find('.board-header')).to have_content('40') expect(page).to have_selector('.card', count: 40) + expect(page).to have_content('Showing 40 of 56 issues') + + evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + wait_for_vue_resource(spinner: false) + + expect(page).to have_selector('.card', count: 56) + expect(page).to have_content('Showing all issues') end end @@ -162,32 +208,6 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'is searchable' do - page.within(find('.board', match: :first)) do - find('.form-control').set issue1.title - - wait_for_vue_resource(spinner: false) - - expect(page).to have_selector('.card', count: 1) - end - end - - it 'clears search' do - page.within(find('.board', match: :first)) do - find('.form-control').set issue1.title - - expect(page).to have_selector('.card', count: 1) - - find('.board-search-clear-btn').click - end - - wait_for_vue_resource - - page.within(find('.board', match: :first)) do - expect(page).to have_selector('.card', count: 6) - end - end - it 'moves issue from backlog into list' do drag_to(list_to_index: 1) @@ -466,13 +486,19 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('20') + expect(page.find('.board-header')).to have_content('51') expect(page).to have_selector('.card', count: 20) + expect(page).to have_content('Showing 20 of 51 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") - expect(page.find('.board-header')).to have_content('40') expect(page).to have_selector('.card', count: 40) + expect(page).to have_content('Showing 40 of 51 issues') + + evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + + expect(page).to have_selector('.card', count: 51) + expect(page).to have_content('Showing all issues') end end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 6eb04cf74c523db554b8574e3eaab06ca8eb85de..79cc50bc18eaa4b97baebf2357bddd23c77e01b7 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -12,7 +12,6 @@ describe 'Awards Emoji', feature: true do describe 'Click award emoji from issue#show' do let!(:issue) do create(:issue, - author: @user, assignee: @user, project: project) end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index ba2dbcfe4b1bb5be1235f9134ba6e82567563871..372ad39676d5d0dc390ac8335a37954cd77468ad 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -8,6 +8,7 @@ describe 'Filter issues', feature: true do let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:issue1) { create(:issue, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } before do project.team << [user, :master] @@ -107,6 +108,15 @@ describe 'Filter issues', feature: true do end expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) end + + it 'filters by wont fix labels' do + find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content wontfix.title + click_link wontfix.title + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title) + end end describe 'Filter issues for assignee and label from issues#index' do diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb index a818679a8748191ff66fec766c853897c1574552..06fad1007e8135d926c8ea9ef84445eec98dc0ce 100644 --- a/spec/features/merge_requests/diff_notes_spec.rb +++ b/spec/features/merge_requests/diff_notes_spec.rb @@ -147,6 +147,37 @@ feature 'Diff notes', js: true, feature: true do end end + context 'when the MR only supports legacy diff notes' do + before do + @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') + end + + context 'with a new line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + + context 'with an old line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + end + + context 'with an unchanged line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + end + end + + context 'with a match line' do + it 'should not allow commenting' do + should_not_allow_commenting(find('.match', match: :first)) + end + end + end + def should_allow_commenting(line_holder, diff_side = nil) line = get_line_components(line_holder, diff_side) line[:content].hover diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0405830057084ddcb96d074b97ad98cd781b936c --- /dev/null +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Download buttons in branches page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('binary-encoding').sha, + ref: 'binary-encoding', # make sure the branch is in the 1st page! + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking branches' do + context 'with artifacts' do + before do + visit namespace_project_branches_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb similarity index 100% rename from spec/features/builds_spec.rb rename to spec/features/projects/builds_spec.rb diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9b487e350f2c7763ef5112f43bd3eb26b9b18e1e --- /dev/null +++ b/spec/features/projects/features_visibility_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' +include WaitForAjax + +describe 'Edit Project Settings', feature: true do + let(:member) { create(:user) } + let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') } + let(:non_member) { create(:user) } + + describe 'project features visibility selectors', js: true do + before do + project.team << [member, :master] + login_as(member) + end + + tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests" } + + tools.each do |tool_name, shortcut_name| + describe "feature #{tool_name}" do + it 'toggles visibility' do + visit edit_namespace_project_path(project.namespace, project) + + select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") + + select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).to have_selector(".shortcuts-#{shortcut_name}") + + select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).to have_selector(".shortcuts-#{shortcut_name}") + + sleep 0.1 + end + end + end + end + + describe 'project features visibility pages' do + before do + @tools = + { + builds: namespace_project_pipelines_path(project.namespace, project), + issues: namespace_project_issues_path(project.namespace, project), + wiki: namespace_project_wiki_path(project.namespace, project, :home), + snippets: namespace_project_snippets_path(project.namespace, project), + merge_requests: namespace_project_merge_requests_path(project.namespace, project), + } + end + + context 'normal user' do + it 'renders 200 if tool is enabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED) + visit url + expect(page.status_code).to eq(200) + end + end + + it 'renders 404 if feature is disabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 404 if feature is enabled only for team members' do + project.team.truncate + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 200 if users is member of group' do + group = create(:group) + project.group = group + project.save + + group.add_owner(member) + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(200) + end + end + end + + context 'admin user' do + before do + non_member.update_attribute(:admin, true) + login_as(non_member) + end + + it 'renders 404 if feature is disabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 200 if feature is enabled only for team members' do + project.team.truncate + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(200) + end + end + end + end +end diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..be5cebcd7c9f9657c255fff604fa161d76a20ee5 --- /dev/null +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Download buttons in files tree', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when files tree' do + context 'with artifacts' do + before do + visit namespace_project_tree_path( + project.namespace, project, project.default_branch) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 7bb0d26b21c03d96db4363bd0543422bcf5df536..e14b27057048b57aaec1f53dcde44a3291876147 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b26c0ea7a1487d3a9225eef9adf66c5eee93d337 --- /dev/null +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Download buttons in project main page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking project main page' do + context 'with artifacts' do + before do + visit namespace_project_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6e0022c179fa855c3cb7f159e9730c8d15b53f16 --- /dev/null +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Download buttons in tags page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:tag) { 'v1.0.0' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit(tag).sha, + ref: tag, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking tags' do + context 'with artifacts' do + before do + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 6ed279ef9be6f3413b683b6c793e72cf7ca6e2ad..abb27c90e0a969ec83590d5162079c886ad663e5 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do MARKDOWN end + let(:singleIncompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [ ] Incomplete entry 1 + MARKDOWN + end + + let(:singleCompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [x] Incomplete entry 1 + MARKDOWN + end + before do Warden.test_mode! @@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do end describe 'for Issues' do - let!(:issue) { create(:issue, description: markdown, author: user, project: project) } + describe 'multiple tasks' do + let!(:issue) { create(:issue, description: markdown, author: user, project: project) } - it 'renders' do - visit_issue(project, issue) + it 'renders' do + visit_issue(project, issue) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + container = '.detail-page-description .description.js-task-list-container' - it 'contains the required selectors' do - visit_issue(project, issue) + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end - container = '.detail-page-description .description.js-task-list-container' + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end end - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') + describe 'single incomplete task' do + let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) } - logout(:user) + it 'renders' do + visit_issue(project, issue) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'provides a summary on Issues#index' do - visit namespace_project_issues_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + describe 'single complete task' do + let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) } + + it 'renders' do + visit_issue(project, issue) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end describe 'for Notes' do let!(:issue) { create(:issue, author: user, project: project) } - let!(:note) do - create(:note, note: markdown, noteable: issue, - project: project, author: user) + describe 'multiple tasks' do + let!(:note) do + create(:note, note: markdown, noteable: issue, + project: project, author: user) + end + + it 'renders for note body' do + visit_issue(project, issue) + + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 6) + expect(page).to have_selector('.note ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + expect(page).to have_selector('.note .js-task-list-container') + expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') + expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + end + + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end end - it 'renders for note body' do - visit_issue(project, issue) - - expect(page).to have_selector('.note ul.task-list', count: 1) - expect(page).to have_selector('.note li.task-list-item', count: 6) - expect(page).to have_selector('.note ul input[checked]', count: 2) - end + describe 'single incomplete task' do + let!(:note) do + create(:note, note: singleIncompleteMarkdown, noteable: issue, + project: project, author: user) + end - it 'contains the required selectors' do - visit_issue(project, issue) + it 'renders for note body' do + visit_issue(project, issue) - expect(page).to have_selector('.note .js-task-list-container') - expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') - expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 0) + end end - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') + describe 'single complete task' do + let!(:note) do + create(:note, note: singleCompleteMarkdown, noteable: issue, + project: project, author: user) + end - logout(:user) + it 'renders for note body' do + visit_issue(project, issue) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 1) + end end end @@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do visit namespace_project_merge_request_path(project.namespace, project, merge) end - let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } + describe 'multiple tasks' do + let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } - it 'renders for description' do - visit_merge_request(project, merge) + it 'renders for description' do + visit_merge_request(project, merge) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end - it 'contains the required selectors' do - visit_merge_request(project, merge) + it 'contains the required selectors' do + visit_merge_request(project, merge) - container = '.detail-page-description .description.js-task-list-container' + container = '.detail-page-description .description.js-task-list-container' - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') - end + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end - it 'is only editable by author' do - visit_merge_request(project, merge) - expect(page).to have_selector('.js-task-list-container') + it 'is only editable by author' do + visit_merge_request(project, merge) + expect(page).to have_selector('.js-task-list-container') - logout(:user) + logout(:user) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end + end + + describe 'single incomplete task' do + let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'provides a summary on MergeRequests#index' do - visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + describe 'single complete task' do + let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end end diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..83cf306437d781fe5343ffda636db3aed3fab213 --- /dev/null +++ b/spec/features/todos/todos_filtering_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe 'Dashboard > User filters todos', feature: true, js: true do + include WaitForAjax + + let(:user_1) { create(:user, username: 'user_1', name: 'user_1') } + let(:user_2) { create(:user, username: 'user_2', name: 'user_2') } + + let(:project_1) { create(:empty_project, name: 'project_1') } + let(:project_2) { create(:empty_project, name: 'project_2') } + + let(:issue) { create(:issue, title: 'issue', project: project_1) } + + let!(:merge_request) { create(:merge_request, source_project: project_2, title: 'merge_request') } + + before do + create(:todo, user: user_1, author: user_2, project: project_1, target: issue, action: 1) + create(:todo, user: user_1, author: user_1, project: project_2, target: merge_request, action: 2) + + project_1.team << [user_1, :developer] + project_2.team << [user_1, :developer] + login_as(user_1) + visit dashboard_todos_path + end + + it 'filters by project' do + click_button 'Project' + within '.dropdown-menu-project' do + fill_in 'Search projects', with: project_1.name_with_namespace + click_link project_1.name_with_namespace + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content project_2.name_with_namespace + end + + it 'filters by author' do + click_button 'Author' + within '.dropdown-menu-author' do + fill_in 'Search authors', with: user_1.name + click_link user_1.name + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content user_2.name + end + + it 'filters by type' do + click_button 'Type' + within '.dropdown-menu-type' do + click_link 'Issue' + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content ' merge request !' + end + + it 'filters by action' do + click_button 'Action' + within '.dropdown-menu-action' do + click_link 'Assigned' + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content ' mentioned ' + end +end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 32544f3f53802b7dbd769a241ea2e19530981257..fc555a74f30fa0105bceea745238ded5f5458b1f 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -118,6 +118,20 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_css("#todo_#{Todo.first.id}") end end + + describe 'mark all as done', js: true do + before do + visit dashboard_todos_path + click_link('Mark all as done') + end + + it 'shows "All done" message!' do + within('.todos-pending-count') { expect(page).to have_content '0' } + expect(page).to have_content 'To do 0' + expect(page).to have_content "You're all done!" + expect(page).not_to have_selector('.gl-pagination') + end + end end context 'User has a Todo in a project pending deletion' do diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2ac810e478ae7de82fbbb925f0882a887d4d8be2 --- /dev/null +++ b/spec/finders/tags_finder_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe TagsFinder do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:repository) { project.repository } + + describe '#execute' do + context 'sort only' do + it 'sorts by name' do + tags_finder = described_class.new(repository, {}) + + result = tags_finder.execute + + expect(result.first.name).to eq("v1.0.0") + end + + it 'sorts by recently_updated' do + tags_finder = described_class.new(repository, { sort: 'updated_desc' }) + + result = tags_finder.execute + recently_updated_tag = repository.tags.max do |a, b| + repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + end + + expect(result.first.name).to eq(recently_updated_tag.name) + end + + it 'sorts by last_updated' do + tags_finder = described_class.new(repository, { sort: 'updated_asc' }) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + end + end + + context 'filter only' do + it 'filters tags by name' do + tags_finder = described_class.new(repository, { search: '1.0.0' }) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + expect(result.count).to eq(1) + end + + it 'does not find any tags with that name' do + tags_finder = described_class.new(repository, { search: 'hey' }) + + result = tags_finder.execute + + expect(result.count).to eq(0) + end + end + + context 'filter and sort' do + it 'filters tags by name and sorts by recently_updated' do + params = { sort: 'updated_desc', search: 'v1' } + tags_finder = described_class.new(repository, params) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.1.0') + expect(result.count).to eq(2) + end + + it 'filters tags by name and sorts by last_updated' do + params = { sort: 'updated_asc', search: 'v1' } + tags_finder = described_class.new(repository, params) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + expect(result.count).to eq(2) + end + end + end +end diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json index 0d2067f704a1877ac765d2fc273e0c9c8a7089c2..70771b21c969d119e7ebf6397d16b9c0c9c31fc0 100644 --- a/spec/fixtures/api/schemas/issues.json +++ b/spec/fixtures/api/schemas/issues.json @@ -1,4 +1,15 @@ { - "type": "array", - "items": { "$ref": "issue.json" } + "type": "object", + "required" : [ + "issues", + "size" + ], + "properties" : { + "issues": { + "type": "array", + "items": { "$ref": "issue.json" } + }, + "size": { "type": "integer" } + }, + "additionalProperties": false } diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index b48026c3b773950e5e58e6816a8909e4719234d1..56b98856614c7c5ab165acaa123da651109ad432 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -13,17 +13,21 @@ gl.utils.preventDisabledButtons(); isClicked = false; $button = $('#test-button'); + expect($button).toExist(); $button.click(function() { return isClicked = true; }); $button.trigger('click'); return expect(isClicked).toBe(false); }); - return it('should be on the same page if a disabled link clicked', function() { - var locationBeforeLinkClick; + + it('should be on the same page if a disabled link clicked', function() { + var locationBeforeLinkClick, $link; locationBeforeLinkClick = window.location.href; gl.utils.preventDisabledButtons(); - $('#test-link').click(); + $link = $('#test-link'); + expect($link).toExist(); + $link.click(); return expect(window.location.href).toBe(locationBeforeLinkClick); }); }); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index c206b794442f913c71a96290381e839cd0730ea8..1688b9961624c74612165a05975a810febc33359 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -60,15 +60,6 @@ describe('List model', () => { }, 0); }); - it('can\'t search when not backlog', () => { - expect(list.canSearch()).toBe(false); - }); - - it('can search when backlog', () => { - list.type = 'backlog'; - expect(list.canSearch()).toBe(true); - }); - it('gets issue from list', (done) => { setTimeout(() => { const issue = list.findIssue(1); diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 0c37ec8354f3efe9385e703b1225bee8240beb3b..f3797ed44d4800466482c1c714c4de01393bbdad 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -26,12 +26,15 @@ const listObjDuplicate = { const BoardsMockData = { 'GET': { - '/test/issue-boards/board/lists{/id}/issues': [{ - title: 'Testing', - iid: 1, - confidential: false, - labels: [] - }] + '/test/issue-boards/board/lists{/id}/issues': { + issues: [{ + title: 'Testing', + iid: 1, + confidential: false, + labels: [] + }], + size: 1 + } }, 'POST': { '/test/issue-boards/board/lists{/id}': listObj diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index 593bd6d5cac994d24bda92071a9d7b5b28e81d30..e6c90ad87ee7d744c0fa86738b4a3b1307b4246c 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -65,14 +65,14 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - it 'includes a title attribute' do + it 'includes no title attribute' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq range.reference_title + expect(doc.css('a').first.attr('title')).to eq "" end it 'includes default classes' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index d46d3f1489e40a7c9d9cd8cdddf7843ed7ecfd50..e0f082825515b1335ad4a41122571b9455f9eeae 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -55,7 +55,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq commit.link_title + expect(doc.css('a').first.attr('title')).to eq commit.title end it 'escapes the title attribute' do @@ -67,7 +67,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index 953466679e4beb276f65b5a1a6adb06f01d91b6f..7116c09fb21670a5ed590144854e81da809a824d 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -64,7 +64,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'includes default classes' do doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' end it 'supports an :only_path context' do diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index a005b4990e7937e9affaa6d651a430c98edc12f5..fce86a9b6ad0952202b05cf794ffa4ed09afb151 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -54,7 +54,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" + expect(doc.css('a').first.attr('title')).to eq issue.title end it 'escapes the title attribute' do @@ -66,7 +66,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9276a1540070e207feaecbe8b7126fa4d4fe70d5..908ccebbf87b0cb14e544da7473b39ba09a9aa89 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -21,7 +21,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Label #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 805acf1c8b365bd7b65a56acd7a9283b715fb531..274258a045cf45abf906b35739e11135728b9224 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -46,7 +46,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" + expect(doc.css('a').first.attr('title')).to eq merge.title end it 'escapes the title attribute' do @@ -58,7 +58,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 9424f2363e1c4db019334dc28b5547fb1037e8f8..7419863d848051b524b89e7ad3b23b94a11f6e90 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -20,7 +20,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Milestone #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 5068ddd7faa5c9f8537830c88617b8d043360e5d..9b92d1a392624f18ec7dfbcd10f59bc589a27a79 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -39,7 +39,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" + expect(doc.css('a').first.attr('title')).to eq snippet.title end it 'escapes the title attribute' do @@ -51,7 +51,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 108b36a97cc8e81cf53a05b14ded6c752cdcddd6..fdbdb21eac10e92edf6853c2dae94e1edac27188 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -104,7 +104,7 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip' end it 'supports an :only_path context' do diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index ac9c66e2663bb9f07d5de7d40af132850588848a..9095d2b1345b1a546cb81eb751336999caf8edb4 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s - expect(Ability.abilities).not_to receive(:allowed?) + expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end @@ -39,7 +39,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(true) @@ -57,7 +57,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(false) @@ -221,7 +221,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'delegates the permissions check to the Ability class' do user = double(:user) - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, project) subject.can?(user, :read_project, project) diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 9a82891297d3f2e10b3e6fd8379ff5f1ede383f2..4e7f82a6e0933952e4c260dd266591f64ad8c046 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -82,7 +82,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do end it 'returns the nodes if the user can read the group' do - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_group, group). and_return(true) @@ -90,7 +90,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do end it 'returns an empty Array if the user can not read the group' do - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_group, group). and_return(false) @@ -103,7 +103,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s - expect(Ability.abilities).not_to receive(:allowed?) + expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end @@ -113,7 +113,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(true) @@ -125,7 +125,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(false) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index b0772cad3123cf6ec9d78e34526cdbc263ce4570..7c23e02d05af9d4af91cad4b37376e5c69757841 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -7,7 +7,8 @@ describe Gitlab::Auth, lib: true do it 'recognizes CI' do token = '123' project = create(:empty_project) - project.update_attributes(runners_token: token, builds_enabled: true) + project.update_attributes(runners_token: token) + ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb similarity index 66% rename from spec/lib/gitlab/ci/config/node/hidden_job_spec.rb rename to spec/lib/gitlab/ci/config/node/hidden_spec.rb index cc44e2cc05448e3c3b9e4b1da069e609c1fb8ec7..61e2a554419a361762b5755245b8f9d8e828e676 100644 --- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/hidden_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::HiddenJob do +describe Gitlab::Ci::Config::Node::Hidden do let(:entry) { described_class.new(config) } describe 'validations' do context 'when entry config value is correct' do - let(:config) { { image: 'ruby:2.2' } } + let(:config) { [:some, :array] } describe '#value' do it 'returns key value' do - expect(entry.value).to eq(image: 'ruby:2.2') + expect(entry.value).to eq [:some, :array] end end @@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do end context 'when entry value is not correct' do - context 'incorrect config value type' do - let(:config) { ['incorrect'] } - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'hidden job config should be a hash' - end - end - end - context 'when config is empty' do let(:config) { {} } diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index b8d9c70479cc1a29e95deb05113565840c3876ba..ae2c88aac3706c216a37c3061745ce094bfdc420 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -74,7 +74,7 @@ describe Gitlab::Ci::Config::Node::Jobs do expect(entry.descendants.first(2)) .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) expect(entry.descendants.last) - .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob) + .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden) end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index b7c3bc4e1a74983c75c024262d4659597d61d1cf..3fb8de8154581e28ff7d74e58f630d2498d338a4 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do describe '#execute' do context 'when an error occurs' do - let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) } + let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index 0e7ffbe9b8eb3dfcd578729e3df2f7a612197c32..d60c4111e99faa6357c0a09e7f1448bfb5e8bdf9 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -48,8 +48,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when issue is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -62,7 +61,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(issue.attributes).to eq(expected) diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb index 5a421e505811167bec0f3d2cb1d79c1aad429666..09337c99a07952c65ad85d34e2df2d6ce7cd9d81 100644 --- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -40,8 +40,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end context 'when milestone is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -52,7 +51,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do state: 'closed', due_date: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(formatter.attributes).to eq(expected) diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb index 0f363b8b0aaa6db109ce6d01ebfe39de5a1689bb..014ee462e5c25b2e6d699844353542d6d8da3144 100644 --- a/spec/lib/gitlab/github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -2,33 +2,59 @@ require 'spec_helper' describe Gitlab::GithubImport::ProjectCreator, lib: true do let(:user) { create(:user) } + let(:namespace) { create(:group, owner: user) } + let(:repo) do OpenStruct.new( login: 'vim', name: 'vim', - private: true, full_name: 'asd/vim', - clone_url: "https://gitlab.com/asd/vim.git", - owner: OpenStruct.new(login: "john") + clone_url: 'https://gitlab.com/asd/vim.git' ) end - let(:namespace) { create(:group, owner: user) } - let(:token) { "asdffg" } - let(:access_params) { { github_access_token: token } } + + subject(:service) { described_class.new(repo, namespace, user, github_access_token: 'asdffg') } before do namespace.add_owner(user) + allow_any_instance_of(Project).to receive(:add_import_job) end - it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) + describe '#execute' do + it 'creates a project' do + expect { service.execute }.to change(Project, :count).by(1) + end + + it 'handle GitHub credentials' do + project = service.execute + + expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git') + expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git') + expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil) + end + + context 'when Github project is private' do + it 'sets project visibility to private' do + repo.private = true + + project = service.execute + + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end + + context 'when Github project is public' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets project visibility to the default project visibility' do + repo.private = false - project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user, access_params) - project = project_creator.execute + project = service.execute - expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") - expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git") - expect(project.import_data.credentials).to eq(user: "asdffg", password: nil) - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index b667abf063d65e088307602081637dd8a7c13878..edfc6ad81c68ef50eae70df03a2f0cd2f69877e4 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -62,8 +62,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when pull request is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -81,7 +80,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(pull_request.attributes).to eq(expected) @@ -108,7 +107,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: merged_at + updated_at: updated_at } expect(pull_request.attributes).to eq(expected) diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index cbbf98dca94f36490dc0e91d5b2c7cb32f17e859..5114f9c55e11db98e831e0a726537aa22d734c1e 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -1,9 +1,5 @@ { "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "snippets_enabled": false, "visibility_level": 10, "archived": false, "issues": [ @@ -7307,4 +7303,4 @@ "protected_branches": [ ] -} \ No newline at end of file +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 4d857945fdef667650806af7b437f344637b02c1..a07ef279e6825ee3d1bab07f34338dd2c729016f 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let(:project) { create(:empty_project, name: 'project', path: 'project') } + let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restored_project_json) { project_tree_restorer.restore } @@ -18,6 +18,17 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(restored_project_json).to be true end + it 'restore correct project features' do + restored_project_json + project = Project.find_by_path('project') + + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED) + end + it 'creates a valid pipeline note' do restored_project_json diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 3a86a4ce07c826aea84bb60cefca3a5c3552a78f..d891c2d0cc651ae8571526a7d012e0b71b80e6c3 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -111,6 +111,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty end + it 'has project feature' do + project_feature = saved_project_json['project_feature'] + expect(project_feature).not_to be_empty + expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED) + expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED) + expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE) + end + it 'does not complain about non UTF-8 characters in MR diffs' do ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") @@ -154,6 +162,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE) + project end diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index b6dec41d218f095fc04cb4daf779c45cc7dd89b1..3ceb1e7e803216aab16c23222e6084b8ee8e9e14 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -32,6 +32,12 @@ describe Gitlab::ImportExport::Reader, lib: true do expect(described_class.new(shared: shared).project_tree).to match(include: [:issues]) end + it 'generates the correct hash for a single project feature relation' do + setup_yaml(project_tree: [:project_feature]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature]) + end + it 'generates the correct hash for a multiple project relation' do setup_yaml(project_tree: [:issues, :snippets]) diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index aa3b2bbf47140e7f1e4895eae53ca92487205b02..1bdf005c8237dbaf87f9f8167bb2667dcccc4e5c 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -171,70 +171,6 @@ 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 @@ -282,4 +218,17 @@ describe Ability, lib: true do end end end + + describe '.project_disabled_features_rules' do + let(:project) { create(:project, wiki_access_level: ProjectFeature::DISABLED) } + + subject { described_class.allowed(project.owner, project) } + + context 'wiki named abilities' do + it 'disables wiki abilities if the project has no wiki' do + expect(project).to receive(:has_external_wiki?).and_return(false) + expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) + end + end + end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ee2c3d049843bc5d1f17eee86b6c3ebe4a3cdc73..c45c2635cf4afc91c24f31c8c93053bfc2bfe69b 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -948,15 +948,17 @@ describe Ci::Build, models: true do before { build.run! } it 'returns false' do - expect(build.retryable?).to be false + expect(build).not_to be_retryable end end context 'when build is finished' do - before { build.success! } + before do + build.success! + end it 'returns true' do - expect(build.retryable?).to be true + expect(build).to be_retryable end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 721b20e0cb28d79c62446c6184e140c4e9bc9a26..598df5760017b5e16e0562e03009a86ac4d8c510 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -195,6 +195,36 @@ describe Ci::Pipeline, models: true do end end + context 'with non-empty project' do + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha) + end + + describe '#latest?' do + context 'with latest sha' do + it 'returns true' do + expect(pipeline).to be_latest + end + end + + context 'with not latest sha' do + before do + pipeline.update( + sha: project.commit("#{project.default_branch}~1").sha) + end + + it 'returns false' do + expect(pipeline).not_to be_latest + end + end + end + end + describe '#manual_actions' do subject { pipeline.manual_actions } diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 384a38ebc6914187ff37cb12be36b73984b844bf..c41359b55a3b5224d3087afe2df2f510b00d6e1b 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -76,16 +76,6 @@ describe CommitRange, models: true do end end - describe '#reference_title' do - it 'returns the correct String for three-dot ranges' do - expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}" - end - - it 'returns the correct String for two-dot ranges' do - expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}" - end - end - describe '#to_param' do it 'includes the correct keys' do expect(range.to_param.keys).to eq %i(from to) diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index a371c4a18a9255b4966978c234b1a0fb2e2ac255..de791abdf3dbc66b26059b37db419a925a61922b 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -45,4 +45,14 @@ describe Issue, "Awardable" do expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1) end end + + describe 'querying award_emoji on an Awardable' do + let(:issue) { create(:issue) } + + it 'sorts in ascending fashion' do + create_list(:award_emoji, 3, awardable: issue) + + expect(issue.award_emoji).to eq issue.award_emoji.sort_by(&:id) + end + end end diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5363aea4d22cc5d642908523fc60d0918514048f --- /dev/null +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe ProjectFeaturesCompatibility do + let(:project) { create(:project) } + let(:features) { %w(issues wiki builds merge_requests snippets) } + + # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table + # All those fields got moved to a new table called project_feature and are now integers instead of booleans + # This spec tests if the described concern makes sure parameters received by the API are correctly parsed to the new table + # So we can keep it compatible + + it "converts fields from 'true' to ProjectFeature::ENABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, "true") + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) + end + end + + it "converts fields from 'false' to ProjectFeature::DISABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, "false") + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) + end + end +end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 913d74645a7fd8ac04eb78a2358c0fa61e24c073..be57957b569dcb456d8eed65feb5e73c20d2308c 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -71,9 +71,6 @@ describe ProjectMember, models: true do describe :import_team do before do - @abilities = Six.new - @abilities << Ability - @project_1 = create :project @project_2 = create :project @@ -92,8 +89,8 @@ describe ProjectMember, models: true do it { expect(@project_2.users).to include(@user_1) } it { expect(@project_2.users).to include(@user_2) } - it { expect(@abilities.allowed?(@user_1, :create_project, @project_2)).to be_truthy } - it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy } + it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy } + it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy } end describe 'project 1 should not be changed' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d67f71bbb9c2dc6320311eabae482d8b3ef6cd67..5bf3b8e609e68e5a80d0ac86335f85de0a410d9c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -477,8 +477,8 @@ describe MergeRequest, models: true do allow(subject).to receive(:diff_head_sha).and_return('123abc') - expect(subject.source_project).to receive(:pipeline). - with('123abc', 'master'). + expect(subject.source_project).to receive(:pipeline_for). + with('master', '123abc'). and_return(pipeline) expect(subject.pipeline).to eq(pipeline) @@ -962,4 +962,80 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy end end + + describe "#forked_source_project_missing?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "when the fork exists" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it { expect(merge_request.forked_source_project_missing?).to be_falsey } + end + + context "when the source project is the same as the target project" do + let(:merge_request) { create(:merge_request, source_project: project) } + + it { expect(merge_request.forked_source_project_missing?).to be_falsey } + end + + context "when the fork does not exist" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns true" do + unlink_project.execute + merge_request.reload + + expect(merge_request.forked_source_project_missing?).to be_truthy + end + end + end + + describe "#closed_without_fork?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "when the merge request is closed" do + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns false if the fork exist" do + expect(closed_merge_request.closed_without_fork?).to be_falsey + end + + it "returns true if the fork does not exist" do + unlink_project.execute + closed_merge_request.reload + + expect(closed_merge_request.closed_without_fork?).to be_truthy + end + end + + context "when the merge request is open" do + let(:open_merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns false" do + expect(open_merge_request.closed_without_fork?).to be_falsey + end + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 9e8ae07e0b2e108a923b19f08558973bb9997442..e6b6e7c06344d7c1de2dcf9f2b60d9aafff1bb5d 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -85,8 +85,6 @@ describe Note, models: true do @u1 = create(:user) @u2 = create(:user) @u3 = create(:user) - @abilities = Six.new - @abilities << Ability end describe 'read' do @@ -95,9 +93,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) end - it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :read_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :read_note, @p1)).to be_falsey } end describe 'write' do @@ -106,9 +104,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) end - it { expect(@abilities.allowed?(@u1, :create_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :create_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :create_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :create_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :create_note, @p1)).to be_falsey } end describe 'admin' do @@ -118,9 +116,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER) end - it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :admin_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :admin_note, @p1)).to be_falsey } end end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d554a01be557bc3d9fe4afae26d7e5dc0ab33d2 --- /dev/null +++ b/spec/models/project_feature_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +describe ProjectFeature do + let(:project) { create(:project) } + let(:user) { create(:user) } + + describe '#feature_available?' do + let(:features) { %w(issues wiki builds merge_requests snippets) } + + context 'when features are disabled' do + it "returns false" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + expect(project.feature_available?(:issues, user)).to eq(false) + end + end + end + + context 'when features are enabled only for team members' do + it "returns false when user is not a team member" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(false) + end + end + + it "returns true when user is a team member" do + project.team << [user, :developer] + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + + it "returns true when user is a member of project group" do + group = create(:group) + project = create(:project, namespace: group) + group.add_developer(user) + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + + it "returns true if user is an admin" do + user.update_attribute(:admin, true) + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + end + + context 'when feature is enabled for everyone' do + it "returns true" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + end + end + + describe '#*_enabled?' do + let(:features) { %w(wiki builds merge_requests) } + + it "returns false when feature is disabled" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + expect(project.public_send("#{feature}_enabled?")).to eq(false) + end + end + + it "returns true when feature is enabled only for team members" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.public_send("#{feature}_enabled?")).to eq(true) + end + end + + it "returns true when feature is enabled for everyone" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) + expect(project.public_send("#{feature}_enabled?")).to eq(true) + end + end + end +end diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb deleted file mode 100644 index 36379074ea0b522cd5d794972b730ae53c59b771..0000000000000000000000000000000000000000 --- a/spec/models/project_security_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'spec_helper' - -describe Project, models: true do - describe 'authorization' do - before do - @p1 = create(:project) - - @u1 = create(:user) - @u2 = create(:user) - @u3 = create(:user) - @u4 = @p1.owner - - @abilities = Six.new - @abilities << Ability - end - - let(:guest_actions) { Ability.project_guest_rules } - let(:report_actions) { Ability.project_report_rules } - let(:dev_actions) { Ability.project_dev_rules } - let(:master_actions) { Ability.project_master_rules } - let(:owner_actions) { Ability.project_owner_rules } - - describe "Non member rules" do - it "denies for non-project users any actions" do - owner_actions.each do |action| - expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey - end - end - end - - describe "Guest Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST) - end - - it "allows for project user any guest actions" do - guest_actions.each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy - end - end - end - - describe "Report Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER) - end - - it "allows for project user any report actions" do - report_actions.each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy - end - end - end - - describe "Developer Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER) - end - - it "denies for developer master-specific actions" do - [dev_actions - report_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project user any dev actions" do - dev_actions.each do |action| - expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy - end - end - end - - describe "Master Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER) - end - - it "denies for developer master-specific actions" do - [master_actions - dev_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project user any master actions" do - master_actions.each do |action| - expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy - end - end - end - - describe "Owner Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER) - end - - it "denies for masters admin-specific actions" do - [owner_actions - master_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project owner any admin actions" do - owner_actions.each do |action| - expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy - end - end - end - end -end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b2baeeb31bbfce9d109accf5c78f409b1d65f033..4a41fafb84d911b4b042b3c1fa9b545a474e0198 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -506,6 +506,18 @@ describe Project, models: true do end end + describe '#has_wiki?' do + let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } + let(:wiki_enabled_project) { build(:project) } + let(:external_wiki_project) { build(:project, has_external_wiki: true) } + + it 'returns true if project is wiki enabled or has external wiki' do + expect(wiki_enabled_project).to have_wiki + expect(external_wiki_project).to have_wiki + expect(no_wiki_project).not_to have_wiki + end + end + describe '#external_wiki' do let(:project) { create(:project) } @@ -685,31 +697,43 @@ describe Project, models: true do end end - describe '#pipeline' do - let(:project) { create :project } - let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } - - subject { project.pipeline(pipeline.sha, 'master') } + describe '#pipeline_for' do + let(:project) { create(:project) } + let!(:pipeline) { create_pipeline } - it { is_expected.to eq(pipeline) } + shared_examples 'giving the correct pipeline' do + it { is_expected.to eq(pipeline) } - context 'return latest' do - let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' } + context 'return latest' do + let!(:pipeline2) { create_pipeline } - before do - pipeline - pipeline2 + it { is_expected.to eq(pipeline2) } end + end + + context 'with explicit sha' do + subject { project.pipeline_for('master', pipeline.sha) } + + it_behaves_like 'giving the correct pipeline' + end - it { is_expected.to eq(pipeline2) } + context 'with implicit sha' do + subject { project.pipeline_for('master') } + + it_behaves_like 'giving the correct pipeline' + end + + def create_pipeline + create(:ci_pipeline, + project: project, + ref: 'master', + sha: project.commit('master').sha) end end describe '#builds_enabled' do let(:project) { create :project } - before { project.builds_enabled = true } - subject { project.builds_enabled } it { expect(project.builds_enabled?).to be_truthy } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 1fea50ad42c549df96069ce8c2367749dee3c7ff..812c72c48cb8588cded8d777d5fdc6eb0c2d4423 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -382,6 +382,24 @@ describe Repository, models: true do end end + describe '#find_branch' do + it 'loads a branch with a fresh repo' do + expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original + + 2.times do + expect(repository.find_branch('feature')).not_to be_nil + end + end + + it 'loads a branch with a cached repo' do + expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original + + 2.times do + expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil + end + end + end + describe '#rm_branch' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:blank_sha) { '0000000000000000000000000000000000000000' } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8eb0c5033c977a08f78fec0c178708a15d6b7541..a1770d96f8319799859b8fad82602c718eb0b8b6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1006,8 +1006,7 @@ describe User, models: true do end it 'does not include projects for which issues are disabled' do - project = create(:project) - project.update_attributes(issues_enabled: false) + project = create(:project, issues_access_level: ProjectFeature::DISABLED) expect(user.projects_where_can_admin_issues.to_a).to be_empty expect(user.can?(:admin_issue, project)).to eq(false) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eda1cafd65e46ab12223d4f3945dd4c73b4495a8 --- /dev/null +++ b/spec/policies/project_policy_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe ProjectPolicy, models: true do + let(:project) { create(:empty_project, :public) } + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:dev) { create(:user) } + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + + let(:users_ordered_by_permissions) do + [nil, guest, reporter, dev, master, owner, admin] + end + + let(:users_permissions) do + users_ordered_by_permissions.map { |u| Ability.allowed(u, project).size } + end + + before do + project.team << [guest, :guest] + project.team << [master, :master] + project.team << [dev, :developer] + project.team << [reporter, :reporter] + + group = create(:group) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::MASTER) + group.add_owner(owner) + end + + it 'returns increasing permissions for each level' do + expect(users_permissions).to eq(users_permissions.sort.uniq) + end +end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 73c268c0d1ef482835ee9f08f1d7cdfb98a58e72..981a679188105809ec9cebb55c43c15a867fba47 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -4,7 +4,7 @@ describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project) } - let(:issue) { create(:issue, project: project, author: user) } + let(:issue) { create(:issue, project: project) } let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } @@ -115,6 +115,8 @@ describe API::API, api: true do end describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do + let(:issue2) { create(:issue, project: project, author: user) } + context "on an issue" do it "creates a new award emoji" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' @@ -136,6 +138,12 @@ describe API::API, api: true do expect(response).to have_http_status(401) end + it "returns a 404 error if the user authored issue" do + post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' @@ -155,6 +163,8 @@ describe API::API, api: true do end describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do + let(:note2) { create(:note, project: project, noteable: issue, author: user) } + it 'creates a new award emoji' do expect do post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' @@ -164,6 +174,12 @@ describe API::API, api: true do expect(json_response['user']['username']).to eq(user.username) end + it "it returns 404 error when user authored note" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c9078b28642cd1d44b9675368ffc7152659c40a --- /dev/null +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' + +describe API::BroadcastMessages, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET /broadcast_messages' do + it 'returns a 401 for anonymous users' do + get api('/broadcast_messages') + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api('/broadcast_messages', user) + + expect(response).to have_http_status(403) + end + + it 'returns an Array of BroadcastMessages for admins' do + create(:broadcast_message) + + get api('/broadcast_messages', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + expect(json_response.first.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'GET /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + get api("/broadcast_messages/#{message.id}") + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api("/broadcast_messages/#{message.id}", user) + + expect(response).to have_http_status(403) + end + + it 'returns the specified message for admins' do + get api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq message.id + expect(json_response.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'POST /broadcast_messages' do + it 'returns a 401 for anonymous users' do + post api('/broadcast_messages'), attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + post api('/broadcast_messages', user), attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'requires the `message` parameter' do + attrs = attributes_for(:broadcast_message) + attrs.delete(:message) + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq 'message is missing' + end + + it 'defines sane default start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + post api('/broadcast_messages', admin), message: 'Test message' + + expect(response).to have_http_status(201) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T11:11:12.000Z' + end + end + + it 'accepts a custom background and foreground color' do + attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece') + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(201) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + end + end + + describe 'PUT /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + put api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + put api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'accepts new background and foreground colors' do + attrs = { color: '#000000', font: '#cecece' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + + it 'accepts new start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T13:11:12.000Z' + end + end + + it 'accepts a new message' do + attrs = { message: 'new message' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect { message.reload }.to change { message.message }.to('new message') + end + end + end + + describe 'DELETE /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + delete api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + delete api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + it 'deletes the broadcast message for admins' do + expect { delete api("/broadcast_messages/#{message.id}", admin) } + .to change { BroadcastMessage.count }.by(-1) + end + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 9a17a705b1e18af4a91abb3c7c7dc161824949fd..ee0b61e2ca4a54ec06f01ad6ccfe0fc10ea7e00d 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -15,7 +15,9 @@ describe API::API, api: true do describe 'GET /projects/:id/builds ' do let(:query) { '' } - before { get api("/projects/#{project.id}/builds?#{query}", api_user) } + before do + get api("/projects/#{project.id}/builds?#{query}", api_user) + end context 'authorized user' do it 'returns project builds' do @@ -122,7 +124,9 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id' do - before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}", api_user) + end context 'authorized user' do it 'returns specific build data' do @@ -141,7 +145,9 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/artifacts' do - before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) + end context 'build with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -292,7 +298,9 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/cancel' do - before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) + end context 'authorized user' do context 'user with :update_build persmission' do @@ -323,7 +331,9 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/retry' do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } - before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) + end context 'authorized user' do context 'user with :update_build permission' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 7ca75d776733343d0988c00cb64f5c190c3042af..5b3dc60aba2fcdf515ac066f7e934316a2c25b05 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -95,7 +95,7 @@ describe API::API, api: true do end it "returns status for CI" do - pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline('master', project.repository.commit.sha) pipeline.update(status: 'success') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) @@ -105,7 +105,7 @@ describe API::API, api: true do end it "returns status for CI when pipeline is created" do - project.ensure_pipeline(project.repository.commit.sha, 'master') + project.ensure_pipeline('master', project.repository.commit.sha) get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index b8038fc85a19774feb57e5ae0bd7db5de60d5383..47344a13b5e8fd66adaab19ad0ce8528e49e57a2 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:non_member) { create(:user) } @@ -404,6 +405,7 @@ describe API::API, api: true do expect(json_response['milestone']).to be_a Hash expect(json_response['assignee']).to be_a Hash expect(json_response['author']).to be_a Hash + expect(json_response['confidential']).to be_falsy end it "returns a project issue by id" do @@ -469,13 +471,63 @@ describe API::API, api: true do end describe "POST /projects/:id/issues" do - it "creates a new project issue" do + it 'creates a new project issue' do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, label2' + expect(response).to have_http_status(201) expect(json_response['title']).to eq('new issue') expect(json_response['description']).to be_nil expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['confidential']).to be_falsy + end + + it 'creates a new confidential project issue' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: true + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a new confidential project issue with a different param' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'y' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a public issue when confidential param is false' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: false + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it 'creates a public issue when confidential param is invalid' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'foo' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it "sends notifications for subscribers of newly added labels" do + label = project.labels.first + label.toggle_subscription(user2) + + perform_enqueued_jobs do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: label.title + end + + should_email(user2) end it "returns a 400 bad request if title not given" do @@ -619,6 +671,30 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end + + it 'sets an issue to confidential' do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + confidential: true + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end + + it 'makes a confidential issue public' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: false + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_falsy + end + + it 'does not update a confidential issue with wrong confidential flag' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: 'foo' + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end end end @@ -633,6 +709,18 @@ describe API::API, api: true do expect(json_response['labels']).to eq([label.title]) end + it "sends notifications for subscribers of newly added labels when issue is updated" do + label = create(:label, title: 'foo', color: '#FFAABB', project: project) + label.toggle_subscription(user2) + + perform_enqueued_jobs do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title', labels: label.title + end + + should_email(user2) + end + it 'removes all labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index baff872e28e01be4583ebc6a820d52bc0e13a8a2..a7930c59df9f385f64dc10bd1eed0c601724ad9e 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,7 +9,7 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } @@ -34,6 +34,13 @@ describe API::API, api: true do expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') + expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) + expect(json_response.last['merge_commit_sha']).to be_nil + expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) + expect(json_response.first['title']).to eq(merge_request_merged.title) + expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) + expect(json_response.first['merge_commit_sha']).not_to be_nil + expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) end it "returns an array of all merge_requests" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 63f2467be63e52e05e00374d5b113d6d2b2913d4..28aa56e8644c513953e1d6bf7bf8f7f02710923b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -73,7 +73,7 @@ describe API::API, api: true do end it 'does not include open_issues_count' do - project.update_attributes( { issues_enabled: false } ) + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get api('/projects', user) expect(response.status).to eq 200 @@ -231,8 +231,15 @@ describe API::API, api: true do post api('/projects', user), project project.each_pair do |k, v| + next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k) expect(json_response[k.to_s]).to eq(v) end + + # Check feature permissions attributes + project = Project.find_by_path(project[:path]) + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) end it 'sets a project as public' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 0bbba64a6d581973ddadbfc91354c1e4ba585434..ef73778efa9ef92f92e26cb741db432ffa2ac397 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -605,6 +605,7 @@ describe API::API, api: true do expect(json_response['can_create_project']).to eq(user.can_create_project?) expect(json_response['can_create_group']).to eq(user.can_create_group?) expect(json_response['projects_limit']).to eq(user.projects_limit) + expect(json_response['private_token']).to be_blank end it "returns 401 error if user is unauthenticated" do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index afaf4b7cefbe0578daa2097e7475dddcfc495f86..9ca3b021aa22fca0574b164a32e0995c6fd670d6 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -289,7 +289,8 @@ describe 'Git HTTP requests', lib: true do let(:project) { FactoryGirl.create :empty_project } before do - project.update_attributes(runners_token: token, builds_enabled: true) + project.update_attributes(runners_token: token) + project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) end it "downloads get status 200" do diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index c6172b9cc7d857fde28d4f9d0ff0f1a4337e875c..fc42b534dca7102c2dbc0cff1c4587c4875b577b 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -22,19 +22,20 @@ describe JwtController do context 'when using authorized request' do context 'using CI token' do - let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } + let(:project) { create(:empty_project, runners_token: 'token') } let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } } - subject! { get '/jwt/auth', parameters, headers } - context 'project with enabled CI' do - let(:builds_enabled) { true } - + subject! { get '/jwt/auth', parameters, headers } it { expect(service_class).to have_received(:new).with(project, nil, parameters) } end context 'project with disabled CI' do - let(:builds_enabled) { false } + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + subject! { get '/jwt/auth', parameters, headers } it { expect(response).to have_http_status(403) } end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 4c9b4a8ba422ef6f921b5130557ca82628bcdec2..fcd6521317a0e5b7180d7f6fb081a198443722b6 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -44,6 +44,113 @@ describe 'Git LFS API and storage' do end end + context 'project specific LFS settings' do + let(:project) { create(:empty_project) } + let(:body) do + { + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ], + 'operation' => 'upload' + } + end + let(:authorization) { authorize_user } + + context 'with LFS disabled globally' do + before do + project.team << [user, :master] + allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) + end + + describe 'LFS disabled in project' do + before do + project.update_attribute(:lfs_enabled, false) + end + + it 'responds with a 501 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(501) + end + + it 'responds with a 501 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(501) + end + end + + describe 'LFS enabled in project' do + before do + project.update_attribute(:lfs_enabled, true) + end + + it 'responds with a 501 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(501) + end + + it 'responds with a 501 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(501) + end + end + end + + context 'with LFS enabled globally' do + before do + project.team << [user, :master] + enable_lfs + end + + describe 'LFS disabled in project' do + before do + project.update_attribute(:lfs_enabled, false) + end + + it 'responds with a 403 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(403) + expect(json_response).to include('message' => 'Access forbidden. Check your access level.') + end + + it 'responds with a 403 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(403) + expect(json_response).to include('message' => 'Access forbidden. Check your access level.') + end + end + + describe 'LFS enabled in project' do + before do + project.update_attribute(:lfs_enabled, true) + end + + it 'responds with a 200 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(200) + expect(json_response['objects'].first['size']).to eq(1575078) + end + + it 'responds with a 200 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(200) + end + end + end + end + describe 'deprecated API' do let(:project) { create(:empty_project) } diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e02f0eacc9349a8ac9edcb2702c5db6966aa8fd4 --- /dev/null +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Projects::ArtifactsController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: 'success') + end + + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do + before do + project.team << [user, :developer] + + login_as(user) + end + + def path_from_ref( + ref = pipeline.ref, job = build.name, path = 'browse') + latest_succeeded_namespace_project_artifacts_path( + project.namespace, + project, + [ref, path].join('/'), + job: job) + end + + context 'cannot find the build' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get path_from_ref('TAIL', build.name) + end + + it_behaves_like 'not found' + end + + context 'has no such build' do + before do + get path_from_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + + context 'has no path' do + before do + get path_from_ref(pipeline.sha, build.name, '') + end + + it_behaves_like 'not found' + end + end + + context 'found the build and redirect' do + shared_examples 'redirect to the build' do + it 'redirects' do + path = browse_namespace_project_build_artifacts_path( + project.namespace, + project, + build) + + expect(response).to redirect_to(path) + end + end + + context 'with regular branch' do + before do + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get path_from_ref('master') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name containing slash' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name and path containing slashes' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome', build.name, 'file/README.md') + end + + it 'redirects' do + path = file_namespace_project_build_artifacts_path( + project.namespace, + project, + build, + 'README.md') + + expect(response).to redirect_to(path) + end + end + end + end +end diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index 5e7e145065ed43dd99dd37bc015f9f71a13b655c..90764b86b16edf2ee5fc1d9dbea3a7f92d21caa3 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -5,7 +5,7 @@ describe Boards::Lists::CreateService, services: true do let(:project) { create(:project_with_board) } let(:board) { project.board } let(:user) { create(:user) } - let(:label) { create(:label, name: 'in-progress') } + let(:label) { create(:label, project: project, name: 'in-progress') } subject(:service) { described_class.new(project, user, label_id: label.id) } @@ -50,5 +50,14 @@ describe Boards::Lists::CreateService, services: true do expect(list2.reload.position).to eq 1 end end + + context 'when provided label does not belongs to the project' do + it 'raises an error' do + label = create(:label, name: 'in-development') + service = described_class.new(project, user, label_id: label.id) + + expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end end end diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index c931c3e4829d2e32bdf5bc0a55ae967fc69c2887..b3e0a7b9b58b19fd4eee1f222083b6e5d37f5a93 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') } + let(:pipeline) { project.ensure_pipeline('master', commit_sha) } let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } describe '#execute' do diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 026d0ca65340146400d3c9f1a43f244eb44a1bf4..1e21a32a062934b6b4da330812ab7261a2f4aff2 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -151,6 +151,25 @@ module Ci it { expect(build.runner).to eq(specific_runner) } end end + + context 'disallow when builds are disabled' do + before do + project.update(shared_runners_enabled: true) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + context 'and uses shared runner' do + let(:build) { service.execute(shared_runner) } + + it { expect(build).to be_nil } + end + + context 'and uses specific runner' do + let(:build) { service.execute(specific_runner) } + + it { expect(build).to be_nil } + end + end end end end diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 8a4b76367e32edce78979525fb064e2abf4197a1..3a71776e81f61ab99cd4a36aa5895017911d1619 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -50,7 +50,7 @@ describe MergeRequests::GetUrlsService do let(:changes) { new_branch_changes } before do - project.merge_requests_enabled = false + project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::DISABLED) end it_behaves_like 'no_merge_request_url' diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d71932458fa049a80140f6337734b7fd6fe16c8d --- /dev/null +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe MergeRequests::ResolveService do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:fork_project) do + create(:forked_project_with_submodules) do |fork_project| + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + end + end + + let(:merge_request) do + create(:merge_request, + source_branch: 'conflict-resolvable', source_project: project, + target_branch: 'conflict-start') + end + + let(:merge_request_from_fork) do + create(:merge_request, + source_branch: 'conflict-resolvable-fork', source_project: fork_project, + target_branch: 'conflict-start', target_project: project) + end + + describe '#execute' do + context 'with valid params' do + let(:params) do + { + sections: { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + }, + commit_message: 'This is a commit message!' + } + end + + context 'when the source and target project are the same' do + before do + MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + end + + it 'creates a commit with the message' do + expect(merge_request.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request.source_branch_head.parents.map(&:id)). + to eq(['1450cd639e0bc6721eb02800169e464f212cde06', + '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + end + end + + context 'when the source project is a fork and does not contain the HEAD of the target branch' do + let!(:target_head) do + project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false) + end + + before do + MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork) + end + + it 'creates a commit with the message' do + expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request_from_fork.source_branch_head.parents.map(&:id)). + to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', + target_head]) + end + end + end + + context 'when a resolution is missing' do + let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingResolution error' do + expect { service.execute(merge_request) }. + to raise_error(Gitlab::Conflict::File::MissingResolution) + end + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index bbced59ff023b1c36fbdbbfbd7600e7af0bb1d0e..3ea1273abc3ff3875a83b8b8c06ce71e87dad6a9 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do context 'wiki_enabled false does not create wiki repository directory' do before do - @opts.merge!(wiki_enabled: false) + @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } }) @project = create_project(@user, @opts) @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end @@ -85,7 +85,7 @@ describe Projects::CreateService, services: true do context 'global builds_enabled false does not enable CI by default' do before do - @opts.merge!(builds_enabled: false) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) end it { is_expected.to be_falsey } @@ -93,7 +93,7 @@ describe Projects::CreateService, services: true do context 'global builds_enabled true does enable CI by default' do before do - @opts.merge!(builds_enabled: true) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end it { is_expected.to be_truthy } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c144cd85487f250be31227711bf6be927f853baf..02b2b3ca101f4ce3d2a0ade7c6fce7ba08267a12 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,9 +26,9 @@ RSpec.configure do |config| config.verbose_retry = true config.display_try_failure_messages = true - config.include Devise::TestHelpers, type: :controller - config.include LoginHelpers, type: :feature - config.include LoginHelpers, type: :request + config.include Devise::TestHelpers, type: :controller + config.include Warden::Test::Helpers, type: :request + config.include LoginHelpers, type: :feature config.include StubConfiguration config.include EmailHelpers config.include TestEnv diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index 927c72c74098f000ea1cd0be098009ba79da13f4..201614e45a4d1d092ea62785fc688820f4a7db27 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -3,30 +3,57 @@ # Requires a context containing: # subject { Issue or MergeRequest } shared_examples 'a Taskable' do - before do - subject.description = <<-EOT.strip_heredoc - * [ ] Task 1 - * [x] Task 2 - * [x] Task 3 - * [ ] Task 4 - * [ ] Task 5 - EOT + describe 'with multiple tasks' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + * [x] Task 2 + * [x] Task 3 + * [ ] Task 4 + * [ ] Task 5 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('2 of') + expect(subject.task_status).to match('5 tasks completed') + end + + describe '#tasks?' do + it 'returns true when object has tasks' do + expect(subject.tasks?).to eq true + end + + it 'returns false when object has no tasks' do + subject.description = 'Now I have no tasks' + expect(subject.tasks?).to eq false + end + end end - it 'returns the correct task status' do - expect(subject.task_status).to match('5 tasks') - expect(subject.task_status).to match('2 completed') - expect(subject.task_status).to match('3 remaining') + describe 'with an incomplete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('0 of') + expect(subject.task_status).to match('1 task completed') + end end - describe '#tasks?' do - it 'returns true when object has tasks' do - expect(subject.tasks?).to eq true + describe 'with a complete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [x] Task 1 + EOT end - it 'returns false when object has no tasks' do - subject.description = 'Now I have no tasks' - expect(subject.tasks?).to eq false + it 'returns the correct task status' do + expect(subject.task_status).to match('1 of') + expect(subject.task_status).to match('1 task completed') end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index c7a45fc4ff9a62681bd9b548e5bfc70f3f9fafcf..0097dbf8fadc14b80a6961db6455f494b4f1cb81 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -6,7 +6,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'empty-branch' => '7efb185', - 'ends-with.json' => '98b0d8b3', + 'ends-with.json' => '98b0d8b', 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', @@ -37,9 +37,10 @@ module TestEnv # need to keep all the branches in sync. # We currently only need a subset of the branches FORKED_BRANCH_SHA = { - 'add-submodule-version-bump' => '3f547c08', - 'master' => '5937ac0', - 'remove-submodule' => '2a33e0c0' + 'add-submodule-version-bump' => '3f547c0', + 'master' => '5937ac0', + 'remove-submodule' => '2a33e0c', + 'conflict-resolvable-fork' => '404fa3f' } # Test environment @@ -117,22 +118,7 @@ module TestEnv system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path})) end - Dir.chdir(repo_path) do - branch_sha.each do |branch, sha| - # Try to reset without fetching to avoid using the network. - reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) - unless system(*reset) - if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) - unless system(*reset) - raise 'The fetched test seed '\ - 'does not contain the required revision.' - end - else - raise 'Could not fetch test seed repository.' - end - end - end - end + set_repo_refs(repo_path, branch_sha) # We must copy bare repositories because we will push to them. system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare})) @@ -144,6 +130,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, BRANCH_SHA) end def repos_path @@ -160,6 +147,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, FORKED_BRANCH_SHA) end # When no cached assets exist, manually hit the root path to create them @@ -209,4 +197,23 @@ module TestEnv def git_env { 'GIT_TEMPLATE_DIR' => '' } end + + def set_repo_refs(repo_path, branch_sha) + Dir.chdir(repo_path) do + branch_sha.each do |branch, sha| + # Try to reset without fetching to avoid using the network. + reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) + unless system(*reset) + if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) + unless system(*reset) + raise 'The fetched test seed '\ + 'does not contain the required revision.' + end + else + raise 'Could not fetch test seed repository.' + end + end + end + end + end end diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..31bbb1506983a3f1105d374ed1d547cb528e4c4c --- /dev/null +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'projects/merge_requests/edit.html.haml' do + include Devise::TestHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project, + author: user) + end + + before do + assign(:project, project) + assign(:merge_request, closed_merge_request) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:current_user) + .and_return(User.find(closed_merge_request.author_id)) + end + + context 'when a merge request without fork' do + it "shows editable fields" do + unlink_project.execute + closed_merge_request.reload + + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false) + end + end + + context 'when a merge request with an existing source project is closed' do + it "shows editable fields" do + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).to have_selector('#merge_request_target_branch', visible: false) + end + end +end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fe0780e72df75b485e870a8874f744b64f1f269b --- /dev/null +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'projects/merge_requests/show.html.haml' do + include Devise::TestHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project, + author: user) + end + + before do + assign(:project, project) + assign(:merge_request, closed_merge_request) + assign(:commits_count, 0) + + allow(view).to receive(:can?).and_return(true) + end + + context 'when the merge request is closed' do + it 'shows the "Reopen" button' do + render + + expect(rendered).to have_css('a', visible: true, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + + it 'does not show the "Reopen" button when the source project does not exist' do + unlink_project.execute + closed_merge_request.reload + + render + + expect(rendered).to have_css('a', visible: false, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + end +end diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 05e07789dac62c4361b3b9f7fe03cf013dc8bb54..59cfb2c8e3a92edac50e474116ee10877cdb472b 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do subject { described_class.new } it 'passes when the project has no push events' do - project = create(:project_empty_repo, wiki_enabled: false) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) project.events.destroy_all break_repo(project) @@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'fails if the wiki repository is broken' do - project = create(:project_empty_repo, wiki_enabled: true) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) project.create_wiki # Test sanity: everything should be fine before the wiki repo is broken @@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'skips wikis when disabled' do - project = create(:project_empty_repo, wiki_enabled: false) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) # Make sure the test would fail if the wiki repo was checked break_wiki(project) @@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'creates missing wikis' do - project = create(:project_empty_repo, wiki_enabled: true) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) FileUtils.rm_rf(wiki_path(project)) subject.perform(project.id)