gitdocs 0.5.0 → 0.6.0

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.
Files changed (65) hide show
  1. checksums.yaml +6 -14
  2. data/.codeclimate.yml +26 -0
  3. data/.rubocop.yml +8 -2
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG +13 -0
  6. data/Gemfile +1 -1
  7. data/README.md +7 -6
  8. data/Rakefile +31 -5
  9. data/bin/gitdocs +1 -0
  10. data/config.ru +6 -4
  11. data/gitdocs.gemspec +22 -19
  12. data/lib/gitdocs.rb +54 -16
  13. data/lib/gitdocs/browser_app.rb +34 -41
  14. data/lib/gitdocs/cli.rb +41 -32
  15. data/lib/gitdocs/configuration.rb +40 -101
  16. data/lib/gitdocs/git_notifier.rb +111 -0
  17. data/lib/gitdocs/initializer.rb +83 -0
  18. data/lib/gitdocs/manager.rb +90 -60
  19. data/lib/gitdocs/migration/004_add_index_for_path.rb +1 -1
  20. data/lib/gitdocs/notifier.rb +70 -104
  21. data/lib/gitdocs/rendering_helper.rb +3 -0
  22. data/lib/gitdocs/repository.rb +324 -307
  23. data/lib/gitdocs/repository/committer.rb +77 -0
  24. data/lib/gitdocs/repository/path.rb +157 -140
  25. data/lib/gitdocs/search.rb +40 -25
  26. data/lib/gitdocs/settings_app.rb +5 -3
  27. data/lib/gitdocs/share.rb +64 -0
  28. data/lib/gitdocs/synchronizer.rb +40 -0
  29. data/lib/gitdocs/version.rb +1 -1
  30. data/lib/gitdocs/views/_header.haml +2 -2
  31. data/lib/gitdocs/views/dir.haml +3 -3
  32. data/lib/gitdocs/views/edit.haml +1 -1
  33. data/lib/gitdocs/views/file.haml +1 -1
  34. data/lib/gitdocs/views/home.haml +3 -3
  35. data/lib/gitdocs/views/layout.haml +13 -13
  36. data/lib/gitdocs/views/revisions.haml +3 -3
  37. data/lib/gitdocs/views/search.haml +1 -1
  38. data/lib/gitdocs/views/settings.haml +6 -6
  39. data/test/integration/cli/full_sync_test.rb +83 -0
  40. data/test/integration/cli/share_management_test.rb +29 -0
  41. data/test/integration/cli/status_test.rb +14 -0
  42. data/test/integration/test_helper.rb +185 -151
  43. data/test/integration/{browse_test.rb → web/browse_test.rb} +11 -29
  44. data/test/integration/web/share_management_test.rb +46 -0
  45. data/test/support/git_factory.rb +276 -0
  46. data/test/unit/browser_app_test.rb +346 -0
  47. data/test/unit/configuration_test.rb +8 -70
  48. data/test/unit/git_notifier_test.rb +116 -0
  49. data/test/unit/gitdocs_test.rb +90 -0
  50. data/test/unit/manager_test.rb +36 -0
  51. data/test/unit/notifier_test.rb +60 -124
  52. data/test/unit/repository_committer_test.rb +111 -0
  53. data/test/unit/repository_path_test.rb +92 -76
  54. data/test/unit/repository_test.rb +243 -356
  55. data/test/unit/search_test.rb +15 -0
  56. data/test/unit/settings_app_test.rb +80 -0
  57. data/test/unit/share_test.rb +97 -0
  58. data/test/unit/test_helper.rb +17 -3
  59. metadata +114 -108
  60. data/lib/gitdocs/runner.rb +0 -108
  61. data/lib/gitdocs/server.rb +0 -62
  62. data/test/integration/full_sync_test.rb +0 -66
  63. data/test/integration/share_management_test.rb +0 -95
  64. data/test/integration/status_test.rb +0 -21
  65. data/test/unit/runner_test.rb +0 -122
@@ -10,18 +10,20 @@ module Gitdocs
10
10
  get('/') do
11
11
  haml(
12
12
  :settings,
13
- locals: { conf: settings.manager, nav_state: 'settings' }
13
+ locals: { nav_state: 'settings' }
14
14
  )
15
15
  end
16
16
 
17
17
  post('/') do
18
- settings.manager.update_all(request.POST)
18
+ Configuration.update(request.POST['config'])
19
+ Share.update_all(request.POST['share'])
20
+ Manager.restart_synchronization
19
21
  redirect to('/')
20
22
  end
21
23
 
22
24
  delete('/:id') do
23
25
  id = params[:id].to_i
24
- halt(404) unless settings.manager.remove_by_id(id)
26
+ halt(404) unless Share.remove_by_id(id)
25
27
  redirect to('/')
26
28
  end
27
29
  end
@@ -0,0 +1,64 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'active_record'
4
+
5
+ module Gitdocs
6
+ class Share < ActiveRecord::Base
7
+ # @return [Array<String>]
8
+ def self.paths
9
+ all.map(&:path)
10
+ end
11
+
12
+ # @param [#to_i] index
13
+ #
14
+ # @return [Share]
15
+ def self.at(index)
16
+ all[index.to_i]
17
+ end
18
+
19
+ # @param [String] path
20
+ #
21
+ # @return [Share]
22
+ def self.find_by_path(path)
23
+ where(path: File.expand_path(path)).first
24
+ end
25
+
26
+ # @param [String] path
27
+ def self.create_by_path!(path)
28
+ new(path: File.expand_path(path)).save!
29
+ end
30
+
31
+ # @param [Hash] updated_shares
32
+ # @return [void]
33
+ def self.update_all(updated_shares)
34
+ updated_shares.each do |index, share_config|
35
+ # Skip the share update if there is no path specified.
36
+ next unless share_config['path'] && !share_config['path'].empty?
37
+
38
+ # Split the remote_branch into remote and branch
39
+ remote_branch = share_config.delete('remote_branch')
40
+ share_config['remote_name'], share_config['branch_name'] =
41
+ remote_branch.split('/', 2) if remote_branch
42
+
43
+ at(index).update_attributes(share_config)
44
+ end
45
+ end
46
+
47
+ # @param [Integer] id of the share to remove
48
+ #
49
+ # @return [true] share was deleted
50
+ # @return [false] share does not exist
51
+ def self.remove_by_id(id)
52
+ find(id).destroy
53
+ true
54
+ rescue ActiveRecord::RecordNotFound
55
+ false
56
+ end
57
+
58
+ # @param [String] path of the share to remove
59
+ # @return [void]
60
+ def self.remove_by_path(path)
61
+ where(path: File.expand_path(path)).destroy_all
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,40 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module Gitdocs
4
+ class Synchronizer
5
+ include Celluloid
6
+ finalizer :stop_timers
7
+
8
+ # @param [Gitdocs::Share] share
9
+ def initialize(share)
10
+ @git_notifier = GitNotifier.new(share.path, share.notification)
11
+ @repository = Repository.new(share)
12
+ @sync_type = share.sync_type
13
+
14
+ # Always to an initial synchronization when beginning.
15
+ synchronize
16
+
17
+ @timer = every(share.polling_interval) { synchronize }
18
+ end
19
+
20
+ # @return [void]
21
+ def stop_timers
22
+ return unless @timer
23
+ @timer.cancel
24
+ end
25
+
26
+ # @return [void]
27
+ def synchronize
28
+ return unless @repository.valid?
29
+
30
+ result = @repository.synchronize(@sync_type)
31
+ @git_notifier.for_merge(result[:merge])
32
+ @git_notifier.for_push(result[:push])
33
+ rescue => e
34
+ # Rescue any standard exceptions which come from the push related
35
+ # commands. This will prevent problems on a single share from killing
36
+ # the entire daemon.
37
+ @git_notifier.on_error(e)
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module Gitdocs
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.0'.freeze
3
3
  end
@@ -3,7 +3,7 @@
3
3
  - if file
4
4
  %ul.tabs
5
5
  %li
6
- %a{ href: "#{request.path}" } View
6
+ %a{ href: request.path } View
7
7
  %li
8
8
  %a{ href: '?mode=raw' } Raw
9
9
  %li
@@ -11,6 +11,6 @@
11
11
  %li
12
12
  %a{ href: '?mode=revisions' } Revisions
13
13
  %li
14
- %form{ method: 'POST', :'data-confirm-submit' => 'Are you sure?' }
14
+ %form{ method: 'POST', 'data-confirm-submit' => 'Are you sure?' }
15
15
  %input{ name: '_method', type: 'hidden', value: 'delete' }
16
16
  %input.btn.danger{ type: 'submit', value: 'Delete' }
@@ -1,9 +1,9 @@
1
1
  - @title = root
2
2
 
3
- = haml(:_header, locals: { file: false, idx: idx })
3
+ = haml(:_header, locals: { file: false })
4
4
 
5
5
  - if contents && contents.any?
6
- %table#fileListing.condensed-table.zebra-striped
6
+ %table.condensed-table.zebra-striped#fileListing
7
7
  %thead
8
8
  %tr
9
9
  %th File
@@ -34,7 +34,7 @@
34
34
  .span8
35
35
  %form.add{ method: 'POST' }
36
36
  %p Add new file or directory
37
- %input{ type: 'text', name: 'filename', placeholder: 'somefile.md or somedir' }
37
+ %input{ type: 'text', name: 'filename', placeholder: 'somefile.md or somedir' }
38
38
  %input.btn.secondary{ type: 'submit', name: 'new_file', value: 'New file' }
39
39
  %input.btn.secondary{ type: 'submit', name: 'new_directory', value: 'New directory' }
40
40
 
@@ -1,6 +1,6 @@
1
1
  - @title = root
2
2
 
3
- = haml(:_header, locals: { file: true, idx: idx })
3
+ = haml(:_header, locals: { file: true })
4
4
 
5
5
  %form.edit{ method: 'POST', style: 'display:none;' }
6
6
  #editor
@@ -1,6 +1,6 @@
1
1
  - @title = root
2
2
 
3
- = haml(:_header, locals: { file: true, idx: idx })
3
+ = haml(:_header, locals: { file: true })
4
4
 
5
5
  .contents
6
6
  = preserve contents
@@ -3,8 +3,8 @@
3
3
  %p Select a share to browse:
4
4
 
5
5
  %table#shares
6
- - shares.each_with_index do |share, idx|
6
+ - Gitdocs::Share.all.each do |share|
7
7
  %tr
8
8
  %td
9
- %a{ href: "/#{idx}" }
10
- = share.root
9
+ %a{ href: "/#{share.id}" }
10
+ = share.path
@@ -3,29 +3,29 @@
3
3
  %head
4
4
  %meta{ 'http-equiv' => 'content-type', content: 'text/html; charset=UTF-8' }
5
5
  %title Gitdocs #{Gitdocs::VERSION}
6
- %link{ href: '/css/bootstrap.css', rel: 'stylesheet'}
7
- %link{ href: '/css/app.css', rel: 'stylesheet' }
8
- %link{ href: '/css/tilt.css', rel: 'stylesheet' }
9
- %link{ href: '/css/coderay.css', rel: 'stylesheet' }
10
- %script{ src: '/js/util.js', type: 'text/javascript', charset: 'utf-8' }
11
- %script{ src: '/js/jquery.js', type: 'text/javascript', charset: 'utf-8' }
6
+ %link{ href: '/css/bootstrap.css', rel: 'stylesheet' }
7
+ %link{ href: '/css/app.css', rel: 'stylesheet' }
8
+ %link{ href: '/css/tilt.css', rel: 'stylesheet' }
9
+ %link{ href: '/css/coderay.css', rel: 'stylesheet' }
10
+ %script{ src: '/js/util.js', type: 'text/javascript', charset: 'utf-8' }
11
+ %script{ src: '/js/jquery.js', type: 'text/javascript', charset: 'utf-8' }
12
12
  %script{ src: '/js/jquery.tablesorter.js', type: 'text/javascript', charset: 'utf-8' }
13
- %script{ src: '/js/bootstrap-alerts.js', type: 'text/javascript', charset: 'utf-8' }
14
- %script{ src: '/js/app.js', type: 'text/javascript', charset: 'utf-8' }
13
+ %script{ src: '/js/bootstrap-alerts.js', type: 'text/javascript', charset: 'utf-8' }
14
+ %script{ src: '/js/app.js', type: 'text/javascript', charset: 'utf-8' }
15
15
  %body
16
- #nav.topbar
16
+ .topbar#nav
17
17
  .fill
18
18
  .container
19
- %a.brand{ href: '/'} Gitdocs
19
+ %a.brand{ href: '/' } Gitdocs
20
20
  %ul.nav
21
21
  %li{ class: ('active' if nav_state == 'home') }
22
- %a(href = "/") Home
22
+ %a{ href: '/' } Home
23
23
  %li{ class: ('active' if nav_state == 'settings') }
24
- %a(href = "/settings") Settings
24
+ %a{ href: '/settings' } Settings
25
25
  %form.pull-left{ action: '/search', method: 'GET' }
26
26
  %input{ type: 'text', placeholder: 'Search', name: 'q' }
27
27
 
28
- #main.container
28
+ .container#main
29
29
  .content
30
30
  - if @title
31
31
  .page-header
@@ -1,9 +1,9 @@
1
1
  - @title = root
2
2
 
3
- = haml(:_header, locals: { file: true, idx: idx })
3
+ = haml(:_header, locals: { file: true })
4
4
 
5
5
  - if revisions && revisions.any?
6
- %table#revisions.condensed-table.zebra-striped
6
+ %table.condensed-table.zebra-striped#revisions
7
7
  %thead
8
8
  %tr
9
9
  %th Commit
@@ -22,7 +22,7 @@
22
22
  %td.author= r[:author]
23
23
  %td.date.reldate= r[:date].iso8601
24
24
  %td.revert
25
- %form{ method: 'POST', :'data-confirm-submit' => 'Are you sure?' }
25
+ %form{ method: 'POST', 'data-confirm-submit' => 'Are you sure?' }
26
26
  %input{ name: '_method', type: 'hidden', value: 'put' }
27
27
  %input.btn{ type: 'submit', name: 'revision', value: r[:commit] }
28
28
  - if revisions.empty?
@@ -10,7 +10,7 @@
10
10
  %dl
11
11
  - search_results.each do |res|
12
12
  %dt
13
- %a{href: "/#{repo.index}/#{res.file}"}
13
+ %a{ href: "/#{repo.index}/#{res.file}" }
14
14
  = "/#{res.file}"
15
15
  %dd
16
16
  = res.context
@@ -3,13 +3,13 @@
3
3
 
4
4
  %form#settings{ method: 'POST', action: url('/') }
5
5
  %h2 Gitdocs
6
- #config.field.config
6
+ .field.config#config
7
7
  %dl
8
8
  %dt Web Frontend Port
9
9
  %dd
10
- %input{ type: 'input', name: 'config[web_frontend_port]', value: conf.web_frontend_port }
10
+ %input{ type: 'input', name: 'config[web_frontend_port]', value: Gitdocs::Configuration.web_frontend_port }
11
11
  %h2 Shares
12
- - conf.shares.each_with_index do |share, idx|
12
+ - Gitdocs::Share.all.each_with_index do |share, idx|
13
13
  .share{ id: "share-#{idx}", class: idx.even? ? 'even' : 'odd' }
14
14
  %dl
15
15
  %dt Path
@@ -23,7 +23,7 @@
23
23
  %dt Sync Type
24
24
  %dd
25
25
  %select{ name: "share[#{idx}][sync_type]" }
26
- %option{ value: 'full', selected: (share.sync_type == 'full' ? 'selected' : nil) }
26
+ %option{ value: 'full', selected: (share.sync_type == 'full' ? 'selected' : nil) }
27
27
  Full
28
28
  %option{ value: 'fetch', selected: (share.sync_type == 'fetch' ? 'selected' : nil) }
29
29
  Fetch only
@@ -47,11 +47,11 @@
47
47
  %dd
48
48
  %input{ name: "share[#{idx}][branch_name]", value: share.branch_name }
49
49
  .notify.field
50
- %input{ type: 'hidden', value: '0', name: "share[#{idx}][notification]"}
50
+ %input{ type: 'hidden', value: '0', name: "share[#{idx}][notification]" }
51
51
  %input{ type: 'checkbox', value: '1', name: "share[#{idx}][notification]", checked: share.notification ? 'checked' : nil }
52
52
  %span Notifications?
53
53
  .delete
54
- %a.remote_share.btn.danger{ href: url("/#{share.id}"), :'data-method' => 'delete' }
54
+ %a.remote_share.btn.danger{ href: url("/#{share.id}"), 'data-method' => 'delete' }
55
55
  Delete
56
56
 
57
57
  %input.btn.primary{ value: 'Save', type: 'submit' }
@@ -0,0 +1,83 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require File.expand_path('../../test_helper', __FILE__)
4
+
5
+ describe 'fully synchronizing repositories' do
6
+ before do
7
+ gitdocs_create_from_remote('clone1', 'clone2', 'clone3')
8
+ gitdocs_start
9
+ end
10
+
11
+ it 'should sync new files' do
12
+ GitFactory.write(:clone1, 'newfile', 'testing')
13
+ assert_clean(:clone1)
14
+
15
+ assert_file_content(:clone1, 'newfile', 'testing')
16
+ assert_file_content(:clone2, 'newfile', 'testing')
17
+ assert_file_content(:clone3, 'newfile', 'testing')
18
+ end
19
+
20
+ it 'should sync changes to an existing file' do
21
+ GitFactory.write(:clone1, 'file', 'testing')
22
+ assert_clean(:clone1)
23
+
24
+ assert_file_content(:clone3, 'file', 'testing')
25
+ GitFactory.append(:clone3, 'file', "\nfoobar")
26
+ assert_clean(:clone3)
27
+
28
+ assert_file_content(:clone1, 'file', "testing\nfoobar")
29
+ assert_file_content(:clone2, 'file', "testing\nfoobar")
30
+ assert_file_content(:clone3, 'file', "testing\nfoobar")
31
+ end
32
+
33
+ it 'should sync empty directories' do
34
+ GitFactory.mkdir(:clone1, 'empty_dir')
35
+ assert_clean(:clone1)
36
+
37
+ assert_file_exist(:clone1, 'empty_dir')
38
+ assert_file_exist(:clone2, 'empty_dir')
39
+ assert_file_exist(:clone3, 'empty_dir')
40
+ end
41
+
42
+ it 'should mark unresolvable conflicts' do
43
+ # HACK: This scenario is so dependent upon timing, that is does not run
44
+ # reliably on TravisCI, even when it is passing locally.
45
+ # So skip it.
46
+ next if ENV['TRAVIS']
47
+
48
+ GitFactory.write(:clone1, 'file', 'testing')
49
+ assert_clean(:clone1)
50
+
51
+ GitFactory.append(:clone2, 'file', 'foobar')
52
+ GitFactory.append(:clone3, 'file', 'deadbeef')
53
+ assert_clean(:clone2)
54
+ assert_clean(:clone3)
55
+
56
+ %w(clone2 clone3 clone1).each do |repo_name|
57
+ assert_file_exist(repo_name, 'file (9a2c773)')
58
+ assert_file_exist(repo_name, 'file (f6ea049)')
59
+ assert_file_exist(repo_name, 'file (e8b5f82)')
60
+ end
61
+ end
62
+ end
63
+
64
+ ################################################################################
65
+
66
+ # @param (see GitInspector.clean?)
67
+ def assert_clean(repo_name)
68
+ wait_for_assert { GitInspector.clean?(repo_name).must_equal(true) }
69
+ end
70
+
71
+ # @param [#to_s] repo_name
72
+ # @param [String] filename
73
+ # @param [String] content
74
+ def assert_file_content(repo_name, filename, content)
75
+ wait_for_assert do
76
+ GitInspector.file_content(repo_name, filename).must_equal(content)
77
+ end
78
+ end
79
+
80
+ # @param (see GitInspector.file_exist?)
81
+ def assert_file_exist(repo_name, filename)
82
+ wait_for_assert { GitInspector.file_exist?(repo_name, filename).must_equal(true) }
83
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require File.expand_path('../../test_helper', __FILE__)
4
+
5
+ describe 'Manage which shares are being watched' do
6
+ it 'should add a local repository' do
7
+ gitdocs_add('local')
8
+ gitdocs_assert_status_contains('local')
9
+ end
10
+
11
+ it 'should add a remote repository' do
12
+ gitdocs_create_from_remote('local')
13
+ end
14
+
15
+ describe 'remove a share' do
16
+ before { gitdocs_add('local') }
17
+ it do
18
+ gitdocs_command('rm', 'local', 'Removed path local from doc list')
19
+ gitdocs_assert_status_not_contain('local')
20
+ end
21
+ end
22
+
23
+ it 'should clear all existing shares' do
24
+ %w(local1 local2 local3).each { |x| gitdocs_add(x) }
25
+
26
+ gitdocs_command('clear', '', 'Cleared paths from gitdocs')
27
+ gitdocs_assert_status_not_contain('local1', 'local2', 'local3')
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require File.expand_path('../../test_helper', __FILE__)
4
+
5
+ describe 'CLI with display daemon and share status' do
6
+ it 'should display information about the daemon' do
7
+ gitdocs_assert_status_contains('Running: false')
8
+ end
9
+
10
+ it 'should display information about the shares' do
11
+ gitdocs_create_from_remote('clone1', 'clone2', 'clone3')
12
+ gitdocs_assert_status_contains('clone1', 'clone2', 'clone3')
13
+ end
14
+ end