gitdocs 0.5.0.pre1 → 0.5.0.pre2
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 +8 -8
- data/CHANGELOG +6 -2
- data/README.md +1 -0
- data/gitdocs.gemspec +3 -1
- data/lib/gitdocs.rb +4 -0
- data/lib/gitdocs/cli.rb +22 -21
- data/lib/gitdocs/configuration.rb +0 -14
- data/lib/gitdocs/manager.rb +2 -13
- data/lib/gitdocs/notifier.rb +38 -0
- data/lib/gitdocs/repository.rb +348 -0
- data/lib/gitdocs/runner.rb +69 -192
- data/lib/gitdocs/server.rb +20 -22
- data/lib/gitdocs/version.rb +1 -1
- data/lib/gitdocs/views/settings.haml +3 -3
- data/test/configuration_test.rb +2 -0
- data/test/integration/full_sync_test.rb +67 -0
- data/test/integration/share_management_test.rb +46 -0
- data/test/integration/status_test.rb +19 -0
- data/test/integration/test_helper.rb +130 -0
- data/test/notifier_test.rb +68 -0
- data/test/repository_test.rb +578 -0
- data/test/runner_test.rb +133 -16
- data/test/test_helper.rb +0 -1
- metadata +46 -4
data/lib/gitdocs/runner.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
module Gitdocs
|
2
2
|
class Runner
|
3
|
-
include ShellTools
|
4
|
-
|
5
|
-
attr_reader :root, :listener
|
6
|
-
|
7
3
|
def self.start_all(shares)
|
8
4
|
runners = shares.map { |share| Runner.new(share) }
|
9
5
|
runners.each(&:run)
|
@@ -12,40 +8,23 @@ module Gitdocs
|
|
12
8
|
|
13
9
|
def initialize(share)
|
14
10
|
@share = share
|
15
|
-
@root = share.path.sub(%r{/+$}, '') if share.path
|
16
11
|
@polling_interval = share.polling_interval
|
17
|
-
@
|
12
|
+
@notifier = Gitdocs::Notifier.new(@share.notification)
|
13
|
+
@repository = Gitdocs::Repository.new(share)
|
18
14
|
end
|
19
15
|
|
20
|
-
|
21
|
-
|
22
|
-
return [] if term.empty?
|
23
|
-
|
24
|
-
results = []
|
25
|
-
if result_test = sh_string("git grep -i #{ShellTools.escape(term)}")
|
26
|
-
result_test.scan(/(.*?):([^\n]*)/) do |(file, context)|
|
27
|
-
if result = results.find { |s| s.file == file }
|
28
|
-
result.context += ' ... ' + context
|
29
|
-
else
|
30
|
-
results << SearchResult.new(file, context)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
results
|
16
|
+
def root
|
17
|
+
@repository.root
|
35
18
|
end
|
36
19
|
|
37
20
|
def run
|
38
|
-
return false unless
|
21
|
+
return false unless @repository.valid?
|
39
22
|
|
40
|
-
@
|
41
|
-
@current_remote = @share.remote_name
|
42
|
-
@current_branch = @share.branch_name
|
43
|
-
@current_revision = sh_string('git rev-parse HEAD')
|
44
|
-
Guard::Notifier.turn_on if @show_notifications
|
23
|
+
@last_synced_revision = @repository.current_oid
|
45
24
|
|
46
25
|
mutex = Mutex.new
|
47
26
|
|
48
|
-
info('Running gitdocs!', "Running gitdocs in
|
27
|
+
@notifier.info('Running gitdocs!', "Running gitdocs in '#{root}'")
|
49
28
|
|
50
29
|
# Pull changes from remote repository
|
51
30
|
syncer = proc do
|
@@ -61,7 +40,7 @@ module Gitdocs
|
|
61
40
|
# Listen for changes in local repository
|
62
41
|
|
63
42
|
EM.defer(proc do
|
64
|
-
listener = Guard::Listener.select_and_init(
|
43
|
+
listener = Guard::Listener.select_and_init(root, watch_all_modifications: true)
|
65
44
|
listener.on_change do |directories|
|
66
45
|
directories.uniq!
|
67
46
|
directories.delete_if { |d| d =~ /\/\.git/ }
|
@@ -82,192 +61,90 @@ module Gitdocs
|
|
82
61
|
end
|
83
62
|
|
84
63
|
def sync_changes
|
85
|
-
|
86
|
-
if status.success?
|
87
|
-
changes = get_latest_changes
|
88
|
-
unless changes.empty?
|
89
|
-
author_list = changes.reduce(Hash.new { |h, k| h[k] = 0 }) { |h, c| h[c['author']] += 1; h }.to_a.sort { |a, b| b[1] <=> a[1] }.map { |(name, count)| "* #{name} (#{count} change#{count == 1 ? '' : 's'})" }.join("\n")
|
90
|
-
info("Updated with #{changes.size} change#{changes.size == 1 ? '' : 's'}", "In `#{@root}':\n#{author_list}")
|
91
|
-
end
|
92
|
-
push_changes
|
93
|
-
elsif out[/CONFLICT/]
|
94
|
-
conflicted_files = sh('git ls-files -u --full-name -z').split("\0")
|
95
|
-
.reduce(Hash.new { |h, k| h[k] = [] }) do|h, line|
|
96
|
-
parts = line.split(/\t/)
|
97
|
-
h[parts.last] << parts.first.split(/ /)
|
98
|
-
h
|
99
|
-
end
|
100
|
-
warn('There were some conflicts', "#{conflicted_files.keys.map { |f| "* #{f}" }.join("\n")}")
|
101
|
-
conflicted_files.each do |conflict, ids|
|
102
|
-
conflict_start, conflict_end = conflict.scan(/(.*?)(|\.[^\.]+)$/).first
|
103
|
-
ids.each do |(mode, sha, id)|
|
104
|
-
author = ' original' if id == '1'
|
105
|
-
system("cd #{@root} && git show :#{id}:#{conflict} > '#{conflict_start} (#{sha[0..6]}#{author})#{conflict_end}'")
|
106
|
-
end
|
107
|
-
system("cd #{@root} && git rm #{conflict}") || fail
|
108
|
-
end
|
109
|
-
push_changes
|
110
|
-
elsif sh_string('git remote').nil? # no remote to pull from
|
111
|
-
# Do nothing, no remote repo yet
|
112
|
-
else
|
113
|
-
error('There was a problem synchronizing this gitdoc', "A problem occurred in #{@root}:\n#{out}")
|
114
|
-
end
|
115
|
-
end
|
64
|
+
result = @repository.pull
|
116
65
|
|
117
|
-
|
118
|
-
message_file = File.expand_path('.gitmessage~', @root)
|
119
|
-
if File.exist? message_file
|
120
|
-
message = File.read message_file
|
121
|
-
File.delete message_file
|
122
|
-
else
|
123
|
-
message = 'Auto-commit from gitdocs'
|
124
|
-
end
|
125
|
-
sh 'find . -type d -regex ``./[^.].*'' -empty -exec touch \'{}/.gitignore\' \;'
|
126
|
-
sh 'git add .'
|
127
|
-
sh "git commit -a -m #{ShellTools.escape(message)}" unless sh('git status -s').strip.empty?
|
128
|
-
if @current_revision.nil? || sh('git status')[/branch is ahead/]
|
129
|
-
out, code = sh_with_code("git push #{@current_remote} #{@current_branch}")
|
130
|
-
if code.success?
|
131
|
-
changes = get_latest_changes
|
132
|
-
info("Pushed #{changes.size} change#{changes.size == 1 ? '' : 's'}", "`#{@root}' has been pushed")
|
133
|
-
elsif @current_revision.nil?
|
134
|
-
# ignorable
|
135
|
-
elsif out[/\[rejected\]/]
|
136
|
-
warn("There was a conflict in #{@root}, retrying", '')
|
137
|
-
else
|
138
|
-
error("BAD Could not push changes in #{@root}", out)
|
139
|
-
# TODO: need to add a status on shares so that the push problem can be
|
140
|
-
# displayed.
|
141
|
-
end
|
142
|
-
end
|
143
|
-
rescue
|
144
|
-
# Rescue any standard exceptions which come from the push related
|
145
|
-
# commands. This will prevent problems on a single share from killing
|
146
|
-
# the entire daemon.
|
147
|
-
error("Unexpected error pushing changes in #{@root}")
|
148
|
-
# TODO: get logging and/or put the error message into a status field in the database
|
149
|
-
end
|
66
|
+
return if result.nil? || result == :no_remote
|
150
67
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
lines = []
|
158
|
-
Yajl::Parser.new.parse(out) do |obj|
|
159
|
-
lines << obj
|
160
|
-
end
|
161
|
-
@current_revision = sh('git rev-parse HEAD').strip
|
162
|
-
lines
|
163
|
-
end
|
164
|
-
else
|
165
|
-
[]
|
68
|
+
if result.kind_of?(String)
|
69
|
+
@notifier.error(
|
70
|
+
'There was a problem synchronizing this gitdoc',
|
71
|
+
"A problem occurred in #{root}:\n#{result}"
|
72
|
+
)
|
73
|
+
return
|
166
74
|
end
|
167
|
-
end
|
168
75
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
# file_meta("path/to/file")
|
178
|
-
# => { :author => "Nick", :size => 1000, :modified => ... }
|
179
|
-
def file_meta(file)
|
180
|
-
file = file.gsub(%r{^/}, '')
|
181
|
-
full_path = File.expand_path(file, @root)
|
182
|
-
log_result = sh_string("git log --format='%aN|%ai' -n1 #{ShellTools.escape(file)}")
|
183
|
-
author, modified = log_result.split('|')
|
184
|
-
modified = Time.parse(modified.sub(' ', 'T')).utc.iso8601
|
185
|
-
size = if File.directory?(full_path)
|
186
|
-
Dir[File.join(full_path, '**', '*')].reduce(0) do |size, file|
|
187
|
-
File.symlink?(file) ? size : size += File.size(file)
|
76
|
+
if result == :ok
|
77
|
+
author_change_count = latest_author_count
|
78
|
+
unless author_change_count.empty?
|
79
|
+
author_list = author_change_count.map { |author, count| "* #{author} (#{change_count(count)})" }.join("\n")
|
80
|
+
@notifier.info(
|
81
|
+
"Updated with #{change_count(author_change_count)}",
|
82
|
+
"In '#{root}':\n#{author_list}"
|
83
|
+
)
|
188
84
|
end
|
189
85
|
else
|
190
|
-
|
86
|
+
#assert result.kind_of?(Array)
|
87
|
+
@notifier.warn(
|
88
|
+
'There were some conflicts',
|
89
|
+
result.map { |f| "* #{f}" }.join("\n")
|
90
|
+
)
|
191
91
|
end
|
192
|
-
size = -1 if size == 0 # A value of 0 breaks the table sort for some reason
|
193
92
|
|
194
|
-
|
93
|
+
push_changes
|
195
94
|
end
|
196
95
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
date = Time.parse(date.sub(' ', 'T')).utc.iso8601
|
205
|
-
{ commit: commit, subject: subject, author: author, date: date }
|
96
|
+
def push_changes
|
97
|
+
message_file = File.expand_path('.gitmessage~', root)
|
98
|
+
if File.exist?(message_file)
|
99
|
+
message = File.read(message_file)
|
100
|
+
File.delete(message_file)
|
101
|
+
else
|
102
|
+
message = 'Auto-commit from gitdocs'
|
206
103
|
end
|
207
|
-
end
|
208
104
|
|
209
|
-
|
210
|
-
# file_revision_at("README", "a4c56h") => "/tmp/some/path/README"
|
211
|
-
def file_revision_at(file, ref)
|
212
|
-
file = file.gsub(%r{^/}, '')
|
213
|
-
content = sh_string("git show #{ref}:#{ShellTools.escape(file)}")
|
214
|
-
tmp_path = File.expand_path(File.basename(file), Dir.tmpdir)
|
215
|
-
File.open(tmp_path, 'w') { |f| f.puts content }
|
216
|
-
tmp_path
|
217
|
-
end
|
105
|
+
result = @repository.push(@last_synced_revision, message)
|
218
106
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
107
|
+
return if result.nil? || result == :no_remote || result == :nothing
|
108
|
+
level, title, message = case result
|
109
|
+
when :ok then [:info, "Pushed #{change_count(latest_author_count)}", "'#{root}' has been pushed"]
|
110
|
+
when :conflict then [:warn, "There was a conflict in #{root}, retrying", '']
|
111
|
+
else
|
112
|
+
# assert result.kind_of?(String)
|
113
|
+
[:error, "BAD Could not push changes in #{root}", result]
|
114
|
+
# TODO: need to add a status on shares so that the push problem can be
|
115
|
+
# displayed.
|
226
116
|
end
|
117
|
+
@notifier.send(level, title, message)
|
118
|
+
rescue => e
|
119
|
+
# Rescue any standard exceptions which come from the push related
|
120
|
+
# commands. This will prevent problems on a single share from killing
|
121
|
+
# the entire daemon.
|
122
|
+
@notifier.error("Unexpected error pushing changes in #{root}", "#{e}")
|
123
|
+
# TODO: get logging and/or put the error message into a status field in the database
|
227
124
|
end
|
228
125
|
|
229
|
-
|
230
|
-
|
231
|
-
@root.present? && status.success?
|
232
|
-
end
|
126
|
+
############################################################################
|
127
|
+
private
|
233
128
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
end
|
129
|
+
# Update the author count for the last synced changes, and then update the
|
130
|
+
# last synced revision id.
|
131
|
+
#
|
132
|
+
# @return [Hash<String,Int>]
|
133
|
+
def latest_author_count
|
134
|
+
last_oid = @last_synced_revision
|
135
|
+
@last_synced_revision = @repository.current_oid
|
242
136
|
|
243
|
-
|
244
|
-
if @show_notifications
|
245
|
-
Guard::Notifier.notify(msg, title: title, image: @icon)
|
246
|
-
else
|
247
|
-
puts("#{title}: #{msg}")
|
248
|
-
end
|
249
|
-
rescue # Prevent StandardErrors from stopping the daemon.
|
137
|
+
@repository.author_count(last_oid)
|
250
138
|
end
|
251
139
|
|
252
|
-
def
|
253
|
-
if
|
254
|
-
|
140
|
+
def change_count(count_or_hash)
|
141
|
+
count = if count_or_hash.respond_to?(:values)
|
142
|
+
count_or_hash .values.reduce(:+)
|
255
143
|
else
|
256
|
-
|
144
|
+
count_or_hash
|
257
145
|
end
|
258
|
-
rescue # Prevent StandardErrors from stopping the daemon.
|
259
|
-
end
|
260
|
-
|
261
|
-
# sh_string("git config branch.`git branch | grep '^\*' | sed -e 's/\* //'`.remote", "origin")
|
262
|
-
def sh_string(cmd, default = nil)
|
263
|
-
val = sh(cmd).strip rescue nil
|
264
|
-
val.nil? || val.empty? ? default : val
|
265
|
-
end
|
266
146
|
|
267
|
-
|
268
|
-
# @see #sh
|
269
|
-
def sh_with_code(cmd)
|
270
|
-
ShellTools.sh_with_code(cmd, @root)
|
147
|
+
"#{count} change#{count == 1 ? '' : 's'}"
|
271
148
|
end
|
272
149
|
end
|
273
150
|
end
|
data/lib/gitdocs/server.rb
CHANGED
@@ -8,15 +8,15 @@ require 'launchy'
|
|
8
8
|
|
9
9
|
module Gitdocs
|
10
10
|
class Server
|
11
|
-
def initialize(manager, port = 8888,
|
12
|
-
@manager
|
13
|
-
@port
|
14
|
-
@
|
11
|
+
def initialize(manager, port = 8888, repositories)
|
12
|
+
@manager = manager
|
13
|
+
@port = port.to_i
|
14
|
+
@repositories = repositories
|
15
15
|
end
|
16
16
|
|
17
17
|
def start
|
18
|
-
|
19
|
-
manager
|
18
|
+
repositories = @repositories
|
19
|
+
manager = @manager
|
20
20
|
Thin::Logging.debug = @manager.debug
|
21
21
|
Thin::Server.start('127.0.0.1', @port) do
|
22
22
|
use Rack::Static, urls: ['/css', '/js', '/img', '/doc'], root: File.expand_path('../public', __FILE__)
|
@@ -49,7 +49,7 @@ module Gitdocs
|
|
49
49
|
end
|
50
50
|
|
51
51
|
path('search').get do
|
52
|
-
render! 'search', layout: 'app', locals: { conf: manager.config, results:
|
52
|
+
render! 'search', layout: 'app', locals: { conf: manager.config, results: Gitdocs::Repository.search(request.GET['q'], repositories), nav_state: nil }
|
53
53
|
end
|
54
54
|
|
55
55
|
path('shares') do
|
@@ -69,20 +69,21 @@ module Gitdocs
|
|
69
69
|
end
|
70
70
|
|
71
71
|
var :int do |idx|
|
72
|
-
|
73
|
-
|
72
|
+
repository = repositories[idx]
|
73
|
+
|
74
|
+
halt 404 if repository.nil?
|
74
75
|
file_path = URI.unescape(request.path_info)
|
75
|
-
expanded_path = File.expand_path(".#{file_path}",
|
76
|
-
message_file = File.expand_path('.gitmessage~',
|
77
|
-
halt 400 unless expanded_path[/^#{Regexp.quote(
|
76
|
+
expanded_path = File.expand_path(".#{file_path}", repository.root)
|
77
|
+
message_file = File.expand_path('.gitmessage~', repository.root)
|
78
|
+
halt 400 unless expanded_path[/^#{Regexp.quote(repository.root)}/]
|
78
79
|
parent = File.dirname(file_path)
|
79
80
|
parent = '' if parent == '/'
|
80
81
|
parent = nil if parent == '.'
|
81
|
-
locals = { idx: idx, parent: parent, root:
|
82
|
+
locals = { idx: idx, parent: parent, root: repository.root, file_path: expanded_path, nav_state: nil }
|
82
83
|
mime = File.mime_type?(File.open(expanded_path)) if File.file?(expanded_path)
|
83
84
|
mode = request.params['mode']
|
84
85
|
if mode == 'meta' # Meta
|
85
|
-
halt 200, { 'Content-Type' => 'application/json' }, [
|
86
|
+
halt 200, { 'Content-Type' => 'application/json' }, [repository.file_meta(file_path).to_json]
|
86
87
|
elsif mode == 'save' # Saving
|
87
88
|
File.open(expanded_path, 'w') { |f| f.print request.params['data'] }
|
88
89
|
File.open(message_file, 'w') { |f| f.print request.params['message'] } unless request.params['message'] == ''
|
@@ -100,19 +101,19 @@ module Gitdocs
|
|
100
101
|
FileUtils.mkdir_p(expanded_path)
|
101
102
|
redirect! '/' + idx.to_s + file_path
|
102
103
|
elsif File.directory?(expanded_path) # list directory
|
103
|
-
contents =
|
104
|
+
contents = Dir[File.join(expanded_path, '*')].map { |x| Docfile.new(x) }
|
104
105
|
rendered_readme = nil
|
105
106
|
if readme = Dir[File.expand_path('README.{md}', expanded_path)].first
|
106
107
|
rendered_readme = '<h3>' + File.basename(readme) + '</h3><div class="tilt">' + render(readme) + '</div>'
|
107
108
|
end
|
108
109
|
render! 'dir', layout: 'app', locals: locals.merge(contents: contents, rendered_readme: rendered_readme)
|
109
110
|
elsif mode == 'revisions' # list revisions
|
110
|
-
revisions =
|
111
|
+
revisions = repository.file_revisions(file_path)
|
111
112
|
render! 'revisions', layout: 'app', locals: locals.merge(revisions: revisions)
|
112
113
|
elsif mode == 'revert' # revert file
|
113
114
|
if revision = request.params['revision']
|
114
115
|
File.open(message_file, 'w') { |f| f.print "Reverting '#{file_path}' to #{revision}" }
|
115
|
-
|
116
|
+
repository.file_revert(file_path, revision)
|
116
117
|
end
|
117
118
|
redirect! '/' + idx.to_s + file_path
|
118
119
|
elsif mode == 'delete' # delete file
|
@@ -123,7 +124,7 @@ module Gitdocs
|
|
123
124
|
render! 'edit', layout: 'app', locals: locals.merge(contents: contents)
|
124
125
|
elsif mode != 'raw' # render file
|
125
126
|
revision = request.params['revision']
|
126
|
-
expanded_path =
|
127
|
+
expanded_path = repository.file_revision_at(file_path, revision) if revision
|
127
128
|
begin # attempting to render file
|
128
129
|
contents = '<div class="tilt">' + render(expanded_path) + '</div>'
|
129
130
|
rescue RuntimeError # not tilt supported
|
@@ -135,7 +136,7 @@ module Gitdocs
|
|
135
136
|
end
|
136
137
|
render! 'file', layout: 'app', locals: locals.merge(contents: contents)
|
137
138
|
else # other file
|
138
|
-
run! Rack::File.new(
|
139
|
+
run! Rack::File.new(repository.root)
|
139
140
|
end
|
140
141
|
end
|
141
142
|
end
|
@@ -151,9 +152,6 @@ module Gitdocs
|
|
151
152
|
begin
|
152
153
|
TCPSocket.open('127.0.0.1', @port).close
|
153
154
|
@manager.log('Web server running!')
|
154
|
-
if !restarting && @manager.config.global.load_browser_on_startup
|
155
|
-
Launchy.open("http://localhost:#{@port}/")
|
156
|
-
end
|
157
155
|
rescue Errno::ECONNREFUSED
|
158
156
|
sleep 0.2
|
159
157
|
i += 1
|
data/lib/gitdocs/version.rb
CHANGED
@@ -25,12 +25,12 @@
|
|
25
25
|
%dd
|
26
26
|
%input{:name=>"share[#{idx}][polling_interval]", :value => share.polling_interval}
|
27
27
|
|
28
|
-
- if share.available_remotes
|
28
|
+
- if Gitdocs::Repository.new(share).available_remotes
|
29
29
|
%dl
|
30
30
|
%dt Remote
|
31
31
|
%dd
|
32
32
|
%select{:name=>"share[#{idx}][remote_branch]"}
|
33
|
-
- share.available_remotes.each do |remote|
|
33
|
+
- Gitdocs::Repository.new(share).available_remotes.each do |remote|
|
34
34
|
%option{:value => remote, :selected => remote == "#{share.remote_name}/#{share.branch_name}" ? 'selected' : nil}
|
35
35
|
= remote
|
36
36
|
- else
|
@@ -52,4 +52,4 @@
|
|
52
52
|
Delete
|
53
53
|
|
54
54
|
%input{:value => 'Save', :type => 'submit', :class => "btn primary" }
|
55
|
-
%a{ :class => "btn secondary new-share", :href => "/shares", :"data-method" => "post" } Add Share
|
55
|
+
%a{ :class => "btn secondary new-share", :href => "/shares", :"data-method" => "post" } Add Share
|