Commit c662b2e1 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'developer_can_push_to_protected_branches_option' into 'master'

Developer can push to protected branches option

Adds option to enable option to enable push to protected branches for developers, per protected branch. Off by default, developers cannot push to protected branches.

If turned on, developers can push to protected branches but still cannot force push or remove the branch.

Allows more control over protected branches.

See merge request !1353
parents 75043a9e e005d419
$ ->
$(":checkbox").change ->
name = $(this).attr("name")
if name == "developers_can_push"
id = $(this).val()
checked = $(this).is(":checked")
url = $(this).data("url")
$.ajax
type: "PUT"
url: url
dataType: "json"
data:
id: id
developers_can_push: checked
success: ->
new Flash("Branch updated.", "notice")
location.reload true
error: ->
new Flash("Failed to update branch!", "alert")
...@@ -308,3 +308,10 @@ ul.nav.nav-projects-tabs { ...@@ -308,3 +308,10 @@ ul.nav.nav-projects-tabs {
display: none; display: none;
} }
} }
table.table.protected-branches-list tr.no-border {
th, td {
border: 0;
}
}
...@@ -15,6 +15,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController ...@@ -15,6 +15,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
redirect_to project_protected_branches_path(@project) redirect_to project_protected_branches_path(@project)
end end
def update
protected_branch = @project.protected_branches.find(params[:id])
if protected_branch &&
protected_branch.update_attributes(
developers_can_push: params[:developers_can_push]
)
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
end
def destroy def destroy
@project.protected_branches.find(params[:id]).destroy @project.protected_branches.find(params[:id]).destroy
...@@ -27,6 +45,6 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController ...@@ -27,6 +45,6 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
private private
def protected_branch_params def protected_branch_params
params.require(:protected_branch).permit(:name) params.require(:protected_branch).permit(:name, :developers_can_push)
end end
end end
...@@ -467,6 +467,10 @@ class Project < ActiveRecord::Base ...@@ -467,6 +467,10 @@ class Project < ActiveRecord::Base
protected_branches_names.include?(branch_name) protected_branches_names.include?(branch_name)
end end
def developers_can_push_to_protected_branch?(branch_name)
protected_branches.any? { |pb| pb.name == branch_name && pb.developers_can_push }
end
def forked? def forked?
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end end
......
- unless @branches.empty?
%h5 Already Protected:
%table.table.protected-branches-list
%thead
%tr.no-border
%th Branch
%th Developers can push
%th
%tbody
- @branches.each do |branch|
- @url = project_protected_branch_path(@project, branch)
%tr
%td
= link_to project_commits_path(@project, branch.name) do
%strong= branch.name
- if @project.root_ref?(branch.name)
%span.label.label-info default
%td
= check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url
%td
.pull-right
- if can? current_user, :admin_project, @project
= link_to 'Unprotect', [@project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small"
%tr.no-border
%td
- if commit = branch.commit
= link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do
= commit.short_id
%span.light
= gfm escape_once(truncate(commit.title, length: 40))
#{time_ago_with_tooltip(commit.committed_date)}
- else
(branch was removed from repository)
%td
%td
...@@ -22,29 +22,14 @@ ...@@ -22,29 +22,14 @@
= f.label :name, "Branch", class: 'control-label' = f.label :name, "Branch", class: 'control-label'
.col-sm-10 .col-sm-10
= f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"}) = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"})
.form-group
= f.label :developers_can_push, class: 'control-label' do
Developers can push
.col-sm-10
.checkbox
= f.check_box :developers_can_push
%span.descr Allow developers to push to this branch
.form-actions .form-actions
= f.submit 'Protect', class: "btn-create btn" = f.submit 'Protect', class: "btn-create btn"
- unless @branches.empty? = render 'branches_list'
%h5 Already Protected:
%ul.bordered-list.protected-branches-list
- @branches.each do |branch|
%li
%h4
= link_to project_commits_path(@project, branch.name) do
%strong= branch.name
- if @project.root_ref?(branch.name)
%span.label.label-info default
%span.label.label-success
%i.fa.fa-lock
.pull-right
- if can? current_user, :admin_project, @project
= link_to 'Unprotect', [@project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small"
- if commit = branch.commit
= link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do
= commit.short_id
%span.light
= gfm escape_once(truncate(commit.title, length: 40))
#{time_ago_with_tooltip(commit.committed_date)}
- else
(branch was removed from repository)
...@@ -263,7 +263,7 @@ Gitlab::Application.routes.draw do ...@@ -263,7 +263,7 @@ Gitlab::Application.routes.draw do
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :protected_branches, only: [:index, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :refs, only: [] do resources :refs, only: [] do
collection do collection do
......
class AddDevelopersCanPushToProtectedBranches < ActiveRecord::Migration
def change
add_column :protected_branches, :developers_can_push, :boolean, default: false, null: false
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20141217125223) do ActiveRecord::Schema.define(version: 20141226080412) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -326,6 +326,7 @@ ActiveRecord::Schema.define(version: 20141217125223) do ...@@ -326,6 +326,7 @@ ActiveRecord::Schema.define(version: 20141217125223) do
t.string "name", null: false t.string "name", null: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "developers_can_push", default: false, null: false
end end
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
...@@ -410,7 +411,6 @@ ActiveRecord::Schema.define(version: 20141217125223) do ...@@ -410,7 +411,6 @@ ActiveRecord::Schema.define(version: 20141217125223) do
t.integer "notification_level", default: 1, null: false t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at" t.datetime "password_expires_at"
t.integer "created_by_id" t.integer "created_by_id"
t.datetime "last_credential_check_at"
t.string "avatar" t.string "avatar"
t.string "confirmation_token" t.string "confirmation_token"
t.datetime "confirmed_at" t.datetime "confirmed_at"
...@@ -418,6 +418,7 @@ ActiveRecord::Schema.define(version: 20141217125223) do ...@@ -418,6 +418,7 @@ ActiveRecord::Schema.define(version: 20141217125223) do
t.string "unconfirmed_email" t.string "unconfirmed_email"
t.boolean "hide_no_ssh_key", default: false t.boolean "hide_no_ssh_key", default: false
t.string "website_url", default: "", null: false t.string "website_url", default: "", null: false
t.datetime "last_credential_check_at"
end end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......
...@@ -28,6 +28,7 @@ If a user is a GitLab administrator they receive all permissions. ...@@ -28,6 +28,7 @@ If a user is a GitLab administrator they receive all permissions.
| Add new team members | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ |
| Enable/disable branch protection | | | | ✓ | ✓ | | Enable/disable branch protection | | | | ✓ | ✓ |
| Turn on/off prot. branch push for devs| | | | ✓ | ✓ |
| Rewrite/remove git tags | | | | ✓ | ✓ | | Rewrite/remove git tags | | | | ✓ | ✓ |
| Edit project | | | | ✓ | ✓ | | Edit project | | | | ✓ | ✓ |
| Add deploy keys to project | | | | ✓ | ✓ | | Add deploy keys to project | | | | ✓ | ✓ |
......
...@@ -8,3 +8,4 @@ ...@@ -8,3 +8,4 @@
- [GitLab Flow](gitlab_flow.md) - [GitLab Flow](gitlab_flow.md)
- [Notifications](notifications.md) - [Notifications](notifications.md)
- [Migrating from SVN to GitLab](migrating_from_svn.md) - [Migrating from SVN to GitLab](migrating_from_svn.md)
- [Protected branches](protected_branches.md)
# Protected branches
Permission in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches.
To prevent people from messing with history or pushing code without review, we've created protected branches.
A protected branch does three simple things:
* it prevents pushes from everybody except users with Master permission
* it prevents anyone from force pushing to the branch
* it prevents anyone from deleting the branch
You can make any branch a protected branch. GitLab makes the master branch a protected branch by default.
To protect a branch, user needs to have at least a Master permission level, see [permissions document](permissions/permissions.md).
![protected branches page](protected_branches/protected_branches1.png)
Navigate to project settings page and select `protected branches`. From the `Branch` dropdown menu select the branch you want to protect.
Some workflows, like [GitLab workflow](gitlab_flow.md), require all users with write access to submit a Merge request in order to get the code into a protected branch.
Since Masters and Owners can already push to protected branches, that means Developers cannot push to protected branch and need to submit a Merge request.
However, there are workflows where that is not needed and only protecting from force pushes and branch removal is useful.
For those workflows, you can allow everyone with write access to push to a protected branch by selecting `Developers can push` check box.
On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box.
![Developers can push](protected_branches/protected_branches2.png)
...@@ -79,16 +79,8 @@ module Gitlab ...@@ -79,16 +79,8 @@ module Gitlab
oldrev, newrev, ref = change.split(' ') oldrev, newrev, ref = change.split(' ')
action = if project.protected_branch?(branch_name(ref)) action = if project.protected_branch?(branch_name(ref))
# we dont allow force push to protected branch protected_branch_action(project, oldrev, newrev, branch_name(ref))
if forced_push?(project, oldrev, newrev) elsif protected_tag?(project, tag_name(ref))
:force_push_code_to_protected_branches
# and we dont allow remove of protected branch
elsif newrev == Gitlab::Git::BLANK_SHA
:remove_protected_branches
else
:push_code_to_protected_branches
end
elsif project.repository.tag_names.include?(tag_name(ref))
# Prevent any changes to existing git tag unless user has permissions # Prevent any changes to existing git tag unless user has permissions
:admin_project :admin_project
else else
...@@ -108,6 +100,24 @@ module Gitlab ...@@ -108,6 +100,24 @@ module Gitlab
private private
def protected_branch_action(project, oldrev, newrev, branch_name)
# we dont allow force push to protected branch
if forced_push?(project, oldrev, newrev)
:force_push_code_to_protected_branches
# and we dont allow remove of protected branch
elsif newrev == Gitlab::Git::BLANK_SHA
:remove_protected_branches
elsif project.developers_can_push_to_protected_branch?(branch_name)
:push_code
else
:push_code_to_protected_branches
end
end
def protected_tag?(project, tag_name)
project.repository.tag_names.include?(tag_name)
end
def user_allowed?(user) def user_allowed?(user)
Gitlab::UserAccess.allowed?(user) Gitlab::UserAccess.allowed?(user)
end end
......
...@@ -129,6 +129,13 @@ describe Gitlab::GitAccess do ...@@ -129,6 +129,13 @@ describe Gitlab::GitAccess do
} }
end end
def self.updated_permissions_matrix
updated_permissions_matrix = permissions_matrix.dup
updated_permissions_matrix[:developer][:push_protected_branch] = true
updated_permissions_matrix[:developer][:push_all] = true
updated_permissions_matrix
end
permissions_matrix.keys.each do |role| permissions_matrix.keys.each do |role|
describe "#{role} access" do describe "#{role} access" do
before { protect_feature_branch } before { protect_feature_branch }
...@@ -143,5 +150,22 @@ describe Gitlab::GitAccess do ...@@ -143,5 +150,22 @@ describe Gitlab::GitAccess do
end end
end end
end end
context "with enabled developers push to protected branches " do
updated_permissions_matrix.keys.each do |role|
describe "#{role} access" do
before { create(:protected_branch, name: 'feature', developers_can_push: true, project: project) }
before { project.team << [user, role] }
updated_permissions_matrix[role].each do |action, allowed|
context action do
subject { access.push_access_check(user, project, changes[action]) }
it { subject.allowed?.should allowed ? be_true : be_false }
end
end
end
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment