gitdocs 0.5.0 → 0.6.0

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