Skip to content

Commit 5145706

Browse files
author
Rubén Dávila
committed
Run custom Git hooks when creating or deleting branches through the UI. #1156
1 parent b5103a8 commit 5145706

8 files changed

+207
-29
lines changed

CHANGELOG

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ v 8.3.0 (unreleased)
66
- Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
77
- Add ignore whitespace change option to commit view
88
- Fire update hook from GitLab
9+
- Run custom Git hooks when branch is created or deleted. #1156
910

1011
v 8.2.2
1112
- Fix 404 in redirection after removing a project (Stan Hu)

app/models/repository.rb

+29-25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
require 'securerandom'
22

33
class Repository
4-
class PreReceiveError < StandardError; end
54
class CommitError < StandardError; end
65

76
include Gitlab::ShellAdapter
@@ -108,10 +107,19 @@ def find_tag(name)
108107
tags.find { |tag| tag.name == name }
109108
end
110109

111-
def add_branch(branch_name, ref)
112-
expire_branches_cache
110+
def add_branch(user, branch_name, target)
111+
oldrev = Gitlab::Git::BLANK_SHA
112+
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
113+
target = commit(target).try(:id)
114+
115+
return false unless target
116+
117+
GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
118+
rugged.branches.create(branch_name, target)
119+
end
113120

114-
gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
121+
expire_branches_cache
122+
find_branch(branch_name)
115123
end
116124

117125
def add_tag(tag_name, ref, message = nil)
@@ -120,10 +128,20 @@ def add_tag(tag_name, ref, message = nil)
120128
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
121129
end
122130

123-
def rm_branch(branch_name)
131+
def rm_branch(user, branch_name)
124132
expire_branches_cache
125133

126-
gitlab_shell.rm_branch(path_with_namespace, branch_name)
134+
branch = find_branch(branch_name)
135+
oldrev = branch.try(:target)
136+
newrev = Gitlab::Git::BLANK_SHA
137+
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
138+
139+
GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
140+
rugged.branches.delete(branch_name)
141+
end
142+
143+
expire_branches_cache
144+
true
127145
end
128146

129147
def rm_tag(tag_name)
@@ -550,7 +568,6 @@ def fetch_ref(source_path, source_ref, target_ref)
550568
def commit_with_hooks(current_user, branch)
551569
oldrev = Gitlab::Git::BLANK_SHA
552570
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
553-
gl_id = Gitlab::ShellEnv.gl_id(current_user)
554571
was_empty = empty?
555572

556573
# Create temporary ref
@@ -569,15 +586,7 @@ def commit_with_hooks(current_user, branch)
569586
raise CommitError.new('Failed to create commit')
570587
end
571588

572-
# Run GitLab pre-receive hook
573-
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo)
574-
pre_receive_hook_status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref)
575-
576-
# Run GitLab update hook
577-
update_hook = Gitlab::Git::Hook.new('update', path_to_repo)
578-
update_hook_status = update_hook.trigger(gl_id, oldrev, newrev, ref)
579-
580-
if pre_receive_hook_status && update_hook_status
589+
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
581590
if was_empty
582591
# Create branch
583592
rugged.references.create(ref, newrev)
@@ -592,16 +601,11 @@ def commit_with_hooks(current_user, branch)
592601
raise CommitError.new('Commit was rejected because branch received new push')
593602
end
594603
end
595-
596-
# Run GitLab post receive hook
597-
post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo)
598-
post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
599-
else
600-
# Remove tmp ref and return error to user
601-
rugged.references.delete(tmp_ref)
602-
603-
raise PreReceiveError.new('Commit was rejected by git hook')
604604
end
605+
rescue GitHooksService::PreReceiveError
606+
# Remove tmp ref and return error to user
607+
rugged.references.delete(tmp_ref)
608+
raise
605609
end
606610

607611
private

app/services/create_branch_service.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ def execute(branch_name, ref)
1313
return error('Branch already exists')
1414
end
1515

16-
repository.add_branch(branch_name, ref)
17-
new_branch = repository.find_branch(branch_name)
16+
new_branch = repository.add_branch(current_user, branch_name, ref)
1817

1918
if new_branch
2019
push_data = build_push_data(project, current_user, new_branch)
@@ -27,6 +26,8 @@ def execute(branch_name, ref)
2726
else
2827
error('Invalid reference name')
2928
end
29+
rescue GitHooksService::PreReceiveError
30+
error('Branch creation was rejected by Git hook')
3031
end
3132

3233
def success(branch)

app/services/delete_branch_service.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def execute(branch_name)
2424
return error('You dont have push access to repo', 405)
2525
end
2626

27-
if repository.rm_branch(branch_name)
27+
if repository.rm_branch(current_user, branch_name)
2828
push_data = build_push_data(branch)
2929

3030
EventCreateService.new.push(project, current_user, push_data)
@@ -35,6 +35,8 @@ def execute(branch_name)
3535
else
3636
error('Failed to remove branch')
3737
end
38+
rescue GitHooksService::PreReceiveError
39+
error('Branch deletion was rejected by Git hook')
3840
end
3941

4042
def error(message, return_code = 400)

app/services/files/base_service.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def execute
2626
else
2727
error("Something went wrong. Your changes were not committed")
2828
end
29-
rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex
29+
rescue Repository::CommitError, GitHooksService::PreReceiveError, ValidationError => ex
3030
error(ex.message)
3131
end
3232

app/services/git_hooks_service.rb

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class GitHooksService
2+
PreReceiveError = Class.new(StandardError)
3+
4+
def execute(user, repo_path, oldrev, newrev, ref)
5+
@repo_path = repo_path
6+
@user = Gitlab::ShellEnv.gl_id(user)
7+
@oldrev = oldrev
8+
@newrev = newrev
9+
@ref = ref
10+
11+
pre_status = run_hook('pre-receive')
12+
13+
if pre_status
14+
yield
15+
16+
run_hook('post-receive')
17+
end
18+
end
19+
20+
private
21+
22+
def run_hook(name)
23+
hook = Gitlab::Git::Hook.new(name, @repo_path)
24+
status = hook.trigger(@user, @oldrev, @newrev, @ref)
25+
26+
if !status && (name != 'post-receive')
27+
raise PreReceiveError.new("Git operation was rejected by #{name} hook")
28+
end
29+
30+
status
31+
end
32+
end

spec/models/repository_spec.rb

+100
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
include RepoHelpers
55

66
let(:repository) { create(:project).repository }
7+
let(:user) { create(:user) }
78

89
describe :branch_names_contains do
910
subject { repository.branch_names_contains(sample_commit.id) }
@@ -99,5 +100,104 @@
99100
it { expect(subject.startline).to eq(186) }
100101
it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
101102
end
103+
102104
end
105+
106+
describe :add_branch do
107+
context 'when pre hooks were successful' do
108+
it 'should run without errors' do
109+
hook = double(trigger: true)
110+
expect(Gitlab::Git::Hook).to receive(:new).twice.and_return(hook)
111+
112+
expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
113+
end
114+
115+
it 'should create the branch' do
116+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
117+
118+
branch = repository.add_branch(user, 'new_feature', 'master')
119+
120+
expect(branch.name).to eq('new_feature')
121+
end
122+
end
123+
124+
context 'when pre hooks failed' do
125+
it 'should get an error' do
126+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
127+
128+
expect do
129+
repository.add_branch(user, 'new_feature', 'master')
130+
end.to raise_error(GitHooksService::PreReceiveError)
131+
end
132+
133+
it 'should not create the branch' do
134+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
135+
136+
expect do
137+
repository.add_branch(user, 'new_feature', 'master')
138+
end.to raise_error(GitHooksService::PreReceiveError)
139+
expect(repository.find_branch('new_feature')).to be_nil
140+
end
141+
end
142+
end
143+
144+
describe :rm_branch do
145+
context 'when pre hooks were successful' do
146+
it 'should run without errors' do
147+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
148+
149+
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
150+
end
151+
152+
it 'should delete the branch' do
153+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
154+
155+
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
156+
157+
expect(repository.find_branch('feature')).to be_nil
158+
end
159+
end
160+
161+
context 'when pre hooks failed' do
162+
it 'should get an error' do
163+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
164+
165+
expect do
166+
repository.rm_branch(user, 'new_feature')
167+
end.to raise_error(GitHooksService::PreReceiveError)
168+
end
169+
170+
it 'should not delete the branch' do
171+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
172+
173+
expect do
174+
repository.rm_branch(user, 'feature')
175+
end.to raise_error(GitHooksService::PreReceiveError)
176+
expect(repository.find_branch('feature')).not_to be_nil
177+
end
178+
end
179+
end
180+
181+
describe :commit_with_hooks do
182+
context 'when pre hooks were successful' do
183+
it 'should run without errors' do
184+
expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true)
185+
186+
expect do
187+
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
188+
end.not_to raise_error
189+
end
190+
end
191+
192+
context 'when pre hooks failed' do
193+
it 'should get an error' do
194+
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
195+
196+
expect do
197+
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
198+
end.to raise_error(GitHooksService::PreReceiveError)
199+
end
200+
end
201+
end
202+
103203
end
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require 'spec_helper'
2+
3+
describe GitHooksService do
4+
include RepoHelpers
5+
6+
let(:user) { create :user }
7+
let(:project) { create :project }
8+
let(:service) { GitHooksService.new }
9+
10+
before do
11+
@blankrev = Gitlab::Git::BLANK_SHA
12+
@oldrev = sample_commit.parent_id
13+
@newrev = sample_commit.id
14+
@ref = 'refs/heads/feature'
15+
@repo_path = project.repository.path_to_repo
16+
end
17+
18+
describe '#execute' do
19+
20+
context 'when pre hooks were successful' do
21+
it 'should call post hooks' do
22+
expect(service).to receive(:run_hook).with('pre-receive').and_return(true)
23+
expect(service).to receive(:run_hook).with('post-receive').and_return(true)
24+
expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq(true)
25+
end
26+
end
27+
28+
context 'when pre hooks failed' do
29+
it 'should not call post hooks' do
30+
expect(service).to receive(:run_hook).with('pre-receive').and_return(false)
31+
expect(service).not_to receive(:run_hook).with('post-receive')
32+
33+
service.execute(user, @repo_path, @blankrev, @newrev, @ref)
34+
end
35+
end
36+
37+
end
38+
end

0 commit comments

Comments
 (0)