gitdocs 0.5.0.pre6 → 0.5.0.pre7

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.
Files changed (61) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +1 -0
  3. data/.haml-lint.yml +3 -0
  4. data/.jslint.yml +84 -0
  5. data/.rubocop.yml +13 -0
  6. data/CHANGELOG +11 -0
  7. data/README.md +6 -2
  8. data/Rakefile +22 -3
  9. data/gitdocs.gemspec +36 -29
  10. data/lib/gitdocs.rb +5 -2
  11. data/lib/gitdocs/cli.rb +31 -8
  12. data/lib/gitdocs/configuration.rb +95 -49
  13. data/lib/gitdocs/manager.rb +36 -28
  14. data/lib/gitdocs/migration/001_create_shares.rb +2 -0
  15. data/lib/gitdocs/migration/002_add_remote_branch.rb +2 -0
  16. data/lib/gitdocs/migration/003_create_configs.rb +2 -0
  17. data/lib/gitdocs/migration/004_add_index_for_path.rb +4 -0
  18. data/lib/gitdocs/migration/005_add_start_web_frontend.rb +2 -0
  19. data/lib/gitdocs/migration/006_add_web_port_to_config.rb +2 -0
  20. data/lib/gitdocs/migration/007_add_sync_type.rb +11 -0
  21. data/lib/gitdocs/notifier.rb +89 -6
  22. data/lib/gitdocs/public/img/file.png +0 -0
  23. data/lib/gitdocs/public/img/folder.png +0 -0
  24. data/lib/gitdocs/public/js/app.js +26 -11
  25. data/lib/gitdocs/public/js/edit.js +3 -3
  26. data/lib/gitdocs/public/js/settings.js +8 -5
  27. data/lib/gitdocs/public/js/util.js +21 -20
  28. data/lib/gitdocs/rendering.rb +14 -9
  29. data/lib/gitdocs/repository.rb +180 -216
  30. data/lib/gitdocs/repository/path.rb +166 -0
  31. data/lib/gitdocs/runner.rb +22 -65
  32. data/lib/gitdocs/search.rb +35 -0
  33. data/lib/gitdocs/server.rb +123 -86
  34. data/lib/gitdocs/version.rb +1 -1
  35. data/lib/gitdocs/views/_header.haml +6 -6
  36. data/lib/gitdocs/views/app.haml +17 -17
  37. data/lib/gitdocs/views/dir.haml +10 -10
  38. data/lib/gitdocs/views/edit.haml +8 -9
  39. data/lib/gitdocs/views/file.haml +1 -1
  40. data/lib/gitdocs/views/home.haml +4 -4
  41. data/lib/gitdocs/views/revisions.haml +6 -6
  42. data/lib/gitdocs/views/search.haml +6 -6
  43. data/lib/gitdocs/views/settings.haml +23 -16
  44. data/test/.rubocop.yml +13 -0
  45. data/test/integration/browse_test.rb +149 -0
  46. data/test/integration/full_sync_test.rb +3 -11
  47. data/test/integration/share_management_test.rb +59 -10
  48. data/test/integration/status_test.rb +2 -0
  49. data/test/integration/test_helper.rb +40 -7
  50. data/test/unit/configuration_test.rb +82 -0
  51. data/test/unit/notifier_test.rb +165 -0
  52. data/test/unit/repository_path_test.rb +368 -0
  53. data/test/{repository_test.rb → unit/repository_test.rb} +426 -245
  54. data/test/unit/runner_test.rb +122 -0
  55. data/test/unit/search_test.rb +52 -0
  56. data/test/{test_helper.rb → unit/test_helper.rb} +5 -0
  57. metadata +138 -41
  58. data/lib/gitdocs/docfile.rb +0 -23
  59. data/test/configuration_test.rb +0 -41
  60. data/test/notifier_test.rb +0 -68
  61. data/test/runner_test.rb +0 -123
@@ -1,12 +1,17 @@
1
- # This shouldn't exist but I can't find any other way to prevent redcarpet from complaining
2
- # WARN: tilt autoloading 'redcarpet' in a non thread-safe way; explicit require 'redcarpet' suggested.
3
- # !! Unexpected error while processing request: Input must be UTF-8 or US-ASCII, ASCII-8BIT given
4
- # Input must be UTF-8 or US-ASCII, ASCII-8BIT given
5
- # gems/redcarpet-2.0.1/lib/redcarpet.rb:70:in `render'
6
- # gems/redcarpet-2.0.1/lib/redcarpet.rb:70:in `to_html'
7
- # gems/tilt-1.3.3/lib/tilt/markdown.rb:38:in `evaluate'
8
- # gems/tilt-1.3.3/lib/tilt/markdown.rb:61:in `evaluate'
9
- # gems/tilt-1.3.3/lib/tilt/template.rb:76:in `render'
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # rubocop:disable LineLength
4
+
5
+ # This should not exist but I cannot find any other way to prevent redcarpet
6
+ # from complaining
7
+ # > WARN: tilt autoloading 'redcarpet' in a non thread-safe way; explicit require'redcarpet' suggested.
8
+ # > !! Unexpected error while processing request: Input must be UTF-8 or US-ASCII, ASCII-8BIT given
9
+ # > Input must be UTF-8 or US-ASCII, ASCII-8BIT given
10
+ # > gems/redcarpet-2.0.1/lib/redcarpet.rb:70:in `render'
11
+ # > gems/redcarpet-2.0.1/lib/redcarpet.rb:70:in `to_html'
12
+ # > gems/tilt-1.3.3/lib/tilt/markdown.rb:38:in `evaluate'
13
+ # > gems/tilt-1.3.3/lib/tilt/markdown.rb:61:in `evaluate'
14
+ # > gems/tilt-1.3.3/lib/tilt/template.rb:76:in `render'
10
15
 
11
16
  require 'redcarpet'
12
17
 
@@ -1,13 +1,17 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  require 'find'
3
4
 
5
+ # rubocop:disable ClassLength
6
+ # This class is long, but at the moment everything in it seems to be
7
+ # appropriate.
8
+
4
9
  # Wrapper for accessing the shared git repositories.
5
10
  # Rugged or Grit will be used, in that order of preference, depending
6
11
  # upon the features which are available with each option.
7
12
  #
8
13
  # @note If a repository is invalid then query methods will return nil, and
9
14
  # command methods will raise exceptions.
10
- #
11
15
  class Gitdocs::Repository
12
16
  attr_reader :invalid_reason
13
17
 
@@ -30,6 +34,7 @@ class Gitdocs::Repository
30
34
  @grit = Grit::Repo.new(path)
31
35
  Grit::Git.git_timeout = 120
32
36
  @invalid_reason = nil
37
+ @commit_message_path = abs_path('.gitmessage~')
33
38
  rescue Rugged::OSError
34
39
  @invalid_reason = :directory_missing
35
40
  rescue Rugged::RepositoryError
@@ -53,56 +58,10 @@ class Gitdocs::Repository
53
58
  repository = new(path)
54
59
  fail("Unable to clone into #{path}") unless repository.valid?
55
60
  repository
56
- rescue Grit::Git::GitTimeout => e
57
- fail("Unable to clone into #{path} because it timed out")
58
- rescue Grit::Git::CommandFailed => e
59
- fail("Unable to clone into #{path} because of #{e.err}")
60
- end
61
-
62
- RepoDescriptor = Struct.new(:name, :index)
63
-
64
- # Search across multiple repositories
65
- #
66
- # @param [String] term
67
- # @param [Array<Repository>} repositories
68
- #
69
- # @return [Hash<RepoDescriptor, Array<SearchResult>>]
70
- def self.search(term, repositories)
71
- results = {}
72
- repositories.each_with_index do |repository, index|
73
- descriptor = RepoDescriptor.new(repository.root, index)
74
- results[descriptor] = repository.search(term)
75
- end
76
- results.delete_if { |key, value| value.empty? }
77
- end
78
-
79
- SearchResult = Struct.new(:file, :context)
80
-
81
- # Search a single repository
82
- #
83
- # @param [String] term
84
- #
85
- # @return [Array<SearchResult>]
86
- def search(term)
87
- return [] if term.empty?
88
-
89
- results = []
90
- options = { raise: true, bare: false, chdir: root, ignore_case: true }
91
- @grit.git.grep(options, term).scan(/(.*?):([^\n]*)/) do |(file, context)|
92
- if result = results.find { |s| s.file == file }
93
- result.context += ' ... ' + context
94
- else
95
- results << SearchResult.new(file, context)
96
- end
97
- end
98
- results
99
- rescue Grit::Git::GitTimeout => e
100
- # TODO: add logging to record the error details
101
- []
61
+ rescue Grit::Git::GitTimeout
62
+ raise("Unable to clone into #{path} because it timed out")
102
63
  rescue Grit::Git::CommandFailed => e
103
- # TODO: add logging to record the error details if they are not just
104
- # nothing found
105
- []
64
+ raise("Unable to clone into #{path} because of #{e.err}")
106
65
  end
107
66
 
108
67
  # @return [String]
@@ -138,107 +97,113 @@ class Gitdocs::Repository
138
97
  nil
139
98
  end
140
99
 
141
- # Fetch and merge the repository
100
+ # Is the working directory dirty
142
101
  #
143
- # @raise [RuntimeError] if there is a problem processing conflicted files
102
+ # @return [Boolean]
103
+ def dirty?
104
+ return false unless valid?
105
+
106
+ return Dir.glob(abs_path('*')).any? unless current_oid
107
+ @rugged.diff_workdir(current_oid, include_untracked: true).deltas.any?
108
+ end
109
+
110
+ # @return [Boolean]
111
+ def need_sync?
112
+ return false unless valid?
113
+ return false unless remote?
114
+
115
+ return !!current_oid unless remote_branch # rubocop:disable DoubleNegation
116
+ remote_branch.tip.oid != current_oid
117
+ end
118
+
119
+ # @param [String] term
120
+ # @yield [file, context] Gives the files and context for each of the results
121
+ # @yieldparam file [String]
122
+ # @yieldparam context [String]
123
+ def grep(term, &block)
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
136
+
137
+ # Fetch all the remote branches
144
138
  #
145
139
  # @return [nil] if the repository is invalid
146
140
  # @return [:no_remote] if the remote is not yet set
147
141
  # @return [String] if there is an error return the message
148
- # @return [Array<String>] if there is a conflict return the Array of
149
- # conflicted file names
150
- # @return [:ok] if pulled and merged with no errors or conflicts
151
- def pull
142
+ # @return [:ok] if the fetch worked
143
+ def fetch
152
144
  return nil unless valid?
153
- return :no_remote unless has_remote?
154
-
155
- begin
156
- @rugged.remotes.each { |x| @grit.remote_fetch(x.name) }
157
- rescue Grit::Git::GitTimeout
158
- return "Fetch timed out for #{root}"
159
- rescue Grit::Git::CommandFailed => e
160
- return e.err
161
- end
145
+ return :no_remote unless remote?
162
146
 
163
- return :ok if remote_branch.nil? || remote_branch.tip.oid == current_oid
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
164
154
 
165
- @grit.git.merge({ raise: true, chdir: root }, "#{@remote_name}/#{@branch_name}")
155
+ # Merge the repository
156
+ #
157
+ # @return [nil] if the repository is invalid
158
+ # @return [:no_remote] if the remote is not yet set
159
+ # @return [String] if there is an error return the message
160
+ # @return [Array<String>] if there is a conflict return the Array of
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
+ )
166
174
  :ok
167
175
  rescue Grit::Git::GitTimeout
168
- "Merged command timed out for #{root}"
176
+ "Merge timed out for #{root}"
169
177
  rescue Grit::Git::CommandFailed => e
170
178
  # HACK: The rugged in-memory index will not have been updated after the
171
179
  # Grit merge command. Reload it before checking for conflicts.
172
180
  @rugged.index.reload
173
181
  return e.err unless @rugged.index.conflicts?
174
-
175
- # Collect all the index entries by their paths.
176
- index_path_entries = Hash.new { |h, k| h[k] = Array.new }
177
- @rugged.index.map do |index_entry|
178
- index_path_entries[index_entry[:path]].push(index_entry)
179
- end
180
-
181
- # Filter to only the conflicted entries.
182
- conflicted_path_entries = index_path_entries.delete_if { |_k, v| v.length == 1 }
183
-
184
- conflicted_path_entries.each_pair do |path, index_entries|
185
- # Write out the different versions of the conflicted file.
186
- index_entries.each do |index_entry|
187
- filename, extension = index_entry[:path].scan(/(.*?)(|\.[^\.]+)$/).first
188
- author = ' original' if index_entry[:stage] == 1
189
- short_oid = index_entry[:oid][0..6]
190
- new_filename = "#{filename} (#{short_oid}#{author})#{extension}"
191
- File.open(File.join(root, new_filename), 'wb') do |f|
192
- f.write(Rugged::Blob.lookup(@rugged, index_entry[:oid]).content)
193
- end
194
- end
195
-
196
- # And remove the original.
197
- FileUtils.remove(File.join(root, path), force: true)
198
- end
199
-
200
- # NOTE: Let commit be handled by the next regular commit.
201
-
202
- conflicted_path_entries.keys
182
+ mark_conflicts
203
183
  end
204
184
 
205
185
  # Commit the working directory
206
186
  #
207
- # @param [String] message
208
- #
209
187
  # @return [nil] if the repository is invalid
210
188
  # @return [Boolean] whether a commit was made or not
211
- def commit(message)
189
+ def commit
212
190
  return nil unless valid?
213
191
 
214
- # Mark any empty directories so they will be committed
215
- Find.find(root).each do |path|
216
- Find.prune if File.basename(path) == '.git'
217
- if File.directory?(path) && Dir.entries(path).count == 2
218
- FileUtils.touch(File.join(path, '.gitignore'))
219
- end
220
- end
192
+ # Do this first to allow the message file to be deleted, if it exists.
193
+ message = read_and_delete_commit_message_file
221
194
 
222
- # Check if there are uncommitted changes
223
- dirty =
224
- if current_oid.nil?
225
- Dir.glob(File.join(root, '*')).any?
226
- else
227
- @rugged.diff_workdir(current_oid, include_untracked: true).deltas.any?
228
- end
195
+ mark_empty_directories
196
+
197
+ return false unless dirty?
229
198
 
230
199
  # Commit any changes in the working directory.
231
- if dirty
232
- Dir.chdir(root) do
233
- @rugged.index.add_all
234
- @rugged.index.update_all
235
- end
236
- @rugged.index.write
237
- @grit.commit_index(message)
238
- true
239
- else
240
- false
200
+ Dir.chdir(root) do
201
+ @rugged.index.add_all
202
+ @rugged.index.update_all
241
203
  end
204
+ @rugged.index.write
205
+ @grit.commit_index(message)
206
+ true
242
207
  end
243
208
 
244
209
  # Push the repository
@@ -250,21 +215,16 @@ class Gitdocs::Repository
250
215
  # @return [:ok] if committed and pushed without errors or conflicts
251
216
  def push
252
217
  return nil unless valid?
253
- return :no_remote unless has_remote?
218
+ return :no_remote unless remote?
254
219
 
255
220
  return :nothing if current_oid.nil?
221
+ return :nothing if remote_branch && remote_branch.tip.oid == current_oid
256
222
 
257
- if remote_branch.nil? || remote_branch.tip.oid != current_oid
258
- begin
259
- @grit.git.push({ raise: true }, @remote_name, @branch_name)
260
- :ok
261
- rescue Grit::Git::CommandFailed => e
262
- return :conflict if e.err[/\[rejected\]/]
263
- e.err # return the output on error
264
- end
265
- else
266
- :nothing
267
- end
223
+ @grit.git.push({ raise: true }, @remote_name, @branch_name)
224
+ :ok
225
+ rescue Grit::Git::CommandFailed => e
226
+ return :conflict if e.err[/\[rejected\]/]
227
+ e.err # return the output on error
268
228
  end
269
229
 
270
230
  # Get the count of commits by author from the head to the specified oid.
@@ -275,7 +235,7 @@ class Gitdocs::Repository
275
235
  def author_count(last_oid)
276
236
  walker = head_walker
277
237
  walker.hide(last_oid) if last_oid
278
- walker.inject(Hash.new(0)) do |result, commit|
238
+ walker.reduce(Hash.new(0)) do |result, commit|
279
239
  result["#{commit.author[:name]} <#{commit.author[:email]}>"] += 1
280
240
  result
281
241
  end
@@ -285,101 +245,51 @@ class Gitdocs::Repository
285
245
  {}
286
246
  end
287
247
 
288
- # Returns file meta data based on relative file path
289
- #
290
- # @example
291
- # file_meta("path/to/file")
292
- # => { :author => "Nick", :size => 1000, :modified => ... }
293
- #
294
- # @param [String] file relative path to file in repository
295
- #
296
- # @raise [RuntimeError] if the file is not found in any commits
297
- #
298
- # @return [Hash<Symbol=>String,Integer,Time>] the author, size and
299
- # modification date of the file
300
- def file_meta(file)
301
- file = file.gsub(%r{^/}, '')
302
-
303
- commit = head_walker.find { |x| x.diff(paths: [file]).size > 0 }
304
-
305
- fail "File #{file} not found" unless commit
306
-
307
- full_path = File.expand_path(file, root)
308
- size = if File.directory?(full_path)
309
- Dir[File.join(full_path, '**', '*')].reduce(0) do |size, file|
310
- File.symlink?(file) ? size : size += File.size(file)
311
- end
312
- else
313
- File.symlink?(full_path) ? 0 : File.size(full_path)
314
- end
315
- size = -1 if size == 0 # A value of 0 breaks the table sort for some reason
248
+ # @param [String] message
249
+ def write_commit_message(message)
250
+ return unless message
251
+ return if message.empty?
316
252
 
317
- { author: commit.author[:name], size: size, modified: commit.author[:time] }
253
+ File.open(@commit_message_path, 'w') { |f| f.print(message) }
318
254
  end
319
255
 
320
- # Returns the revisions available for a particular file
256
+ # Excluding the initial commit (without a parent) which keeps things
257
+ # consistent with the original behaviour.
258
+ # TODO: reconsider if this is the correct behaviour
321
259
  #
322
- # @example
323
- # file_revisions("README")
260
+ # @param [String] relative_path
261
+ # @param [Integer] limit the number of commits which will be returned
324
262
  #
325
- # @param [String] file
326
- #
327
- # @return [Array<Hash>]
328
- def file_revisions(file)
329
- file = file.gsub(%r{^/}, '')
330
- # Excluding the initial commit (without a parent) which keeps things
331
- # consistent with the original behaviour.
332
- # TODO: reconsider if this is the correct behaviour
333
- head_walker.select{|x| x.parents.size == 1 && x.diff(paths: [file]).size > 0 }
334
- .first(100)
335
- .map do |commit|
336
- {
337
- commit: commit.oid[0, 7],
338
- subject: commit.message.split("\n")[0],
339
- author: commit.author[:name],
340
- date: commit.author[:time]
341
- }
342
- end
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
269
+ end
270
+ # TODO: should re-write this limit in a way that will skip walking all of
271
+ # the commits.
272
+ commits.first(limit)
343
273
  end
344
274
 
345
- # Put the contents of the specified file revision into a temporary file
346
- #
347
- # @example
348
- # file_revision_at("README", "a4c56h")
349
- # => "/tmp/some/path/README"
350
- #
351
- # @param [String] file
352
- # @param [String] ref
275
+ # @param [String] relative_path
353
276
  #
354
- # @return [String] path of the temporary file
355
- def file_revision_at(file, ref)
356
- file = file.gsub(%r{^/}, '')
357
- content = @rugged.blob_at(ref, file).text
358
- tmp_path = File.expand_path(File.basename(file), Dir.tmpdir)
359
- File.open(tmp_path, 'w') { |f| f.puts content }
360
- tmp_path
277
+ # @return [Rugged::Commit]
278
+ def last_commit_for(relative_path)
279
+ head_walker.find { |commit| commit.diff(paths: [relative_path]).size > 0 }
361
280
  end
362
281
 
363
- # Revert file to the specified ref
364
- #
365
- # @param [String] file
366
- # @param [String] ref
367
- def file_revert(file, ref)
368
- file = file.gsub(%r{^/}, '')
369
- blob = @rugged.blob_at(ref, file)
370
- # Silently fail if the file/ref do not existing in the repository.
371
- # Which is consistent with the original behaviour.
372
- # TODO: should consider throwing an exception on this condition
373
- return unless blob
374
-
375
- File.open(File.expand_path(file, root), 'w') { |f| f.puts(blob.text) }
282
+ # @param [String] relative_path
283
+ # @param [String] oid
284
+ def blob_at(relative_path, ref)
285
+ @rugged.blob_at(ref, relative_path)
376
286
  end
377
287
 
378
288
  ##############################################################################
379
289
 
380
290
  private
381
291
 
382
- def has_remote?
292
+ def remote?
383
293
  @rugged.remotes.any?
384
294
  end
385
295
 
@@ -400,4 +310,58 @@ class Gitdocs::Repository
400
310
  walker.push(@rugged.head.target)
401
311
  walker
402
312
  end
313
+
314
+ def read_and_delete_commit_message_file
315
+ return 'Auto-commit from gitdocs' unless File.exist?(@commit_message_path)
316
+
317
+ message = File.read(@commit_message_path)
318
+ File.delete(@commit_message_path)
319
+ message
320
+ end
321
+
322
+ def mark_empty_directories
323
+ Find.find(root).each do |path| # rubocop:disable Style/Next
324
+ Find.prune if File.basename(path) == '.git'
325
+ if File.directory?(path) && Dir.entries(path).count == 2
326
+ FileUtils.touch(File.join(path, '.gitignore'))
327
+ end
328
+ end
329
+ end
330
+
331
+ def mark_conflicts
332
+ # assert(@rugged.index.conflicts?)
333
+
334
+ # Collect all the index entries by their paths.
335
+ index_path_entries = Hash.new { |h, k| h[k] = Array.new }
336
+ @rugged.index.map do |index_entry|
337
+ index_path_entries[index_entry[:path]].push(index_entry)
338
+ end
339
+
340
+ # Filter to only the conflicted entries.
341
+ conflicted_path_entries = index_path_entries.delete_if { |_k, v| v.length == 1 }
342
+
343
+ conflicted_path_entries.each_pair do |path, index_entries|
344
+ # Write out the different versions of the conflicted file.
345
+ index_entries.each do |index_entry|
346
+ filename, extension = index_entry[:path].scan(/(.*?)(|\.[^\.]+)$/).first
347
+ author = ' original' if index_entry[:stage] == 1
348
+ short_oid = index_entry[:oid][0..6]
349
+ new_filename = "#{filename} (#{short_oid}#{author})#{extension}"
350
+ File.open(abs_path(new_filename), 'wb') do |f|
351
+ f.write(Rugged::Blob.lookup(@rugged, index_entry[:oid]).content)
352
+ end
353
+ end
354
+
355
+ # And remove the original.
356
+ FileUtils.remove(abs_path(path), force: true)
357
+ end
358
+
359
+ # NOTE: Let commit be handled by the next regular commit.
360
+
361
+ conflicted_path_entries.keys
362
+ end
363
+
364
+ def abs_path(*path)
365
+ File.join(root, *path)
366
+ end
403
367
  end