gitdocs 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|