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.
@@ -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