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