gitdocs 0.5.0.pre1 → 0.5.0.pre2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/CHANGELOG +6 -2
- data/README.md +1 -0
- data/gitdocs.gemspec +3 -1
- data/lib/gitdocs.rb +4 -0
- data/lib/gitdocs/cli.rb +22 -21
- data/lib/gitdocs/configuration.rb +0 -14
- data/lib/gitdocs/manager.rb +2 -13
- data/lib/gitdocs/notifier.rb +38 -0
- data/lib/gitdocs/repository.rb +348 -0
- data/lib/gitdocs/runner.rb +69 -192
- data/lib/gitdocs/server.rb +20 -22
- data/lib/gitdocs/version.rb +1 -1
- data/lib/gitdocs/views/settings.haml +3 -3
- data/test/configuration_test.rb +2 -0
- data/test/integration/full_sync_test.rb +67 -0
- data/test/integration/share_management_test.rb +46 -0
- data/test/integration/status_test.rb +19 -0
- data/test/integration/test_helper.rb +130 -0
- data/test/notifier_test.rb +68 -0
- data/test/repository_test.rb +578 -0
- data/test/runner_test.rb +133 -16
- data/test/test_helper.rb +0 -1
- metadata +46 -4
data/test/configuration_test.rb
CHANGED
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe 'fully synchronizing repositories' do
|
4
|
+
before do
|
5
|
+
git_clone_and_gitdocs_add(git_init_remote, 'clone1', 'clone2', 'clone3')
|
6
|
+
|
7
|
+
# TODO: apply this configuration through the CLI in future
|
8
|
+
configuration = Gitdocs::Configuration.new
|
9
|
+
configuration.shares.each do |share|
|
10
|
+
share.update_attributes(polling_interval: 0.1, notification: false)
|
11
|
+
end
|
12
|
+
configuration.global.update_attributes(load_browser_on_startup: false)
|
13
|
+
|
14
|
+
start_cmd = 'gitdocs start --debug --pid=gitdocs.pid --port 7777'
|
15
|
+
run(start_cmd, 15)
|
16
|
+
assert_success(true)
|
17
|
+
assert_partial_output('Started gitdocs', output_from(start_cmd))
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should sync new files' do
|
21
|
+
write_file('clone1/newfile', 'testing')
|
22
|
+
|
23
|
+
sleep 3
|
24
|
+
check_file_presence(['clone2/newfile', 'clone3/newfile'], true)
|
25
|
+
check_exact_file_content('clone1/newfile', 'testing')
|
26
|
+
check_exact_file_content('clone2/newfile', 'testing')
|
27
|
+
check_exact_file_content('clone3/newfile', 'testing')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should sync changes to an existing file' do
|
31
|
+
write_file('clone1/file', 'testing')
|
32
|
+
sleep(3) # Allow the initial files to sync
|
33
|
+
|
34
|
+
append_to_file('clone3/file', "\nfoobar")
|
35
|
+
|
36
|
+
sleep(3)
|
37
|
+
check_exact_file_content('clone1/file', "testing\nfoobar")
|
38
|
+
check_exact_file_content('clone2/file', "testing\nfoobar")
|
39
|
+
check_exact_file_content('clone3/file', "testing\nfoobar")
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should sync empty directories' do
|
43
|
+
in_current_dir { _mkdir('clone1/empty_dir') }
|
44
|
+
sleep(3)
|
45
|
+
|
46
|
+
check_directory_presence(
|
47
|
+
['clone1/empty_dir', 'clone2/empty_dir', 'clone3/empty_dir'],
|
48
|
+
true
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should mark unresolvable conflicts' do
|
53
|
+
write_file('clone1/file', 'testing')
|
54
|
+
sleep(3) # Allow the initial files to sync
|
55
|
+
|
56
|
+
append_to_file('clone2/file', 'foobar')
|
57
|
+
append_to_file('clone3/file', 'deadbeef')
|
58
|
+
|
59
|
+
sleep(3)
|
60
|
+
in_current_dir do
|
61
|
+
# Remember expected file counts include '.', '..', and '.git'
|
62
|
+
assert_equal(6, Dir.entries('clone1').count)
|
63
|
+
assert_equal(6, Dir.entries('clone2').count)
|
64
|
+
assert_equal(6, Dir.entries('clone3').count)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe 'Manage which shares are being watched' do
|
4
|
+
it 'should add a local repository' do
|
5
|
+
git_init_local
|
6
|
+
gitdocs_add
|
7
|
+
gitdocs_status
|
8
|
+
assert_gitdocs_status_contains(abs_current_dir('local'))
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should add a remote repository' do
|
12
|
+
git_init_remote
|
13
|
+
abs_remote_path = abs_current_dir('remote')
|
14
|
+
cmd = "gitdocs create local #{abs_remote_path} --pid=gitdocs.pid"
|
15
|
+
run_simple(cmd, true, 15)
|
16
|
+
assert_success(true)
|
17
|
+
assert_partial_output("Added path local to doc list", output_from(cmd))
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should remove a share' do
|
21
|
+
git_init_local
|
22
|
+
gitdocs_add
|
23
|
+
|
24
|
+
cmd = 'gitdocs rm local --pid=gitdocs.pid'
|
25
|
+
run_simple(cmd, true, 15)
|
26
|
+
assert_success(true)
|
27
|
+
assert_partial_output("Removed path local from doc list", output_from(cmd))
|
28
|
+
|
29
|
+
gitdocs_status
|
30
|
+
assert_gitdocs_status_not_contain(abs_current_dir('local'))
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should clear all existing shares' do
|
34
|
+
['local1', 'local2', 'local3'].each { |x| git_init_local(x) ; gitdocs_add(x) }
|
35
|
+
|
36
|
+
cmd = 'gitdocs clear --pid=gitdocs.pid'
|
37
|
+
run_simple(cmd, true, 15)
|
38
|
+
assert_success(true)
|
39
|
+
assert_partial_output('Cleared paths from gitdocs', output_from(cmd))
|
40
|
+
|
41
|
+
gitdocs_status
|
42
|
+
assert_gitdocs_status_not_contain(abs_current_dir('local1'))
|
43
|
+
assert_gitdocs_status_not_contain(abs_current_dir('local2'))
|
44
|
+
assert_gitdocs_status_not_contain(abs_current_dir('local3'))
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe 'CLI with display daemon and share status' do
|
4
|
+
it 'should display information about the daemon' do
|
5
|
+
gitdocs_status
|
6
|
+
assert_gitdocs_status_contains(Gitdocs::VERSION)
|
7
|
+
assert_gitdocs_status_contains('Running: false')
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should display information about the shares' do
|
11
|
+
git_clone_and_gitdocs_add(git_init_remote, 'clone1', 'clone2', 'clone3')
|
12
|
+
|
13
|
+
gitdocs_status
|
14
|
+
|
15
|
+
assert_gitdocs_status_contains(abs_current_dir('clone1'))
|
16
|
+
assert_gitdocs_status_contains(abs_current_dir('clone2'))
|
17
|
+
assert_gitdocs_status_contains(abs_current_dir('clone3'))
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../../lib')
|
4
|
+
require 'gitdocs'
|
5
|
+
require 'aruba'
|
6
|
+
require 'aruba/api'
|
7
|
+
|
8
|
+
module MiniTest::Aruba
|
9
|
+
class ArubaApiWrapper
|
10
|
+
include Aruba::Api
|
11
|
+
end
|
12
|
+
|
13
|
+
def aruba
|
14
|
+
@aruba ||= ArubaApiWrapper.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def run(*args)
|
18
|
+
if args.length == 0
|
19
|
+
super
|
20
|
+
else
|
21
|
+
aruba.run(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method, *args, &block)
|
26
|
+
aruba.send(method, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def before_setup
|
30
|
+
super
|
31
|
+
original = (ENV['PATH'] || '').split(File::PATH_SEPARATOR)
|
32
|
+
set_env('PATH', ([File.expand_path('bin')] + original).join(File::PATH_SEPARATOR))
|
33
|
+
FileUtils.rm_rf(current_dir)
|
34
|
+
end
|
35
|
+
|
36
|
+
def after_teardown
|
37
|
+
super
|
38
|
+
restore_env
|
39
|
+
processes.clear
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Helper
|
44
|
+
include MiniTest::Aruba
|
45
|
+
|
46
|
+
def before_setup
|
47
|
+
super
|
48
|
+
set_env('HOME', abs_current_dir)
|
49
|
+
ENV['TEST'] = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def after_teardown
|
53
|
+
super
|
54
|
+
|
55
|
+
terminate_processes!
|
56
|
+
prep_for_fs_check do
|
57
|
+
next unless File.exists?('gitdocs.pid')
|
58
|
+
|
59
|
+
pid = IO.read('gitdocs.pid').to_i
|
60
|
+
Process.kill('KILL', pid)
|
61
|
+
begin
|
62
|
+
Process.wait(pid)
|
63
|
+
rescue SystemCallError
|
64
|
+
# This means that the process is already gone.
|
65
|
+
# Nothing to do.
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @overload abs_current_dir
|
71
|
+
# @return [String] absolute path for current_dir
|
72
|
+
# @overload abs_current_dir(relative_path)
|
73
|
+
# @param [String] relative_path to the current directory
|
74
|
+
# @return [String] the absolute path
|
75
|
+
def abs_current_dir(relative_path = nil)
|
76
|
+
return File.absolute_path(File.join(current_dir)) unless relative_path
|
77
|
+
File.absolute_path(File.join(current_dir, relative_path))
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String] the absolute path for the repository
|
81
|
+
def git_init_local(path = 'local')
|
82
|
+
abs_path = abs_current_dir(path)
|
83
|
+
Rugged::Repository.init_at(abs_path)
|
84
|
+
abs_path
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [String] the absolute path for the repository
|
88
|
+
def git_init_remote(path = 'remote')
|
89
|
+
abs_path = abs_current_dir(path)
|
90
|
+
Rugged::Repository.init_at(abs_path, :bare)
|
91
|
+
abs_path
|
92
|
+
end
|
93
|
+
|
94
|
+
def gitdocs_add(path = 'local')
|
95
|
+
add_cmd = "gitdocs add #{path} --pid=gitdocs.pid"
|
96
|
+
run_simple(add_cmd, true, 15)
|
97
|
+
assert_success(true)
|
98
|
+
assert_partial_output("Added path #{path} to doc list", output_from(add_cmd))
|
99
|
+
end
|
100
|
+
|
101
|
+
def git_clone_and_gitdocs_add(remote_path, *clone_paths)
|
102
|
+
clone_paths.each do |clone_path|
|
103
|
+
repo = Rugged::Repository.clone_at(
|
104
|
+
"file://#{remote_path}",
|
105
|
+
abs_current_dir(clone_path)
|
106
|
+
)
|
107
|
+
repo.config['user.email'] = 'afish@example.com'
|
108
|
+
repo.config['user.name'] = 'Art T. Fish'
|
109
|
+
gitdocs_add(clone_path)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def gitdocs_status
|
114
|
+
@status_cmd = 'gitdocs status --pid=gitdocs.pid'
|
115
|
+
run(@status_cmd, 15)
|
116
|
+
assert_success(true)
|
117
|
+
end
|
118
|
+
|
119
|
+
def assert_gitdocs_status_contains(expected)
|
120
|
+
assert_partial_output(expected, output_from(@status_cmd))
|
121
|
+
end
|
122
|
+
|
123
|
+
def assert_gitdocs_status_not_contain(expected)
|
124
|
+
assert_no_partial_output(expected, output_from(@status_cmd))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class MiniTest::Spec
|
129
|
+
include Helper
|
130
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require File.expand_path('../test_helper', __FILE__)
|
3
|
+
|
4
|
+
describe Gitdocs::Notifier do
|
5
|
+
let(:notifier) { Gitdocs::Notifier.new(show_notifications) }
|
6
|
+
|
7
|
+
describe '#info' do
|
8
|
+
subject { notifier.info('title', 'message') }
|
9
|
+
|
10
|
+
describe 'without notifications' do
|
11
|
+
let(:show_notifications) { false }
|
12
|
+
before { notifier.expects(:puts).with('title: message') }
|
13
|
+
it { subject }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'with notifications' do
|
17
|
+
let(:show_notifications) { true }
|
18
|
+
before do
|
19
|
+
Guard::Notifier.expects(:turn_on)
|
20
|
+
Guard::Notifier.expects(:notify)
|
21
|
+
.with(
|
22
|
+
'message',
|
23
|
+
title: 'title',
|
24
|
+
image: File.expand_path('../../lib/img/icon.png', __FILE__)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
it { subject }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#warn' do
|
32
|
+
subject { notifier.warn('title', 'message') }
|
33
|
+
|
34
|
+
describe 'without notifications' do
|
35
|
+
let(:show_notifications) { false }
|
36
|
+
before { Kernel.expects(:warn).with('title: message') }
|
37
|
+
it { subject }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'with notifications' do
|
41
|
+
let(:show_notifications) { true }
|
42
|
+
before do
|
43
|
+
Guard::Notifier.expects(:turn_on)
|
44
|
+
Guard::Notifier.expects(:notify).with('message', title: 'title',)
|
45
|
+
end
|
46
|
+
it { subject }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#error' do
|
51
|
+
subject { notifier.error('title', 'message') }
|
52
|
+
|
53
|
+
describe 'without notifications' do
|
54
|
+
let(:show_notifications) { false }
|
55
|
+
before { Kernel.expects(:warn).with('title: message') }
|
56
|
+
it { subject }
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'with notifications' do
|
60
|
+
let(:show_notifications) { true }
|
61
|
+
before do
|
62
|
+
Guard::Notifier.expects(:turn_on)
|
63
|
+
Guard::Notifier.expects(:notify).with('message', title: 'title', image: :failure)
|
64
|
+
end
|
65
|
+
it { subject }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,578 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require File.expand_path('../test_helper', __FILE__)
|
3
|
+
|
4
|
+
describe Gitdocs::Repository do
|
5
|
+
let(:local_repo_path) { 'tmp/unit/local' }
|
6
|
+
let(:author1) { 'Art T. Fish <afish@example.com>' }
|
7
|
+
let(:author2) { 'A U Thor <author@example.com>' }
|
8
|
+
|
9
|
+
before do
|
10
|
+
FileUtils.rm_rf('tmp/unit')
|
11
|
+
FileUtils.mkdir_p(local_repo_path)
|
12
|
+
repo = Rugged::Repository.init_at(local_repo_path)
|
13
|
+
repo.config['user.email'] = 'afish@example.com'
|
14
|
+
repo.config['user.name'] = 'Art T. Fish'
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:repository) { Gitdocs::Repository.new(path_or_share) }
|
18
|
+
# Default path for the repository object, which can be overridden by the
|
19
|
+
# tests when necessary.
|
20
|
+
let(:path_or_share) { local_repo_path }
|
21
|
+
let(:remote_repo) { Rugged::Repository.init_at('tmp/unit/remote', :bare) }
|
22
|
+
let(:local_repo) { Rugged::Repository.new(local_repo_path) }
|
23
|
+
|
24
|
+
|
25
|
+
describe 'initialize' do
|
26
|
+
subject { repository }
|
27
|
+
|
28
|
+
describe 'with a missing path' do
|
29
|
+
let(:path_or_share) { 'tmp/unit/missing_path' }
|
30
|
+
it { subject.must_be_kind_of Gitdocs::Repository }
|
31
|
+
it { subject.valid?.must_equal false }
|
32
|
+
it { subject.invalid_reason.must_equal :directory_missing }
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'with a path that is not a repository' do
|
36
|
+
let(:path_or_share) { 'tmp/unit/not_a_repo' }
|
37
|
+
before { FileUtils.mkdir_p(path_or_share) }
|
38
|
+
it { subject.must_be_kind_of Gitdocs::Repository }
|
39
|
+
it { subject.valid?.must_equal false }
|
40
|
+
it { subject.invalid_reason.must_equal :no_repository }
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'with a string path that is a repository' do
|
44
|
+
it { subject.must_be_kind_of Gitdocs::Repository }
|
45
|
+
it { subject.valid?.must_equal true }
|
46
|
+
it { subject.invalid_reason.must_be_nil }
|
47
|
+
it { subject.instance_variable_get(:@rugged).wont_be_nil }
|
48
|
+
it { subject.instance_variable_get(:@grit).wont_be_nil }
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'with a share that is a repository' do
|
52
|
+
let(:path_or_share) { stub(
|
53
|
+
path: local_repo_path,
|
54
|
+
remote_name: 'remote',
|
55
|
+
branch_name: 'branch'
|
56
|
+
) }
|
57
|
+
it { subject.must_be_kind_of Gitdocs::Repository }
|
58
|
+
it { subject.valid?.must_equal true }
|
59
|
+
it { subject.invalid_reason.must_be_nil }
|
60
|
+
it { subject.instance_variable_get(:@rugged).wont_be_nil }
|
61
|
+
it { subject.instance_variable_get(:@grit).wont_be_nil }
|
62
|
+
it { subject.instance_variable_get(:@branch_name).must_equal 'branch' }
|
63
|
+
it { subject.instance_variable_get(:@remote_name).must_equal 'remote' }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '.clone' do
|
68
|
+
subject { Gitdocs::Repository.clone(path, remote) }
|
69
|
+
|
70
|
+
let(:path) { 'tmp/unit/clone' }
|
71
|
+
let(:remote) { 'tmp/unit/remote' }
|
72
|
+
|
73
|
+
describe 'with invalid remote' do
|
74
|
+
it { assert_raises(RuntimeError) { subject } }
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'with valid remote' do
|
78
|
+
before { Rugged::Repository.init_at(remote, :bare) }
|
79
|
+
it { subject.must_be_kind_of Gitdocs::Repository }
|
80
|
+
it { subject.valid?.must_equal true }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '.search' do
|
85
|
+
subject { Gitdocs::Repository.search('term', repositories) }
|
86
|
+
|
87
|
+
let(:repositories) { Array.new(4, stub(root: 'root')) }
|
88
|
+
before do
|
89
|
+
repositories[0].expects(:search).with('term').returns(:result1)
|
90
|
+
repositories[1].expects(:search).with('term').returns(:result2)
|
91
|
+
repositories[2].expects(:search).with('term').returns(:result3)
|
92
|
+
repositories[3].expects(:search).with('term').returns([])
|
93
|
+
end
|
94
|
+
|
95
|
+
it do
|
96
|
+
subject.must_equal({
|
97
|
+
Gitdocs::Repository::RepoDescriptor.new('root', 1) => :result3,
|
98
|
+
Gitdocs::Repository::RepoDescriptor.new('root', 2) => :result2,
|
99
|
+
Gitdocs::Repository::RepoDescriptor.new('root', 3) => :result1
|
100
|
+
})
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#search' do
|
105
|
+
subject { repository.search(term) }
|
106
|
+
|
107
|
+
describe 'empty term' do
|
108
|
+
let(:term) { '' }
|
109
|
+
it { subject.must_equal [] }
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'nothing found' do
|
113
|
+
let(:term) { 'foo' }
|
114
|
+
before do
|
115
|
+
write_and_commit('file1', 'bar', 'commit', author1)
|
116
|
+
write_and_commit('file2', 'beef', 'commit', author1)
|
117
|
+
end
|
118
|
+
it { subject.must_equal [] }
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
describe 'term found' do
|
123
|
+
let(:term) { 'foo' }
|
124
|
+
before do
|
125
|
+
write_and_commit('file1', 'foo', 'commit', author1)
|
126
|
+
write_and_commit('file2', 'beef', 'commit', author1)
|
127
|
+
write_and_commit('file3', 'foobar', 'commit', author1)
|
128
|
+
end
|
129
|
+
it do
|
130
|
+
subject.must_equal([
|
131
|
+
Gitdocs::Repository::SearchResult.new('file1', 'foo'),
|
132
|
+
Gitdocs::Repository::SearchResult.new('file3', 'foobar')
|
133
|
+
])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '#root' do
|
139
|
+
subject { repository.root }
|
140
|
+
|
141
|
+
describe 'when invalid' do
|
142
|
+
let(:path_or_share) { 'tmp/unit/missing' }
|
143
|
+
it { subject.must_be_nil }
|
144
|
+
end
|
145
|
+
|
146
|
+
describe 'when valid' do
|
147
|
+
it { subject.must_equal File.expand_path(local_repo_path) }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe '#available_remotes' do
|
152
|
+
subject { repository.available_remotes }
|
153
|
+
|
154
|
+
describe 'when invalid' do
|
155
|
+
let(:path_or_share) { 'tmp/unit/missing' }
|
156
|
+
it { subject.must_be_nil }
|
157
|
+
end
|
158
|
+
|
159
|
+
describe 'when valid' do
|
160
|
+
it { subject.must_equal [] }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe '#available_branches' do
|
165
|
+
subject { repository.available_branches }
|
166
|
+
|
167
|
+
describe 'when invalid' do
|
168
|
+
let(:path_or_share) { 'tmp/unit/missing' }
|
169
|
+
it { subject.must_be_nil }
|
170
|
+
end
|
171
|
+
|
172
|
+
describe 'when valid' do
|
173
|
+
it { subject.must_equal [] }
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe '#current_oid' do
|
178
|
+
subject { repository.current_oid }
|
179
|
+
|
180
|
+
describe 'no commits' do
|
181
|
+
it { subject.must_equal nil }
|
182
|
+
end
|
183
|
+
|
184
|
+
describe 'has commits' do
|
185
|
+
before { @head_oid = write_and_commit('touch_me', '', 'commit', author1) }
|
186
|
+
it { subject.must_equal @head_oid }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe '#pull' do
|
191
|
+
subject { repository.pull }
|
192
|
+
|
193
|
+
let(:path_or_share) { stub(
|
194
|
+
path: local_repo_path,
|
195
|
+
remote_name: 'origin',
|
196
|
+
branch_name: 'master'
|
197
|
+
) }
|
198
|
+
|
199
|
+
describe 'when invalid' do
|
200
|
+
let(:path_or_share) { 'tmp/unit/missing' }
|
201
|
+
it { subject.must_be_nil }
|
202
|
+
end
|
203
|
+
|
204
|
+
describe 'when there is no remote' do
|
205
|
+
it { subject.must_equal :no_remote }
|
206
|
+
end
|
207
|
+
|
208
|
+
describe 'when there is an error' do
|
209
|
+
before do
|
210
|
+
Rugged::Remote.add(
|
211
|
+
Rugged::Repository.new(local_repo_path),
|
212
|
+
'origin',
|
213
|
+
'file:///bad/remote'
|
214
|
+
)
|
215
|
+
end
|
216
|
+
it { subject.must_equal "Fetching origin\n" }
|
217
|
+
end
|
218
|
+
|
219
|
+
describe 'with a valid remote' do
|
220
|
+
before { create_local_repo_with_remote }
|
221
|
+
|
222
|
+
describe 'when there is nothing to pull' do
|
223
|
+
it { subject.must_equal :ok }
|
224
|
+
end
|
225
|
+
|
226
|
+
describe 'when there is a conflict' do
|
227
|
+
before do
|
228
|
+
bare_commit(
|
229
|
+
remote_repo,
|
230
|
+
'file1', 'dead',
|
231
|
+
'second commit',
|
232
|
+
'author@example.com', 'A U Thor'
|
233
|
+
)
|
234
|
+
write_and_commit('file1', 'beef', 'conflict commit', author1)
|
235
|
+
end
|
236
|
+
|
237
|
+
it { subject.must_equal ['file1'] }
|
238
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
239
|
+
it { subject ; local_repo_files.count.must_equal 3 }
|
240
|
+
it { subject ; local_repo_files.must_include 'file1 (f6ea049 original)' }
|
241
|
+
it { subject ; local_repo_files.must_include 'file1 (18ed963)' }
|
242
|
+
it { subject ; local_repo_files.must_include 'file1 (7bfce5c)' }
|
243
|
+
end
|
244
|
+
|
245
|
+
describe 'when new commits are pulled and merged' do
|
246
|
+
before do
|
247
|
+
bare_commit(
|
248
|
+
remote_repo,
|
249
|
+
'file2', 'deadbeef',
|
250
|
+
'second commit',
|
251
|
+
'author@example.com', 'A U Thor'
|
252
|
+
)
|
253
|
+
end
|
254
|
+
it { subject.must_equal :ok }
|
255
|
+
it { subject ; File.exists?(File.join(local_repo_path, 'file2')).must_equal true }
|
256
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
describe '#push' do
|
262
|
+
subject { repository.push(last_oid, 'message') }
|
263
|
+
|
264
|
+
let(:path_or_share) { stub(
|
265
|
+
path: local_repo_path,
|
266
|
+
remote_name: 'origin',
|
267
|
+
branch_name: 'master'
|
268
|
+
) }
|
269
|
+
|
270
|
+
describe 'when invalid' do
|
271
|
+
let(:last_oid) { nil }
|
272
|
+
let(:path_or_share) { 'tmp/unit/missing' }
|
273
|
+
it { subject.must_be_nil }
|
274
|
+
end
|
275
|
+
|
276
|
+
describe 'when no remote' do
|
277
|
+
let(:last_oid) { nil }
|
278
|
+
it { subject.must_equal :no_remote }
|
279
|
+
end
|
280
|
+
|
281
|
+
describe 'remote exists' do
|
282
|
+
before { create_local_repo_with_remote }
|
283
|
+
|
284
|
+
describe 'last sync is nil' do
|
285
|
+
let(:last_oid) { nil }
|
286
|
+
|
287
|
+
describe 'and there is an error on push' do
|
288
|
+
# Force an error to occur in the push
|
289
|
+
before { repository.instance_variable_set(:@branch_name, 'missing') }
|
290
|
+
it { subject.must_equal :nothing }
|
291
|
+
end
|
292
|
+
|
293
|
+
describe 'and there is a conflicted file to push' do
|
294
|
+
before do
|
295
|
+
bare_commit(remote_repo, 'file1', 'dead', 'commit', 'A U Thor', 'author@example.com')
|
296
|
+
write('file1', 'beef')
|
297
|
+
end
|
298
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
299
|
+
it { subject ; commit_count(remote_repo).must_equal 2 }
|
300
|
+
it { subject.must_equal :nothing }
|
301
|
+
end
|
302
|
+
|
303
|
+
describe 'and there is an empty directory to push' do
|
304
|
+
before { FileUtils.mkdir_p(File.join(local_repo_path, 'directory')) }
|
305
|
+
it { subject.must_equal :ok }
|
306
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
307
|
+
it { subject ; commit_count(remote_repo).must_equal 2 }
|
308
|
+
it { subject ; head_commit(remote_repo).message.must_equal "message\n" }
|
309
|
+
it { subject ; head_tree_files(remote_repo).count.must_equal 2 }
|
310
|
+
it { subject ; head_tree_files(remote_repo).must_include 'file1' }
|
311
|
+
it { subject ; head_tree_files(remote_repo).must_include 'directory' }
|
312
|
+
end
|
313
|
+
|
314
|
+
describe 'and there is an existing file update to push' do
|
315
|
+
before { write('file1', 'deadbeef') }
|
316
|
+
it { subject.must_equal :ok }
|
317
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
318
|
+
it { subject ; commit_count(remote_repo).must_equal 2 }
|
319
|
+
it { subject ; head_commit(remote_repo).message.must_equal "message\n" }
|
320
|
+
it { subject ; head_tree_files(remote_repo).count.must_equal 1 }
|
321
|
+
it { subject ; head_tree_files(remote_repo).must_include 'file1' }
|
322
|
+
end
|
323
|
+
|
324
|
+
describe 'and there is a new file to push' do
|
325
|
+
before { write('file2', 'foobar') }
|
326
|
+
it { subject.must_equal :ok }
|
327
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
328
|
+
it { subject ; commit_count(remote_repo).must_equal 2 }
|
329
|
+
it { subject ; head_commit(remote_repo).message.must_equal "message\n" }
|
330
|
+
it { subject ; head_tree_files(remote_repo).count.must_equal 2 }
|
331
|
+
it { subject ; head_tree_files(remote_repo).must_include 'file1' }
|
332
|
+
it { subject ; head_tree_files(remote_repo).must_include 'file2' }
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
describe 'last sync is not nil' do
|
337
|
+
let(:last_oid) { 'oid' }
|
338
|
+
|
339
|
+
describe 'and this is an error on the push' do
|
340
|
+
before do
|
341
|
+
write('file2', 'foobar')
|
342
|
+
# Force an error to occur in the push
|
343
|
+
repository.instance_variable_set(:@branch_name, 'missing')
|
344
|
+
end
|
345
|
+
it { subject.must_be_kind_of String }
|
346
|
+
end
|
347
|
+
|
348
|
+
describe 'and this is nothing to push' do
|
349
|
+
it { subject.must_equal :nothing }
|
350
|
+
end
|
351
|
+
|
352
|
+
describe 'and there is a conflicted commit to push' do
|
353
|
+
before do
|
354
|
+
bare_commit(remote_repo, 'file1', 'dead', 'commit', 'A U Thor', 'author@example.com')
|
355
|
+
write('file1', 'beef')
|
356
|
+
end
|
357
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
358
|
+
it { subject ; commit_count(remote_repo).must_equal 2 }
|
359
|
+
it { subject.must_equal :conflict }
|
360
|
+
end
|
361
|
+
|
362
|
+
describe 'and there is a commit to push' do
|
363
|
+
before { write('file2', 'foobar') }
|
364
|
+
it { subject.must_equal :ok }
|
365
|
+
it { subject ; commit_count(local_repo).must_equal 2 }
|
366
|
+
it { subject ; commit_count(remote_repo).must_equal 2 }
|
367
|
+
it { subject ; head_commit(remote_repo).message.must_equal "message\n" }
|
368
|
+
it { subject ; head_tree_files(remote_repo).count.must_equal 2 }
|
369
|
+
it { subject ; head_tree_files(remote_repo).must_include 'file1' }
|
370
|
+
it { subject ; head_tree_files(remote_repo).must_include 'file2' }
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
describe '#author_count' do
|
377
|
+
subject { repository.author_count(last_oid) }
|
378
|
+
|
379
|
+
describe 'no commits' do
|
380
|
+
let(:last_oid) { nil }
|
381
|
+
it { subject.must_equal({}) }
|
382
|
+
end
|
383
|
+
|
384
|
+
describe 'commits' do
|
385
|
+
before do
|
386
|
+
@intermediate_oid = write_and_commit('touch_me', 'first', 'initial commit', author1)
|
387
|
+
write_and_commit('touch_me', 'second', 'commit', author1)
|
388
|
+
write_and_commit('touch_me', 'third', 'commit', author2)
|
389
|
+
write_and_commit('touch_me', 'fourth', 'commit', author1)
|
390
|
+
end
|
391
|
+
|
392
|
+
describe 'all' do
|
393
|
+
let(:last_oid) { nil }
|
394
|
+
it { subject.must_equal({ author1 => 3, author2 => 1 }) }
|
395
|
+
end
|
396
|
+
|
397
|
+
describe 'some' do
|
398
|
+
let(:last_oid) { @intermediate_oid }
|
399
|
+
it { subject.must_equal({ author1 => 2, author2 => 1 }) }
|
400
|
+
end
|
401
|
+
|
402
|
+
describe 'missing oid' do
|
403
|
+
let(:last_oid) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }
|
404
|
+
it { subject.must_equal({}) }
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
describe '#file_meta' do
|
410
|
+
subject { repository.file_meta(file_name) }
|
411
|
+
|
412
|
+
before do
|
413
|
+
write_and_commit('directory0/file0', '', 'initial commit', author1)
|
414
|
+
write_and_commit('directory/file1', 'foo', 'commit1', author1)
|
415
|
+
write_and_commit('directory/file2', 'bar', 'commit2', author2)
|
416
|
+
write_and_commit('directory/file2', 'beef', 'commit3', author2)
|
417
|
+
end
|
418
|
+
|
419
|
+
describe 'on a missing file' do
|
420
|
+
let(:file_name) { 'missing_file' }
|
421
|
+
it { assert_raises(RuntimeError) { subject } }
|
422
|
+
end
|
423
|
+
|
424
|
+
describe 'on a file' do
|
425
|
+
describe 'of size zero' do
|
426
|
+
let(:file_name) { 'directory0/file0' }
|
427
|
+
it { subject[:author].must_equal 'Art T. Fish' }
|
428
|
+
it { subject[:size].must_equal -1 }
|
429
|
+
it { subject[:modified].wont_be_nil }
|
430
|
+
end
|
431
|
+
|
432
|
+
describe 'of non-zero size' do
|
433
|
+
let(:file_name) { 'directory/file1' }
|
434
|
+
it { subject[:author].must_equal 'Art T. Fish' }
|
435
|
+
it { subject[:size].must_equal 3 }
|
436
|
+
it { subject[:modified].wont_be_nil }
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
describe 'on a directory' do
|
441
|
+
describe 'of size zero' do
|
442
|
+
let(:file_name) { 'directory0' }
|
443
|
+
it { subject[:author].must_equal 'Art T. Fish' }
|
444
|
+
it { subject[:size].must_equal -1 }
|
445
|
+
it { subject[:modified].wont_be_nil }
|
446
|
+
end
|
447
|
+
|
448
|
+
describe 'of non-zero size' do
|
449
|
+
let(:file_name) { 'directory' }
|
450
|
+
it { subject[:author].must_equal 'A U Thor' }
|
451
|
+
it { subject[:size].must_equal 7 }
|
452
|
+
it { subject[:modified].wont_be_nil }
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
describe '#file_revisions' do
|
458
|
+
subject { repository.file_revisions('directory') }
|
459
|
+
|
460
|
+
before do
|
461
|
+
write_and_commit('directory0/file0', '', 'initial commit', author1)
|
462
|
+
@commit1 = write_and_commit('directory/file1', 'foo', 'commit1', author1)
|
463
|
+
@commit2 = write_and_commit('directory/file2', 'bar', 'commit2', author2)
|
464
|
+
@commit3 = write_and_commit('directory/file2', 'beef', 'commit3', author2)
|
465
|
+
end
|
466
|
+
|
467
|
+
it { subject.length.must_equal 3 }
|
468
|
+
it { subject.map { |x| x[:author] }.must_equal ['A U Thor', 'A U Thor', 'Art T. Fish'] }
|
469
|
+
it { subject.map { |x| x[:commit] }.must_equal [@commit3[0, 7], @commit2[0, 7], @commit1[0, 7]] }
|
470
|
+
it { subject.map { |x| x[:subject] }.must_equal ['commit3', 'commit2', 'commit1'] }
|
471
|
+
end
|
472
|
+
|
473
|
+
describe '#file_revision_at' do
|
474
|
+
subject { repository.file_revision_at('directory/file2', @commit) }
|
475
|
+
|
476
|
+
before do
|
477
|
+
write_and_commit('directory0/file0', '', 'initial commit', author1)
|
478
|
+
write_and_commit('directory/file1', 'foo', 'commit1', author1)
|
479
|
+
write_and_commit('directory/file2', 'bar', 'commit2', author2)
|
480
|
+
@commit = write_and_commit('directory/file2', 'beef', 'commit3', author2)
|
481
|
+
end
|
482
|
+
|
483
|
+
it { subject.must_equal '/tmp/file2' }
|
484
|
+
it { File.read(subject).must_equal "beef\n" }
|
485
|
+
end
|
486
|
+
|
487
|
+
describe '#file_revert' do
|
488
|
+
subject { repository.file_revert('directory/file2', ref) }
|
489
|
+
|
490
|
+
let(:file_name) { File.join(local_repo_path, 'directory', 'file2') }
|
491
|
+
|
492
|
+
before do
|
493
|
+
@commit0 = write_and_commit('directory0/file0', '', 'initial commit', author1)
|
494
|
+
write_and_commit('directory/file1', 'foo', 'commit1', author1)
|
495
|
+
@commit2 = write_and_commit('directory/file2', 'bar', 'commit2', author2)
|
496
|
+
write_and_commit('directory/file2', 'beef', 'commit3', author2)
|
497
|
+
end
|
498
|
+
|
499
|
+
describe 'file does not include the revision' do
|
500
|
+
let(:ref) { @commit0 }
|
501
|
+
it { subject ; File.read(file_name).must_equal 'beef' }
|
502
|
+
end
|
503
|
+
|
504
|
+
describe 'file does include the revision' do
|
505
|
+
let(:ref) { @commit2 }
|
506
|
+
it { subject ; File.read(file_name).must_equal "bar\n" }
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
##############################################################################
|
511
|
+
|
512
|
+
private
|
513
|
+
|
514
|
+
def create_local_repo_with_remote
|
515
|
+
bare_commit(
|
516
|
+
remote_repo,
|
517
|
+
'file1', 'foobar',
|
518
|
+
'initial commit',
|
519
|
+
'author@example.com', 'A U Thor'
|
520
|
+
)
|
521
|
+
FileUtils.rm_rf(local_repo_path)
|
522
|
+
repo = Rugged::Repository.clone_at(remote_repo.path, local_repo_path)
|
523
|
+
repo.config['user.email'] = 'afish@example.com'
|
524
|
+
repo.config['user.name'] = 'Art T. Fish'
|
525
|
+
end
|
526
|
+
|
527
|
+
def write(filename, content)
|
528
|
+
FileUtils.mkdir_p(File.join(local_repo_path, File.dirname(filename)))
|
529
|
+
File.write(File.join(local_repo_path, filename), content)
|
530
|
+
end
|
531
|
+
|
532
|
+
def write_and_commit(filename, content, commit_msg, author)
|
533
|
+
FileUtils.mkdir_p(File.join(local_repo_path, File.dirname(filename)))
|
534
|
+
File.write(File.join(local_repo_path, filename), content)
|
535
|
+
`cd #{local_repo_path} ; git add #{filename}; git commit -m '#{commit_msg}' --author='#{author}'`
|
536
|
+
`cd #{local_repo_path} ; git rev-parse HEAD`.strip
|
537
|
+
end
|
538
|
+
|
539
|
+
def bare_commit(repo, filename, content, message, email, name)
|
540
|
+
index = Rugged::Index.new
|
541
|
+
index.add(
|
542
|
+
path: filename,
|
543
|
+
oid: repo.write(content, :blob),
|
544
|
+
mode: 0100644
|
545
|
+
)
|
546
|
+
|
547
|
+
Rugged::Commit.create(remote_repo, {
|
548
|
+
tree: index.write_tree(repo),
|
549
|
+
author: { email: email, name: name, time: Time.now },
|
550
|
+
committer: { email: email, name: name, time: Time.now },
|
551
|
+
message: message,
|
552
|
+
parents: repo.empty? ? [] : [ repo.head.target ].compact,
|
553
|
+
update_ref: 'HEAD'
|
554
|
+
})
|
555
|
+
end
|
556
|
+
|
557
|
+
def commit_count(repo)
|
558
|
+
walker = Rugged::Walker.new(repo)
|
559
|
+
walker.push(repo.head.target)
|
560
|
+
walker.count
|
561
|
+
end
|
562
|
+
|
563
|
+
def head_commit(repo)
|
564
|
+
walker = Rugged::Walker.new(repo)
|
565
|
+
walker.push(repo.head.target)
|
566
|
+
walker.first
|
567
|
+
end
|
568
|
+
|
569
|
+
def head_tree_files(repo)
|
570
|
+
head_commit(repo).tree.map { |x| x[:name] }
|
571
|
+
end
|
572
|
+
|
573
|
+
def local_repo_files
|
574
|
+
Dir.chdir(local_repo_path) do
|
575
|
+
Dir.glob('*')
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|