diff options
author | Luke "Jared" Bennett <lbennett@gitlab.com> | 2017-04-07 21:00:40 +0100 |
---|---|---|
committer | Luke "Jared" Bennett <lbennett@gitlab.com> | 2017-04-07 21:00:40 +0100 |
commit | 8b98ca50b3068fecc76233714fa7d363703ee55c (patch) | |
tree | 97d67331552de76faa9bfb8cec11616a369351fe /app | |
parent | 501d403202d8b4f603dd1864790f56e85633b8ef (diff) | |
parent | d37f1f1cd013a8b7a307e1004e020df086cba9d5 (diff) | |
download | gitlab-ce-8b98ca50b3068fecc76233714fa7d363703ee55c.tar.gz |
Merge branch 'master' into new-resolvable-discussion
Diffstat (limited to 'app')
62 files changed, 848 insertions, 307 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 67106e85a37..ce426741637 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) { <h5 class="emoji-menu-title"> ${name} </h5> - <ul class="clearfix emoji-menu-list ${opts.menuListClass}"> + <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}"> ${emojiList.map(emojiName => ` <li class="emoji-menu-list-item"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index e057ac8df02..37094c5c9be 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -38,6 +38,10 @@ $(() => { Store.create(); + // hack to allow sidebar scripts like milestone_select manipulate the BoardsStore + gl.issueBoards.boardStoreIssueSet = (...args) => Vue.set(Store.detail.issue, ...args); + gl.issueBoards.boardStoreIssueDelete = (...args) => Vue.delete(Store.detail.issue, ...args); + gl.IssueBoardsApp = new Vue({ el: $boardApp, components: { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 5f3ed9374bf..86d99dd87da 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -12,20 +12,18 @@ Vue.use(VueResource); * Renders Pipelines table in pipelines tab in the commits show view. */ +// export for use in merge_request_tabs.js (TODO: remove this hack) +window.gl = window.gl || {}; +window.gl.CommitPipelinesTable = CommitPipelinesTable; + $(() => { - window.gl = window.gl || {}; gl.commits = gl.commits || {}; gl.commits.pipelines = gl.commits.pipelines || {}; - if (gl.commits.PipelinesTableBundle) { - document.querySelector('#commit-pipeline-table-view').removeChild(this.pipelinesTableBundle.$el); - gl.commits.PipelinesTableBundle.$destroy(true); - } - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); - document.querySelector('#commit-pipeline-table-view').appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); + pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); } }); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 0a1a62fb012..4e68f9c77e9 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -44,6 +44,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; +import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; const ShortcutsBlob = require('./shortcuts_blob'); @@ -329,8 +330,12 @@ const ShortcutsBlob = require('./shortcuts_blob'); new Search(); break; case 'projects:repository:show': + // Initialize Protected Branch Settings new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); + // Initialize Protected Tag Settings + new ProtectedTagCreate(); + new ProtectedTagEditList(); break; case 'projects:ci_cd:show': new gl.ProjectVariables(); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 8528e0800ae..f7f6a773036 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -3,9 +3,6 @@ /* global Flash */ import Cookies from 'js-cookie'; - -import CommitPipelinesTable from './commit/pipelines/pipelines_table'; - import './breakpoints'; import './flash'; @@ -102,10 +99,10 @@ import './flash'; destroyPipelinesView() { if (this.commitPipelinesTable) { - document.querySelector('#commit-pipeline-table-view') - .removeChild(this.commitPipelinesTable.$el); - this.commitPipelinesTable.$destroy(); + this.commitPipelinesTable = null; + + document.querySelector('#commit-pipeline-table-view').innerHTML = ''; } } @@ -234,7 +231,7 @@ import './flash'; } mountPipelinesView() { - this.commitPipelinesTable = new CommitPipelinesTable().$mount(); + this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount(); // $mount(el) replaces the el with the new rendered component. We need it in order to mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount document.querySelector('#commit-pipeline-table-view') diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index ac4fad88fe5..773fe3233a7 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -2,8 +2,6 @@ /* global Issuable */ /* global ListMilestone */ -import Vue from 'vue'; - (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject, els) { @@ -151,12 +149,12 @@ import Vue from 'vue'; return $dropdown.closest('form').submit(); } else if ($dropdown.hasClass('js-issue-board-sidebar')) { if (selected.id !== -1) { - Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({ + gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({ id: selected.id, title: selected.name })); } else { - Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone'); + gl.issueBoards.boardStoreIssueDelete('milestone'); } $dropdown.trigger('loading.gl.dropdown'); diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js new file mode 100644 index 00000000000..61e7ba53862 --- /dev/null +++ b/app/assets/javascripts/protected_tags/index.js @@ -0,0 +1,2 @@ +export { default as ProtectedTagCreate } from './protected_tag_create'; +export { default as ProtectedTagEditList } from './protected_tag_edit_list'; diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js new file mode 100644 index 00000000000..fff83f3af3b --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js @@ -0,0 +1,26 @@ +export default class ProtectedTagAccessDropdown { + constructor(options) { + this.options = options; + this.initDropdown(); + } + + initDropdown() { + const { onSelect } = this.options; + this.options.$dropdown.glDropdown({ + data: this.options.data, + selectable: true, + inputId: this.options.$dropdown.data('input-id'), + fieldName: this.options.$dropdown.data('field-name'), + toggleLabel(item, $el) { + if ($el.is('.is-active')) { + return item.text; + } + return 'Select'; + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + }, + }); + } +} diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js new file mode 100644 index 00000000000..91bd140bd12 --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -0,0 +1,41 @@ +import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; +import ProtectedTagDropdown from './protected_tag_dropdown'; + +export default class ProtectedTagCreate { + constructor() { + this.$form = $('.js-new-protected-tag'); + this.buildDropdowns(); + } + + buildDropdowns() { + const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create'); + + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); + + // Allowed to Create dropdown + this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ + $dropdown: $allowedToCreateDropdown, + data: gon.create_access_levels, + onSelect: this.onSelectCallback, + }); + + // Select default + $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0); + + // Protected tag dropdown + this.protectedTagDropdown = new ProtectedTagDropdown({ + $dropdown: this.$form.find('.js-protected-tag-select'), + onSelect: this.onSelectCallback, + }); + } + + // This will run after clicked callback + onSelect() { + // Enable submit button + const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); + const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes'); + + this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); + } +} diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js new file mode 100644 index 00000000000..5ff4e443262 --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -0,0 +1,86 @@ +export default class ProtectedTagDropdown { + /** + * @param {Object} options containing + * `$dropdown` target element + * `onSelect` event callback + * $dropdown must be an element created using `dropdown_tag()` rails helper + */ + constructor(options) { + this.onSelect = options.onSelect; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$protectedTag = this.$dropdownContainer.find('.create-new-protected-tag'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.toggleFooter(true); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getProtectedTags.bind(this), + filterable: true, + remote: false, + search: { + fields: ['title'], + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? selected.title : 'Protected Tag'; + }, + fieldName: 'protected_tag[name]', + text(protectedTag) { + return _.escape(protectedTag.title); + }, + id(protectedTag) { + return _.escape(protectedTag.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (item, $el, e) => { + e.preventDefault(); + this.onSelect(); + }, + }); + } + + bindEvents() { + this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard(e) { + this.$dropdown.data('glDropdown').remote.execute(); + this.$dropdown.data('glDropdown').selectRowAtIndex(); + e.preventDefault(); + } + + getProtectedTags(term, callback) { + if (this.selectedTag) { + callback(gon.open_tags.concat(this.selectedTag)); + } else { + callback(gon.open_tags); + } + } + + toggleCreateNewButton(tagName) { + if (tagName) { + this.selectedTag = { + title: tagName, + id: tagName, + text: tagName, + }; + + this.$dropdownContainer + .find('.create-new-protected-tag code') + .text(tagName); + } + + this.toggleFooter(!tagName); + } + + toggleFooter(toggleState) { + this.$dropdownFooter.toggleClass('hidden', toggleState); + } +} diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js new file mode 100644 index 00000000000..09a387c0f9e --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -0,0 +1,52 @@ +/* eslint-disable no-new */ +/* global Flash */ + +import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; + +export default class ProtectedTagEdit { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create'); + this.onSelectCallback = this.onSelect.bind(this); + + this.buildDropdowns(); + } + + buildDropdowns() { + // Allowed to create dropdown + this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ + $dropdown: this.$allowedToCreateDropdownButton, + data: gon.create_access_levels, + onSelect: this.onSelectCallback, + }); + } + + onSelect() { + const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`); + + // Do not update if one dropdown has not selected any option + if (!$allowedToCreateInput.length) return; + + this.$allowedToCreateDropdownButton.disable(); + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + protected_tag: { + create_access_levels_attributes: [{ + id: this.$allowedToCreateDropdownButton.data('access-level-id'), + access_level: $allowedToCreateInput.val(), + }], + }, + }, + error() { + new Flash('Failed to update tag!', null, $('.js-protected-tags-list')); + }, + }).always(() => { + this.$allowedToCreateDropdownButton.enable(); + }); + } +} diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js new file mode 100644 index 00000000000..bd9fc872266 --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js @@ -0,0 +1,18 @@ +/* eslint-disable no-new */ + +import ProtectedTagEdit from './protected_tag_edit'; + +export default class ProtectedTagEditList { + constructor() { + this.$wrap = $('.protected-tags-list'); + this.initEditForm(); + } + + initEditForm() { + this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => { + new ProtectedTagEdit({ + $wrap: $(el), + }); + }); + } +} diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js index 9c307915ec4..5f9a3e00c22 100644 --- a/app/assets/javascripts/subscription.js +++ b/app/assets/javascripts/subscription.js @@ -1,5 +1,3 @@ -import Vue from 'vue'; - (() => { class Subscription { constructor(containerElm) { @@ -29,8 +27,7 @@ import Vue from 'vue'; // hack to allow this to work with the issue boards Vue object if (document.querySelector('html').classList.contains('issue-boards-page')) { - Vue.set( - gl.issueBoards.BoardsStore.detail.issue, + gl.issueBoards.boardStoreIssueSet( 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed, ); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 48e20cf501f..3325a7d429c 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -2,8 +2,6 @@ /* global Issuable */ /* global ListUser */ -import Vue from 'vue'; - (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, slice = [].slice; @@ -74,7 +72,7 @@ import Vue from 'vue'; e.preventDefault(); if ($dropdown.hasClass('js-issue-board-sidebar')) { - Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({ + gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({ id: _this.currentUser.id, username: _this.currentUser.username, name: _this.currentUser.name, @@ -225,14 +223,14 @@ import Vue from 'vue'; return $dropdown.closest('form').submit(); } else if ($dropdown.hasClass('js-issue-board-sidebar')) { if (user.id) { - Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({ + gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({ id: user.id, username: user.username, name: user.name, avatar_url: user.avatar_url })); } else { - Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee'); + gl.issueBoards.boardStoreIssueDelete('assignee'); } updateIssueBoardsIssue(); diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0fa1f68e034..717ebb44a23 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -744,7 +744,8 @@ pre.light-well { text-align: left; } -.protected-branches-list { +.protected-branches-list, +.protected-tags-list { margin-bottom: 30px; a { @@ -776,6 +777,17 @@ pre.light-well { } } +.protected-tags-list { + .dropdown-menu-toggle { + width: 100%; + max-width: 300px; + } + + .flash-container { + padding: 0; + } +} + .custom-notifications-form { .is-loading { .custom-notification-event-loading { diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index add66ce9f84..04e8cdf6256 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -19,6 +19,11 @@ class Projects::BuildsController < Projects::ApplicationController else @builds end + @builds = @builds.includes([ + { pipeline: :project }, + :project, + :tags + ]) @builds = @builds.page(params[:page]).per(30) end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index a8cb07eb67a..ba24fa9acfe 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,58 +1,23 @@ -class Projects::ProtectedBranchesController < Projects::ApplicationController - include RepositorySettingsRedirect - # Authorize - before_action :require_non_empty_project - before_action :authorize_admin_project! - before_action :load_protected_branch, only: [:show, :update, :destroy] +class Projects::ProtectedBranchesController < Projects::ProtectedRefsController + protected - layout "project_settings" - - def index - redirect_to_repository_settings(@project) - end - - def create - @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute - unless @protected_branch.persisted? - flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe - end - redirect_to_repository_settings(@project) - end - - def show - @matching_branches = @protected_branch.matching(@project.repository.branches) + def project_refs + @project.repository.branches end - def update - @protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch) - - if @protected_branch.valid? - respond_to do |format| - format.json { render json: @protected_branch, status: :ok } - end - else - respond_to do |format| - format.json { render json: @protected_branch.errors, status: :unprocessable_entity } - end - end + def create_service_class + ::ProtectedBranches::CreateService end - def destroy - @protected_branch.destroy - - respond_to do |format| - format.html { redirect_to_repository_settings(@project) } - format.js { head :ok } - end + def update_service_class + ::ProtectedBranches::UpdateService end - private - - def load_protected_branch - @protected_branch = @project.protected_branches.find(params[:id]) + def load_protected_ref + @protected_ref = @project.protected_branches.find(params[:id]) end - def protected_branch_params + def protected_ref_params params.require(:protected_branch).permit(:name, merge_access_levels_attributes: [:access_level, :id], push_access_levels_attributes: [:access_level, :id]) diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb new file mode 100644 index 00000000000..083a70968e5 --- /dev/null +++ b/app/controllers/projects/protected_refs_controller.rb @@ -0,0 +1,47 @@ +class Projects::ProtectedRefsController < Projects::ApplicationController + include RepositorySettingsRedirect + + # Authorize + before_action :require_non_empty_project + before_action :authorize_admin_project! + before_action :load_protected_ref, only: [:show, :update, :destroy] + + layout "project_settings" + + def index + redirect_to_repository_settings(@project) + end + + def create + protected_ref = create_service_class.new(@project, current_user, protected_ref_params).execute + + unless protected_ref.persisted? + flash[:alert] = protected_ref.errors.full_messages.join(', ').html_safe + end + + redirect_to_repository_settings(@project) + end + + def show + @matching_refs = @protected_ref.matching(project_refs) + end + + def update + @protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref) + + if @protected_ref.valid? + render json: @protected_ref, status: :ok + else + render json: @protected_ref.errors, status: :unprocessable_entity + end + end + + def destroy + @protected_ref.destroy + + respond_to do |format| + format.html { redirect_to_repository_settings(@project) } + format.js { head :ok } + end + end +end diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb new file mode 100644 index 00000000000..c61ddf145e6 --- /dev/null +++ b/app/controllers/projects/protected_tags_controller.rb @@ -0,0 +1,23 @@ +class Projects::ProtectedTagsController < Projects::ProtectedRefsController + protected + + def project_refs + @project.repository.tags + end + + def create_service_class + ::ProtectedTags::CreateService + end + + def update_service_class + ::ProtectedTags::UpdateService + end + + def load_protected_ref + @protected_ref = @project.protected_tags.find(params[:id]) + end + + def protected_ref_params + params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id]) + end +end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index b6ce4abca45..44de8a49593 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -4,46 +4,48 @@ module Projects before_action :authorize_admin_project! def show - @deploy_keys = DeployKeysPresenter - .new(@project, current_user: current_user) + @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user) - define_protected_branches + define_protected_refs end private - def define_protected_branches - load_protected_branches + def define_protected_refs + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) + @protected_tags = @project.protected_tags.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new + @protected_tag = @project.protected_tags.new load_gon_index end - def load_protected_branches - @protected_branches = @project.protected_branches.order(:name).page(params[:page]) - end - def access_levels_options { - push_access_levels: { - roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text| - { id: id, text: text, before_divider: true } - end - }, - merge_access_levels: { - roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text| - { id: id, text: text, before_divider: true } - end - } + create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel), + push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel), + merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel) } end - def open_branches - branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } - { open_branches: branches } + def levels_for_dropdown(access_level_type) + roles = access_level_type.human_access_levels.map do |id, text| + { id: id, text: text, before_divider: true } + end + { roles: roles } + end + + def protectable_tags_for_dropdown + { open_tags: ProtectableDropdown.new(@project, :tags).hash } + end + + def protectable_branches_for_dropdown + { open_branches: ProtectableDropdown.new(@project, :branches).hash } end def load_gon_index - gon.push(open_branches.merge(access_levels_options)) + gon.push(protectable_tags_for_dropdown) + gon.push(protectable_branches_for_dropdown) + gon.push(access_levels_options) end end end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index c47198c5eb6..afa56de920b 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -11,7 +11,7 @@ class Projects::TriggersController < Projects::ApplicationController end def create - @trigger = project.triggers.create(create_params.merge(owner: current_user)) + @trigger = project.triggers.create(trigger_params.merge(owner: current_user)) if @trigger.valid? flash[:notice] = 'Trigger was created successfully.' @@ -36,7 +36,7 @@ class Projects::TriggersController < Projects::ApplicationController end def update - if trigger.update(update_params) + if trigger.update(trigger_params) redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.' else render action: "edit" @@ -67,11 +67,10 @@ class Projects::TriggersController < Projects::ApplicationController @trigger ||= project.triggers.find(params[:id]) || render_404 end - def create_params - params.require(:trigger).permit(:description) - end - - def update_params - params.require(:trigger).permit(:description) + def trigger_params + params.require(:trigger).permit( + :description, + trigger_schedule_attributes: [:id, :active, :cron, :cron_timezone, :ref] + ) end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 3fc85dc6b2b..b7a28b1b4a7 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -1,6 +1,6 @@ module BranchesHelper def can_remove_branch?(project, branch_name) - if project.protected_branch? branch_name + if ProtectedBranch.protected?(project, branch_name) false elsif branch_name == project.repository.root_ref false @@ -29,4 +29,8 @@ module BranchesHelper def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) end + + def protected_branch?(project, branch) + ProtectedBranch.protected?(project, branch.name) + end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index c0ec1634cdb..31aaf9e5607 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -21,4 +21,8 @@ module TagsHelper html.html_safe end + + def protected_tag?(project, tag) + ProtectedTag.protected?(project, tag.name) + end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 37a81fa7781..445247f1b41 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -31,7 +31,6 @@ module Ci validate :valid_commit_sha, unless: :importing? after_create :keep_around_commits, unless: :importing? - after_create :refresh_build_status_cache state_machine :status, initial: :created do event :enqueue do @@ -351,7 +350,6 @@ module Ci when 'manual' then block end end - refresh_build_status_cache end def predefined_variables @@ -393,10 +391,6 @@ module Ci .fabricate! end - def refresh_build_status_cache - Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed - end - private def pipeline_data diff --git a/app/models/ci/pipeline_status.rb b/app/models/ci/pipeline_status.rb deleted file mode 100644 index 048047d0e34..00000000000 --- a/app/models/ci/pipeline_status.rb +++ /dev/null @@ -1,86 +0,0 @@ -# This class is not backed by a table in the main database. -# It loads the latest Pipeline for the HEAD of a repository, and caches that -# in Redis. -module Ci - class PipelineStatus - attr_accessor :sha, :status, :project, :loaded - - delegate :commit, to: :project - - def self.load_for_project(project) - new(project).tap do |status| - status.load_status - end - end - - def initialize(project, sha: nil, status: nil) - @project = project - @sha = sha - @status = status - end - - def has_status? - loaded? && sha.present? && status.present? - end - - def load_status - return if loaded? - - if has_cache? - load_from_cache - else - load_from_commit - store_in_cache - end - - self.loaded = true - end - - def load_from_commit - return unless commit - - self.sha = commit.sha - self.status = commit.status - end - - # We only cache the status for the HEAD commit of a project - # This status is rendered in project lists - def store_in_cache_if_needed - return unless sha - return delete_from_cache unless commit - store_in_cache if commit.sha == self.sha - end - - def load_from_cache - Gitlab::Redis.with do |redis| - self.sha, self.status = redis.hmget(cache_key, :sha, :status) - end - end - - def store_in_cache - Gitlab::Redis.with do |redis| - redis.mapped_hmset(cache_key, { sha: sha, status: status }) - end - end - - def delete_from_cache - Gitlab::Redis.with do |redis| - redis.del(cache_key) - end - end - - def has_cache? - Gitlab::Redis.with do |redis| - redis.exists(cache_key) - end - end - - def loaded? - self.loaded - end - - def cache_key - "projects/#{project.id}/build_status" - end - end -end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 0a89f3e0640..b59e235c425 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -14,6 +14,8 @@ module Ci before_validation :set_default_values + accepts_nested_attributes_for :trigger_schedule + def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? end @@ -37,5 +39,9 @@ module Ci def can_access_project? self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project) end + + def trigger_schedule + super || build_trigger_schedule(project: project) + end end end diff --git a/app/models/ci/trigger_schedule.rb b/app/models/ci/trigger_schedule.rb index 10ea381ee31..012a18eb439 100644 --- a/app/models/ci/trigger_schedule.rb +++ b/app/models/ci/trigger_schedule.rb @@ -8,15 +8,19 @@ module Ci belongs_to :project belongs_to :trigger - delegate :ref, to: :trigger - validates :trigger, presence: { unless: :importing? } - validates :cron, cron: true, presence: { unless: :importing? } - validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } - validates :ref, presence: { unless: :importing? } + validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? } + validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? } + validates :ref, presence: { unless: :importing_or_inactive? } before_save :set_next_run_at + scope :active, -> { where(active: true) } + + def importing_or_inactive? + importing? || !active? + end + def set_next_run_at self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) end @@ -26,5 +30,12 @@ module Ci rescue ActiveRecord::RecordInvalid update_attribute(:next_run_at, nil) # update without validation end + + def real_next_run( + worker_cron: Settings.cron_jobs['trigger_schedule_worker']['cron'], + worker_time_zone: Time.zone.name) + Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone) + .next_time_from(next_run_at) + end end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 2f61709110c..dff7b6e3523 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -68,7 +68,7 @@ module HasStatus end scope :created, -> { where(status: 'created') } - scope :relevant, -> { where.not(status: 'created') } + scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) } scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 9dd4d9c6f24..c41b807df8a 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,20 +2,10 @@ module ProtectedBranchAccess extend ActiveSupport::Concern included do - belongs_to :protected_branch - delegate :project, to: :protected_branch - - scope :master, -> { where(access_level: Gitlab::Access::MASTER) } - scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } - end + include ProtectedRefAccess - def humanize - self.class.human_access_levels[self.access_level] - end - - def check_access(user) - return true if user.is_admin? + belongs_to :protected_branch - project.team.max_member_access(user.id) >= access_level + delegate :project, to: :protected_branch end end diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb new file mode 100644 index 00000000000..62eaec2407f --- /dev/null +++ b/app/models/concerns/protected_ref.rb @@ -0,0 +1,42 @@ +module ProtectedRef + extend ActiveSupport::Concern + + included do + belongs_to :project + + validates :name, presence: true + validates :project, presence: true + + delegate :matching, :matches?, :wildcard?, to: :ref_matcher + + def self.protected_ref_accessible_to?(ref, user, action:) + access_levels_for_ref(ref, action: action).any? do |access_level| + access_level.check_access(user) + end + end + + def self.developers_can?(action, ref) + access_levels_for_ref(ref, action: action).any? do |access_level| + access_level.access_level == Gitlab::Access::DEVELOPER + end + end + + def self.access_levels_for_ref(ref, action:) + self.matching(ref).map(&:"#{action}_access_levels").flatten + end + + def self.matching(ref_name, protected_refs: nil) + ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) + end + end + + def commit + project.commit(self.name) + end + + private + + def ref_matcher + @ref_matcher ||= ProtectedRefMatcher.new(self) + end +end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb new file mode 100644 index 00000000000..c4f158e569a --- /dev/null +++ b/app/models/concerns/protected_ref_access.rb @@ -0,0 +1,18 @@ +module ProtectedRefAccess + extend ActiveSupport::Concern + + included do + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } + end + + def humanize + self.class.human_access_levels[self.access_level] + end + + def check_access(user) + return true if user.admin? + + project.team.max_member_access(user.id) >= access_level + end +end diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb new file mode 100644 index 00000000000..ee65de24dd8 --- /dev/null +++ b/app/models/concerns/protected_tag_access.rb @@ -0,0 +1,11 @@ +module ProtectedTagAccess + extend ActiveSupport::Concern + + included do + include ProtectedRefAccess + + belongs_to :protected_tag + + delegate :project, to: :protected_tag + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 77c902fdf09..b71a9e17a93 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -443,7 +443,7 @@ class MergeRequest < ActiveRecord::Base end def can_remove_source_branch?(current_user) - !source_project.protected_branch?(source_branch) && + !ProtectedBranch.protected?(source_project, source_branch) && !source_project.root_ref?(source_branch) && Ability.allowed?(current_user, :push_code, source_project) && diff_head_commit == source_branch_head diff --git a/app/models/project.rb b/app/models/project.rb index 639615b91a2..a160efba912 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -135,6 +135,7 @@ class Project < ActiveRecord::Base has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy + has_many :protected_tags, dependent: :destroy has_many :project_authorizations has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' @@ -859,14 +860,6 @@ class Project < ActiveRecord::Base @repo_exists = false end - # Branches that are not _exactly_ matched by a protected branch. - def open_branches - exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name) - branch_names = repository.branches.map(&:name) - non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names)) - repository.branches.reject { |branch| non_open_branch_names.include? branch.name } - end - def root_ref?(branch) repository.root_ref == branch end @@ -881,16 +874,8 @@ class Project < ActiveRecord::Base Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url end - # 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 + !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end def forked? @@ -1197,7 +1182,7 @@ class Project < ActiveRecord::Base end def pipeline_status - @pipeline_status ||= Ci::PipelineStatus.load_for_project(self) + @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self) end def mark_import_as_failed(error_message) @@ -1353,11 +1338,6 @@ class Project < ActiveRecord::Base "projects/#{id}/pushes_since_gc" 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 - end - # Similar to the normal callbacks that hook into the life cycle of an # Active Record object, you can also define callbacks that get triggered # when you add an object to an association collection. If any of these diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb new file mode 100644 index 00000000000..122fbce257d --- /dev/null +++ b/app/models/protectable_dropdown.rb @@ -0,0 +1,33 @@ +class ProtectableDropdown + def initialize(project, ref_type) + @project = project + @ref_type = ref_type + end + + # Tags/branches which are yet to be individually protected + def protectable_ref_names + @protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names + end + + def hash + protectable_ref_names.map { |ref_name| { text: ref_name, id: ref_name, title: ref_name } } + end + + private + + def refs + @project.repository.public_send(@ref_type) + end + + def ref_names + refs.map(&:name) + end + + def protections + @project.public_send("protected_#{@ref_type}") + end + + def non_wildcard_protected_ref_names + protections.reject(&:wildcard?).map(&:name) + end +end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 39e979ef15b..28b7d5ad072 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -1,9 +1,6 @@ class ProtectedBranch < ActiveRecord::Base include Gitlab::ShellAdapter - - belongs_to :project - validates :name, presence: true - validates :project, presence: true + include ProtectedRef has_many :merge_access_levels, dependent: :destroy has_many :push_access_levels, dependent: :destroy @@ -14,54 +11,15 @@ class ProtectedBranch < ActiveRecord::Base accepts_nested_attributes_for :push_access_levels accepts_nested_attributes_for :merge_access_levels - def commit - project.commit(self.name) - end - - # Returns all protected branches that match the given branch name. - # This realizes all records from the scope built up so far, and does - # _not_ return a relation. - # - # This method optionally takes in a list of `protected_branches` to search - # through, to avoid calling out to the database. - def self.matching(branch_name, protected_branches: nil) - (protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) } - end - - # Returns all branches (among the given list of branches [`Gitlab::Git::Branch`]) - # that match the current protected branch. - def matching(branches) - branches.select { |branch| self.matches?(branch.name) } - end - - # Checks if the protected branch matches the given branch name. - def matches?(branch_name) - return false if self.name.blank? - - exact_match?(branch_name) || wildcard_match?(branch_name) - end - - # Checks if this protected branch contains a wildcard - def wildcard? - self.name && self.name.include?('*') - end - - protected - - def exact_match?(branch_name) - self.name == branch_name - end + # Check if branch name is marked as protected in the system + def self.protected?(project, ref_name) + return true if project.empty_repo? && default_branch_protected? - def wildcard_match?(branch_name) - wildcard_regex === branch_name + self.matching(ref_name, protected_refs: project.protected_branches).present? end - def wildcard_regex - @wildcard_regex ||= begin - name = self.name.gsub('*', 'STAR_DONT_ESCAPE') - quoted_name = Regexp.quote(name) - regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') - /\A#{regex_string}\z/ - end + def self.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 end diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb new file mode 100644 index 00000000000..d970f2b01fc --- /dev/null +++ b/app/models/protected_ref_matcher.rb @@ -0,0 +1,54 @@ +class ProtectedRefMatcher + def initialize(protected_ref) + @protected_ref = protected_ref + end + + # Returns all protected refs that match the given ref name. + # This checks all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_refs` to search + # through, to avoid calling out to the database. + def self.matching(type, ref_name, protected_refs: nil) + (protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) } + end + + # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) + # that match the current protected ref. + def matching(refs) + refs.select { |ref| @protected_ref.matches?(ref.name) } + end + + # Checks if the protected ref matches the given ref name. + def matches?(ref_name) + return false if @protected_ref.name.blank? + + exact_match?(ref_name) || wildcard_match?(ref_name) + end + + # Checks if this protected ref contains a wildcard + def wildcard? + @protected_ref.name && @protected_ref.name.include?('*') + end + + protected + + def exact_match?(ref_name) + @protected_ref.name == ref_name + end + + def wildcard_match?(ref_name) + return false unless wildcard? + + wildcard_regex === ref_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end +end diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb new file mode 100644 index 00000000000..83964095516 --- /dev/null +++ b/app/models/protected_tag.rb @@ -0,0 +1,14 @@ +class ProtectedTag < ActiveRecord::Base + include Gitlab::ShellAdapter + include ProtectedRef + + has_many :create_access_levels, dependent: :destroy + + validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." } + + accepts_nested_attributes_for :create_access_levels + + def self.protected?(project, ref_name) + self.matching(ref_name, protected_refs: project.protected_tags).present? + end +end diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb new file mode 100644 index 00000000000..c7e1319719d --- /dev/null +++ b/app/models/protected_tag/create_access_level.rb @@ -0,0 +1,21 @@ +class ProtectedTag::CreateAccessLevel < ActiveRecord::Base + include ProtectedTagAccess + + validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS] } + + def self.human_access_levels + { + Gitlab::Access::MASTER => "Masters", + Gitlab::Access::DEVELOPER => "Developers + Masters", + Gitlab::Access::NO_ACCESS => "No one" + }.with_indifferent_access + end + + def check_access(user) + return false if access_level == Gitlab::Access::NO_ACCESS + + super + end +end diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb index c0e4a798f6a..91d9c1d2ba1 100644 --- a/app/services/ci/expire_pipeline_cache_service.rb +++ b/app/services/ci/expire_pipeline_cache_service.rb @@ -10,6 +10,8 @@ module Ci store.touch(commit_pipelines_path) if pipeline.commit store.touch(new_merge_request_pipelines_path) merge_requests_pipelines_paths.each { |path| store.touch(path) } + + Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline) end private diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 11a045f4c31..38a113caec7 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -11,7 +11,7 @@ class DeleteBranchService < BaseService return error('Cannot remove HEAD branch', 405) end - if project.protected_branch?(branch_name) + if ProtectedBranch.protected?(project, branch_name) return error('Protected branch cant be removed', 405) end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index bc7431c89a8..45411c779cc 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -127,7 +127,7 @@ class GitPushService < BaseService project.change_head(branch_name) # Set protection on the default branch if configured - if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch) + if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch) params = { name: @project.default_branch, diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index 89d8ba60134..4b3337a5c9d 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -1,13 +1,10 @@ module ProtectedBranches class UpdateService < BaseService - attr_reader :protected_branch - def execute(protected_branch) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) - @protected_branch = protected_branch - @protected_branch.update(params) - @protected_branch + protected_branch.update(params) + protected_branch end end end diff --git a/app/services/protected_tags/create_service.rb b/app/services/protected_tags/create_service.rb new file mode 100644 index 00000000000..faba7865a17 --- /dev/null +++ b/app/services/protected_tags/create_service.rb @@ -0,0 +1,11 @@ +module ProtectedTags + class CreateService < BaseService + attr_reader :protected_tag + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + + project.protected_tags.create(params) + end + end +end diff --git a/app/services/protected_tags/update_service.rb b/app/services/protected_tags/update_service.rb new file mode 100644 index 00000000000..aea6a48968d --- /dev/null +++ b/app/services/protected_tags/update_service.rb @@ -0,0 +1,10 @@ +module ProtectedTags + class UpdateService < BaseService + def execute(protected_tag) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + + protected_tag.update(params) + protected_tag + end + end +end diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 724675e659c..0f9ef3eded3 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -15,7 +15,7 @@ %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" } merged - - if @project.protected_branch? branch.name + - if protected_branch?(@project, branch) %span.label.label-success protected .controls.hidden-xs< diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/projects/protected_branches/show.html.haml index 4d8169815b3..f8cfe5e4b11 100644 --- a/app/views/projects/protected_branches/show.html.haml +++ b/app/views/projects/protected_branches/show.html.haml @@ -1,13 +1,13 @@ -- page_title @protected_branch.name, "Protected Branches" +- page_title @protected_ref.name, "Protected Branches" .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 - = @protected_branch.name + = @protected_ref.name .col-lg-9 %h5 Matching Branches - - if @matching_branches.present? + - if @matching_refs.present? .table-responsive %table.table.protected-branches-list %colgroup @@ -18,7 +18,7 @@ %th Branch %th Last commit %tbody - - @matching_branches.each do |matching_branch| + - @matching_refs.each do |matching_branch| = render partial: "matching_branch", object: matching_branch - else %p.settings-message.text-center diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml new file mode 100644 index 00000000000..6e187b54a59 --- /dev/null +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -0,0 +1,32 @@ += form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'js-new-protected-tag' } do |f| + .panel.panel-default + .panel-heading + %h3.panel-title + Protect a tag + .panel-body + .form-horizontal + = form_errors(@protected_tag) + .form-group + = f.label :name, class: 'col-md-2 text-right' do + Tag: + .col-md-10 + = render partial: "projects/protected_tags/dropdown", locals: { f: f } + .help-block + = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags') + such as + %code v* + or + %code *-release + are supported + .form-group + %label.col-md-2.text-right{ for: 'create_access_levels_attributes' } + Allowed to create: + .col-md-10 + .create_access_levels-container + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-create wide', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }}) + + .panel-footer + = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_tags/_dropdown.html.haml b/app/views/projects/protected_tags/_dropdown.html.haml new file mode 100644 index 00000000000..74851519077 --- /dev/null +++ b/app/views/projects/protected_tags/_dropdown.html.haml @@ -0,0 +1,15 @@ += f.hidden_field(:name) + += dropdown_tag('Select tag or create wildcard', + options: { toggle_class: 'js-protected-tag-select js-filter-submit wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected tag", + footer_content: true, + data: { show_no: true, show_any: true, show_upcoming: true, + selected: params[:protected_tag_name], + project_id: @project.try(:id) } }) do + + %ul.dropdown-footer-list + %li + = link_to '#', title: "New Protected Tag", class: "create-new-protected-tag" do + Create wildcard + %code diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml new file mode 100644 index 00000000000..0bfb1ad191d --- /dev/null +++ b/app/views/projects/protected_tags/_index.html.haml @@ -0,0 +1,18 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('protected_tags') + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Protected tags + %p.prepend-top-20 + By default, Protected tags are designed to: + %ul + %li Prevent tag creation by everybody except Masters + %li Prevent <strong>anyone</strong> from updating the tag + %li Prevent <strong>anyone</strong> from deleting the tag + .col-lg-9 + - if can? current_user, :admin_project, @project + = render 'projects/protected_tags/create_protected_tag' + + = render "projects/protected_tags/tags_list" diff --git a/app/views/projects/protected_tags/_matching_tag.html.haml b/app/views/projects/protected_tags/_matching_tag.html.haml new file mode 100644 index 00000000000..97e5cd6f9d2 --- /dev/null +++ b/app/views/projects/protected_tags/_matching_tag.html.haml @@ -0,0 +1,9 @@ +%tr + %td + = link_to matching_tag.name, namespace_project_tree_path(@project.namespace, @project, matching_tag.name) + - if @project.root_ref?(matching_tag.name) + %span.label.label-info.prepend-left-5 default + %td + - commit = @project.commit(matching_tag.name) + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/protected_tags/_protected_tag.html.haml b/app/views/projects/protected_tags/_protected_tag.html.haml new file mode 100644 index 00000000000..26bd3a1f5ed --- /dev/null +++ b/app/views/projects/protected_tags/_protected_tag.html.haml @@ -0,0 +1,21 @@ +%tr.js-protected-tag-edit-form{ data: { url: namespace_project_protected_tag_path(@project.namespace, @project, protected_tag) } } + %td + = protected_tag.name + - if @project.root_ref?(protected_tag.name) + %span.label.label-info.prepend-left-5 default + %td + - if protected_tag.wildcard? + - matching_tags = protected_tag.matching(repository.tags) + = link_to pluralize(matching_tags.count, "matching tag"), namespace_project_protected_tag_path(@project.namespace, @project, protected_tag) + - else + - if commit = protected_tag.commit + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) + - else + (tag was removed from repository) + + = render partial: 'projects/protected_tags/update_protected_tag', locals: { protected_tag: protected_tag } + + - if can_admin_project + %td + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml new file mode 100644 index 00000000000..728afd75b50 --- /dev/null +++ b/app/views/projects/protected_tags/_tags_list.html.haml @@ -0,0 +1,28 @@ +.panel.panel-default.protected-tags-list.js-protected-tags-list + - if @protected_tags.empty? + .panel-heading + %h3.panel-title + Protected tag (#{@protected_tags.size}) + %p.settings-message.text-center + There are currently no protected tags, protect a tag with the form above. + - else + - can_admin_project = can?(current_user, :admin_project, @project) + + %table.table.table-bordered + %colgroup + %col{ width: "25%" } + %col{ width: "25%" } + %col{ width: "50%" } + %thead + %tr + %th Protected tag (#{@protected_tags.size}) + %th Last commit + %th Allowed to create + - if can_admin_project + %th + %tbody + %tr + %td.flash-container{ colspan: 4 } + = render partial: 'projects/protected_tags/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project} + + = paginate @protected_tags, theme: 'gitlab' diff --git a/app/views/projects/protected_tags/_update_protected_tag.haml b/app/views/projects/protected_tags/_update_protected_tag.haml new file mode 100644 index 00000000000..62823bee46e --- /dev/null +++ b/app/views/projects/protected_tags/_update_protected_tag.haml @@ -0,0 +1,5 @@ +%td + = hidden_field_tag "allowed_to_create_#{protected_tag.id}", protected_tag.create_access_levels.first.access_level + = dropdown_tag( (protected_tag.create_access_levels.first.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable js-allowed-to-create-container', + data: { field_name: "allowed_to_create_#{protected_tag.id}", access_level_id: protected_tag.create_access_levels.first.id }}) diff --git a/app/views/projects/protected_tags/show.html.haml b/app/views/projects/protected_tags/show.html.haml new file mode 100644 index 00000000000..63743f28b3c --- /dev/null +++ b/app/views/projects/protected_tags/show.html.haml @@ -0,0 +1,25 @@ +- page_title @protected_ref.name, "Protected Tags" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = @protected_ref.name + + .col-lg-9 + %h5 Matching Tags + - if @matching_refs.present? + .table-responsive + %table.table.protected-tags-list + %colgroup + %col{ width: "30%" } + %col{ width: "30%" } + %thead + %tr + %th Tag + %th Last commit + %tbody + - @matching_refs.each do |matching_tag| + = render partial: "matching_tag", object: matching_tag + - else + %p.settings-message.text-center + Couldn't find any matching tags. diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 4c02302e161..5402320cb66 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -3,3 +3,4 @@ = render @deploy_keys = render "projects/protected_branches/index" += render "projects/protected_tags/index" diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index dffe908e85a..451e011a4b8 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -6,6 +6,11 @@ %span.item-title = icon('tag') = tag.name + + - if protected_tag?(@project, tag) + %span.label.label-success + protected + - if tag.message.present? = strip_gpg_signature(tag.message) @@ -30,5 +35,5 @@ = icon("pencil") - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index fad3c5c2173..1c4135c8a54 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -7,6 +7,9 @@ .nav-text .title %span.item-title= @tag.name + - if protected_tag?(@project, @tag) + %span.label.label-success + protected - if @commit = render 'projects/branches/commit', commit: @commit, project: @project - else @@ -24,7 +27,7 @@ = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .btn-container.controls-item-full - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o - if @tag.message.present? diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml index 5f708b3a2ed..8582bcbb8cc 100644 --- a/app/views/projects/triggers/_form.html.haml +++ b/app/views/projects/triggers/_form.html.haml @@ -8,4 +8,26 @@ .form-group = f.label :key, "Description", class: "label-light" = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description" + - if @trigger.persisted? + %hr + = f.fields_for :trigger_schedule do |schedule_fields| + = schedule_fields.hidden_field :id + .form-group + .checkbox + = schedule_fields.label :active do + = schedule_fields.check_box :active + %strong Schedule trigger (experimental) + .help-block + If checked, this trigger will be executed periodically according to cron and timezone. + = link_to icon('question-circle'), help_page_path('ci/triggers', anchor: 'schedule') + .form-group + = schedule_fields.label :cron, "Cron", class: "label-light" + = schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *" + .form-group + = schedule_fields.label :cron, "Timezone", class: "label-light" + = schedule_fields.text_field :cron_timezone, class: "form-control", title: 'Timezone is required.', placeholder: "UTC" + .form-group + = schedule_fields.label :ref, "Branch or tag", class: "label-light" + = schedule_fields.text_field :ref, class: "form-control", title: 'Branch or tag is required.', placeholder: "master" + .help-block Existing branch name, tag = f.submit btn_text, class: "btn btn-save" diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index cc74e50a5e3..84e945ee0df 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -22,6 +22,8 @@ %th %strong Last used %th + %strong Next run at + %th = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger - else %p.settings-message.text-center.append-bottom-default diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 9b5f63ae81a..ebd91a8e2af 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -29,6 +29,12 @@ - else Never + %td + - if trigger.trigger_schedule&.active? + = trigger.trigger_schedule.real_next_run + - else + Never + %td.text-right.trigger-actions - take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?" - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?" diff --git a/app/workers/trigger_schedule_worker.rb b/app/workers/trigger_schedule_worker.rb index 440c579b99d..9c1baf7e6c5 100644 --- a/app/workers/trigger_schedule_worker.rb +++ b/app/workers/trigger_schedule_worker.rb @@ -3,7 +3,7 @@ class TriggerScheduleWorker include CronjobQueue def perform - Ci::TriggerSchedule.where("next_run_at < ?", Time.now).find_each do |trigger_schedule| + Ci::TriggerSchedule.active.where("next_run_at < ?", Time.now).find_each do |trigger_schedule| begin Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project, trigger_schedule.trigger, |