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
@@ -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