gitdocs 0.5.0.pre1 → 0.5.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|