diff options
34 files changed, 583 insertions, 277 deletions
diff --git a/CHANGELOG b/CHANGELOG index 678e93cfc6c..f5416434ab1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,7 +24,9 @@ v 8.11.0 (unreleased) - Remove unused images (ClemMakesApps) - Limit git rev-list output count to one in forced push check - Clean up unused routes (Josef Strzibny) + - Fix issue on empty project to allow developers to only push to protected branches if given permission - Add green outline to New Branch button. !5447 (winniehell) + - Optimize generating of cache keys for issues and notes - Improve performance of syntax highlighting Markdown code blocks - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects - Remove delay when hitting "Reply..." button on page with a lot of discussions @@ -39,6 +41,7 @@ v 8.11.0 (unreleased) - Include old revision in merge request update hooks (Ben Boeckel) - Add build event color in HipChat messages (David Eisner) - Make fork counter always clickable. !5463 (winniehell) + - Document that webhook secret token is sent in X-Gitlab-Token HTTP header !5664 (lycoperdon) - Gitlab::Highlight is now instrumented - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333 - The overhead of instrumented method calls has been reduced diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f522c1e39a..d94673e82ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -336,6 +336,10 @@ request is as follows: 1. If your code creates new files on disk please read the [shared files guidelines](doc/development/shared_files.md). 1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/). +1. If your merge request adds one or more migrations, make sure to execute all + 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. 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/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 9e6901962c6..20f2b1d69b5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -173,8 +173,8 @@ new Search(); break; case 'projects:protected_branches:index': - new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true); - new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false); + new gl.ProtectedBranchCreate(); + new gl.ProtectedBranchEditList(); break; } switch (path.first()) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index cc7e422fd89..d3394fae3f9 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -607,7 +607,7 @@ return this.dropdown.before($input); }; - GitLabDropdown.prototype.selectRowAtIndex = function(e, index) { + GitLabDropdown.prototype.selectRowAtIndex = function(index) { var $el, selector; selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a"; if (this.dropdown.find(".dropdown-toggle-page").length) { @@ -615,8 +615,6 @@ } $el = $(selector, this.dropdown); if ($el.length) { - e.preventDefault(); - e.stopImmediatePropagation(); return $el.first().trigger('click'); } }; @@ -653,7 +651,7 @@ return false; } if (currentKeyCode === 13 && currentIndex !== -1) { - return _this.selectRowAtIndex(e, $('.is-focused', _this.dropdown).closest('li').index() - 1); + return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1); } }; })(this)); diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 new file mode 100644 index 00000000000..2fbb088fa04 --- /dev/null +++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 @@ -0,0 +1,24 @@ +(global => { + global.gl = global.gl ||Â {}; + + gl.ProtectedBranchAccessDropdown = class { + constructor(options) { + const { $dropdown, data, onSelect } = options; + + $dropdown.glDropdown({ + data: data, + selectable: true, + inputId: $dropdown.data('input-id'), + fieldName: $dropdown.data('field-name'), + toggleLabel(item) { + return item.text; + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + } + }); + } + } + +})(window); diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 new file mode 100644 index 00000000000..00e20a03b04 --- /dev/null +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -0,0 +1,56 @@ +(global => { + global.gl = global.gl ||Â {}; + + gl.ProtectedBranchCreate = class { + constructor() { + this.$wrap = this.$form = $('#new_protected_branch'); + this.buildDropdowns(); + } + + buildDropdowns() { + const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); + + // Allowed to Merge dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: $allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelectCallback + }); + + // Allowed to Push dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: $allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelectCallback + }); + + // Select default + $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); + $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); + + // Protected branch dropdown + new ProtectedBranchDropdown({ + $dropdown: this.$wrap.find('.js-protected-branch-select'), + onSelect: this.onSelectCallback + }); + } + + // This will run after clicked callback + onSelect() { + + // Enable submit button + const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); + const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); + + if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ + this.$form.find('input[type="submit"]').removeAttr('disabled'); + } + } + } + +})(window); diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6 new file mode 100644 index 00000000000..6738dc8862d --- /dev/null +++ b/app/assets/javascripts/protected_branch_dropdown.js.es6 @@ -0,0 +1,75 @@ +class ProtectedBranchDropdown { + constructor(options) { + this.onSelect = options.onSelect; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.$dropdownFooter.addClass('hidden'); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getProtectedBranches.bind(this), + filterable: true, + remote: false, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; + }, + fieldName: 'protected_branch[name]', + text(protectedBranch) { + return _.escape(protectedBranch.title); + }, + id(protectedBranch) { + return _.escape(protectedBranch.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (item, $el, e) => { + e.preventDefault(); + this.onSelect(); + } + }); + } + + bindEvents() { + this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard() { + this.$dropdown.data('glDropdown').remote.execute(); + this.$dropdown.data('glDropdown').selectRowAtIndex(0); + } + + getProtectedBranches(term, callback) { + if (this.selectedBranch) { + callback(gon.open_branches.concat(this.selectedBranch)); + } else { + callback(gon.open_branches); + } + } + + toggleCreateNewButton(branchName) { + this.selectedBranch = { + title: branchName, + id: branchName, + text: branchName + }; + + if (branchName) { + this.$dropdownContainer + .find('.create-new-protected-branch code') + .text(branchName); + } + + this.$dropdownFooter.toggleClass('hidden', !branchName); + } +} diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 new file mode 100644 index 00000000000..8d42e268ebc --- /dev/null +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -0,0 +1,61 @@ +(global => { + global.gl = global.gl ||Â {}; + + gl.ProtectedBranchEdit = class { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + this.buildDropdowns(); + } + + buildDropdowns() { + + // Allowed to merge dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelect.bind(this) + }); + + // Allowed to push dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + } + + onSelect() { + const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + id: this.$wrap.data('banchId'), + protected_branch: { + merge_access_level_attributes: { + access_level: $allowedToMergeInput.val() + }, + push_access_level_attributes: { + access_level: $allowedToPushInput.val() + } + } + }, + success: () => { + this.$wrap.effect('highlight'); + }, + error() { + $.scrollTo(0); + new Flash('Failed to update branch!'); + } + }); + } + } + +})(window); diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branch_edit_list.js.es6 new file mode 100644 index 00000000000..9ff0fd12c76 --- /dev/null +++ b/app/assets/javascripts/protected_branch_edit_list.js.es6 @@ -0,0 +1,17 @@ +(global => { + global.gl = global.gl ||Â {}; + + gl.ProtectedBranchEditList = class { + constructor()Â { + this.$wrap = $('.protected-branches-list'); + + // Build edit forms + this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { + new gl.ProtectedBranchEdit({ + $wrap: $(el) + }); + }); + } + } + +})(window); diff --git a/app/assets/javascripts/protected_branch_select.js b/app/assets/javascripts/protected_branch_select.js deleted file mode 100644 index 3a47fc972dc..00000000000 --- a/app/assets/javascripts/protected_branch_select.js +++ /dev/null @@ -1,72 +0,0 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.ProtectedBranchSelect = (function() { - function ProtectedBranchSelect(currentProject) { - this.toggleCreateNewButton = bind(this.toggleCreateNewButton, this); - this.getProtectedBranches = bind(this.getProtectedBranches, this); - $('.dropdown-footer').hide(); - this.dropdown = $('.js-protected-branch-select').glDropdown({ - data: this.getProtectedBranches, - filterable: true, - remote: false, - search: { - fields: ['title'] - }, - selectable: true, - toggleLabel: function(selected) { - if (selected && 'id' in selected) { - return selected.title; - } else { - return 'Protected Branch'; - } - }, - fieldName: 'protected_branch[name]', - text: function(protected_branch) { - return _.escape(protected_branch.title); - }, - id: function(protected_branch) { - return _.escape(protected_branch.id); - }, - onFilter: this.toggleCreateNewButton, - clicked: function() { - return $('.protect-branch-btn').attr('disabled', false); - } - }); - $('.create-new-protected-branch').on('click', (function(_this) { - return function(event) { - _this.dropdown.data('glDropdown').remote.execute(); - return _this.dropdown.data('glDropdown').selectRowAtIndex(event, 0); - }; - })(this)); - } - - ProtectedBranchSelect.prototype.getProtectedBranches = function(term, callback) { - if (this.selectedBranch) { - return callback(gon.open_branches.concat(this.selectedBranch)); - } else { - return callback(gon.open_branches); - } - }; - - ProtectedBranchSelect.prototype.toggleCreateNewButton = function(branchName) { - this.selectedBranch = { - title: branchName, - id: branchName, - text: branchName - }; - if (branchName === '') { - $('.protected-branch-select-footer-list').addClass('hidden'); - return $('.dropdown-footer').hide(); - } else { - $('.create-new-protected-branch').text("Create Protected Branch: " + branchName); - $('.protected-branch-select-footer-list').removeClass('hidden'); - return $('.dropdown-footer').show(); - } - }; - - return ProtectedBranchSelect; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/protected_branches_access_select.js.es6 b/app/assets/javascripts/protected_branches_access_select.js.es6 deleted file mode 100644 index e98312bbf37..00000000000 --- a/app/assets/javascripts/protected_branches_access_select.js.es6 +++ /dev/null @@ -1,63 +0,0 @@ -class ProtectedBranchesAccessSelect { - constructor(container, saveOnSelect, selectDefault) { - this.container = container; - this.saveOnSelect = saveOnSelect; - - this.container.find(".allowed-to-merge").each((i, element) => { - var fieldName = $(element).data('field-name'); - var dropdown = $(element).glDropdown({ - data: gon.merge_access_levels, - selectable: true, - fieldName: fieldName, - clicked: _.chain(this.onSelect).partial(element).bind(this).value() - }); - - if (selectDefault) { - dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); - } - }); - - - this.container.find(".allowed-to-push").each((i, element) => { - var fieldName = $(element).data('field-name'); - var dropdown = $(element).glDropdown({ - data: gon.push_access_levels, - selectable: true, - fieldName: fieldName, - clicked: _.chain(this.onSelect).partial(element).bind(this).value() - }); - - if (selectDefault) { - dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); - } - }); - } - - onSelect(dropdown, selected, element, e) { - $(dropdown).find('.dropdown-toggle-text').text(selected.text); - if (this.saveOnSelect) { - return $.ajax({ - type: "POST", - url: $(dropdown).data('url'), - dataType: "json", - data: { - _method: 'PATCH', - id: $(dropdown).data('id'), - protected_branch: { - ["" + ($(dropdown).data('type')) + "_attributes"]: { - "access_level": selected.id - } - } - }, - success: function() { - var row; - row = $(e.target); - return row.closest('tr').effect('highlight'); - }, - error: function() { - return new Flash("Failed to update branch!", "alert"); - } - }); - } - } -} diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 4af2a214e12..65d362e072c 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -13,14 +13,15 @@ } $('.js-user-search').each((function(_this) { return function(i, dropdown) { + var options = {}; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; $dropdown = $(dropdown); - _this.projectId = $dropdown.data('project-id'); - _this.showCurrentUser = $dropdown.data('current-user'); + options.projectId = $dropdown.data('project-id'); + options.showCurrentUser = $dropdown.data('current-user'); showNullUser = $dropdown.data('null-user'); showAnyUser = $dropdown.data('any-user'); firstUser = $dropdown.data('first-user'); - _this.authorId = $dropdown.data('author-id'); + options.authorId = $dropdown.data('author-id'); selectedId = $dropdown.data('selected'); defaultLabel = $dropdown.data('default-label'); issueURL = $dropdown.data('issueUpdate'); @@ -75,7 +76,7 @@ data: function(term, callback) { var isAuthorFilter; isAuthorFilter = $('.js-author-search'); - return _this.users(term, function(users) { + return _this.users(term, options, function(users) { var anyUser, index, j, len, name, obj, showDivider; if (term.length === 0) { showDivider = 0; @@ -185,11 +186,14 @@ $('.ajax-users-select').each((function(_this) { return function(i, select) { var firstUser, showAnyUser, showEmailUser, showNullUser; - _this.projectId = $(select).data('project-id'); - _this.groupId = $(select).data('group-id'); - _this.showCurrentUser = $(select).data('current-user'); - _this.authorId = $(select).data('author-id'); - _this.skipUsers = $(select).data('skip-users'); + var options = {}; + options.skipLdap = $(select).hasClass('skip_ldap'); + options.projectId = $(select).data('project-id'); + options.groupId = $(select).data('group-id'); + options.showCurrentUser = $(select).data('current-user'); + options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches'); + options.authorId = $(select).data('author-id'); + options.skipUsers = $(select).data('skip-users'); showNullUser = $(select).data('null-user'); showAnyUser = $(select).data('any-user'); showEmailUser = $(select).data('email-user'); @@ -199,7 +203,7 @@ multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - return _this.users(query.term, function(users) { + return _this.users(query.term, options, function(users) { var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref; data = { results: users @@ -309,7 +313,7 @@ }); }; - UsersSelect.prototype.users = function(query, callback) { + UsersSelect.prototype.users = function(query, options, callback) { var url; url = this.buildUrl(this.usersPath); return $.ajax({ @@ -318,11 +322,13 @@ search: query, per_page: 20, active: true, - project_id: this.projectId, - group_id: this.groupId, - current_user: this.showCurrentUser, - author_id: this.authorId, - skip_users: this.skipUsers + project_id: options.projectId || null, + group_id: options.groupId || null, + skip_ldap: options.skipLdap || null, + current_user: options.showCurrentUser || null, + push_code_to_protected_branches: options.pushCodeToProtectedBranches || null, + author_id: options.authorId || null, + skip_users: options.skipUsers || null }, dataType: "json" }).done(function(users) { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index c54eb0d6479..e8eafa15899 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -72,6 +72,14 @@ &.large { width: 200px; } + + &.wide { + width: 100%; + + + .dropdown-select { + width: 100%; + } + } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 874416e1007..c6f30e144fd 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -23,4 +23,9 @@ margin-top: $gl-padding; } } + + .panel-title { + font-size: inherit; + line-height: inherit; + } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d91c8e61165..cf9aa02600d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -656,13 +656,9 @@ pre.light-well { } .new_protected_branch { - .dropdown { - display: inline; - margin-left: 15px; - } - label { - min-width: 120px; + margin-top: 6px; + font-weight: normal; } } @@ -678,6 +674,21 @@ pre.light-well { font-weight: 600; } } + + .settings-message { + margin: 0; + border-radius: 0 0 1px 1px; + padding: 20px 0; + border: none; + } + + .table-bordered { + border-radius: 1px; + + th:not(:last-child), td:not(:last-child) { + border-right: solid 1px transparent; + } + } } .custom-notifications-form { diff --git a/app/models/concerns/faster_cache_keys.rb b/app/models/concerns/faster_cache_keys.rb new file mode 100644 index 00000000000..5b14723fa2d --- /dev/null +++ b/app/models/concerns/faster_cache_keys.rb @@ -0,0 +1,16 @@ +module FasterCacheKeys + # A faster version of Rails' "cache_key" method. + # + # Rails' default "cache_key" method uses all kind of complex logic to figure + # out the cache key. In many cases this complexity and overhead may not be + # needed. + # + # This method does not do any timestamp parsing as this process is quite + # expensive and not needed when generating cache keys. This method also relies + # on the table name instead of the cache namespace name as the latter uses + # complex logic to generate the exact same value (as when using the table + # name) in 99% of the cases. + def cache_key + "#{self.class.table_name}/#{id}-#{read_attribute_before_type_cast(:updated_at)}" + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 11f734cfc6d..d62ffb21467 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base include Sortable include Taskable include Spammable + include FasterCacheKeys DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/app/models/note.rb b/app/models/note.rb index b6b2ac6aa42..ddcd7f9d034 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -5,6 +5,7 @@ class Note < ActiveRecord::Base include Mentionable include Awardable include Importable + include FasterCacheKeys # Attribute containing rendered and redacted Markdown as generated by # Banzai::ObjectRenderer. diff --git a/app/models/project.rb b/app/models/project.rb index a18aef09acd..a667857d058 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -865,10 +865,16 @@ class Project < ActiveRecord::Base # Check if current branch name is marked as protected in the system def protected_branch?(branch_name) + return true if empty_repo? && default_branch_protected? + @protected_branches ||= self.protected_branches.to_a ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end + def user_can_push_to_empty_repo?(user) + !default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER + end + def forked? !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end @@ -1260,6 +1266,11 @@ class Project < ActiveRecord::Base private + 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 + end + def authorized_for_user_by_group?(user, min_access_level) member = user.group_members.find_by(source_id: group) diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 0603a014008..04b19a8c5a7 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,26 +1,28 @@ -%h5.prepend-top-0 - Already Protected (#{@protected_branches.size}) -- if @protected_branches.empty? - %p.settings-message.text-center - No branches are protected, protect a branch with the form above. -- else - - can_admin_project = can?(current_user, :admin_project, @project) +.panel.panel-default.protected-branches-list + - if @protected_branches.empty? + .panel-heading + %h3.panel-title + Protected branch (#{@protected_branches.size}) + %p.settings-message.text-center + There are currently no protected branches, protect a branch with the form above. + - else + - can_admin_project = can?(current_user, :admin_project, @project) - %table.table.protected-branches-list - %colgroup - %col{ width: "20%" } - %col{ width: "30%" } - %col{ width: "25%" } - %col{ width: "25%" } - %thead - %tr - %th Branch - %th Last commit - %th Allowed to merge - %th Allowed to push - - if can_admin_project - %th - %tbody - = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + %table.table.table-bordered + %colgroup + %col{ width: "25%" } + %col{ width: "30%" } + %col{ width: "25%" } + %col{ width: "20%" } + %thead + %tr + %th Protected branch (#{@protected_branches.size}) + %th Last commit + %th Allowed to merge + %th Allowed to push + - if can_admin_project + %th + %tbody + = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } - = paginate @protected_branches, theme: 'gitlab' + = paginate @protected_branches, theme: 'gitlab' diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml new file mode 100644 index 00000000000..85d0c494ba8 --- /dev/null +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -0,0 +1,36 @@ += form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| + .panel.panel-default + .panel-heading + %h3.panel-title + Protect a branch + .panel-body + .form-horizontal + .form-group + = f.label :name, class: 'col-md-2 text-right' do + Branch: + .col-md-10 + = render partial: "dropdown", locals: { f: f } + .help-block + = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') + such as + %code *-stable + or + %code production/* + are supported + .form-group + %label.col-md-2.text-right{ for: 'merge_access_level_attributes' } + Allowed to merge: + .col-md-10 + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-merge wide', + data: { field_name: 'protected_branch[merge_access_level_attributes][access_level]', input_id: 'merge_access_level_attributes' }}) + .form-group + %label.col-md-2.text-right{ for: 'push_access_level_attributes' } + Allowed to push: + .col-md-10 + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-push wide', + data: { field_name: 'protected_branch[push_access_level_attributes][access_level]', input_id: 'push_access_level_attributes' }}) + + .panel-footer + = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml index b803d932e67..a9e27df5a87 100644 --- a/app/views/projects/protected_branches/_dropdown.html.haml +++ b/app/views/projects/protected_branches/_dropdown.html.haml @@ -1,17 +1,15 @@ = f.hidden_field(:name) -= dropdown_tag("Protected Branch", - options: { title: "Pick protected branch", toggle_class: 'js-protected-branch-select js-filter-submit', += dropdown_tag('Select branch or create wildcard', + options: { toggle_class: 'js-protected-branch-select js-filter-submit wide', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches", footer_content: true, data: { show_no: true, show_any: true, show_upcoming: true, selected: params[:protected_branch_name], project_id: @project.try(:id) } }) do - %ul.dropdown-footer-list.hidden.protected-branch-select-footer-list + %ul.dropdown-footer-list %li = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do - Create new - -:javascript - new ProtectedBranchSelect(); + Create wildcard + %code diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 498e412235e..e2e01ee78f8 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -1,5 +1,4 @@ -- url = namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) -%tr +%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } } %td = protected_branch.name - if @project.root_ref?(protected_branch.name) @@ -16,14 +15,14 @@ (branch was removed from repository) %td = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level - = dropdown_tag(protected_branch.merge_access_level.humanize, - options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', - data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "merge_access_level" }}) + = dropdown_tag( (protected_branch.merge_access_level.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container', + data: { field_name: "allowed_to_merge_#{protected_branch.id}" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level - = dropdown_tag(protected_branch.push_access_level.humanize, - options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', - data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "push_access_level" }}) + = dropdown_tag( (protected_branch.push_access_level.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', + data: { field_name: "allowed_to_push_#{protected_branch.id}" }}) - if can_admin_project %td - = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 4efe44c7233..49dcc9a6ba4 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -14,41 +14,7 @@ %li prevent <strong>anyone</strong> from deleting the branch %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}. .col-lg-9 - %h5.prepend-top-0 - Protect a branch - if can? current_user, :admin_project, @project - = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| - = form_errors(@protected_branch) + = render 'create_protected_branch' - .form-group - = f.label :name, "Branch", class: "label-light" - = render partial: "dropdown", locals: { f: f } - %p.help-block - = link_to "Wildcards", help_page_path('user/project/protected_branches', anchor: "wildcard-protected-branches") - such as - %code *-stable - or - %code production/* - are supported. - - .form-group - = hidden_field_tag 'protected_branch[merge_access_level_attributes][access_level]' - = label_tag "Allowed to merge: ", nil, class: "label-light append-bottom-0" - = dropdown_tag("<Make a selection>", - options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', - dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[merge_access_level_attributes][access_level]" }}) - - .form-group - = hidden_field_tag 'protected_branch[push_access_level_attributes][access_level]' - = label_tag "Allowed to push: ", nil, class: "label-light append-bottom-0" - = dropdown_tag("<Make a selection>", - options: { title: "Allowed to push", toggle_class: 'allowed-to-push', - dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[push_access_level_attributes][access_level]" }}) - - - = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true - - %hr = render "branches_list" diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 2585ed9360b..470dac6d75b 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -19,7 +19,7 @@ = f.label :token, "Secret Token", class: 'label-light' = f.text_field :token, class: "form-control", placeholder: '' %p.help-block - Use this token to validate received payloads + Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. .form-group = f.label :url, "Trigger", class: 'label-light' %ul.list-unstyled diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 8559b67af04..d4b28d875cd 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -26,6 +26,10 @@ GitLab webhooks keep in mind the following things: you are writing a low-level hook this is important to remember. - GitLab ignores the HTTP status code returned by your endpoint. +## Secret Token + +If you specify a secret token, it will be sent with the hook request in the `X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify that the request is legitimate. + ## SSL Verification By default, the SSL certificate of the webhook endpoint is verified based on diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 3a69027368f..c55a7fc4d3d 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -30,6 +30,8 @@ module Gitlab return false unless user if project.protected_branch?(ref) + return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) + access_levels = project.protected_branches.matching(ref).map(&:push_access_level) access_levels.any? { |access_level| access_level.check_access(user) } else diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 57734b33a44..3499460c84d 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -11,7 +11,7 @@ feature 'Projected Branches', feature: true, js: true do def set_protected_branch_name(branch_name) find(".js-protected-branch-select").click find(".dropdown-input-field").set(branch_name) - click_on "Create Protected Branch: #{branch_name}" + click_on("Create wildcard #{branch_name}") end describe "explicit protected branches" do @@ -90,7 +90,7 @@ feature 'Projected Branches', feature: true, js: true do visit namespace_project_protected_branches_path(project.namespace, project) set_protected_branch_name('master') within('.new_protected_branch') do - find(".allowed-to-push").click + find(".js-allowed-to-push").click within(".dropdown.open .dropdown-menu") { click_on access_type_name } end click_on "Protect" @@ -107,8 +107,8 @@ feature 'Projected Branches', feature: true, js: true do expect(ProtectedBranch.count).to eq(1) within(".protected-branches-list") do - find(".allowed-to-push").click - within('.dropdown-menu.push') { click_on access_type_name } + find(".js-allowed-to-push").click + within('.js-allowed-to-push-container') { click_on access_type_name } end wait_for_ajax @@ -121,7 +121,7 @@ feature 'Projected Branches', feature: true, js: true do visit namespace_project_protected_branches_path(project.namespace, project) set_protected_branch_name('master') within('.new_protected_branch') do - find(".allowed-to-merge").click + find(".js-allowed-to-merge").click within(".dropdown.open .dropdown-menu") { click_on access_type_name } end click_on "Protect" @@ -138,8 +138,8 @@ feature 'Projected Branches', feature: true, js: true do expect(ProtectedBranch.count).to eq(1) within(".protected-branches-list") do - find(".allowed-to-merge").click - within('.dropdown-menu.merge') { click_on access_type_name } + find(".js-allowed-to-merge").click + within('.js-allowed-to-merge-container') { click_on access_type_name } end wait_for_ajax diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 5bb095366fa..d3c3b800b94 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -9,35 +9,80 @@ describe Gitlab::UserAccess, lib: true do describe 'push to none protected branch' do it 'returns true if user is a master' do project.team << [user, :master] + expect(access.can_push_to_branch?('random_branch')).to be_truthy end it 'returns true if user is a developer' do project.team << [user, :developer] + expect(access.can_push_to_branch?('random_branch')).to be_truthy end it 'returns false if user is a reporter' do project.team << [user, :reporter] + expect(access.can_push_to_branch?('random_branch')).to be_falsey end end + describe 'push to empty project' do + let(:empty_project) { create(:project_empty_repo) } + let(:project_access) { Gitlab::UserAccess.new(user, project: empty_project) } + + it 'returns true if user is master' do + empty_project.team << [user, :master] + + expect(project_access.can_push_to_branch?('master')).to be_truthy + end + + it 'returns false if user is developer and project is fully protected' do + empty_project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) + + expect(project_access.can_push_to_branch?('master')).to be_falsey + end + + it 'returns false if user is developer and it is not allowed to push new commits but can merge into branch' do + empty_project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(project_access.can_push_to_branch?('master')).to be_falsey + end + + it 'returns true if user is developer and project is unprotected' do + empty_project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) + + expect(project_access.can_push_to_branch?('master')).to be_truthy + end + + it 'returns true if user is developer and project grants developers permission' do + empty_project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(project_access.can_push_to_branch?('master')).to be_truthy + end + end + describe 'push to protected branch' do let(:branch) { create :protected_branch, project: project } it 'returns true if user is a master' do project.team << [user, :master] + expect(access.can_push_to_branch?(branch.name)).to be_truthy end it 'returns false if user is a developer' do project.team << [user, :developer] + expect(access.can_push_to_branch?(branch.name)).to be_falsey end it 'returns false if user is a reporter' do project.team << [user, :reporter] + expect(access.can_push_to_branch?(branch.name)).to be_falsey end end @@ -49,16 +94,19 @@ describe Gitlab::UserAccess, lib: true do it 'returns true if user is a master' do project.team << [user, :master] + expect(access.can_push_to_branch?(@branch.name)).to be_truthy end it 'returns true if user is a developer' do project.team << [user, :developer] + expect(access.can_push_to_branch?(@branch.name)).to be_truthy end it 'returns false if user is a reporter' do project.team << [user, :reporter] + expect(access.can_push_to_branch?(@branch.name)).to be_falsey end end @@ -70,19 +118,21 @@ describe Gitlab::UserAccess, lib: true do it 'returns true if user is a master' do project.team << [user, :master] + expect(access.can_merge_to_branch?(@branch.name)).to be_truthy end it 'returns true if user is a developer' do project.team << [user, :developer] + expect(access.can_merge_to_branch?(@branch.name)).to be_truthy end it 'returns false if user is a reporter' do project.team << [user, :reporter] + expect(access.can_merge_to_branch?(@branch.name)).to be_falsey end end - end end diff --git a/spec/models/concerns/faster_cache_keys_spec.rb b/spec/models/concerns/faster_cache_keys_spec.rb new file mode 100644 index 00000000000..8d3f94267fa --- /dev/null +++ b/spec/models/concerns/faster_cache_keys_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe FasterCacheKeys do + describe '#cache_key' do + it 'returns a String' do + # We're using a fixed string here so it's easier to set an expectation for + # the resulting cache key. + time = '2016-08-08 16:39:00+02' + issue = build(:issue, updated_at: time) + issue.extend(described_class) + + expect(issue).to receive(:id).and_return(1) + + expect(issue.cache_key).to eq("issues/1-#{time}") + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e365e4e98b2..567f87b9970 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -69,6 +69,7 @@ describe Project, models: true do it { is_expected.to include_module(Gitlab::ConfigHelper) } it { is_expected.to include_module(Gitlab::ShellAdapter) } it { is_expected.to include_module(Gitlab::VisibilityLevel) } + it { is_expected.to include_module(Gitlab::CurrentSettings) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } end @@ -1070,28 +1071,97 @@ describe Project, models: true do end describe '#protected_branch?' do + context 'existing project' do + let(:project) { create(:project) } + + it 'returns true when the branch matches a protected branch via direct match' do + project.protected_branches.create!(name: 'foo') + + expect(project.protected_branch?('foo')).to eq(true) + end + + it 'returns true when the branch matches a protected branch via wildcard match' do + project.protected_branches.create!(name: 'production/*') + + expect(project.protected_branch?('production/some-branch')).to eq(true) + end + + it 'returns false when the branch does not match a protected branch via direct match' do + expect(project.protected_branch?('foo')).to eq(false) + end + + it 'returns false when the branch does not match a protected branch via wildcard match' do + project.protected_branches.create!(name: 'production/*') + + expect(project.protected_branch?('staging/some-branch')).to eq(false) + end + end + + context "new project" do + let(:project) { create(:empty_project) } + + it 'returns false when default_protected_branch is unprotected' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) + + expect(project.protected_branch?('master')).to be false + end + + it 'returns false when default_protected_branch lets developers push' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(project.protected_branch?('master')).to be false + end + + it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(project.protected_branch?('master')).to be true + end + + it 'returns true when default_branch_protection is in full protection' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) + + expect(project.protected_branch?('master')).to be true + end + end + end + + describe '#user_can_push_to_empty_repo?' do let(:project) { create(:empty_project) } + let(:user) { create(:user) } - it 'returns true when the branch matches a protected branch via direct match' do - project.protected_branches.create!(name: 'foo') + it 'returns false when default_branch_protection is in full protection and user is developer' do + project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) - expect(project.protected_branch?('foo')).to eq(true) + expect(project.user_can_push_to_empty_repo?(user)).to be_falsey end - it 'returns true when the branch matches a protected branch via wildcard match' do - project.protected_branches.create!(name: 'production/*') + it 'returns false when default_branch_protection only lets devs merge and user is dev' do + project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) - expect(project.protected_branch?('production/some-branch')).to eq(true) + expect(project.user_can_push_to_empty_repo?(user)).to be_falsey end - it 'returns false when the branch does not match a protected branch via direct match' do - expect(project.protected_branch?('foo')).to eq(false) + it 'returns true when default_branch_protection lets devs push and user is developer' do + project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(project.user_can_push_to_empty_repo?(user)).to be_truthy + end + + it 'returns true when default_branch_protection is unprotected and user is developer' do + project.team << [user, :developer] + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) + + expect(project.user_can_push_to_empty_repo?(user)).to be_truthy end - it 'returns false when the branch does not match a protected branch via wildcard match' do - project.protected_branches.create!(name: 'production/*') + it 'returns true when user is master' do + project.team << [user, :master] - expect(project.protected_branch?('staging/some-branch')).to eq(false) + expect(project.user_can_push_to_empty_repo?(user)).to be_truthy end end diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore index faa18382a3c..d9960081c98 100644 --- a/vendor/gitignore/Global/VisualStudioCode.gitignore +++ b/vendor/gitignore/Global/VisualStudioCode.gitignore @@ -1,2 +1,4 @@ -.vscode - +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json diff --git a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml index b468d79bcad..908463c9d12 100644 --- a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml @@ -1,25 +1,17 @@ # Full project: https://gitlab.com/pages/hexo -image: python:2.7 - -cache: - paths: - - vendor/ - -test: - stage: test - script: - - pip install hyde - - hyde gen - except: - - master +image: node:4.2.2 pages: - stage: deploy + cache: + paths: + - node_modules/ + script: - - pip install hyde - - hyde gen -d public + - npm install hexo-cli -g + - npm install + - hexo deploy artifacts: paths: - public only: - - master + - master diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml index 16a685ee03d..166f146ee05 100644 --- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml @@ -10,6 +10,9 @@ services: - redis:latest - postgres:latest +variables: + POSTGRES_DB: database_name + # Cache gems in between builds cache: paths: @@ -34,6 +37,9 @@ rspec: - rspec spec rails: + variables: + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB" script: - bundle exec rake db:migrate + - bundle exec rake db:seed - bundle exec rake test |