test/unit/repository_test.rb
# -*- encoding : utf-8 -*-
require File.expand_path('../test_helper', __FILE__)
describe Gitdocs::Repository do
before do
FileUtils.rm_rf(GitFactory.working_directory)
GitFactory.init(:local)
GitFactory.init_bare(:remote)
end
let(:repository) { Gitdocs::Repository.new(path_or_share) }
# Default Share for the repository object, which can be overridden by the
# tests when necessary.
let(:path_or_share) do
stub(
path: expand_path(:local),
remote_name: 'origin',
branch_name: 'master'
)
end
describe 'initialize' do
subject { repository }
describe 'with a missing path' do
let(:path_or_share) { expand_path(:missing) }
it { subject.must_be_kind_of Gitdocs::Repository }
it { subject.valid?.must_equal false }
it { subject.invalid_reason.must_equal :directory_missing }
end
describe 'with a path that is not a repository' do
let(:path_or_share) { expand_path(:not_a_repo) }
before { FileUtils.mkdir_p(path_or_share) }
it { subject.must_be_kind_of Gitdocs::Repository }
it { subject.valid?.must_equal false }
it { subject.invalid_reason.must_equal :no_repository }
end
describe 'with a string path that is a repository' do
let(:path_or_share) { expand_path(:local) }
it { subject.must_be_kind_of Gitdocs::Repository }
it { subject.valid?.must_equal true }
it { subject.invalid_reason.must_be_nil }
it { subject.instance_variable_get(:@rugged).wont_be_nil }
it { subject.instance_variable_get(:@grit).wont_be_nil }
end
describe 'with a share that is a repository' do
it { subject.must_be_kind_of Gitdocs::Repository }
it { subject.valid?.must_equal true }
it { subject.invalid_reason.must_be_nil }
it { subject.instance_variable_get(:@rugged).wont_be_nil }
it { subject.instance_variable_get(:@grit).wont_be_nil }
it { subject.instance_variable_get(:@remote_name).must_equal 'origin' }
it { subject.instance_variable_get(:@branch_name).must_equal 'master' }
end
end
describe '.clone' do
subject { Gitdocs::Repository.clone('tmp/unit/clone', remote) }
describe 'with invalid remote' do
let(:remote) { expand_path(:invalid) }
it { assert_raises(RuntimeError) { subject } }
end
describe 'with valid remote' do
let(:remote) { expand_path(:remote) }
it { subject.must_be_kind_of Gitdocs::Repository }
it { subject.valid?.must_equal true }
end
end
describe '#root' do
subject { repository.root }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when valid' do
it { subject.must_equal expand_path(:local) }
end
end
describe '#available_remotes' do
subject { repository.available_remotes }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when valid' do
it { subject.must_equal [] }
end
end
describe '#available_branches' do
subject { repository.available_branches }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when valid' do
it { subject.must_equal [] }
end
end
describe '#current_oid' do
subject { repository.current_oid }
describe 'no commits' do
it { subject.must_equal nil }
end
describe 'has commits' do
before { @head_oid = commit('touch_me', '') }
it { subject.must_equal @head_oid }
end
end
describe '#dirty?' do
subject { repository.dirty? }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_equal false }
end
describe 'when no existing commits' do
describe 'and no new files' do
it { subject.must_equal false }
end
describe 'and new files' do
before { write('file1', 'foobar') }
it { subject.must_equal true }
end
describe 'and new empty directory' do
before { GitFactory.mkdir(:local, 'directory') }
it { subject.must_equal true }
end
end
describe 'when commits exist' do
before { commit('file1', 'foobar') }
describe 'and no changes' do
it { subject.must_equal false }
end
describe 'add empty directory' do
before { GitFactory.mkdir(:local, 'directory') }
it { subject.must_equal false }
end
describe 'add file' do
before { write('file2', 'foobar') }
it { subject.must_equal true }
end
describe 'modify existing file' do
before { write('file1', 'deadbeef') }
it { subject.must_equal true }
end
describe 'delete file' do
before { GitFactory.rm(:local, 'file1') }
it { subject.must_equal true }
end
end
end
describe '#need_sync' do
subject { repository.need_sync? }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_equal false }
end
describe 'when no remotes' do
it { subject.must_equal false }
end
describe 'when no remote commits' do
before { clone_remote }
describe 'no local commits' do
it { subject.must_equal false }
end
describe 'local commits' do
before { commit('file1', 'beef') }
it { subject.must_equal true }
end
end
describe 'when remote commits' do
before { clone_remote_with_commit }
describe 'no local commits' do
it { subject.must_equal false }
end
describe 'new local commit' do
before { commit('file2', 'beef') }
it { subject.must_equal true }
end
describe 'new remote commit' do
before do
bare_commit('file3', 'dead')
repository.fetch
end
it { subject.must_equal true }
end
describe 'new local and remote commit' do
before do
bare_commit('file3', 'dead')
repository.fetch
commit('file4', 'beef')
end
it { subject.must_equal true }
end
end
end
describe '#grep' do
subject { repository.grep('foo') { |file, context| @grep_result << "#{file} #{context}" } }
before { @grep_result = [] }
describe 'timeout' do
before do
Grit::Repo
.any_instance
.stubs(:remote_fetch)
.raises(Grit::Git::GitTimeout.new)
end
it { subject ; @grep_result.must_equal([]) }
it { subject.must_equal '' }
end
describe 'command failure' do
before do
Grit::Repo
.any_instance
.stubs(:remote_fetch)
.raises(Grit::Git::CommandFailed.new('', 1, 'grep error output'))
end
it { subject ; @grep_result.must_equal([]) }
it { subject.must_equal '' }
end
describe 'success' do
before do
commit('file1', 'foo')
commit('file2', 'beef')
commit('file3', 'foobar')
commit('file4', "foo\ndead\nbeef\nfoobar")
end
it { subject ; @grep_result.must_equal(['file1 foo', 'file3 foobar', 'file4 foo', 'file4 foobar']) }
it { subject.must_equal("file1:foo\nfile3:foobar\nfile4:foo\nfile4:foobar\n") }
end
end
describe '#fetch' do
subject { repository.fetch }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when no remote' do
it { subject.must_equal :no_remote }
end
describe 'with remote' do
before { clone_remote }
describe 'and times out' do
before do
Grit::Repo
.any_instance
.stubs(:remote_fetch)
.raises(Grit::Git::GitTimeout.new)
end
it do
assert_raises(
Gitdocs::Repository::FetchError,
"Fetch timed out for #{expand_path(:local)}"
) { subject }
end
end
describe 'and command fails' do
before do
Grit::Repo
.any_instance
.stubs(:remote_fetch)
.raises(Grit::Git::CommandFailed.new('', 1, 'fetch error output'))
end
it do
assert_raises(
Gitdocs::Repository::FetchError,
'fetch error output'
) { subject }
end
end
describe 'and success' do
before { bare_commit('file1', 'deadbeef') }
it { subject.must_equal :ok }
describe 'side effects' do
before { subject }
it { GitInspector.remote_oid(:local).wont_be_nil }
end
end
end
end
describe '#merge' do
subject { repository.merge }
before { repository.stubs(:author_count).returns(:author_counts) }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when no remote' do
it { subject.must_equal :no_remote }
end
describe 'has remote but nothing to merge' do
before { clone_remote }
it { subject.must_equal :ok }
end
describe 'has remote and times out' do
before do
clone_remote
bare_commit('file1', 'deadbeef')
repository.fetch
Grit::Git
.any_instance
.stubs(:merge)
.raises(Grit::Git::GitTimeout.new)
end
it do
assert_raises(
Gitdocs::Repository::MergeError,
"Merge timed out for #{expand_path(:local)}"
) { subject }
end
end
describe 'and fails, but does not conflict' do
before do
clone_remote
bare_commit('file1', 'deadbeef')
repository.fetch
Grit::Git
.any_instance
.stubs(:merge)
.raises(Grit::Git::CommandFailed.new('', 1, 'merge error output'))
end
it do
assert_raises(
Gitdocs::Repository::MergeError,
'merge error output'
) { subject }
end
end
describe 'and there is a conflict' do
before do
clone_remote_with_commit
bare_commit('file1', 'dead')
commit('file1', 'beef')
repository.fetch
end
it { subject.must_equal ['file1'] }
describe 'side effects' do
before { subject }
it { GitInspector.commit_count(:local).must_equal 2 }
it { GitInspector.file_count(:local).must_equal 3 }
it { GitInspector.file_content(:local, 'file1 (f6ea049 original)').must_equal 'foobar' }
it { GitInspector.file_content(:local, 'file1 (18ed963)').must_equal 'beef' }
it { GitInspector.file_content(:local, 'file1 (7bfce5c)').must_equal 'dead' }
end
end
describe 'and there is a conflict, with additional files' do
before do
clone_remote_with_commit
bare_commit('file1', 'dead')
bare_commit('file2', 'foo')
commit('file1', 'beef')
repository.fetch
end
it { subject.must_equal ['file1'] }
describe 'side effects' do
before { subject }
it { GitInspector.commit_count(:local).must_equal 2 }
it { GitInspector.file_count(:local).must_equal 3 }
it { GitInspector.file_content(:local, 'file1 (f6ea049 original)').must_equal 'foobar' }
it { GitInspector.file_content(:local, 'file1 (18ed963)').must_equal 'beef' }
it { GitInspector.file_content(:local, 'file2').must_equal 'foo' }
end
end
describe 'and there are non-conflicted local commits' do
before do
clone_remote_with_commit
commit('file1', 'beef')
repository.fetch
end
it { subject.must_equal :author_counts }
describe 'side effects' do
before { subject }
it { GitInspector.file_count(:local).must_equal 1 }
it { GitInspector.commit_count(:local).must_equal 2 }
end
end
describe 'when new remote commits are merged' do
before do
clone_remote_with_commit
bare_commit('file2', 'deadbeef')
repository.fetch
end
it { subject.must_equal :author_counts }
describe 'side effects' do
before { subject }
it { GitInspector.file_exist?(:local, 'file2').must_equal true }
it { GitInspector.commit_count(:local).must_equal 2 }
end
end
end
describe '#commit' do
subject { repository.commit }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when valid' do
before do
Gitdocs::Repository::Committer.expects(:new).returns(committer = mock)
committer.expects(:commit).returns(:result)
end
it { subject.must_equal(:result) }
end
end
describe '#push' do
subject { repository.push }
before { repository.stubs(:author_count).returns(:author_counts) }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when no remote' do
it { subject.must_equal :no_remote }
end
describe 'remote exists with no commits' do
before { clone_remote }
describe 'and no local commits' do
it { subject.must_equal :nothing }
describe 'side effects' do
before { subject }
it { GitInspector.commit_count(:remote).must_equal 0 }
end
end
describe 'and a local commit' do
before { commit('file2', 'foobar') }
describe 'and the push fails' do
# Simulate an error occurring during the push
before do
Grit::Git
.any_instance
.stubs(:push)
.raises(Grit::Git::CommandFailed.new('', 1, 'error message'))
end
it { subject.must_equal 'error message' }
end
describe 'and the push succeeds' do
it { subject.must_equal :author_counts }
describe 'side effects' do
before { subject }
it { GitInspector.commit_count(:remote).must_equal 1 }
end
end
end
end
describe 'remote exists with commits' do
before { clone_remote_with_commit }
describe 'and no local commits' do
it { subject.must_equal :nothing }
describe 'side effects' do
before { subject }
it { GitInspector.commit_count(:remote).must_equal 1 }
end
end
describe 'and a local commit' do
before { commit('file2', 'foobar') }
describe 'and the push fails' do
# Simulate an error occurring during the push
before do
Grit::Git
.any_instance
.stubs(:push)
.raises(Grit::Git::CommandFailed.new('', 1, 'error message'))
end
it { subject.must_equal 'error message' }
end
describe 'and the push conflicts' do
before { bare_commit('file2', 'dead') }
it { subject.must_equal :conflict }
describe 'side effects' do
before { subject }
it { GitInspector.commit_count(:remote).must_equal 2 }
end
end
describe 'and the push succeeds' do
it { subject.must_equal :author_counts }
describe 'side effects' do
before { subject }
it { GitInspector.commit_count(:remote).must_equal 2 }
end
end
end
end
end
describe '#author_count' do
subject { repository.author_count(last_oid) }
describe 'no commits' do
let(:last_oid) { nil }
it { subject.must_equal({}) }
end
describe 'commits' do
before do
@intermediate_oid = commit('touch_me', 'first', 0)
commit('touch_me', 'second', 0)
commit('touch_me', 'third', 1)
commit('touch_me', 'fourth', 0)
end
describe 'all' do
let(:last_oid) { nil }
it { subject.must_equal(GitFactory.authors[0] => 3, GitFactory.authors[1] => 1) }
end
describe 'some' do
let(:last_oid) { @intermediate_oid }
it { subject.must_equal(GitFactory.authors[0] => 2, GitFactory.authors[1] => 1) }
end
describe 'missing oid' do
let(:last_oid) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }
it { subject.must_equal({}) }
end
end
end
describe '#synchronize' do
subject { repository.synchronize(type) }
describe 'invalid repository' do
let(:type) { :noop }
it { subject.must_equal(merge: nil, push: nil) }
end
describe 'fetch only' do
let(:type) { 'fetch' }
describe 'fetch failure' do
before do
repository.expects(:fetch).raises(Gitdocs::Repository::FetchError)
end
it { subject.must_equal(merge: nil, push: nil) }
end
describe 'success' do
before { repository.expects(:fetch) }
it { subject.must_equal(merge: nil, push: nil) }
end
end
describe 'full' do
let(:type) { 'full' }
describe 'fetch failure' do
before do
repository.expects(:commit)
repository.expects(:fetch).raises(Gitdocs::Repository::FetchError)
end
it { subject.must_equal(merge: nil, push: nil) }
end
describe 'merge failure' do
before do
repository.expects(:commit)
repository.expects(:fetch)
repository.expects(:merge).raises(Gitdocs::Repository::MergeError, 'error')
end
it { subject.must_equal(merge: 'error', push: nil) }
end
describe 'success' do
before do
repository.expects(:commit)
repository.expects(:fetch)
repository.expects(:merge).returns('merge')
repository.expects(:push).returns('push')
end
it { subject.must_equal(merge: 'merge', push: 'push') }
end
end
end
describe '#write_commit_message' do
subject { repository.write_commit_message(:message) }
describe 'when invalid' do
let(:path_or_share) { 'tmp/unit/missing' }
it { subject.must_be_nil }
end
describe 'when valid' do
before do
Gitdocs::Repository::Committer.expects(:new).returns(committer = mock)
committer.expects(:write_commit_message).with(:message).returns(:result)
end
it { subject.must_equal(:result) }
end
end
describe '#commits_for' do
subject { repository.commits_for('directory/file', 2) }
before do
commit('directory0/file0', '', 0)
commit('directory/file', 'foo', 0)
@commit2 = commit('directory/file', 'bar', 1)
@commit3 = commit('directory/file', 'beef', 1)
end
it { subject.map(&:oid).must_equal([@commit3, @commit2]) }
end
describe '#last_commit_for' do
subject { repository.last_commit_for('directory/file') }
before do
commit('directory/file', 'foo', 0)
commit('directory/file', 'bar', 1)
@commit3 = commit('directory/file', 'beef', 1)
end
it { subject.oid.must_equal(@commit3) }
end
describe '#blob_at' do
subject { repository.blob_at('directory/file', @commit) }
before do
commit('directory/file', 'foo')
@commit = commit('directory/file', 'bar', 1)
commit('directory/file', 'beef', 1)
end
it { subject.text.must_equal('bar') }
end
##############################################################################
private
# @param (see GitFactory.expand_path)
# @return [String]
def expand_path(*args)
GitFactory.expand_path(*args)
end
# @return (see GitFactory.clone)
def clone_remote
GitFactory.clone(:remote, 'local')
end
# @return (see GitFactory.clone)
def clone_remote_with_commit
bare_commit('file1', 'foobar')
clone_remote
end
# @param [String] filename
# @param [String] content
#
# @return [void]
def write(filename, content)
GitFactory.write(:local, filename, content)
end
# @param (see GitFactory.commit)
# @return (see GitFactory.commit)
def commit(*args)
GitFactory.commit(:local, *args)
end
# @param (see GitFactory.bare_commit)
# @return (see GitFactory.bare_commit)
def bare_commit(*args)
GitFactory.bare_commit(:remote, *args)
end
end