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
@@ -4,83 +4,113 @@ module Gitdocs
4
4
  Restart = Class.new(RuntimeError)
5
5
 
6
6
  class Manager
7
- attr_reader :config, :debug
7
+ # @param (see #start)
8
+ # @return (see #start)
9
+ def self.start(web_port)
10
+ Manager.new.start(web_port)
11
+ end
12
+
13
+ # @return [void]
14
+ def self.restart_synchronization
15
+ Thread.main.raise(Restart, 'restarting ... ')
16
+ end
8
17
 
9
- def initialize(config_root, debug)
10
- @config = Configuration.new(config_root)
11
- @logger = Logger.new(File.expand_path('log', @config.config_root))
12
- @debug = debug
13
- yield @config if block_given?
18
+ # @return [:notification, :polling]
19
+ def self.listen_method
20
+ return :polling if Listen::Adapter.select == Listen::Adapter::Polling
21
+ :notification
14
22
  end
15
23
 
16
- def start(web_port = nil)
17
- log("Starting Gitdocs v#{VERSION}...")
18
- log("Using configuration root: '#{config.config_root}'")
19
- log("Shares: (#{shares.length}) #{shares.map(&:inspect).join(', ')}")
24
+ # @param [Integer] web_port
25
+ # @return [void]
26
+ def start(web_port)
27
+ Gitdocs.log_info("Starting Gitdocs v#{VERSION}...")
28
+ Gitdocs.log_info(
29
+ "Using configuration root: '#{Initializer.root_dirname}'"
30
+ )
20
31
 
21
- begin
22
- EM.run do
23
- log('Starting EM loop...')
32
+ Celluloid.boot unless Celluloid.running?
33
+ @supervisor = Celluloid::SupervisionGroup.run!
24
34
 
25
- @runners = Runner.start_all(shares)
26
- repositories = shares.map { |x| Repository.new(x) }
27
- Server.start_and_wait(self, web_port, repositories)
35
+ # Start the web server ###################################################
36
+ app =
37
+ Rack::Builder.new do
38
+ use Rack::Static,
39
+ urls: %w(/css /js /img /doc),
40
+ root: File.expand_path('../public', __FILE__)
41
+ use Rack::MethodOverride
42
+ map('/settings') { run SettingsApp }
43
+ map('/') { run BrowserApp }
28
44
  end
29
- rescue Restart
30
- retry
45
+ @supervisor.add(
46
+ Reel::Rack::Server,
47
+ as: :reel_rack_server,
48
+ args: [
49
+ app,
50
+ {
51
+ Host: '127.0.0.1',
52
+ Port: web_port,
53
+ quiet: true
54
+ }
55
+ ]
56
+ )
57
+
58
+ # Start the synchronizers ################################################
59
+ @synchronization_supervisor = Celluloid::SupervisionGroup.run!
60
+ Share.all.each do |share|
61
+ @synchronization_supervisor.add(
62
+ Synchronizer, as: share.id.to_s, args: [share]
63
+ )
31
64
  end
65
+
66
+ # Start the repository listeners #########################################
67
+ @listener =
68
+ Listen.to(
69
+ *Share.paths,
70
+ ignore: /#{File::SEPARATOR}\.git#{File::SEPARATOR}/
71
+ ) do |modified, added, removed|
72
+ all_changes = modified + added + removed
73
+ changed_repository_paths =
74
+ Share.paths.select do |directory|
75
+ all_changes.any? { |x| x.start_with?(directory) }
76
+ end
77
+
78
+ changed_repository_paths.each do |directory|
79
+ actor_id = Share.find_by_path(directory).id.to_s
80
+ Celluloid::Actor[actor_id].async.synchronize
81
+ end
82
+ end
83
+ @listener.start
84
+
85
+ # ... and wait ###########################################################
86
+ sleep
87
+
88
+ rescue Interrupt
89
+ Gitdocs.log_info('Interrupt received...')
32
90
  rescue Exception => e # rubocop:disable RescueException
33
- # Report all errors in log
34
- log("#{e.class.inspect} - #{e.inspect} - #{e.message.inspect}", :error)
35
- log(e.backtrace.join("\n"), :error)
36
- Gitdocs::Notifier.error(
91
+ Gitdocs.log_error(
92
+ "#{e.class.inspect} - #{e.inspect} - #{e.message.inspect}"
93
+ )
94
+ Gitdocs.log_error(e.backtrace.join("\n"))
95
+ Notifier.error(
37
96
  'Unexpected exit',
38
97
  'Something went wrong. Please see the log for details.'
39
98
  )
40
99
  raise
41
100
  ensure
42
- log("Gitdocs is terminating...goodbye\n\n")
43
- end
101
+ Gitdocs.log_info('stopping listeners...')
102
+ @listener.stop if @listener
44
103
 
45
- def restart
46
- Thread.new do
47
- Thread.main.raise Restart, 'restarting ... '
48
- sleep 0.1 while EM.reactor_running?
49
- start
50
- end
51
- end
52
-
53
- def stop
54
- EM.stop
55
- end
56
-
57
- # Logs and outputs to file or stdout based on debugging state
58
- # log("message")
59
- def log(msg, level = :info)
60
- @debug ? puts(msg) : @logger.send(level, msg)
61
- end
104
+ Gitdocs.log_info('stopping synchronizers...')
105
+ @synchronization_supervisor.terminate if @synchronization_supervisor
62
106
 
63
- def start_web_frontend
64
- config.start_web_frontend
65
- end
66
-
67
- def web_frontend_port
68
- config.web_frontend_port
69
- end
107
+ Gitdocs.log_info('terminate supervisor...')
108
+ @supervisor.terminate if @supervisor
70
109
 
71
- def shares
72
- config.shares
73
- end
74
-
75
- # @see Gitdocs::Configuration#update_all
76
- def update_all(new_config)
77
- config.update_all(new_config)
78
- EM.add_timer(0.1) { restart }
79
- end
110
+ Gitdocs.log_info('disconnect notifier...')
111
+ Notifier.disconnect
80
112
 
81
- # @see Gitdocs::Configuration#remove_by_id
82
- def remove_by_id(id)
83
- config.remove_by_id(id)
113
+ Gitdocs.log_info("Gitdocs is terminating...goodbye\n\n")
84
114
  end
85
115
  end
86
116
  end
@@ -4,7 +4,7 @@
4
4
 
5
5
  class AddIndexForPath < ActiveRecord::Migration
6
6
  def self.up
7
- shares = Gitdocs::Configuration::Share.all.reduce(Hash.new { |h, k| h[k] = [] }) { |h, s| h[s.path] << s; h }
7
+ shares = Gitdocs::Share.all.reduce(Hash.new { |h, k| h[k] = [] }) { |h, s| h[s.path] << s; h }
8
8
  shares.each do |path, shares|
9
9
  shares.shift
10
10
  shares.each(&:destroy) unless shares.empty?
@@ -1,121 +1,87 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  # Wrapper for the UI notifier
4
- class Gitdocs::Notifier
5
- INFO_ICON = File.expand_path('../../img/icon.png', __FILE__)
4
+ module Gitdocs
5
+ class Notifier
6
+ include Singleton
6
7
 
7
- # Wrapper around #error for a single call to the notifier.
8
- # @param (see #error)
9
- def self.error(title, message)
10
- Gitdocs::Notifier.new(true).error(title, message)
11
- end
12
-
13
- # @param [Boolean] show_notifications
14
- def initialize(show_notifications)
15
- @show_notifications = show_notifications
16
- Guard::Notifier.turn_on if @show_notifications
17
- end
18
-
19
- # @param [String] title
20
- # @param [String] message
21
- def info(title, message)
22
- if @show_notifications
23
- Guard::Notifier.notify(message, title: title, image: INFO_ICON)
24
- else
25
- puts("#{title}: #{message}")
26
- end
27
- rescue # rubocop:disable Lint/HandleExceptions
28
- # Prevent StandardErrors from stopping the daemon.
29
- end
30
-
31
- # @param [String] title
32
- # @param [String] message
33
- def warn(title, message)
34
- if @show_notifications
35
- Guard::Notifier.notify(message, title: title)
36
- else
37
- Kernel.warn("#{title}: #{message}")
8
+ # @param [String] title
9
+ # @param [String] message
10
+ # @param [Boolean] show_notification
11
+ #
12
+ # @return [void]
13
+ def self.info(title, message, show_notification)
14
+ Gitdocs.log_info("#{title}: #{message}")
15
+ if show_notification
16
+ instance.notify(title, message, :success)
17
+ else
18
+ puts("#{title}: #{message}")
19
+ end
20
+ rescue # rubocop:disable Lint/HandleExceptions
21
+ # Prevent StandardErrors from stopping the daemon.
38
22
  end
39
- rescue # rubocop:disable Lint/HandleExceptions
40
- # Prevent StandardErrors from stopping the daemon.
41
- end
42
23
 
43
- # @param [String] title
44
- # @param [String] message
45
- def error(title, message)
46
- if @show_notifications
47
- Guard::Notifier.notify(message, title: title, image: :failure)
48
- else
49
- Kernel.warn("#{title}: #{message}")
24
+ # @param [String] title
25
+ # @param [String] message
26
+ # @param [Boolean] show_notification
27
+ #
28
+ # @return [void]
29
+ def self.warn(title, message, show_notification)
30
+ Gitdocs.log_warn("#{title}: #{message}")
31
+ if show_notification
32
+ instance.notify(title, message, :pending)
33
+ else
34
+ Kernel.warn("#{title}: #{message}")
35
+ end
36
+ rescue # rubocop:disable Lint/HandleExceptions
37
+ # Prevent StandardErrors from stopping the daemon.
50
38
  end
51
- rescue # rubocop:disable Lint/HandleExceptions
52
- # Prevent StandardErrors from stopping the daemon.
53
- end
54
39
 
55
- # @param [nil, Symbol, Array<String>, Hash<String => Integer>, #to_s] result
56
- # @param [String] root
57
- def merge_notification(result, root)
58
- return if result.nil?
59
- return if result == :no_remote
60
- return if result == :ok
61
- return if result == {}
40
+ # @overload error(title, message)
41
+ # @param [String] title
42
+ # @param [String] message
43
+ #
44
+ # @overload error(title, message, show_notification)
45
+ # @param [String] title
46
+ # @param [String] message
47
+ # @param [Boolean] show_notification
48
+ #
49
+ # @return [void]
50
+ def self.error(title, message, show_notification = true)
51
+ Gitdocs.log_error("#{title}: #{message}")
62
52
 
63
- if result.is_a?(Array)
64
- warn(
65
- 'There were some conflicts',
66
- result.map { |f| "* #{f}" }.join("\n")
67
- )
68
- elsif result.is_a?(Hash)
69
- info(
70
- "Updated with #{change_to_s(result)}",
71
- "In #{root}:\n#{author_list(result)}"
72
- )
73
- else
74
- error(
75
- 'There was a problem synchronizing this gitdoc',
76
- "A problem occurred in #{root}:\n#{result}"
77
- )
53
+ if show_notification
54
+ instance.notify(title, message, :failed)
55
+ else
56
+ Kernel.warn("#{title}: #{message}")
57
+ end
58
+ rescue # rubocop:disable Lint/HandleExceptions
59
+ # Prevent StandardErrors from stopping the daemon.
78
60
  end
79
- end
80
61
 
81
- # @param [nil, Symbol, Hash<String => Integer>, #to_s] result push operation
82
- # @param [String] root
83
- def push_notification(result, root)
84
- return if result.nil?
85
- return if result == :no_remote
86
- return if result == :nothing
87
-
88
- if result == :conflict
89
- warn("There was a conflict in #{root}, retrying", '')
90
- elsif result.is_a?(Hash)
91
- info("Pushed #{change_to_s(result)}", "#{root} has been pushed")
92
- else
93
- error("BAD Could not push changes in #{root}", result.to_s)
62
+ # @return [void]
63
+ def self.disconnect
64
+ instance.disconnect
94
65
  end
95
- end
96
-
97
- ##############################################################################
98
66
 
99
- private
67
+ ############################################################################
100
68
 
101
- # @param [Hash<String => Integer>] changes
102
- # @return [String]
103
- def author_list(changes)
104
- changes
105
- .map { |author, count| "* #{author} (#{change_to_s(count)})" }
106
- .join("\n")
107
- end
108
-
109
- # @param [Integer, Hash<String => Integer>] count_or_hash
110
- # @return [String]
111
- def change_to_s(count_or_hash)
112
- count =
113
- if count_or_hash.respond_to?(:values)
114
- count_or_hash.values.reduce(:+)
115
- else
116
- count_or_hash
117
- end
69
+ # @private
70
+ # @param [String] title
71
+ # @param [String] message
72
+ # @param [:success, :pending, :failed] type
73
+ #
74
+ # @return [void]
75
+ def notify(title, message, type)
76
+ @notifier ||= Notiffany.connect
77
+ @notifier.notify(message, title: title, image: type)
78
+ end
118
79
 
119
- "#{count} change#{count == 1 ? '' : 's'}"
80
+ # @private
81
+ # @return [void]
82
+ def disconnect
83
+ @notifier.disconnect if @notifier
84
+ @notifier = nil
85
+ end
120
86
  end
121
87
  end
@@ -3,6 +3,9 @@
3
3
  require 'redcarpet'
4
4
  require 'coderay'
5
5
  require 'tilt'
6
+ require 'tilt/erb'
7
+ require 'tilt/haml'
8
+ require 'tilt/redcarpet'
6
9
 
7
10
  module Gitdocs
8
11
  module RenderingHelper
@@ -12,356 +12,373 @@ require 'find'
12
12
  #
13
13
  # @note If a repository is invalid then query methods will return nil, and
14
14
  # command methods will raise exceptions.
15
- class Gitdocs::Repository
16
- attr_reader :invalid_reason
17
-
18
- # Initialize the repository on the specified path. If the path is not valid
19
- # for some reason, the object will be initialized but it will be put into an
20
- # invalid state.
21
- # @see #valid?
22
- # @see #invalid_reason
23
- #
24
- # @param [String, Configuration::Share] path_or_share
25
- def initialize(path_or_share)
26
- path = path_or_share
27
- if path_or_share.respond_to?(:path)
28
- path = path_or_share.path
29
- @remote_name = path_or_share.remote_name
30
- @branch_name = path_or_share.branch_name
31
- end
32
-
33
- @rugged = Rugged::Repository.new(path)
34
- @grit = Grit::Repo.new(path)
35
- Grit::Git.git_timeout = 120
36
- @invalid_reason = nil
37
- @commit_message_path = abs_path('.gitmessage~')
38
- rescue Rugged::OSError
39
- @invalid_reason = :directory_missing
40
- rescue Rugged::RepositoryError
41
- @invalid_reason = :no_repository
42
- end
43
-
44
- # Clone a repository, and create the destination path if necessary.
45
- #
46
- # @param [String] path to clone the repository to
47
- # @param [String] remote URI of the git repository to clone
48
- #
49
- # @raise [RuntimeError] if the clone fails
50
- #
51
- # @return [Gitdocs::Repository]
52
- def self.clone(path, remote)
53
- FileUtils.mkdir_p(File.dirname(path))
54
- # TODO: determine how to do this with rugged, and handle SSH and HTTPS
55
- # credentials.
56
- Grit::Git.new(path).clone({ raise: true, quiet: true }, remote, path)
57
-
58
- repository = new(path)
59
- fail("Unable to clone into #{path}") unless repository.valid?
60
- repository
61
- rescue Grit::Git::GitTimeout
62
- raise("Unable to clone into #{path} because it timed out")
63
- rescue Grit::Git::CommandFailed => e
64
- raise("Unable to clone into #{path} because of #{e.err}")
65
- end
66
-
67
- # @return [String]
68
- def root
69
- return nil unless valid?
70
- @rugged.path.sub(/.\.git./, '')
71
- end
72
-
73
- # @return [Boolean]
74
- def valid?
75
- !@invalid_reason
76
- end
15
+ module Gitdocs
16
+ class Repository
17
+ attr_reader :invalid_reason
18
+
19
+ class InvalidError < StandardError; end
20
+ class FetchError < StandardError; end
21
+ class MergeError < StandardError; end
22
+
23
+ # Initialize the repository on the specified path. If the path is not valid
24
+ # for some reason, the object will be initialized but it will be put into an
25
+ # invalid state.
26
+ # @see #valid?
27
+ # @see #invalid_reason
28
+ #
29
+ # @param [String, Share] path_or_share
30
+ def initialize(path_or_share)
31
+ path = path_or_share
32
+ if path_or_share.respond_to?(:path)
33
+ path = path_or_share.path
34
+ @remote_name = path_or_share.remote_name
35
+ @branch_name = path_or_share.branch_name
36
+ end
77
37
 
78
- # @return [nil] if the repository is invalid
79
- # @return [Array<String>] sorted list of remote branches
80
- def available_remotes
81
- return nil unless valid?
82
- Rugged::Branch.each_name(@rugged, :remote).sort
83
- end
38
+ @rugged = Rugged::Repository.new(path)
39
+ @grit = Grit::Repo.new(path)
40
+ Grit::Git.git_timeout = 120
41
+ @invalid_reason = nil
42
+ @commit_message_path = abs_path('.gitmessage~')
43
+ rescue Rugged::OSError
44
+ @invalid_reason = :directory_missing
45
+ rescue Rugged::RepositoryError
46
+ @invalid_reason = :no_repository
47
+ end
84
48
 
85
- # @return [nil] if the repository is invalid
86
- # @return [Array<String>] sorted list of local branches
87
- def available_branches
88
- return nil unless valid?
89
- Rugged::Branch.each_name(@rugged, :local).sort
90
- end
49
+ # Clone a repository, and create the destination path if necessary.
50
+ #
51
+ # @param [String] path to clone the repository to
52
+ # @param [String] remote URI of the git repository to clone
53
+ #
54
+ # @raise [RuntimeError] if the clone fails
55
+ #
56
+ # @return [Gitdocs::Repository]
57
+ def self.clone(path, remote)
58
+ FileUtils.mkdir_p(File.dirname(path))
59
+ # TODO: determine how to do this with rugged, and handle SSH and HTTPS
60
+ # credentials.
61
+ Grit::Git.new(path).clone({ raise: true, quiet: true }, remote, path)
62
+
63
+ repository = new(path)
64
+ fail("Unable to clone into #{path}") unless repository.valid?
65
+ repository
66
+ rescue Grit::Git::GitTimeout
67
+ raise("Unable to clone into #{path} because it timed out")
68
+ rescue Grit::Git::CommandFailed => e
69
+ raise("Unable to clone into #{path} because of #{e.err}")
70
+ end
91
71
 
92
- # @return [nil] if there are no commits present
93
- # @return [String] oid of the HEAD of the working directory
94
- def current_oid
95
- @rugged.head.target
96
- rescue Rugged::ReferenceError
97
- nil
98
- end
72
+ # @return [String]
73
+ def root
74
+ return nil unless valid?
75
+ @rugged.path.sub(/.\.git./, '')
76
+ end
99
77
 
100
- # Is the working directory dirty
101
- #
102
- # @return [Boolean]
103
- def dirty?
104
- return false unless valid?
78
+ # @return [Boolean]
79
+ def valid?
80
+ !@invalid_reason
81
+ end
105
82
 
106
- return Dir.glob(abs_path('*')).any? unless current_oid
107
- @rugged.diff_workdir(current_oid, include_untracked: true).deltas.any?
108
- end
83
+ # @return [nil] if the repository is invalid
84
+ # @return [Array<String>] sorted list of remote branches
85
+ def available_remotes
86
+ return nil unless valid?
87
+ @rugged.branches.each_name(:remote).sort
88
+ end
109
89
 
110
- # @return [Boolean]
111
- def need_sync?
112
- return false unless valid?
113
- return false unless remote?
90
+ # @return [nil] if the repository is invalid
91
+ # @return [Array<String>] sorted list of local branches
92
+ def available_branches
93
+ return nil unless valid?
94
+ @rugged.branches.each_name(:local).sort
95
+ end
114
96
 
115
- return !!current_oid unless remote_branch # rubocop:disable DoubleNegation
116
- remote_branch.tip.oid != current_oid
117
- end
97
+ # @return [nil] if there are no commits present
98
+ # @return [String] oid of the HEAD of the working directory
99
+ def current_oid
100
+ @rugged.head.target_id
101
+ rescue Rugged::ReferenceError
102
+ nil
103
+ end
118
104
 
119
- # @param [String] term
120
- # @yield [file, context] Gives the files and context for each of the results
121
- # @yieldparam file [String]
122
- # @yieldparam context [String]
123
- def grep(term, &block)
124
- @grit.git.grep(
125
- { raise: true, bare: false, chdir: root, ignore_case: true },
126
- term
127
- ).scan(/(.*?):([^\n]*)/, &block)
128
- rescue Grit::Git::GitTimeout
129
- # TODO: add logging to record the error details
130
- ''
131
- rescue Grit::Git::CommandFailed
132
- # TODO: add logging to record the error details if they are not just
133
- # nothing found
134
- ''
135
- end
105
+ # Is the working directory dirty
106
+ #
107
+ # @return [Boolean]
108
+ def dirty?
109
+ return false unless valid?
136
110
 
137
- # Fetch all the remote branches
138
- #
139
- # @return [nil] if the repository is invalid
140
- # @return [:no_remote] if the remote is not yet set
141
- # @return [String] if there is an error return the message
142
- # @return [:ok] if the fetch worked
143
- def fetch
144
- return nil unless valid?
145
- return :no_remote unless remote?
146
-
147
- @rugged.remotes.each { |x| @grit.remote_fetch(x.name) }
148
- :ok
149
- rescue Grit::Git::GitTimeout
150
- "Fetch timed out for #{root}"
151
- rescue Grit::Git::CommandFailed => e
152
- e.err
153
- end
111
+ return Dir.glob(abs_path('*')).any? unless current_oid
112
+ @rugged.diff_workdir(current_oid, include_untracked: true).deltas.any?
113
+ end
154
114
 
155
- # Merge the repository
156
- #
157
- # @return [nil] if the repository is invalid
158
- # @return [:no_remote] if the remote is not yet set
159
- # @return [String] if there is an error return the message
160
- # @return [Array<String>] if there is a conflict return the Array of
161
- # conflicted file names
162
- # @return [:ok] if the merged with no errors or conflicts
163
- def merge
164
- return nil unless valid?
165
- return :no_remote unless remote?
166
-
167
- return :ok unless remote_branch
168
- return :ok if remote_branch.tip.oid == current_oid
169
-
170
- @grit.git.merge(
171
- { raise: true, chdir: root },
172
- "#{@remote_name}/#{@branch_name}"
173
- )
174
- :ok
175
- rescue Grit::Git::GitTimeout
176
- "Merge timed out for #{root}"
177
- rescue Grit::Git::CommandFailed => e
178
- # HACK: The rugged in-memory index will not have been updated after the
179
- # Grit merge command. Reload it before checking for conflicts.
180
- @rugged.index.reload
181
- return e.err unless @rugged.index.conflicts?
182
- mark_conflicts
183
- end
115
+ # @return [Boolean]
116
+ def need_sync?
117
+ return false unless valid?
118
+ return false unless remote?
119
+ remote_oid != current_oid
120
+ end
184
121
 
185
- # Commit the working directory
186
- #
187
- # @return [nil] if the repository is invalid
188
- # @return [Boolean] whether a commit was made or not
189
- def commit
190
- return nil unless valid?
122
+ # @param [String] term
123
+ # @yield [file, context] Gives the files and context for each of the results
124
+ # @yieldparam file [String]
125
+ # @yieldparam context [String]
126
+ def grep(term, &block)
127
+ @grit.git.grep(
128
+ { raise: true, bare: false, chdir: root, ignore_case: true },
129
+ term
130
+ ).scan(/(.*?):([^\n]*)/, &block)
131
+ rescue Grit::Git::GitTimeout
132
+ # TODO: add logging to record the error details
133
+ ''
134
+ rescue Grit::Git::CommandFailed
135
+ # TODO: add logging to record the error details if they are not just
136
+ # nothing found
137
+ ''
138
+ end
191
139
 
192
- # Do this first to allow the message file to be deleted, if it exists.
193
- message = read_and_delete_commit_message_file
140
+ # Fetch all the remote branches
141
+ #
142
+ # @raise [FetchError] if there is an error return message
143
+ #
144
+ # @return [nil] if the repository is invalid
145
+ # @return [:no_remote] if the remote is not yet set
146
+ # @return [:ok] if the fetch worked
147
+ def fetch
148
+ return nil unless valid?
149
+ return :no_remote unless remote?
150
+
151
+ @rugged.remotes.each { |x| @grit.remote_fetch(x.name) }
152
+ :ok
153
+ rescue Grit::Git::GitTimeout
154
+ raise(FetchError, "Fetch timed out for #{root}")
155
+ rescue Grit::Git::CommandFailed => e
156
+ raise(FetchError, e.err)
157
+ end
194
158
 
195
- mark_empty_directories
159
+ # Merge the repository
160
+ #
161
+ # @raise [MergeError] if there is an error, it it will include the message
162
+ #
163
+ # @return [nil] if the repository is invalid
164
+ # @return [:no_remote] if the remote is not yet set
165
+ # @return [Array<String>] if there is a conflict return the Array of
166
+ # conflicted file names
167
+ # @return (see #author_count) if merged with no errors or conflicts
168
+ def merge
169
+ return nil unless valid?
170
+ return :no_remote unless remote?
171
+ return :ok unless remote_oid
172
+ return :ok if remote_oid == current_oid
173
+
174
+ last_oid = current_oid
175
+ @grit.git.merge(
176
+ { raise: true, chdir: root },
177
+ "#{@remote_name}/#{@branch_name}"
178
+ )
179
+ author_count(last_oid)
180
+ rescue Grit::Git::GitTimeout
181
+ raise(MergeError, "Merge timed out for #{root}")
182
+ rescue Grit::Git::CommandFailed => e
183
+ # HACK: The rugged in-memory index will not have been updated after the
184
+ # Grit merge command. Reload it before checking for conflicts.
185
+ @rugged.index.reload
186
+ raise(MergeError, e.err) unless @rugged.index.conflicts?
187
+ mark_conflicts
188
+ end
196
189
 
197
- return false unless dirty?
190
+ # @return [nil]
191
+ # @return (see Gitdocs::Repository::Comitter#commit)
192
+ def commit
193
+ return unless valid?
194
+ Committer.new(root).commit
195
+ end
198
196
 
199
- # Commit any changes in the working directory.
200
- Dir.chdir(root) do
201
- @rugged.index.add_all
202
- @rugged.index.update_all
197
+ # Push the repository
198
+ #
199
+ # @return [nil] if the repository is invalid
200
+ # @return [:no_remote] if the remote is not yet set
201
+ # @return [:nothing] if there was nothing to do
202
+ # @return [String] if there is an error return the message
203
+ # @return (see #author_count) if pushed without errors or conflicts
204
+ def push
205
+ return unless valid?
206
+ return :no_remote unless remote?
207
+ return :nothing unless current_oid
208
+ return :nothing if remote_oid == current_oid
209
+
210
+ last_oid = remote_oid
211
+ @grit.git.push({ raise: true }, @remote_name, @branch_name)
212
+ author_count(last_oid)
213
+ rescue Grit::Git::CommandFailed => e
214
+ return :conflict if e.err[/\[rejected\]/]
215
+ e.err # return the output on error
203
216
  end
204
- @rugged.index.write
205
- @grit.commit_index(message)
206
- true
207
- end
208
217
 
209
- # Push the repository
210
- #
211
- # @return [nil] if the repository is invalid
212
- # @return [:no_remote] if the remote is not yet set
213
- # @return [:nothing] if there was nothing to do
214
- # @return [String] if there is an error return the message
215
- # @return [:ok] if committed and pushed without errors or conflicts
216
- def push
217
- return nil unless valid?
218
- return :no_remote unless remote?
219
-
220
- return :nothing if current_oid.nil?
221
- return :nothing if remote_branch && remote_branch.tip.oid == current_oid
222
-
223
- @grit.git.push({ raise: true }, @remote_name, @branch_name)
224
- :ok
225
- rescue Grit::Git::CommandFailed => e
226
- return :conflict if e.err[/\[rejected\]/]
227
- e.err # return the output on error
228
- end
218
+ # Get the count of commits by author from the head to the specified oid.
219
+ #
220
+ # @param [String] last_oid
221
+ #
222
+ # @return [Hash<String, Int>]
223
+ def author_count(last_oid)
224
+ walker = head_walker
225
+ walker.hide(last_oid) if last_oid
226
+ walker.reduce(Hash.new(0)) do |result, commit|
227
+ result["#{commit.author[:name]} <#{commit.author[:email]}>"] += 1
228
+ result
229
+ end
230
+ rescue Rugged::ReferenceError
231
+ {}
232
+ rescue Rugged::OdbError
233
+ {}
234
+ end
229
235
 
230
- # Get the count of commits by author from the head to the specified oid.
231
- #
232
- # @param [String] last_oid
233
- #
234
- # @return [Hash<String, Int>]
235
- def author_count(last_oid)
236
- walker = head_walker
237
- walker.hide(last_oid) if last_oid
238
- walker.reduce(Hash.new(0)) do |result, commit|
239
- result["#{commit.author[:name]} <#{commit.author[:email]}>"] += 1
236
+ # @return [Hash{:merge,:push => Object}]
237
+ def synchronize(type)
238
+ result = { merge: nil, push: nil }
239
+ return result unless valid?
240
+
241
+ case type
242
+ when 'fetch'
243
+ fetch
244
+ when 'full'
245
+ commit
246
+ fetch
247
+ result[:merge] = merge
248
+ result[:push] = push
249
+ end
250
+ result
251
+ rescue Gitdocs::Repository::FetchError
252
+ result
253
+ rescue Gitdocs::Repository::MergeError => e
254
+ result[:merge] = e.message
240
255
  result
241
256
  end
242
- rescue Rugged::ReferenceError
243
- {}
244
- rescue Rugged::OdbError
245
- {}
246
- end
247
257
 
248
- # @param [String] message
249
- def write_commit_message(message)
250
- return unless message
251
- return if message.empty?
258
+ # @param (see Gitdocs::Repository::Comitter#write_commit_message)
259
+ # @return [void]
260
+ def write_commit_message(message)
261
+ return unless valid?
262
+ Committer.new(root).write_commit_message(message)
263
+ end
252
264
 
253
- File.open(@commit_message_path, 'w') { |f| f.print(message) }
254
- end
265
+ # Excluding the initial commit (without a parent) which keeps things
266
+ # consistent with the original behaviour.
267
+ # TODO: reconsider if this is the correct behaviour
268
+ #
269
+ # @param [String] relative_path
270
+ # @param [Integer] limit the number of commits which will be returned
271
+ #
272
+ # @return [Array<Rugged::Commit>]
273
+ def commits_for(relative_path, limit)
274
+ # TODO: should add a filter here for checking that the commit actually has
275
+ # an associated blob.
276
+ commits = head_walker.select do |commit|
277
+ commit.parents.size == 1 && changes?(commit, relative_path)
278
+ end
279
+ # TODO: should re-write this limit in a way that will skip walking all of
280
+ # the commits.
281
+ commits.first(limit)
282
+ end
255
283
 
256
- # Excluding the initial commit (without a parent) which keeps things
257
- # consistent with the original behaviour.
258
- # TODO: reconsider if this is the correct behaviour
259
- #
260
- # @param [String] relative_path
261
- # @param [Integer] limit the number of commits which will be returned
262
- #
263
- # @return [Array<Rugged::Commit>]
264
- def commits_for(relative_path, limit)
265
- # TODO: should add a filter here for checking that the commit actually has
266
- # an associated blob.
267
- commits = head_walker.select do |commit|
268
- commit.parents.size == 1 && commit.diff(paths: [relative_path]).size > 0
284
+ # @param [String] relative_path
285
+ #
286
+ # @return [Rugged::Commit]
287
+ def last_commit_for(relative_path)
288
+ head_walker.find { |commit| changes?(commit, relative_path) }
269
289
  end
270
- # TODO: should re-write this limit in a way that will skip walking all of
271
- # the commits.
272
- commits.first(limit)
273
- end
274
290
 
275
- # @param [String] relative_path
276
- #
277
- # @return [Rugged::Commit]
278
- def last_commit_for(relative_path)
279
- head_walker.find { |commit| commit.diff(paths: [relative_path]).size > 0 }
280
- end
291
+ # @param [String] relative_path
292
+ # @param [String] oid
293
+ def blob_at(relative_path, ref)
294
+ @rugged.blob_at(ref, relative_path)
295
+ end
281
296
 
282
- # @param [String] relative_path
283
- # @param [String] oid
284
- def blob_at(relative_path, ref)
285
- @rugged.blob_at(ref, relative_path)
286
- end
297
+ ############################################################################
287
298
 
288
- ##############################################################################
299
+ private
289
300
 
290
- private
301
+ # @param [Rugged::Commit] commit
302
+ # @param [String] relative_path
303
+ # @return [Boolean]
304
+ def changes?(commit, relative_path)
305
+ commit.diff(paths: [relative_path]).size > 0 # rubocop:disable ZeroLengthPredicate
306
+ end
291
307
 
292
- def remote?
293
- @rugged.remotes.any?
294
- end
308
+ # @return [Boolean]
309
+ def remote?
310
+ @rugged.remotes.any?
311
+ end
295
312
 
296
- # HACK: This will return nil if there are no commits in the remote branch.
297
- # It is not the response that I would expect but it mostly gets the job
298
- # done. This should probably be reviewed when upgrading to the next version
299
- # of Rugged.
300
- #
301
- # @return [nil] if the remote branch does not exist
302
- # @return [Rugged::Remote]
303
- def remote_branch
304
- Rugged::Branch.lookup(@rugged, "#{@remote_name}/#{@branch_name}", :remote)
305
- end
313
+ # @return [nil]
314
+ # @return [String]
315
+ def remote_oid
316
+ branch = @rugged.branches["#{@remote_name}/#{@branch_name}"]
317
+ return unless branch
318
+ branch.target_id
319
+ end
306
320
 
307
- def head_walker
308
- walker = Rugged::Walker.new(@rugged)
309
- walker.sorting(Rugged::SORT_DATE)
310
- walker.push(@rugged.head.target)
311
- walker
312
- end
321
+ def head_walker
322
+ walker = Rugged::Walker.new(@rugged)
323
+ walker.sorting(Rugged::SORT_DATE)
324
+ walker.push(@rugged.head.target)
325
+ walker
326
+ end
313
327
 
314
- def read_and_delete_commit_message_file
315
- return 'Auto-commit from gitdocs' unless File.exist?(@commit_message_path)
328
+ def read_and_delete_commit_message_file
329
+ return 'Auto-commit from gitdocs' unless File.exist?(@commit_message_path)
316
330
 
317
- message = File.read(@commit_message_path)
318
- File.delete(@commit_message_path)
319
- message
320
- end
331
+ message = File.read(@commit_message_path)
332
+ File.delete(@commit_message_path)
333
+ message
334
+ end
321
335
 
322
- def mark_empty_directories
323
- Find.find(root).each do |path| # rubocop:disable Style/Next
324
- Find.prune if File.basename(path) == '.git'
325
- if File.directory?(path) && Dir.entries(path).count == 2
326
- FileUtils.touch(File.join(path, '.gitignore'))
336
+ def mark_empty_directories
337
+ Find.find(root).each do |path|
338
+ Find.prune if File.basename(path) == '.git'
339
+ if File.directory?(path) && Dir.entries(path).count == 2
340
+ FileUtils.touch(File.join(path, '.gitignore'))
341
+ end
327
342
  end
328
343
  end
329
- end
330
344
 
331
- def mark_conflicts
332
- # assert(@rugged.index.conflicts?)
345
+ def mark_conflicts
346
+ # assert(@rugged.index.conflicts?)
333
347
 
334
- # Collect all the index entries by their paths.
335
- index_path_entries = Hash.new { |h, k| h[k] = Array.new }
336
- @rugged.index.map do |index_entry|
337
- index_path_entries[index_entry[:path]].push(index_entry)
338
- end
348
+ # Collect all the index entries by their paths.
349
+ index_path_entries = Hash.new { |h, k| h[k] = [] }
350
+ @rugged.index.map do |index_entry|
351
+ index_path_entries[index_entry[:path]].push(index_entry)
352
+ end
339
353
 
340
- # Filter to only the conflicted entries.
341
- conflicted_path_entries = index_path_entries.delete_if { |_k, v| v.length == 1 }
342
-
343
- conflicted_path_entries.each_pair do |path, index_entries|
344
- # Write out the different versions of the conflicted file.
345
- index_entries.each do |index_entry|
346
- filename, extension = index_entry[:path].scan(/(.*?)(|\.[^\.]+)$/).first
347
- author = ' original' if index_entry[:stage] == 1
348
- short_oid = index_entry[:oid][0..6]
349
- new_filename = "#{filename} (#{short_oid}#{author})#{extension}"
350
- File.open(abs_path(new_filename), 'wb') do |f|
351
- f.write(Rugged::Blob.lookup(@rugged, index_entry[:oid]).content)
354
+ # Filter to only the conflicted entries.
355
+ conflicted_path_entries =
356
+ index_path_entries.delete_if { |_k, v| v.length == 1 }
357
+
358
+ conflicted_path_entries.each_pair do |path, index_entries|
359
+ # Write out the different versions of the conflicted file.
360
+ index_entries.each do |index_entry|
361
+ filename, extension =
362
+ index_entry[:path].scan(/(.*?)(|\.[^\.]+)$/).first
363
+ author = ' original' if index_entry[:stage] == 1
364
+ short_oid = index_entry[:oid][0..6]
365
+ new_filename = "#{filename} (#{short_oid}#{author})#{extension}"
366
+ File.open(abs_path(new_filename), 'wb') do |f|
367
+ f.write(Rugged::Blob.lookup(@rugged, index_entry[:oid]).content)
368
+ end
352
369
  end
353
- end
354
370
 
355
- # And remove the original.
356
- FileUtils.remove(abs_path(path), force: true)
357
- end
371
+ # And remove the original.
372
+ FileUtils.remove(abs_path(path), force: true)
373
+ end
358
374
 
359
- # NOTE: Let commit be handled by the next regular commit.
375
+ # NOTE: Let commit be handled by the next regular commit.
360
376
 
361
- conflicted_path_entries.keys
362
- end
377
+ conflicted_path_entries.keys
378
+ end
363
379
 
364
- def abs_path(*path)
365
- File.join(root, *path)
380
+ def abs_path(*path)
381
+ File.join(root, *path)
382
+ end
366
383
  end
367
384
  end