gollum 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of gollum might be problematic. Click here for more details.

@@ -0,0 +1,3 @@
1
+ class String
2
+ alias :lines :to_a if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
3
+ end
@@ -9,6 +9,12 @@ module Gollum
9
9
  # Sets the file class used by all instances of this Wiki.
10
10
  attr_writer :file_class
11
11
 
12
+ # Sets the default name for commits.
13
+ attr_accessor :default_committer_name
14
+
15
+ # Sets the default email for commits.
16
+ attr_accessor :default_committer_email
17
+
12
18
  # Gets the page class used by all instances of this Wiki.
13
19
  # Default: Gollum::Page.
14
20
  def page_class
@@ -32,6 +38,8 @@ module Gollum
32
38
  end
33
39
  end
34
40
 
41
+ self.default_committer_name = 'Anonymous'
42
+ self.default_committer_email = 'anon@anon.com'
35
43
 
36
44
  # The String base path to prefix to internal links. For example, when set
37
45
  # to "/wiki", the page "Hobbit" will be linked as "/wiki/Hobbit". Defaults
@@ -43,7 +51,7 @@ module Gollum
43
51
  # repo - The String path to the Git repository that holds the Gollum
44
52
  # site.
45
53
  # options - Optional Hash:
46
- # :base_path - String base path for all Wiki links.
54
+ # :base_path - String base path for all Wiki links.
47
55
  # Default: "/"
48
56
  # :page_class - The page Class. Default: Gollum::Page
49
57
  # :file_class - The file Class. Default: Gollum::File
@@ -55,6 +63,7 @@ module Gollum
55
63
  @base_path = options[:base_path] || "/"
56
64
  @page_class = options[:page_class] || self.class.page_class
57
65
  @file_class = options[:file_class] || self.class.file_class
66
+ clear_cache
58
67
  end
59
68
 
60
69
  # Public: check whether the wiki's git repo exists on the filesystem.
@@ -115,17 +124,23 @@ module Gollum
115
124
  #
116
125
  # Returns the String SHA1 of the newly written version.
117
126
  def write_page(name, format, data, commit = {})
118
- map = {}
127
+ commit = normalize_commit(commit)
128
+ index = self.repo.index
129
+
119
130
  if pcommit = @repo.commit('master')
120
- map = tree_map(pcommit.tree)
131
+ index.read_tree(pcommit.tree.id)
121
132
  end
122
133
 
123
- map = add_to_tree_map(map, '', name, format, data)
124
- index = tree_map_to_index(map)
134
+ add_to_index(index, '', name, format, data)
125
135
 
126
136
  parents = pcommit ? [pcommit] : []
127
137
  actor = Grit::Actor.new(commit[:name], commit[:email])
128
- index.commit(commit[:message], parents, actor)
138
+ sha1 = index.commit(commit[:message], parents, actor)
139
+
140
+ @ref_map.clear
141
+ update_working_dir(index, '', name, format)
142
+
143
+ sha1
129
144
  end
130
145
 
131
146
  # Public: Update an existing page with new content. The location of the
@@ -144,24 +159,32 @@ module Gollum
144
159
  #
145
160
  # Returns the String SHA1 of the newly written version.
146
161
  def update_page(page, name, format, data, commit = {})
162
+ commit = normalize_commit(commit)
147
163
  pcommit = @repo.commit('master')
148
- map = tree_map(pcommit.tree)
149
164
  name ||= page.name
150
165
  format ||= page.format
151
- index = nil
166
+ index = self.repo.index
167
+
168
+ dir = ::File.dirname(page.path)
169
+ dir = '' if dir == '.'
170
+
171
+ index.read_tree(pcommit.tree.id)
152
172
 
153
173
  if page.name == name && page.format == format
154
- index = tree_map_to_index(map)
155
174
  index.add(page.path, normalize(data))
156
175
  else
157
- map = delete_from_tree_map(map, page.path)
158
- dir = ::File.dirname(page.path)
159
- map = add_to_tree_map(map, dir, name, format, data)
160
- index = tree_map_to_index(map)
176
+ index.delete(page.path)
177
+ add_to_index(index, dir, name, format, data, :allow_same_ext)
161
178
  end
162
179
 
163
180
  actor = Grit::Actor.new(commit[:name], commit[:email])
164
- index.commit(commit[:message], [pcommit], actor)
181
+ sha1 = index.commit(commit[:message], [pcommit], actor)
182
+
183
+ @ref_map.clear
184
+ update_working_dir(index, dir, page.name, page.format)
185
+ update_working_dir(index, dir, name, format)
186
+
187
+ sha1
165
188
  end
166
189
 
167
190
  # Public: Delete a page.
@@ -169,19 +192,27 @@ module Gollum
169
192
  # page - The Gollum::Page to delete.
170
193
  # commit - The commit Hash details:
171
194
  # :message - The String commit message.
172
- # :author - The String author full name.
195
+ # :name - The String author full name.
173
196
  # :email - The String email address.
174
197
  #
175
198
  # Returns the String SHA1 of the newly written version.
176
199
  def delete_page(page, commit)
177
200
  pcommit = @repo.commit('master')
178
- map = tree_map(pcommit.tree)
179
201
 
180
- map = delete_from_tree_map(map, page.path)
181
- index = tree_map_to_index(map)
202
+ index = self.repo.index
203
+ index.read_tree(pcommit.tree.id)
204
+ index.delete(page.path)
205
+
206
+ dir = ::File.dirname(page.path)
207
+ dir = '' if dir == '.'
182
208
 
183
209
  actor = Grit::Actor.new(commit[:name], commit[:email])
184
- index.commit(commit[:message], [pcommit], actor)
210
+ sha1 = index.commit(commit[:message], [pcommit], actor)
211
+
212
+ @ref_map.clear
213
+ update_working_dir(index, dir, page.name, page.format)
214
+
215
+ sha1
185
216
  end
186
217
 
187
218
  # Public: Lists all pages for this wiki.
@@ -190,11 +221,39 @@ module Gollum
190
221
  #
191
222
  # Returns an Array of Gollum::Page instances.
192
223
  def pages(treeish = nil)
193
- treeish ||= 'master'
194
- if commit = @repo.commit(treeish)
195
- tree_list(commit)
196
- else
197
- []
224
+ tree_list(treeish || 'master')
225
+ end
226
+
227
+ # Public: Returns the number of pages accessible from a commit
228
+ #
229
+ # ref - A String ref that is either a commit SHA or references one.
230
+ #
231
+ # Returns a Fixnum
232
+ def size(ref = nil)
233
+ tree_map_for(ref || 'master').inject(0) do |num, entry|
234
+ num + (@page_class.valid_page_name?(entry.name) ? 1 : 0)
235
+ end
236
+ rescue Grit::GitRuby::Repository::NoSuchShaFound
237
+ 0
238
+ end
239
+
240
+ # Public: Search all pages for this wiki.
241
+ #
242
+ # query - The string to search for
243
+ #
244
+ # Returns an Array with Objects of page name and count of matches
245
+ def search(query)
246
+ # See: http://github.com/Sirupsen/gollum/commit/f0a6f52bdaf6bee8253ca33bb3fceaeb27bfb87e
247
+ search_output = @repo.git.grep({:c => query}, 'master')
248
+
249
+ search_output.split("\n").collect do |line|
250
+ result = line.split(':')
251
+ file_name = Gollum::Page.canonicalize_filename(::File.basename(result[1]))
252
+
253
+ {
254
+ :count => result[2].to_i,
255
+ :name => file_name
256
+ }
198
257
  end
199
258
  end
200
259
 
@@ -225,6 +284,26 @@ module Gollum
225
284
  # Returns the String path.
226
285
  attr_reader :path
227
286
 
287
+ # Gets a Hash cache of refs to commit SHAs.
288
+ #
289
+ # {"master" => "abc123", ...}
290
+ #
291
+ # Returns the Hash cache.
292
+ attr_reader :ref_map
293
+
294
+ # Gets a Hash cache of commit SHAs to a recursive tree of blobs.
295
+ #
296
+ # {"abc123" => [["lib/foo.rb", "blob-sha"], [file, sha], ...], ...}
297
+ #
298
+ # Returns the Hash cache.
299
+ attr_reader :tree_map
300
+
301
+ # Gets the page class used by all instances of this Wiki.
302
+ attr_reader :page_class
303
+
304
+ # Gets the file class used by all instances of this Wiki.
305
+ attr_reader :file_class
306
+
228
307
  # Normalize the data.
229
308
  #
230
309
  # data - The String data to be normalized.
@@ -234,102 +313,236 @@ module Gollum
234
313
  data.gsub(/\r/, '')
235
314
  end
236
315
 
316
+ # Assemble a Page's filename from its name and format.
317
+ #
318
+ # name - The String name of the page (may be in human format).
319
+ # format - The Symbol format of the page.
320
+ #
321
+ # Returns the String filename.
322
+ def page_file_name(name, format)
323
+ ext = @page_class.format_to_ext(format)
324
+ @page_class.cname(name) + '.' + ext
325
+ end
326
+
327
+ # Update the given file in the repository's working directory if there
328
+ # is a working directory present.
329
+ #
330
+ # index - The Grit::Index with which to sync.
331
+ # dir - The String directory in which the file lives.
332
+ # name - The String name of the page (may be in human format).
333
+ # format - The Symbol format of the page.
334
+ #
335
+ # Returns nothing.
336
+ def update_working_dir(index, dir, name, format)
337
+ unless @repo.bare
338
+ path =
339
+ if dir == ''
340
+ page_file_name(name, format)
341
+ else
342
+ ::File.join(dir, page_file_name(name, format))
343
+ end
344
+
345
+ Dir.chdir(::File.join(@repo.path, '..')) do
346
+ if file_path_scheduled_for_deletion?(index.tree, path)
347
+ @repo.git.rm({'f' => true}, '--', path)
348
+ else
349
+ @repo.git.checkout({}, 'HEAD', '--', path)
350
+ end
351
+ end
352
+ end
353
+ end
354
+
237
355
  # Fill an array with a list of pages.
238
356
  #
239
- # commit - The Grit::Commit
240
- # tree - The Grit::Tree to start with.
241
- # sub_tree - Optional String specifying the parent path of the Page.
357
+ # ref - A String ref that is either a commit SHA or references one.
242
358
  #
243
359
  # Returns a flat Array of Gollum::Page instances.
244
- def tree_list(commit, tree = commit.tree, sub_tree = nil)
245
- list = []
246
- path = tree.name ? "#{sub_tree}/#{tree.name}" : ''
247
- tree.contents.each do |item|
248
- case item
249
- when Grit::Blob
250
- if @page_class.valid_page_name?(item.name)
251
- page = @page_class.new(self).populate(item, path)
252
- page.version = commit
253
- list << page
254
- end
255
- when Grit::Tree
256
- list.push *tree_list(commit, item, path)
257
- end
360
+ def tree_list(ref)
361
+ tree_map_for(ref).inject([]) do |list, entry|
362
+ next list unless @page_class.valid_page_name?(entry.name)
363
+ sha = ref_map[ref]
364
+ list << entry.page(self, @repo.commit(sha))
258
365
  end
259
- list
260
366
  end
261
367
 
262
- # Fill an index with the existing state of the repository.
368
+ # Determine if a given file is scheduled to be deleted in the next commit
369
+ # for the given Index.
263
370
  #
264
- # tree - The Grit::Tree to start with.
371
+ # map - The Hash map:
372
+ # key - The String directory or filename.
373
+ # val - The Hash submap or the String contents of the file.
374
+ # path - The String path of the file including extension.
265
375
  #
266
- # Returns a nested Hash of filename to content mappings.
267
- def tree_map(tree)
268
- map = {}
269
- tree.contents.each do |item|
270
- case item
271
- when Grit::Blob
272
- map[item.name] = item.data
273
- when Grit::Tree
274
- map[item.name] = tree_map(item)
376
+ # Returns the Boolean response.
377
+ def file_path_scheduled_for_deletion?(map, path)
378
+ parts = path.split('/')
379
+ if parts.size == 1
380
+ deletions = map.keys.select { |k| !map[k] }
381
+ deletions.any? { |d| d == parts.first }
382
+ else
383
+ part = parts.shift
384
+ if rest = map[part]
385
+ file_path_scheduled_for_deletion?(rest, parts.join('/'))
386
+ else
387
+ false
275
388
  end
276
389
  end
277
- map
278
390
  end
279
391
 
280
- # Use a treemap to fill in the index.
392
+ # Determine if a given page (regardless of format) is scheduled to be
393
+ # deleted in the next commit for the given Index.
281
394
  #
282
395
  # map - The Hash map:
283
396
  # key - The String directory or filename.
284
397
  # val - The Hash submap or the String contents of the file.
285
- # index - The Grit::Index to use. Leave blank when calling from outside
286
- # this method (default: nil).
287
- #
288
- # Returns the Grit::Index.
289
- def tree_map_to_index(map, prefix = '', index = nil)
290
- index ||= @repo.index
291
- map.each do |k, v|
292
- case k
293
- when String
294
- name = [prefix, k].join('/')[1..-1]
295
- index.add(k, v)
296
- when Hash
297
- new_prefix = [prefix, k].join('/')[1..-1]
298
- tree_map_to_index(v, new_prefix, index)
398
+ # path - The String path of the page file. This may include the format
399
+ # extension in which case it will be ignored.
400
+ #
401
+ # Returns the Boolean response.
402
+ def page_path_scheduled_for_deletion?(map, path)
403
+ parts = path.split('/')
404
+ if parts.size == 1
405
+ deletions = map.keys.select { |k| !map[k] }
406
+ downfile = parts.first.downcase.sub(/\.\w+$/, '')
407
+ deletions.any? { |d| d.downcase.sub(/\.\w+$/, '') == downfile }
408
+ else
409
+ part = parts.shift
410
+ if rest = map[part]
411
+ page_path_scheduled_for_deletion?(rest, parts.join('/'))
412
+ else
413
+ false
299
414
  end
300
415
  end
301
- index
302
416
  end
303
417
 
304
- def add_to_tree_map(map, dir, name, format, data)
305
- ext = @page_class.format_to_ext(format)
306
- path = @page_class.cname(name) + '.' + ext
418
+ # Adds a page to the given Index.
419
+ #
420
+ # index - The Grit::Index to which the page will be added.
421
+ # dir - The String subdirectory of the Gollum::Page without any
422
+ # prefix or suffix slashes (e.g. "foo/bar").
423
+ # name - The String Gollum::Page name.
424
+ # format - The Symbol Gollum::Page format.
425
+ # data - The String wiki data to store in the tree map.
426
+ # allow_same_ext - A Boolean determining if the tree map allows the same
427
+ # filename with the same extension.
428
+ #
429
+ # Raises Gollum::DuplicatePageError if a matching filename already exists.
430
+ # This way, pages are not inadvertently overwritten.
431
+ #
432
+ # Returns nothing (modifies the Index in place).
433
+ def add_to_index(index, dir, name, format, data, allow_same_ext = false)
434
+ path = page_file_name(name, format)
435
+
436
+ dir = '/' if dir.strip.empty?
307
437
 
308
- parts = dir.split('/')
309
- container = nil
310
- parts.each do |part|
311
- container = map[part]
438
+ fullpath = ::File.join(dir, path)
439
+ fullpath = fullpath[1..-1] if fullpath =~ /^\//
440
+
441
+ if index.current_tree && tree = index.current_tree / dir
442
+ downpath = path.downcase.sub(/\.\w+$/, '')
443
+
444
+ tree.blobs.each do |blob|
445
+ next if page_path_scheduled_for_deletion?(index.tree, fullpath)
446
+ file = blob.name.downcase.sub(/\.\w+$/, '')
447
+ file_ext = ::File.extname(blob.name).sub(/^\./, '')
448
+ if downpath == file && !(allow_same_ext && file_ext == ext)
449
+ raise DuplicatePageError.new(dir, blob.name, path)
450
+ end
451
+ end
312
452
  end
313
453
 
314
- (container || map)[path] = normalize(data)
315
- map
454
+ index.add(fullpath, normalize(data))
316
455
  end
317
456
 
318
- # Delete an entry from a tree map.
457
+ # Ensures a commit hash has all the required fields for a commit.
319
458
  #
320
- # map - The Hash tree map of the repository.
321
- # path - The String path of the file to delete.
459
+ # commit - The commit Hash details:
460
+ # :message - The String commit message.
461
+ # :name - The String author full name.
462
+ # :email - The String email address.
322
463
  #
323
- # Returns the modified Hash tree map.
324
- def delete_from_tree_map(map, path)
325
- parts = path.split('/')
326
- name = parts.pop
327
- container = nil
328
- parts.each do |part|
329
- container = map[part]
464
+ # Returns the commit Hash
465
+ def normalize_commit(commit = {})
466
+ commit[:name] = default_committer_name if commit[:name].to_s.empty?
467
+ commit[:email] = default_committer_email if commit[:email].to_s.empty?
468
+ commit
469
+ end
470
+
471
+ # Gets the default name for commits.
472
+ def default_committer_name
473
+ @default_committer_name ||= \
474
+ @repo.config['user.name'] || self.class.default_committer_name
475
+ end
476
+
477
+ # Gets the default email for commits.
478
+ def default_committer_email
479
+ @default_committer_email ||= \
480
+ @repo.config['user.email'] || self.class.default_committer_email
481
+ end
482
+
483
+ # Finds a full listing of files and their blob SHA for a given ref. Each
484
+ # listing is cached based on its actual commit SHA.
485
+ #
486
+ # ref - A String ref that is either a commit SHA or references one.
487
+ #
488
+ # Returns an Array of BlobEntry instances.
489
+ def tree_map_for(ref)
490
+ sha = @ref_map[ref] || ref
491
+ @tree_map[sha] || begin
492
+ real_sha = @repo.git.rev_list({:max_count=>1}, ref)
493
+ @ref_map[ref] = real_sha if real_sha != ref
494
+ @tree_map[real_sha] ||= parse_tree_for(real_sha)
495
+ end
496
+ rescue Grit::GitRuby::Repository::NoSuchShaFound
497
+ []
498
+ end
499
+
500
+ # Finds the full listing of files and their blob SHA for a given commit
501
+ # SHA. No caching or ref lookups are performed.
502
+ #
503
+ # sha - String commit SHA.
504
+ #
505
+ # Returns an Array of BlobEntry instances.
506
+ def parse_tree_for(sha)
507
+ tree = @repo.git.native(:ls_tree, {:r => true, :z => true}, sha)
508
+ items = []
509
+ tree.split("\0").each do |line|
510
+ items << parse_tree_line(line)
511
+ end
512
+ items
513
+ end
514
+
515
+ # Parses a line of output from the `ls-tree` command.
516
+ #
517
+ # line - A String line of output:
518
+ # "100644 blob 839c2291b30495b9a882c17d08254d3c90d8fb53 Home.md"
519
+ #
520
+ # Returns an Array of BlobEntry instances.
521
+ def parse_tree_line(line)
522
+ data, name = line.split("\t")
523
+ mode, type, sha = data.split(' ')
524
+ name = decode_git_path(name)
525
+ BlobEntry.new sha, name
526
+ end
527
+
528
+ # Decode octal sequences (\NNN) in tree path names.
529
+ #
530
+ # path - String path name.
531
+ #
532
+ # Returns a decoded String.
533
+ def decode_git_path(path)
534
+ if path[0] == ?" && path[-1] == ?"
535
+ path = path[1...-1]
536
+ path.gsub!(/\\\d{3}/) { |m| m[1..-1].to_i(8).chr }
330
537
  end
331
- (container || map).delete(name)
332
- map
538
+ path.gsub!(/\\[rn"\\]/) { |m| eval(%("#{m.to_s}")) }
539
+ path
540
+ end
541
+
542
+ # Resets the ref and tree caches for this wiki.
543
+ def clear_cache
544
+ @ref_map = {}
545
+ @tree_map = {}
333
546
  end
334
547
  end
335
- end
548
+ end