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