gitlab-org--gitlab-foss/spec/services/git_push_service_spec.rb
Yorick Peterse ffb9b3ef18
Refactor cache refreshing/expiring
This refactors repository caching so it's possible to selectively
refresh certain caches, instead of just expiring and refreshing
everything.

To allow this the various methods that were cached (e.g. "tag_count" and
"readme") use a similar pattern that makes expiring and refreshing
their data much easier.

In this new setup caches are refreshed as follows:

1. After a commit (but before running ProjectCacheWorker) we expire some
   basic caches such as the commit count and repository size.

2. ProjectCacheWorker will recalculate the commit count, repository
   size, then refresh a specific set of caches based on the list of
   files changed in a push payload.

This requires a bunch of changes to the various methods that may be
cached. For one, data should not be cached if a branch used or the
entire repository does not exist. To prevent all these methods from
handling this manually this is taken care of in
Repository#cache_method_output. Some methods still manually check for
the existence of a repository but this result is also cached.

With selective flushing implemented ProjectCacheWorker no longer uses an
exclusive lease for all of its work. Instead this worker only uses a
lease to limit the number of times the repository size is updated as
this is a fairly expensive operation.
2016-11-21 15:05:13 +01:00

612 lines
21 KiB
Ruby

require 'spec_helper'
describe GitPushService, services: true do
include RepoHelpers
let(:user) { create :user }
let(:project) { create :project }
before do
project.team << [user, :master]
@blankrev = Gitlab::Git::BLANK_SHA
@oldrev = sample_commit.parent_id
@newrev = sample_commit.id
@ref = 'refs/heads/master'
end
describe 'Push branches' do
let(:oldrev) { @oldrev }
let(:newrev) { @newrev }
subject do
execute_service(project, user, oldrev, newrev, @ref )
end
context 'new branch' do
let(:oldrev) { @blankrev }
it { is_expected.to be_truthy }
it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
it 'calls the after_create_branch hook' do
expect(project.repository).to receive(:after_create_branch)
subject
end
end
context 'existing branch' do
it { is_expected.to be_truthy }
it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
end
context 'rm branch' do
let(:newrev) { @blankrev }
it { is_expected.to be_truthy }
it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
it 'calls the after_remove_branch hook' do
expect(project.repository).to receive(:after_remove_branch)
subject
end
end
end
describe "Git Push Data" do
before do
service = execute_service(project, user, @oldrev, @newrev, @ref )
@push_data = service.push_data
@commit = project.commit(@newrev)
end
subject { @push_data }
it { is_expected.to include(object_kind: 'push') }
it { is_expected.to include(before: @oldrev) }
it { is_expected.to include(after: @newrev) }
it { is_expected.to include(ref: @ref) }
it { is_expected.to include(user_id: user.id) }
it { is_expected.to include(user_name: user.name) }
it { is_expected.to include(project_id: project.id) }
context "with repository data" do
subject { @push_data[:repository] }
it { is_expected.to include(name: project.name) }
it { is_expected.to include(url: project.url_to_repo) }
it { is_expected.to include(description: project.description) }
it { is_expected.to include(homepage: project.web_url) }
end
context "with commits" do
subject { @push_data[:commits] }
it { is_expected.to be_an(Array) }
it 'has 1 element' do
expect(subject.size).to eq(1)
end
context "the commit" do
subject { @push_data[:commits].first }
it { is_expected.to include(id: @commit.id) }
it { is_expected.to include(message: @commit.safe_message) }
it { is_expected.to include(timestamp: @commit.date.xmlschema) }
it do
is_expected.to include(
url: [
Gitlab.config.gitlab.url,
project.namespace.to_param,
project.to_param,
'commit',
@commit.id
].join('/')
)
end
context "with a author" do
subject { @push_data[:commits].first[:author] }
it { is_expected.to include(name: @commit.author_name) }
it { is_expected.to include(email: @commit.author_email) }
end
end
end
end
describe "Push Event" do
before do
service = execute_service(project, user, @oldrev, @newrev, @ref )
@event = Event.find_by_action(Event::PUSHED)
@push_data = service.push_data
end
it { expect(@event).not_to be_nil }
it { expect(@event.project).to eq(project) }
it { expect(@event.action).to eq(Event::PUSHED) }
it { expect(@event.data).to eq(@push_data) }
context "Updates merge requests" do
it "when pushing a new branch for the first time" do
expect(UpdateMergeRequestsWorker).to receive(:perform_async).
with(project.id, user.id, @blankrev, 'newrev', 'refs/heads/master')
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
end
end
end
describe "Updates git attributes" do
context "for default branch" do
it "calls the copy attributes method for the first push to the default branch" do
expect(project.repository).to receive(:copy_gitattributes).with('master')
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master')
end
it "calls the copy attributes method for changes to the default branch" do
expect(project.repository).to receive(:copy_gitattributes).with('refs/heads/master')
execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master')
end
end
context "for non-default branch" do
before do
# Make sure the "default" branch is different
allow(project).to receive(:default_branch).and_return('not-master')
end
it "does not call copy attributes method" do
expect(project.repository).not_to receive(:copy_gitattributes)
execute_service(project, user, @oldrev, @newrev, @ref)
end
end
end
describe "Webhooks" do
context "execute webhooks" do
it "when pushing a branch for the first time" do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
it "when pushing a branch for the first time with default branch protection disabled" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).to be_empty
end
it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
it "when pushing a branch for the first time with an existing branch permission configured" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
create(:protected_branch, :no_one_can_push, :developers_can_merge, project: project, name: 'master')
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
expect_any_instance_of(ProtectedBranches::CreateService).not_to receive(:execute)
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS])
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it "when pushing new commits to existing branch" do
expect(project).to receive(:execute_hooks)
execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master' )
end
end
end
describe "cross-reference notes" do
let(:issue) { create :issue, project: project }
let(:commit_author) { create :user }
let(:commit) { project.commit }
before do
project.team << [commit_author, :developer]
project.team << [user, :developer]
allow(commit).to receive_messages(
safe_message: "this commit \n mentions #{issue.to_reference}",
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email
)
allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
and_return(commit)
allow(project.repository).to receive(:commits_between).and_return([commit])
end
it "creates a note if a pushed commit mentions an issue" do
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author)
execute_service(project, user, @oldrev, @newrev, @ref )
end
it "only creates a cross-reference note if one doesn't already exist" do
SystemNoteService.cross_reference(issue, commit, user)
expect(SystemNoteService).not_to receive(:cross_reference).with(issue, commit, commit_author)
execute_service(project, user, @oldrev, @newrev, @ref )
end
it "defaults to the pushing user if the commit's author is not known" do
allow(commit).to receive_messages(
author_name: 'unknown name',
author_email: 'unknown@email.com'
)
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, user)
execute_service(project, user, @oldrev, @newrev, @ref )
end
it "finds references in the first push to a non-default branch" do
allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([])
allow(project.repository).to receive(:commits_between).with("master", @newrev).and_return([commit])
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author)
execute_service(project, user, @blankrev, @newrev, 'refs/heads/other' )
end
end
describe "issue metrics" do
let(:issue) { create :issue, project: project }
let(:commit_author) { create :user }
let(:commit) { project.commit }
let(:commit_time) { Time.now }
before do
project.team << [commit_author, :developer]
project.team << [user, :developer]
allow(commit).to receive_messages(
safe_message: "this commit \n mentions #{issue.to_reference}",
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email,
committed_date: commit_time
)
allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
and_return(commit)
allow(project.repository).to receive(:commits_between).and_return([commit])
end
context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do
it 'sets the metric for referenced issues' do
execute_service(project, user, @oldrev, @newrev, @ref)
expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_like_time(commit_time)
end
it 'does not set the metric for non-referenced issues' do
non_referenced_issue = create(:issue, project: project)
execute_service(project, user, @oldrev, @newrev, @ref)
expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil
end
end
end
describe "closing issues from pushed commits containing a closing reference" do
let(:issue) { create :issue, project: project }
let(:other_issue) { create :issue, project: project }
let(:commit_author) { create :user }
let(:closing_commit) { project.commit }
before do
allow(closing_commit).to receive_messages(
issue_closing_regex: /^([Cc]loses|[Ff]ixes) #\d+/,
safe_message: "this is some work.\n\ncloses ##{issue.iid}",
author_name: commit_author.name,
author_email: commit_author.email
)
allow(project.repository).to receive(:commits_between).
and_return([closing_commit])
allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
and_return(closing_commit)
project.team << [commit_author, :master]
end
context "to default branches" do
it "closes issues" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(Issue.find(issue.id)).to be_closed
end
it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit)
execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
it "doesn't create additional cross-reference notes" do
expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
it "doesn't close issues when external issue tracker is in use" do
allow_any_instance_of(Project).to receive(:default_issues_tracker?).
and_return(false)
external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern)
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
# The push still shouldn't create cross-reference notes.
expect do
execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' )
end.not_to change { Note.where(project_id: project.id, system: true).count }
end
end
context "to non-default branches" do
before do
# Make sure the "default" branch is different
allow(project).to receive(:default_branch).and_return('not-master')
end
it "creates cross-reference notes" do
expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author)
execute_service(project, user, @oldrev, @newrev, @ref )
end
it "doesn't close issues" do
execute_service(project, user, @oldrev, @newrev, @ref )
expect(Issue.find(issue.id)).to be_opened
end
end
context "for jira issue tracker" do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
before do
# project.create_jira_service doesn't seem to invalidate the cache here
project.has_external_issue_tracker = true
jira_service_settings
stub_jira_urls("JIRA-1")
allow(closing_commit).to receive_messages({
issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern),
safe_message: message,
author_name: commit_author.name,
author_email: commit_author.email
})
allow(project.repository).to receive_messages(commits_between: [closing_commit])
end
after do
jira_tracker.destroy!
end
context "mentioning an issue" do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
it "initiates one api call to jira server to mention the issue" do
execute_service(project, user, @oldrev, @newrev, @ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: /mentioned this issue in/
).once
end
end
context "closing an issue" do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
before do
open_issue = JIRA::Resource::Issue.new(jira_tracker.client, attrs: { "id" => "JIRA-1" })
closed_issue = open_issue.dup
allow(open_issue).to receive(:resolution).and_return(false)
allow(closed_issue).to receive(:resolution).and_return(true)
allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-1")
end
context "using right markdown" do
it "initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
end
it "initiates one api call to jira server to comment on the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
end
end
context "using wrong markdown" do
let(:message) { "this is some work.\n\ncloses #1" }
it "does not initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
end
it "does not initiates one api call to jira server to comment on the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
end
end
end
end
end
describe "empty project" do
let(:project) { create(:project_empty_repo) }
let(:new_ref) { 'refs/heads/feature'}
before do
allow(project).to receive(:default_branch).and_return('feature')
expect(project).to receive(:change_head) { 'feature'}
end
it 'push to first branch updates HEAD' do
execute_service(project, user, @blankrev, @newrev, new_ref )
end
end
describe "housekeeping" do
let(:housekeeping) { Projects::HousekeepingService.new(project) }
before do
# Flush any raw Redis data stored by the housekeeping code.
Gitlab::Redis.with { |conn| conn.flushall }
allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping)
end
after do
Gitlab::Redis.with { |conn| conn.flushall }
end
it 'does not perform housekeeping when not needed' do
expect(housekeeping).not_to receive(:execute)
execute_service(project, user, @oldrev, @newrev, @ref)
end
context 'when housekeeping is needed' do
before do
allow(housekeeping).to receive(:needed?).and_return(true)
end
it 'performs housekeeping' do
expect(housekeeping).to receive(:execute)
execute_service(project, user, @oldrev, @newrev, @ref)
end
it 'does not raise an exception' do
allow(housekeeping).to receive(:try_obtain_lease).and_return(false)
execute_service(project, user, @oldrev, @newrev, @ref)
end
end
it 'increments the push counter' do
expect(housekeeping).to receive(:increment!)
execute_service(project, user, @oldrev, @newrev, @ref)
end
end
describe '#update_caches' do
let(:service) do
described_class.new(project,
user,
oldrev: sample_commit.parent_id,
newrev: sample_commit.id,
ref: 'refs/heads/master')
end
context 'on the default branch' do
before do
allow(service).to receive(:is_default_branch?).and_return(true)
end
it 'flushes the caches of any special files that have been changed' do
commit = double(:commit)
diff = double(:diff, new_path: 'README.md')
expect(commit).to receive(:raw_diffs).with(deltas_only: true).
and_return([diff])
service.push_commits = [commit]
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, %i(readme))
service.update_caches
end
end
context 'on a non-default branch' do
before do
allow(service).to receive(:is_default_branch?).and_return(false)
end
it 'does not flush any conditional caches' do
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, []).
and_call_original
service.update_caches
end
end
end
def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute
service
end
end