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
@@ -13,20 +13,28 @@ module Gitdocs
13
13
  end
14
14
 
15
15
  desc 'start', 'Starts a daemonized gitdocs process'
16
- method_option :debug, type: :boolean, aliases: '-D'
17
- method_option :port, type: :string, aliases: '-p'
18
- method_option :pid, type: :string, aliases: '-P'
16
+ method_option :foreground, type: :boolean, aliases: '-fg'
17
+ method_option :verbose, type: :boolean, aliases: '-v'
18
+ method_option :port, type: :string, aliases: '-p'
19
+ method_option :pid, type: :string, aliases: '-P'
19
20
  def start
20
21
  unless stopped?
21
22
  say 'Gitdocs is already running, please use restart', :red
22
23
  return
23
24
  end
24
25
 
25
- if options[:debug]
26
- say 'Starting in debug mode', :yellow
27
- Gitdocs.start(debug: true, port: options[:port])
26
+ Gitdocs::Initializer.verbose = options[:verbose]
27
+
28
+ if options[:foreground]
29
+ say 'Run in the foreground', :yellow
30
+ Gitdocs::Initializer.foreground = true
31
+ Manager.start(web_port)
28
32
  else
29
- runner.execute { Gitdocs.start(port: options[:port]) }
33
+ # Clear the arguments so that they will not be processed by the
34
+ # Dante execution.
35
+ ARGV.clear
36
+ runner.execute { Manager.start(web_port) }
37
+
30
38
  if running?
31
39
  say 'Started gitdocs', :green
32
40
  else
@@ -57,7 +65,7 @@ module Gitdocs
57
65
  method_option :pid, type: :string, aliases: '-P'
58
66
  desc 'add PATH', 'Adds a path to gitdocs'
59
67
  def add(path)
60
- config.add_path(path)
68
+ Share.create_by_path!(normalize_path(path))
61
69
  say "Added path #{path} to doc list"
62
70
  restart if running?
63
71
  end
@@ -65,21 +73,23 @@ module Gitdocs
65
73
  method_option :pid, type: :string, aliases: '-P'
66
74
  desc 'rm PATH', 'Removes a path from gitdocs'
67
75
  def rm(path)
68
- config.remove_path(path)
76
+ Share.remove_by_path(path)
69
77
  say "Removed path #{path} from doc list"
70
78
  restart if running?
71
79
  end
72
80
 
81
+ method_option :pid, type: :string, aliases: '-P'
73
82
  desc 'clear', 'Clears all paths from gitdocs'
74
83
  def clear
75
- config.clear
84
+ Share.destroy_all
76
85
  say 'Cleared paths from gitdocs'
86
+ restart if running?
77
87
  end
78
88
 
79
89
  method_option :pid, type: :string, aliases: '-P'
80
90
  desc 'create PATH REMOTE', 'Creates a new gitdoc root based on an existing remote'
81
91
  def create(path, remote)
82
- Gitdocs::Repository.clone(path, remote)
92
+ Repository.clone(path, remote)
83
93
  add(path)
84
94
  say "Created #{path} path for gitdoc"
85
95
  end
@@ -89,11 +99,11 @@ module Gitdocs
89
99
  def status
90
100
  say "GitDoc v#{VERSION}"
91
101
  say "Running: #{running?}"
92
- say "File System Watch Method: #{file_system_watch_method}"
102
+ say "File System Watch Method: #{Gitdocs::Manager.listen_method}"
93
103
  say 'Watched repositories:'
94
104
  tp.set(:max_width, 100)
95
105
  status_display = lambda do |share|
96
- repository = Gitdocs::Repository.new(share)
106
+ repository = Repository.new(share)
97
107
 
98
108
  status = ''
99
109
  status += '*' if repository.dirty?
@@ -103,7 +113,7 @@ module Gitdocs
103
113
  status
104
114
  end
105
115
  tp(
106
- config.shares,
116
+ Share.all,
107
117
  { sync: { display_method: :sync_type } },
108
118
  { s: status_display },
109
119
  :path
@@ -119,8 +129,6 @@ module Gitdocs
119
129
  return
120
130
  end
121
131
 
122
- web_port = options[:port]
123
- web_port ||= config.web_frontend_port
124
132
  Launchy.open("http://localhost:#{web_port}/")
125
133
  end
126
134
 
@@ -137,42 +145,43 @@ module Gitdocs
137
145
 
138
146
  # Helpers for thor
139
147
  no_tasks do
148
+ # @return [Dante::Runner]
140
149
  def runner
141
150
  Dante::Runner.new(
142
151
  'gitdocs',
143
152
  debug: false,
144
153
  daemonize: true,
145
- pid_path: pid_path
154
+ pid_path: pid_path,
155
+ log_path: Gitdocs.log_path
146
156
  )
147
157
  end
148
158
 
149
- def config
150
- @config ||= Configuration.new
151
- end
152
-
159
+ # @return [Boolean]
153
160
  def running?
154
161
  runner.daemon_running?
155
162
  end
156
163
 
164
+ # @return [Boolean]
157
165
  def stopped?
158
166
  runner.daemon_stopped?
159
167
  end
160
168
 
169
+ # @return [String]
161
170
  def pid_path
162
171
  options[:pid] || '/tmp/gitdocs.pid'
163
172
  end
164
173
 
165
- # @return [Symbol] to indicate how the file system is being watched
166
- def file_system_watch_method # rubocop:disable CyclomaticComplexity
167
- if Guard::Listener.mac? && Guard::Darwin.usable?
168
- :notification
169
- elsif Guard::Listener.linux? && Guard::Linux.usable?
170
- :notification
171
- elsif Guard::Listener.windows? && Guard::Windows.usable?
172
- :notification
173
- else
174
- :polling
175
- end
174
+ # @return [Integer]
175
+ def web_port
176
+ result = options[:port]
177
+ result ||= Configuration.web_frontend_port
178
+ result.to_i
179
+ end
180
+
181
+ # @param [String] path
182
+ # @return [String]
183
+ def normalize_path(path)
184
+ File.expand_path(path, Dir.pwd)
176
185
  end
177
186
  end
178
187
  end
@@ -1,112 +1,51 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  require 'active_record'
4
- require 'grit'
5
4
 
6
- class Gitdocs::Configuration
7
- attr_reader :config_root
8
-
9
- def initialize(config_root = nil)
10
- @config_root = config_root || File.expand_path('.gitdocs', ENV['HOME'])
11
- FileUtils.mkdir_p(@config_root)
12
- ActiveRecord::Base.establish_connection(
13
- adapter: 'sqlite3',
14
- database: ENV['TEST'] ? ':memory:' : File.join(@config_root, 'config.db')
15
- )
16
- ActiveRecord::Migrator.migrate(File.expand_path('../migration', __FILE__))
17
- import_old_shares unless ENV['TEST']
18
- end
19
-
20
- class Share < ActiveRecord::Base
21
- #attr_accessible :polling_interval, :path, :notification, :branch_name, :remote_name, :sync_type
22
- end
23
-
24
- class Config < ActiveRecord::Base
25
- #attr_accessible :start_web_frontend, :web_frontend_port
26
- end
27
-
28
- # return [Boolean]
29
- def start_web_frontend
30
- global.start_web_frontend
31
- end
32
-
33
- # @return [Integer]
34
- def web_frontend_port
35
- global.web_frontend_port
36
- end
37
-
38
- # @param [String] path
39
- # @param [Hash] opts
40
- def add_path(path, opts = nil)
41
- path = normalize_path(path)
42
- path_opts = { path: path }
43
- path_opts.merge!(opts) if opts
44
- Share.new(path_opts).save!
45
- end
46
-
47
- # @param [Hash] new_config
48
- # @option new_config [Hash] 'config'
49
- # @option new_config [Array<Hash>] 'share'
50
- def update_all(new_config)
51
- global.update_attributes(new_config['config'])
52
- new_config['share'].each do |index, share_config|
53
- # Skip the share update if there is no path specified.
54
- next unless share_config['path'] && !share_config['path'].empty?
55
-
56
- # Split the remote_branch into remote and branch
57
- remote_branch = share_config.delete('remote_branch')
58
- if remote_branch
59
- share_config['remote_name'], share_config['branch_name'] = remote_branch.split('/', 2)
60
- end
61
- shares[index.to_i].update_attributes(share_config)
5
+ # @!attribute path
6
+ # @return [String]
7
+ # @!attribute polling_interval
8
+ # @return [Double] defaults to 15.0
9
+ # @!attribute notification
10
+ # @return [Boolean] default to true
11
+ # @!attribute remote_name
12
+ # @return [String] default to 'origin'
13
+ # @!attribute remote_branch
14
+ # @return [String] default to 'master'
15
+ # @attribute sync_type
16
+ # @return ['full','fetch']
17
+ module Gitdocs
18
+ class Configuration
19
+ # @return [Boolean]
20
+ def self.start_web_frontend
21
+ Config.global.start_web_frontend
62
22
  end
63
- end
64
23
 
65
- # @param [String] path of the share to remove
66
- def remove_path(path)
67
- path = normalize_path(path)
68
- Share.where(path: path).destroy_all
69
- end
70
-
71
- # @param [Integer] id of the share to remove
72
- #
73
- # @return [true] share was deleted
74
- # @return [false] share does not exist
75
- def remove_by_id(id)
76
- Share.find(id).destroy
77
- true
78
- rescue ActiveRecord::RecordNotFound
79
- false
80
- end
81
-
82
- def clear
83
- Share.destroy_all
84
- end
85
-
86
- def shares
87
- Share.all
88
- end
89
-
90
- ##############################################################################
91
-
92
- private
93
-
94
- def global
95
- fail if Config.all.size > 1
96
- Config.create! if Config.all.empty?
97
- Config.all.first
98
- end
99
-
100
- def normalize_path(path)
101
- File.expand_path(path, Dir.pwd)
102
- end
24
+ # @return [Integer]
25
+ def self.web_frontend_port
26
+ Config.global.web_frontend_port
27
+ end
103
28
 
104
- def import_old_shares
105
- full_path = File.expand_path('paths', config_root)
106
- return unless File.exist?(full_path)
29
+ # @param [Hash] new_config
30
+ def self.update(new_config)
31
+ Config.global.update_attributes(new_config)
32
+ end
107
33
 
108
- File.read(full_path).split("\n").each do |path|
109
- Share.find_or_create_by_path(path)
34
+ # NOTE: This record has been kept as a subclass to avoid changing the
35
+ # database table. There are other ways to achieve this, but this seemed most
36
+ # clear for now. [2015-06-26 -- acant]
37
+ #
38
+ # @!attribute start_frontend_port
39
+ # @return [Boolean] defaults to true
40
+ # @!attribute web_frontend_port
41
+ # @return [Integer] defaults to 8888
42
+ class Config < ActiveRecord::Base
43
+ # @return [Gitdocs::Configuration::Config]
44
+ def self.global
45
+ fail if all.size > 1
46
+ create! if all.empty?
47
+ all.first
48
+ end
110
49
  end
111
50
  end
112
51
  end
@@ -0,0 +1,111 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # Notifications about git specific operations
4
+ module Gitdocs
5
+ class GitNotifier
6
+ # @param [String] root
7
+ # @param [Boolean] show_notifications
8
+ def initialize(root, show_notifications)
9
+ @root = root
10
+ @show_notifications = show_notifications
11
+ end
12
+
13
+ # @param [nil, Symbol, Array<String>, Hash<String => Integer>, #to_s] result
14
+ #
15
+ # @return [void]
16
+ def for_merge(result)
17
+ return if result.nil?
18
+ return if result == :no_remote
19
+ return if result == :ok
20
+ return if result == {}
21
+
22
+ if result.is_a?(Array)
23
+ Notifier.warn(
24
+ 'There were some conflicts',
25
+ result.map { |f| "* #{f}" }.join("\n"),
26
+ @show_notifications
27
+ )
28
+ elsif result.is_a?(Hash)
29
+ Notifier.info(
30
+ "Updated with #{change_to_s(result)}",
31
+ "In #{@root}:\n#{author_list(result)}",
32
+ @show_notifications
33
+ )
34
+ else
35
+ Notifier.error(
36
+ 'There was a problem synchronizing this gitdoc',
37
+ "A problem occurred in #{@root}:\n#{result}",
38
+ @show_notifications
39
+ )
40
+ end
41
+ nil
42
+ end
43
+
44
+ # @param [nil, Symbol, Hash<String => Integer>, #to_s] result of push
45
+ #
46
+ # @return [void]
47
+ def for_push(result)
48
+ return if result.nil?
49
+ return if result == :no_remote
50
+ return if result == :nothing
51
+
52
+ if result == :conflict
53
+ Notifier.warn(
54
+ "There was a conflict in #{@root}, retrying",
55
+ '',
56
+ @show_notifications
57
+ )
58
+ elsif result.is_a?(Hash)
59
+ Notifier.info(
60
+ "Pushed #{change_to_s(result)}",
61
+ "#{@root} has been pushed",
62
+ @show_notifications
63
+ )
64
+ else
65
+ Notifier.error(
66
+ "BAD Could not push changes in #{@root}",
67
+ result.to_s,
68
+ @show_notifications
69
+ )
70
+ end
71
+ nil
72
+ end
73
+
74
+ # @param [Exception] exception
75
+ #
76
+ # @return [void]
77
+ def on_error(exception)
78
+ Notifier.error(
79
+ "Unexpected error when fetching/pushing in #{@root}",
80
+ exception.to_s,
81
+ @show_notifications
82
+ )
83
+ nil
84
+ end
85
+
86
+ ############################################################################
87
+
88
+ private
89
+
90
+ # @param [Hash<String => Integer>] changes
91
+ # @return [String]
92
+ def author_list(changes)
93
+ changes
94
+ .map { |author, count| "* #{author} (#{change_to_s(count)})" }
95
+ .join("\n")
96
+ end
97
+
98
+ # @param [Integer, Hash<String => Integer>] count_or_hash
99
+ # @return [String]
100
+ def change_to_s(count_or_hash)
101
+ count =
102
+ if count_or_hash.respond_to?(:values)
103
+ count_or_hash.values.reduce(:+)
104
+ else
105
+ count_or_hash
106
+ end
107
+
108
+ "#{count} change#{count == 1 ? '' : 's'}"
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,83 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'active_record'
4
+
5
+ module Gitdocs
6
+ class Initializer
7
+ # @return [nil]
8
+ def self.initialize_all
9
+ initialize_database
10
+ initialize_old_paths
11
+ end
12
+
13
+ # @return [nil]
14
+ def self.initialize_database
15
+ FileUtils.mkdir_p(root_dirname)
16
+ ActiveRecord::Base.establish_connection(
17
+ adapter: 'sqlite3',
18
+ database: database
19
+ )
20
+ ActiveRecord::Migrator.migrate(
21
+ File.expand_path('../migration', __FILE__)
22
+ )
23
+ end
24
+
25
+ # @return [nil]
26
+ def self.initialize_old_paths
27
+ old_path_dirname = File.expand_path('paths', root_dirname)
28
+ return unless File.exist?(old_path_dirname)
29
+
30
+ File.read(old_path_dirname).split("\n").each do |path|
31
+ begin
32
+ Share.create_by_path!(path)
33
+ rescue # rubocop:disable HandleExceptions
34
+ # Nothing to do, because we want the process to keep going.
35
+ end
36
+ end
37
+ end
38
+
39
+ # @return [String]
40
+ def self.root_dirname
41
+ @root_dirname ||= File.expand_path('.gitdocs', ENV['HOME'])
42
+ end
43
+
44
+ # @param [nil, String] value
45
+ # @return [nil]
46
+ def self.root_dirname=(value)
47
+ return if value.nil?
48
+ @root_dirname = value
49
+ end
50
+
51
+ # @return [String]
52
+ def self.database
53
+ @database ||= File.join(root_dirname, 'config.db')
54
+ end
55
+
56
+ # @param [nil, String] value
57
+ # @return [nil]
58
+ def self.database=(value)
59
+ return if value.nil?
60
+ @database = value
61
+ end
62
+
63
+ # @return [Boolean]
64
+ def self.foreground
65
+ @foreground ||= false
66
+ end
67
+
68
+ def self.foreground=(value)
69
+ return if value.nil?
70
+ @foreground = value
71
+ end
72
+
73
+ # @return [Boolean]
74
+ def self.verbose
75
+ @verbose ||= false
76
+ end
77
+
78
+ # @param [Boolean] value
79
+ def self.verbose=(value)
80
+ @verbose = !!value # rubocop:disable DoubleNegation
81
+ end
82
+ end
83
+ end