spec/lib/release_tools/remote_repository_spec.rb (275 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
describe ReleaseTools::RemoteRepository, :slow do
include RuggedMatchers
let(:fixture) { ReleaseFixture.new }
let(:repo_path) { File.join(Dir.tmpdir, fixture.class.repository_name) }
let(:rugged_repo) { Rugged::Repository.new(repo_path) }
let(:repo_url) { "file://#{fixture.fixture_path}" }
let(:repo_remotes) do
{ canonical: repo_url, foo: 'https://example.com/foo/bar/baz.git' }
end
def write_file(file, content)
Dir.chdir(repo_path) do
File.write(file, content)
end
end
before do
fixture.rebuild_fixture!
end
describe '.get' do
let(:remotes) do
{
dev: 'https://example.com/foo/bar/dev.git',
canonical: 'https://gitlab.com/gitlab-org/foo/gitlab.git'
}
end
it 'generates a name from the first remote' do
expect(described_class).to receive(:new).with("#{Dir.tmpdir}/dev", anything, anything)
described_class.get(remotes)
end
it 'accepts a repository name' do
expect(described_class).to receive(:new).with("#{Dir.tmpdir}/foo", anything, anything)
described_class.get(remotes, 'foo')
end
it 'passes remotes to the initializer' do
expect(described_class).to receive(:new).with(anything, remotes, anything)
described_class.get(remotes)
end
it 'accepts a :global_depth option' do
expect(described_class).to receive(:new)
.with(anything, anything, a_hash_including(global_depth: 100))
described_class.get(remotes, global_depth: 100)
end
end
describe 'initialize' do
it 'performs cleanup' do
expect_any_instance_of(described_class).to receive(:cleanup)
described_class.new(repo_path, {})
end
it 'performs a shallow clone of the repository' do
described_class.new(repo_path, repo_remotes)
# Note: Rugged has no clean way to do this, so we'll shell out
expect(`git -C #{repo_path} log --oneline | wc -l`.to_i)
.to eq(1)
end
it 'adds remotes to the repository' do
expect_any_instance_of(described_class).to receive(:remotes=)
.with(:remotes)
described_class.new('foo', :remotes)
end
it 'assigns path' do
repository = described_class.new('foo', {})
expect(repository.path).to eq 'foo'
end
end
describe '#remotes=' do
it 'assigns the canonical remote' do
remotes = { canonical: repo_url }
repository = described_class.new(repo_path, remotes)
expect(repository.canonical_remote.name).to eq(:canonical)
expect(repository.canonical_remote.url).to eq(repo_url)
end
it 'assigns remotes' do
remotes = { canonical: repo_url }
repository = described_class.new(repo_path, remotes)
expect(repository.remotes).to eq(remotes)
end
it 'adds remotes to the repository', :aggregate_failures do
remotes = {
canonical: repo_url,
foo: '/foo/bar/baz.git'
}
repository = described_class.new(repo_path, remotes)
rugged = Rugged::Repository.new(repository.path)
expect(rugged.remotes.count).to eq(2)
expect(rugged.remotes['canonical'].url).to eq(repo_url)
expect(rugged.remotes['foo'].url).to eq('/foo/bar/baz.git')
end
end
describe '#ensure_branch_exists' do
subject { described_class.get(repo_remotes) }
context 'with an existing branch' do
it 'fetches and checks out the branch with the configured global depth', :aggregate_failures do
expect(subject.logger).not_to receive(:fatal)
subject.ensure_branch_exists('branch-1', base: 'master')
expect(rugged_repo).to have_head('branch-1')
expect(rugged_repo).to have_blob('README.md').with('Sample README.md')
expect(`git -C #{repo_path} log --oneline | wc -l`.to_i).to eq(1)
end
end
context 'with a non-existing branch' do
it 'creates and checks out the branch with the configured global depth', :aggregate_failures do
expect(subject.logger).to receive(:fatal).and_call_original
subject.ensure_branch_exists('branch-2', base: 'master')
expect(rugged_repo).to have_head('branch-2')
expect(rugged_repo).to have_blob('README.md').with('Sample README.md')
expect(`git -C #{repo_path} log --oneline | wc -l`.to_i).to eq(1)
end
end
end
describe '#fetch' do
subject { described_class.get(repo_remotes) }
it 'fetches the branch with the default configured global depth' do
subject.fetch('branch-1')
expect(`git -C #{repo_path} log --oneline refs/heads/branch-1 | wc -l`.to_i).to eq(1)
end
context 'with a depth option given' do
it 'fetches the branch up to the given depth' do
subject.fetch('branch-1', depth: 2)
expect(`git -C #{repo_path} log --oneline refs/heads/branch-1 | wc -l`.to_i).to eq(2)
end
end
end
describe '#checkout_new_branch' do
subject { described_class.get(repo_remotes) }
it 'creates and checks out a new branch' do
subject.checkout_new_branch('new-branch', base: 'master')
expect(rugged_repo).to have_version('pages').at('4.5.0')
end
context 'with a given base branch' do
it 'creates and checks out a new branch based on the given base branch' do
subject.checkout_new_branch('new-branch', base: '9-1-stable')
expect(rugged_repo).to have_version('pages').at('4.4.4')
end
end
end
describe '#merge' do
subject { described_class.get(repo_remotes, global_depth: 10) }
before do
subject.fetch('master')
subject.ensure_branch_exists('branch-1', base: 'master')
subject.ensure_branch_exists('branch-2', base: 'master')
write_file('README.md', 'Nice')
subject.__send__(:commit, 'README.md', message: 'Update README.md')
subject.ensure_branch_exists('branch-1', base: 'master')
end
it 'commits the given files with the given message in the current branch' do
expect(subject.merge('branch-2', no_ff: true).status).to be_success
log = subject.__send__(:log, format: :message)
expect(log).to start_with("Merge branch 'branch-2' into branch-1\n")
expect(File.read(File.join(repo_path, 'README.md'))).to eq 'Nice'
end
it 'performs an octopus merge with an array of commits' do
write_file('foo', 'bar')
subject.__send__(:commit, 'foo', message: 'Add foo')
subject.ensure_branch_exists('branch-3', base: 'master')
expect(subject.merge(%w[branch-1 branch-2], no_ff: true).status).to be_success
log = subject.__send__(:log, format: :message)
expect(File.read(File.join(repo_path, 'README.md'))).to eq 'Nice'
expect(File.read(File.join(repo_path, 'foo'))).to eq 'bar'
expect(log).to start_with("Merge branches 'branch-1' and 'branch-2' into branch-3\n")
end
context 'in case of conflicts' do
before do
subject.ensure_branch_exists('branch-1', base: 'master')
write_file('README.md', 'branch-1')
subject.__send__(:commit, 'README.md', message: 'Update README.md')
subject.ensure_branch_exists('branch-2', base: 'master')
write_file('foo', 'foo')
subject.__send__(:commit, 'foo', message: 'Add foo')
subject.ensure_branch_exists('branch-1', base: 'master')
end
it 'fails when no recursive strategy is specified' do
expect(subject.merge('branch-2', no_ff: true).status).not_to be_success
end
end
end
describe '#pull' do
subject { described_class.get(repo_remotes) }
it 'pulls the branch with the configured depth' do
expect { subject.pull('master') }
.not_to(change { subject.__send__(:log, format: :message).lines.size })
end
context 'with a depth option given' do
it 'pulls the branch with to the given depth' do
expect { subject.pull('master', depth: 2) }
.to change { subject.__send__(:log, format: :message).lines.size }.from(1).to(2)
end
end
end
describe '#pull_from_all_remotes' do
subject { described_class.get(Hash[*repo_remotes.first]) }
context 'when there are conflicts' do
it 'raises a CannotPullError error' do
allow(subject).to receive(:conflicts?).and_return(true)
expect { subject.pull_from_all_remotes('1-9-stable') }
.to raise_error(described_class::CannotPullError)
end
end
it 'does not raise error' do
expect { subject.pull_from_all_remotes('master') }
.not_to raise_error
end
context 'with a depth option given' do
it 'pulls the branch down to the given depth' do
expect { subject.pull_from_all_remotes('master', depth: 2) }
.to change { subject.__send__(:log, format: :message).lines.size }.from(1).to(2)
end
end
end
describe '#push' do
subject { described_class.get(repo_remotes, global_depth: 10) }
let(:remote) { 'canonical' }
let(:branch) { 'master' }
context 'when in dry run mode' do
it 'always returns true' do
expect(subject.push("non-existent-remote", "non-existent-branch")).to be true
end
end
context 'when not in dry run mode' do
context 'when push is successful' do
it 'returns true' do
without_dry_run do
expect(subject.push(remote, branch)).to be true
end
end
end
context 'when feature flag is disabled' do
before do
disable_feature(:retry_git_push)
end
context 'when push is failed' do
let(:remote) { "non-existent-remote" }
it 'returns false' do
without_dry_run do
# 3 times - clone, remote, push
expect(described_class).to receive(:run_git).and_call_original.at_most(3).times
expect(subject.push(remote, branch)).to be false
end
end
end
end
context 'when feature flag is enabled' do
before do
enable_feature(:retry_git_push)
end
context 'when push is failed' do
let(:remote) { "non-existent-remote" }
it 'returns false' do
without_dry_run do
# 5 times - clone, remote, push, push, push
expect(described_class).to receive(:run_git).and_call_original.at_least(5).times
expect(subject.push(remote, branch)).to be false
end
end
end
end
end
end
describe '#push_to_all_remotes' do
subject { described_class.get(Hash[*repo_remotes.first]) }
it 'returns push status' do
expect(subject.push_to_all_remotes('master')).to eq([true])
end
end
describe '#cleanup' do
it 'removes the repository path' do
repository = described_class.new(repo_path, {})
expect(FileUtils).to receive(:rm_rf).with(repo_path, { secure: true })
repository.cleanup
end
end
describe described_class::GitCommandError do
it 'adds indented output to the error message' do
error = described_class.new("Foo", "bar\nbaz")
expect(error.message).to eq "Foo\n\n bar\n baz"
end
end
end