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.
@@ -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
- @icon = File.expand_path('../../img/icon.png', __FILE__)
12
+ @notifier = Gitdocs::Notifier.new(@share.notification)
13
+ @repository = Gitdocs::Repository.new(share)
18
14
  end
19
15
 
20
- SearchResult = Struct.new(:file, :context)
21
- def search(term)
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 self.valid?
21
+ return false unless @repository.valid?
39
22
 
40
- @show_notifications = @share.notification
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 `#{@root}'")
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(@root, watch_all_modifications: true)
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
- out, status = sh_with_code("git fetch --all && git merge #{@current_remote}/#{@current_branch}")
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
- def push_changes
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
- def get_latest_changes
152
- if @current_revision
153
- out = sh "git log #{@current_revision}.. --pretty='format:{\"commit\": \"%H\",%n \"author\": \"%an <%ae>\",%n \"date\": \"%ad\",%n \"message\": \"%s\"%n}'"
154
- if out.empty?
155
- []
156
- else
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
- IGNORED_FILES = ['.gitignore']
170
- # Returns the list of files in a given directory
171
- # dir_files("some/dir") => [<Docfile>, <Docfile>]
172
- def dir_files(dir_path)
173
- Dir[File.join(dir_path, '*')].to_a.map { |path| Docfile.new(path) }
174
- end
175
-
176
- # Returns file meta data based on relative file path
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
- File.symlink?(full_path) ? 0 : File.size(full_path)
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
- { author: author, size: size, modified: modified }
93
+ push_changes
195
94
  end
196
95
 
197
- # Returns the revisions available for a particular file
198
- # file_revisions("README")
199
- def file_revisions(file)
200
- file = file.gsub(%r{^/}, '')
201
- output = sh_string("git log --format='%h|%s|%aN|%ai' -n100 #{ShellTools.escape(file)}")
202
- output.to_s.split("\n").map do |log_result|
203
- commit, subject, author, date = log_result.split('|')
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
- # Returns the temporary path of a particular revision of a file
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
- # Revert a file to a particular revision
220
- def file_revert(file, ref)
221
- if file_revisions(file).map { |r| r[:commit] }.include? ref
222
- file = file.gsub(%r{^/}, '')
223
- full_path = File.expand_path(file, @root)
224
- content = File.read(file_revision_at(file, ref))
225
- File.open(full_path, 'w') { |f| f.puts content }
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
- def valid?
230
- out, status = sh_with_code 'git status'
231
- @root.present? && status.success?
232
- end
126
+ ############################################################################
127
+ private
233
128
 
234
- def warn(title, msg)
235
- if @show_notifications
236
- Guard::Notifier.notify(msg, title: title)
237
- else
238
- Kernel.warn("#{title}: #{msg}")
239
- end
240
- rescue # Prevent StandardErrors from stopping the daemon.
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
- def info(title, msg)
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 error(title, msg)
253
- if @show_notifications
254
- Guard::Notifier.notify(msg, title: title, image: :failure)
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
- Kernel.warn("#{title}: #{msg}")
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
- # Run in shell, return both status and output
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
@@ -8,15 +8,15 @@ require 'launchy'
8
8
 
9
9
  module Gitdocs
10
10
  class Server
11
- def initialize(manager, port = 8888, *gitdocs)
12
- @manager = manager
13
- @port = port.to_i
14
- @gitdocs = gitdocs
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
- gds = @gitdocs
19
- manager = @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: manager.search(request.GET['q']), nav_state: nil }
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
- gd = gds[idx]
73
- halt 404 if gd.nil?
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}", gd.root)
76
- message_file = File.expand_path('.gitmessage~', gd.root)
77
- halt 400 unless expanded_path[/^#{Regexp.quote(gd.root)}/]
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: gd.root, file_path: expanded_path, nav_state: nil }
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' }, [gd.file_meta(file_path).to_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 = gd.dir_files(expanded_path)
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 = gd.file_revisions(file_path)
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
- gd.file_revert(file_path, revision)
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 = gd.file_revision_at(file_path, revision) if revision
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(gd.root)
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
@@ -1,3 +1,3 @@
1
1
  module Gitdocs
2
- VERSION = '0.5.0.pre1'
2
+ VERSION = '0.5.0.pre2'
3
3
  end
@@ -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