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.
- checksums.yaml +6 -14
- data/.codeclimate.yml +26 -0
- data/.rubocop.yml +8 -2
- data/.travis.yml +8 -0
- data/CHANGELOG +13 -0
- data/Gemfile +1 -1
- data/README.md +7 -6
- data/Rakefile +31 -5
- data/bin/gitdocs +1 -0
- data/config.ru +6 -4
- data/gitdocs.gemspec +22 -19
- data/lib/gitdocs.rb +54 -16
- data/lib/gitdocs/browser_app.rb +34 -41
- data/lib/gitdocs/cli.rb +41 -32
- data/lib/gitdocs/configuration.rb +40 -101
- data/lib/gitdocs/git_notifier.rb +111 -0
- data/lib/gitdocs/initializer.rb +83 -0
- data/lib/gitdocs/manager.rb +90 -60
- data/lib/gitdocs/migration/004_add_index_for_path.rb +1 -1
- data/lib/gitdocs/notifier.rb +70 -104
- data/lib/gitdocs/rendering_helper.rb +3 -0
- data/lib/gitdocs/repository.rb +324 -307
- data/lib/gitdocs/repository/committer.rb +77 -0
- data/lib/gitdocs/repository/path.rb +157 -140
- data/lib/gitdocs/search.rb +40 -25
- data/lib/gitdocs/settings_app.rb +5 -3
- data/lib/gitdocs/share.rb +64 -0
- data/lib/gitdocs/synchronizer.rb +40 -0
- data/lib/gitdocs/version.rb +1 -1
- data/lib/gitdocs/views/_header.haml +2 -2
- data/lib/gitdocs/views/dir.haml +3 -3
- data/lib/gitdocs/views/edit.haml +1 -1
- data/lib/gitdocs/views/file.haml +1 -1
- data/lib/gitdocs/views/home.haml +3 -3
- data/lib/gitdocs/views/layout.haml +13 -13
- data/lib/gitdocs/views/revisions.haml +3 -3
- data/lib/gitdocs/views/search.haml +1 -1
- data/lib/gitdocs/views/settings.haml +6 -6
- data/test/integration/cli/full_sync_test.rb +83 -0
- data/test/integration/cli/share_management_test.rb +29 -0
- data/test/integration/cli/status_test.rb +14 -0
- data/test/integration/test_helper.rb +185 -151
- data/test/integration/{browse_test.rb → web/browse_test.rb} +11 -29
- data/test/integration/web/share_management_test.rb +46 -0
- data/test/support/git_factory.rb +276 -0
- data/test/unit/browser_app_test.rb +346 -0
- data/test/unit/configuration_test.rb +8 -70
- data/test/unit/git_notifier_test.rb +116 -0
- data/test/unit/gitdocs_test.rb +90 -0
- data/test/unit/manager_test.rb +36 -0
- data/test/unit/notifier_test.rb +60 -124
- data/test/unit/repository_committer_test.rb +111 -0
- data/test/unit/repository_path_test.rb +92 -76
- data/test/unit/repository_test.rb +243 -356
- data/test/unit/search_test.rb +15 -0
- data/test/unit/settings_app_test.rb +80 -0
- data/test/unit/share_test.rb +97 -0
- data/test/unit/test_helper.rb +17 -3
- metadata +114 -108
- data/lib/gitdocs/runner.rb +0 -108
- data/lib/gitdocs/server.rb +0 -62
- data/test/integration/full_sync_test.rb +0 -66
- data/test/integration/share_management_test.rb +0 -95
- data/test/integration/status_test.rb +0 -21
- data/test/unit/runner_test.rb +0 -122
data/lib/gitdocs/settings_app.rb
CHANGED
@@ -10,18 +10,20 @@ module Gitdocs
|
|
10
10
|
get('/') do
|
11
11
|
haml(
|
12
12
|
:settings,
|
13
|
-
locals: {
|
13
|
+
locals: { nav_state: 'settings' }
|
14
14
|
)
|
15
15
|
end
|
16
16
|
|
17
17
|
post('/') do
|
18
|
-
|
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
|
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
|
data/lib/gitdocs/version.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
- if file
|
4
4
|
%ul.tabs
|
5
5
|
%li
|
6
|
-
%a{ href:
|
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',
|
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' }
|
data/lib/gitdocs/views/dir.haml
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
- @title = root
|
2
2
|
|
3
|
-
= haml(:_header, locals: { file: false
|
3
|
+
= haml(:_header, locals: { file: false })
|
4
4
|
|
5
5
|
- if contents && contents.any?
|
6
|
-
%table
|
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',
|
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
|
|
data/lib/gitdocs/views/edit.haml
CHANGED
data/lib/gitdocs/views/file.haml
CHANGED
data/lib/gitdocs/views/home.haml
CHANGED
@@ -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',
|
8
|
-
%link{ href: '/css/tilt.css',
|
9
|
-
%link{ href: '/css/coderay.css',
|
10
|
-
%script{ src: '/js/util.js',
|
11
|
-
%script{ src: '/js/jquery.js',
|
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',
|
14
|
-
%script{ src: '/js/app.js',
|
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
|
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
|
22
|
+
%a{ href: '/' } Home
|
23
23
|
%li{ class: ('active' if nav_state == 'settings') }
|
24
|
-
%a
|
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
|
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
|
3
|
+
= haml(:_header, locals: { file: true })
|
4
4
|
|
5
5
|
- if revisions && revisions.any?
|
6
|
-
%table
|
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',
|
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?
|
@@ -3,13 +3,13 @@
|
|
3
3
|
|
4
4
|
%form#settings{ method: 'POST', action: url('/') }
|
5
5
|
%h2 Gitdocs
|
6
|
-
|
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:
|
10
|
+
%input{ type: 'input', name: 'config[web_frontend_port]', value: Gitdocs::Configuration.web_frontend_port }
|
11
11
|
%h2 Shares
|
12
|
-
-
|
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',
|
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',
|
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}"),
|
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
|