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.
@@ -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