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