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.
@@ -1,7 +1,9 @@
1
1
  require File.expand_path('../test_helper', __FILE__)
2
2
 
3
+
3
4
  describe 'gitdocs configuration' do
4
5
  before do
6
+ ENV['TEST'] = 'true'
5
7
  ShellTools.capture { @config = Gitdocs::Configuration.new('/tmp/gitdocs') }
6
8
  end
7
9
 
@@ -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