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.
- checksums.yaml +6 -14
- data/.codeclimate.yml +26 -0
- data/.rubocop.yml +8 -2
- data/.travis.yml +8 -0
- data/CHANGELOG +13 -0
- data/Gemfile +1 -1
- data/README.md +7 -6
- data/Rakefile +31 -5
- data/bin/gitdocs +1 -0
- data/config.ru +6 -4
- data/gitdocs.gemspec +22 -19
- data/lib/gitdocs.rb +54 -16
- data/lib/gitdocs/browser_app.rb +34 -41
- data/lib/gitdocs/cli.rb +41 -32
- data/lib/gitdocs/configuration.rb +40 -101
- data/lib/gitdocs/git_notifier.rb +111 -0
- data/lib/gitdocs/initializer.rb +83 -0
- data/lib/gitdocs/manager.rb +90 -60
- data/lib/gitdocs/migration/004_add_index_for_path.rb +1 -1
- data/lib/gitdocs/notifier.rb +70 -104
- data/lib/gitdocs/rendering_helper.rb +3 -0
- data/lib/gitdocs/repository.rb +324 -307
- data/lib/gitdocs/repository/committer.rb +77 -0
- data/lib/gitdocs/repository/path.rb +157 -140
- data/lib/gitdocs/search.rb +40 -25
- data/lib/gitdocs/settings_app.rb +5 -3
- data/lib/gitdocs/share.rb +64 -0
- data/lib/gitdocs/synchronizer.rb +40 -0
- data/lib/gitdocs/version.rb +1 -1
- data/lib/gitdocs/views/_header.haml +2 -2
- data/lib/gitdocs/views/dir.haml +3 -3
- data/lib/gitdocs/views/edit.haml +1 -1
- data/lib/gitdocs/views/file.haml +1 -1
- data/lib/gitdocs/views/home.haml +3 -3
- data/lib/gitdocs/views/layout.haml +13 -13
- data/lib/gitdocs/views/revisions.haml +3 -3
- data/lib/gitdocs/views/search.haml +1 -1
- data/lib/gitdocs/views/settings.haml +6 -6
- data/test/integration/cli/full_sync_test.rb +83 -0
- data/test/integration/cli/share_management_test.rb +29 -0
- data/test/integration/cli/status_test.rb +14 -0
- data/test/integration/test_helper.rb +185 -151
- data/test/integration/{browse_test.rb → web/browse_test.rb} +11 -29
- data/test/integration/web/share_management_test.rb +46 -0
- data/test/support/git_factory.rb +276 -0
- data/test/unit/browser_app_test.rb +346 -0
- data/test/unit/configuration_test.rb +8 -70
- data/test/unit/git_notifier_test.rb +116 -0
- data/test/unit/gitdocs_test.rb +90 -0
- data/test/unit/manager_test.rb +36 -0
- data/test/unit/notifier_test.rb +60 -124
- data/test/unit/repository_committer_test.rb +111 -0
- data/test/unit/repository_path_test.rb +92 -76
- data/test/unit/repository_test.rb +243 -356
- data/test/unit/search_test.rb +15 -0
- data/test/unit/settings_app_test.rb +80 -0
- data/test/unit/share_test.rb +97 -0
- data/test/unit/test_helper.rb +17 -3
- metadata +114 -108
- data/lib/gitdocs/runner.rb +0 -108
- data/lib/gitdocs/server.rb +0 -62
- data/test/integration/full_sync_test.rb +0 -66
- data/test/integration/share_management_test.rb +0 -95
- data/test/integration/status_test.rb +0 -21
- data/test/unit/runner_test.rb +0 -122
data/lib/gitdocs/manager.rb
CHANGED
@@ -4,83 +4,113 @@ module Gitdocs
|
|
4
4
|
Restart = Class.new(RuntimeError)
|
5
5
|
|
6
6
|
class Manager
|
7
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
22
|
-
|
23
|
-
log('Starting EM loop...')
|
32
|
+
Celluloid.boot unless Celluloid.running?
|
33
|
+
@supervisor = Celluloid::SupervisionGroup.run!
|
24
34
|
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
Gitdocs
|
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
|
-
|
43
|
-
|
101
|
+
Gitdocs.log_info('stopping listeners...')
|
102
|
+
@listener.stop if @listener
|
44
103
|
|
45
|
-
|
46
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
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::
|
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?
|
data/lib/gitdocs/notifier.rb
CHANGED
@@ -1,121 +1,87 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
|
3
3
|
# Wrapper for the UI notifier
|
4
|
-
|
5
|
-
|
4
|
+
module Gitdocs
|
5
|
+
class Notifier
|
6
|
+
include Singleton
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
67
|
+
############################################################################
|
100
68
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
80
|
+
# @private
|
81
|
+
# @return [void]
|
82
|
+
def disconnect
|
83
|
+
@notifier.disconnect if @notifier
|
84
|
+
@notifier = nil
|
85
|
+
end
|
120
86
|
end
|
121
87
|
end
|
data/lib/gitdocs/repository.rb
CHANGED
@@ -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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
return false unless valid?
|
78
|
+
# @return [Boolean]
|
79
|
+
def valid?
|
80
|
+
!@invalid_reason
|
81
|
+
end
|
105
82
|
|
106
|
-
return
|
107
|
-
@
|
108
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
116
|
-
|
117
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
#
|
193
|
-
|
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
|
-
|
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
|
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
|
-
#
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
254
|
-
|
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
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
293
|
-
|
294
|
-
|
308
|
+
# @return [Boolean]
|
309
|
+
def remote?
|
310
|
+
@rugged.remotes.any?
|
311
|
+
end
|
295
312
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
315
|
-
|
328
|
+
def read_and_delete_commit_message_file
|
329
|
+
return 'Auto-commit from gitdocs' unless File.exist?(@commit_message_path)
|
316
330
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
331
|
+
message = File.read(@commit_message_path)
|
332
|
+
File.delete(@commit_message_path)
|
333
|
+
message
|
334
|
+
end
|
321
335
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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
|
-
|
332
|
-
|
345
|
+
def mark_conflicts
|
346
|
+
# assert(@rugged.index.conflicts?)
|
333
347
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
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
|
-
|
356
|
-
|
357
|
-
|
371
|
+
# And remove the original.
|
372
|
+
FileUtils.remove(abs_path(path), force: true)
|
373
|
+
end
|
358
374
|
|
359
|
-
|
375
|
+
# NOTE: Let commit be handled by the next regular commit.
|
360
376
|
|
361
|
-
|
362
|
-
|
377
|
+
conflicted_path_entries.keys
|
378
|
+
end
|
363
379
|
|
364
|
-
|
365
|
-
|
380
|
+
def abs_path(*path)
|
381
|
+
File.join(root, *path)
|
382
|
+
end
|
366
383
|
end
|
367
384
|
end
|