summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/dispatcher.js5
-rw-r--r--app/assets/javascripts/protected_tags/index.js5
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js26
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js41
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_dropdown.js85
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js52
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit_list.js17
-rw-r--r--app/assets/stylesheets/pages/projects.scss9
-rw-r--r--app/controllers/projects/protected_branches_controller.rb59
-rw-r--r--app/controllers/projects/protected_refs_controller.rb48
-rw-r--r--app/controllers/projects/protected_tags_controller.rb35
-rw-r--r--app/controllers/projects/settings/repository_controller.rb48
-rw-r--r--app/helpers/branches_helper.rb2
-rw-r--r--app/helpers/tags_helper.rb4
-rw-r--r--app/models/concerns/protected_branch_access.rb15
-rw-r--r--app/models/concerns/protected_ref.rb41
-rw-r--r--app/models/concerns/protected_ref_access.rb18
-rw-r--r--app/models/concerns/protected_tag_access.rb10
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/project.rb24
-rw-r--r--app/models/protectable_dropdown.rb26
-rw-r--r--app/models/protected_branch.rb58
-rw-r--r--app/models/protected_ref_matcher.rb52
-rw-r--r--app/models/protected_tag.rb14
-rw-r--r--app/models/protected_tag/create_access_level.rb21
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/git_push_service.rb2
-rw-r--r--app/services/protected_tags/create_service.rb11
-rw-r--r--app/services/protected_tags/update_service.rb13
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/protected_tags/_create_protected_tag.html.haml32
-rw-r--r--app/views/projects/protected_tags/_dropdown.html.haml15
-rw-r--r--app/views/projects/protected_tags/_index.html.haml18
-rw-r--r--app/views/projects/protected_tags/_matching_tag.html.haml9
-rw-r--r--app/views/projects/protected_tags/_protected_tag.html.haml21
-rw-r--r--app/views/projects/protected_tags/_tags_list.html.haml26
-rw-r--r--app/views/projects/protected_tags/_update_protected_tag.haml5
-rw-r--r--app/views/projects/protected_tags/show.html.haml25
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/tags/_tag.html.haml7
-rw-r--r--app/views/projects/tags/show.html.haml5
41 files changed, 754 insertions, 157 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 80490052389..2e7c9bc2689 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -44,6 +44,7 @@ import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import UserCallout from './user_callout';
+import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -321,8 +322,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/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js
new file mode 100644
index 00000000000..da036f5437e
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/index.js
@@ -0,0 +1,5 @@
+export { default as ProtectedTagAccessDropdown } from './protected_tag_access_dropdown';
+export { default as ProtectedTagCreate } from './protected_tag_create';
+export { default as ProtectedTagDropdown } from './protected_tag_dropdown';
+export { default as ProtectedTagEdit } from './protected_tag_edit';
+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..681b060f859
--- /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..964e67c9de0
--- /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 = $('.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('input[name="protected_tag[create_access_levels_attributes][0][access_level]"]');
+
+ 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..9c78f2816a4
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js
@@ -0,0 +1,85 @@
+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() {
+ this.$dropdown.data('glDropdown').remote.execute();
+ this.$dropdown.data('glDropdown').selectRowAtIndex();
+ }
+
+ 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..b5092877138
--- /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.$allowedToCreateDropdown = this.$wrap.find('.js-allowed-to-create');
+
+ this.buildDropdowns();
+ }
+
+ buildDropdowns() {
+ // Allowed to create dropdown
+ this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
+ $dropdown: this.$allowedToCreateDropdown,
+ data: gon.create_access_levels,
+ onSelect: this.onSelect.bind(this),
+ });
+ }
+
+ onSelect() {
+ const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdown.data('fieldName')}"]`);
+
+ // Do not update if one dropdown has not selected any option
+ if (!$allowedToCreateInput.length) return;
+
+ this.$allowedToCreateDropdown.disable();
+
+ $.ajax({
+ type: 'POST',
+ url: this.$wrap.data('url'),
+ dataType: 'json',
+ data: {
+ _method: 'PATCH',
+ protected_tag: {
+ create_access_levels_attributes: [{
+ id: this.$allowedToCreateDropdown.data('access-level-id'),
+ access_level: $allowedToCreateInput.val(),
+ }],
+ },
+ },
+ error() {
+ $.scrollTo(0);
+ new Flash('Failed to update tag!');
+ },
+ }).always(() => {
+ this.$allowedToCreateDropdown.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..88c7accdec6
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -0,0 +1,17 @@
+import ProtectedTagEdit from './protected_tag_edit';
+
+export default class ProtectedTagEditList {
+ constructor() {
+ this.$wrap = $('.protected-tags-list');
+ this.protectedTagList = [];
+ this.initEditForm();
+ }
+
+ initEditForm() {
+ this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
+ this.protectedTagList[i] = new ProtectedTagEdit({
+ $wrap: $(el),
+ });
+ });
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0fa1f68e034..960875674cc 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,12 @@ pre.light-well {
}
}
+.protected-tags-list {
+ .dropdown-menu-toggle {
+ width: 300px;
+ }
+}
+
.custom-notifications-form {
.is-loading {
.custom-notification-event-loading {
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index a8cb07eb67a..c2a55c9500a 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,58 +1,35 @@
-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)
+ def protected_ref
+ @protected_branch
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)
+ def protected_ref=(val)
+ @protected_branch = val
end
- def show
- @matching_branches = @protected_branch.matching(@project.repository.branches)
+ def matching_refs=(val)
+ @matching_branches = val
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 project_refs
+ @project.repository.branches
end
- def destroy
- @protected_branch.destroy
-
- respond_to do |format|
- format.html { redirect_to_repository_settings(@project) }
- format.js { head :ok }
- end
+ def create_service
+ ::ProtectedBranches::CreateService
end
- private
+ def update_service
+ ::ProtectedBranches::UpdateService
+ end
- def load_protected_branch
- @protected_branch = @project.protected_branches.find(params[:id])
+ def load_protected_ref
+ self.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..63f005124a9
--- /dev/null
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -0,0 +1,48 @@
+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
+ self.protected_ref = create_service.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
+ self.matching_refs = protected_ref.matching(project_refs)
+ end
+
+ def update
+ self.protected_ref = update_service.new(@project, current_user, protected_ref_params).execute(protected_ref)
+
+ if protected_ref.valid?
+ respond_to do |format|
+ format.json { render json: protected_ref, status: :ok }
+ end
+ else
+ respond_to do |format|
+ format.json { render json: protected_ref.errors, status: :unprocessable_entity }
+ end
+ 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..0e00baedbdf
--- /dev/null
+++ b/app/controllers/projects/protected_tags_controller.rb
@@ -0,0 +1,35 @@
+class Projects::ProtectedTagsController < Projects::ProtectedRefsController
+ protected
+
+ def protected_ref
+ @protected_tag
+ end
+
+ def protected_ref=(val)
+ @protected_tag = val
+ end
+
+ def matching_refs=(val)
+ @matching_tags = val
+ end
+
+ def project_refs
+ @project.repository.tags
+ end
+
+ def create_service
+ ::ProtectedTags::CreateService
+ end
+
+ def update_service
+ ::ProtectedTags::UpdateService
+ end
+
+ def load_protected_ref
+ self.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/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 3fc85dc6b2b..a852b90c57e 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
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/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 9dd4d9c6f24..06cae00249a 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -2,20 +2,9 @@ module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
+ include ProtectedRefAccess
+
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
-
- def humanize
- self.class.human_access_levels[self.access_level]
- end
-
- def check_access(user)
- return true if user.is_admin?
-
- project.team.max_member_access(user.id) >= access_level
end
end
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
new file mode 100644
index 00000000000..7c0183952a0
--- /dev/null
+++ b/app/models/concerns/protected_ref.rb
@@ -0,0 +1,41 @@
+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..0c7e5157cdf
--- /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.is_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..9b7d31a6fd5
--- /dev/null
+++ b/app/models/concerns/protected_tag_access.rb
@@ -0,0 +1,10 @@
+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 8d740adb771..b2725a314ad 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -442,7 +442,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 12fd0668ff8..2469e6f8523 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -134,6 +134,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'
@@ -865,14 +866,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
@@ -887,16 +880,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?
@@ -1363,11 +1348,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..c9b2b213cd2
--- /dev/null
+++ b/app/models/protectable_dropdown.rb
@@ -0,0 +1,26 @@
+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
+ non_wildcard_protections = protections.reject(&:wildcard?)
+ refs.map(&:name) - non_wildcard_protections.map(&:name)
+ 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 protections
+ @project.public_send("protected_#{@ref_type}")
+ 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..83f44240259
--- /dev/null
+++ b/app/models/protected_ref_matcher.rb
@@ -0,0 +1,52 @@
+class ProtectedRefMatcher
+ def initialize(protected_ref)
+ @protected_ref = protected_ref
+ end
+
+ # Returns all protected refs that match the given ref 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_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)
+ 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/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_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..8a2419efd7b
--- /dev/null
+++ b/app/services/protected_tags/update_service.rb
@@ -0,0 +1,13 @@
+module ProtectedTags
+ class UpdateService < BaseService
+ attr_reader :protected_tag
+
+ def execute(protected_tag)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
+
+ @protected_tag = protected_tag
+ @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 9eb610ba9c0..d84fa9e55c0 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 ProtectedBranch.protected?(@project, branch.name)
%span.label.label-success
protected
.controls.hidden-xs<
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..af332f942d6
--- /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: '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_branches', anchor: 'wildcard-protected-branches')
+ such as
+ %code *-stable
+ or
+ %code production/*
+ 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..cc006ed8a0b
--- /dev/null
+++ b/app/views/projects/protected_tags/_tags_list.html.haml
@@ -0,0 +1,26 @@
+.panel.panel-default.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
+ = 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..185807a7e8d
--- /dev/null
+++ b/app/views/projects/protected_tags/show.html.haml
@@ -0,0 +1,25 @@
+- page_title @protected_tag.name, "Protected Tags"
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = @protected_tag.name
+
+ .col-lg-9
+ %h5 Matching Tags
+ - if @matching_tags.present?
+ .table-responsive
+ %table.table.protected-tags-list
+ %colgroup
+ %col{ width: "30%" }
+ %col{ width: "30%" }
+ %thead
+ %tr
+ %th Tag
+ %th Last commit
+ %tbody
+ - @matching_tags.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?
&nbsp;
= 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?