From aae9eb188332f3c34a2dff90892c21c415bf74e2 Mon Sep 17 00:00:00 2001 From: rick Date: Wed, 19 Jan 2011 05:11:11 -0800 Subject: [PATCH] refactor wiki commit logic into Gollum::Committer class --- lib/gollum.rb | 1 + lib/gollum/committer.rb | 216 +++++++++++++++++++++++++ lib/gollum/wiki.rb | 347 +++++++++++++--------------------------- test/test_committer.rb | 27 ++++ test/test_wiki.rb | 17 -- 5 files changed, 357 insertions(+), 251 deletions(-) create mode 100644 lib/gollum/committer.rb create mode 100644 test/test_committer.rb diff --git a/lib/gollum.rb b/lib/gollum.rb index 6970d0be..2320b281 100644 --- a/lib/gollum.rb +++ b/lib/gollum.rb @@ -12,6 +12,7 @@ require 'gollum/ruby1.8' # internal require 'gollum/git_access' +require 'gollum/committer' require 'gollum/pagination' require 'gollum/blob_entry' require 'gollum/wiki' diff --git a/lib/gollum/committer.rb b/lib/gollum/committer.rb new file mode 100644 index 00000000..9038f2ff --- /dev/null +++ b/lib/gollum/committer.rb @@ -0,0 +1,216 @@ +module Gollum + # Responsible for handling the commit process for a Wiki. It sets up the + # Git index, provides methods for modifying the tree, and stores callbacks + # to be fired after the commit has been made. This is specifically + # designed to handle multiple updated pages in a single commit. + class Committer + # Gets the instance of the Gollum::Wiki that is being updated. + attr_reader :wiki + + # Gets a Hash of commit options. + attr_reader :options + + # Initializes the Committer. + # + # wiki - The Gollum::Wiki instance that is being updated. + # options - The commit Hash details: + # :message - The String commit message. + # :name - The String author full name. + # :email - The String email address. + # :parent - Optional Grit::Commit parent to this update. + # :tree - Optional String SHA of the tree to create the + # index from. + # :committer - Optional Gollum::Committer instance. If provided, + # assume that this operation is part of batch of + # updates and the commit happens later. + # + # Returns the Committer instance. + def initialize(wiki, options = {}) + @wiki = wiki + @options = options + @callbacks = [] + end + + # Public: References the Git index for this commit. + # + # Returns a Grit::Index. + def index + @index ||= begin + idx = @wiki.repo.index + if tree = options[:tree] + idx.read_tree(tree) + elsif parent = parents.first + idx.read_tree(parent.tree.id) + end + idx + end + end + + # Public: The committer for this commit. + # + # Returns a Grit::Actor. + def actor + @actor ||= begin + @options[:name] = @wiki.default_committer_name if @options[:name].to_s.empty? + @options[:email] = @wiki.default_committer_email if @options[:email].to_s.empty? + Grit::Actor.new(@options[:name], @options[:email]) + end + end + + # Public: The parent commits to this pending commit. + # + # Returns an array of Grit::Commit instances. + def parents + @parents ||= begin + arr = [@options[:parent] || @wiki.repo.commit('master')] + arr.flatten! + arr.compact! + arr + end + end + + # Adds a page to the given Index. + # + # dir - The String subdirectory of the Gollum::Page without any + # prefix or suffix slashes (e.g. "foo/bar"). + # name - The String Gollum::Page name. + # format - The Symbol Gollum::Page format. + # data - The String wiki data to store in the tree map. + # allow_same_ext - A Boolean determining if the tree map allows the same + # filename with the same extension. + # + # Raises Gollum::DuplicatePageError if a matching filename already exists. + # This way, pages are not inadvertently overwritten. + # + # Returns nothing (modifies the Index in place). + def add_to_index(dir, name, format, data, allow_same_ext = false) + path = @wiki.page_file_name(name, format) + + dir = '/' if dir.strip.empty? + + fullpath = ::File.join(*[@wiki.page_file_dir, dir, path].compact) + fullpath = fullpath[1..-1] if fullpath =~ /^\// + + if index.current_tree && tree = index.current_tree / dir + downpath = path.downcase.sub(/\.\w+$/, '') + + tree.blobs.each do |blob| + next if page_path_scheduled_for_deletion?(index.tree, fullpath) + file = blob.name.downcase.sub(/\.\w+$/, '') + file_ext = ::File.extname(blob.name).sub(/^\./, '') + if downpath == file && !(allow_same_ext && file_ext == ext) + raise DuplicatePageError.new(dir, blob.name, path) + end + end + end + + index.add(fullpath, @wiki.normalize(data)) + end + + # Update the given file in the repository's working directory if there + # is a working directory present. + # + # dir - The String directory in which the file lives. + # name - The String name of the page (may be in human format). + # format - The Symbol format of the page. + # + # Returns nothing. + def update_working_dir(dir, name, format) + unless @wiki.repo.bare + if @wiki.page_file_dir + dir = dir.size.zero? ? @wiki.page_file_dir : File.join(dir, @wiki.page_file_dir) + end + + path = + if dir == '' + @wiki.page_file_name(name, format) + else + ::File.join(dir, @wiki.page_file_name(name, format)) + end + + Dir.chdir(::File.join(@wiki.repo.path, '..')) do + if file_path_scheduled_for_deletion?(index.tree, path) + @wiki.repo.git.rm({'f' => true}, '--', path) + else + @wiki.repo.git.checkout({}, 'HEAD', '--', path) + end + end + end + end + + # Writes the commit to Git and runs the after_commit callbacks. + # + # Returns the String SHA1 of the new commit. + def commit + sha1 = index.commit(@options[:message], parents, actor) + @callbacks.each do |cb| + cb.call(self) + end + sha1 + end + + # Adds a callback to be fired after a commit. + # + # block - A block that expects this Committer instance as the argument. + # + # Returns nothing. + def after_commit(&block) + @callbacks << block + end + + # Determine if a given page (regardless of format) is scheduled to be + # deleted in the next commit for the given Index. + # + # map - The Hash map: + # key - The String directory or filename. + # val - The Hash submap or the String contents of the file. + # path - The String path of the page file. This may include the format + # extension in which case it will be ignored. + # + # Returns the Boolean response. + def page_path_scheduled_for_deletion?(map, path) + parts = path.split('/') + if parts.size == 1 + deletions = map.keys.select { |k| !map[k] } + downfile = parts.first.downcase.sub(/\.\w+$/, '') + deletions.any? { |d| d.downcase.sub(/\.\w+$/, '') == downfile } + else + part = parts.shift + if rest = map[part] + page_path_scheduled_for_deletion?(rest, parts.join('/')) + else + false + end + end + end + + # Determine if a given file is scheduled to be deleted in the next commit + # for the given Index. + # + # map - The Hash map: + # key - The String directory or filename. + # val - The Hash submap or the String contents of the file. + # path - The String path of the file including extension. + # + # Returns the Boolean response. + def file_path_scheduled_for_deletion?(map, path) + parts = path.split('/') + if parts.size == 1 + deletions = map.keys.select { |k| !map[k] } + deletions.any? { |d| d == parts.first } + else + part = parts.shift + if rest = map[part] + file_path_scheduled_for_deletion?(rest, parts.join('/')) + else + false + end + end + end + + # Proxies methods t + def method_missing(name, *args) + index.send(name, *args) + end + end +end \ No newline at end of file diff --git a/lib/gollum/wiki.rb b/lib/gollum/wiki.rb index 76107586..211bbe66 100644 --- a/lib/gollum/wiki.rb +++ b/lib/gollum/wiki.rb @@ -181,22 +181,36 @@ module Gollum # format - The Symbol format of the page. # data - The new String contents of the page. # commit - The commit Hash details: - # :message - The String commit message. - # :name - The String author full name. - # :email - The String email address. + # :message - The String commit message. + # :name - The String author full name. + # :email - The String email address. + # :parent - Optional Grit::Commit parent to this update. + # :tree - Optional String SHA of the tree to create the + # index from. + # :committer - Optional Gollum::Committer instance. If provided, + # assume that this operation is part of batch of + # updates and the commit happens later. # - # Returns the String SHA1 of the newly written version. + # Returns the String SHA1 of the newly written version, or the + # Gollum::Committer instance if this is part of a batch update. def write_page(name, format, data, commit = {}) - index = nil - sha1 = commit_index(commit) do |idx| - index = idx - add_to_index(index, '', name, format, data) + multi_commit = false + + committer = if obj = commit[:committer] + multi_commit = true + obj + else + Committer.new(self, commit) end - @access.refresh - update_working_dir(index, '', name, format) + committer.add_to_index('', name, format, data) - sha1 + committer.after_commit do |index| + @access.refresh + index.update_working_dir('', name, format) + end + + multi_commit ? committer : committer.commit end # Public: Update an existing page with new content. The location of the @@ -209,57 +223,85 @@ module Gollum # format - The Symbol format of the page. # data - The new String contents of the page. # commit - The commit Hash details: - # :message - The String commit message. - # :name - The String author full name. - # :email - The String email address. + # :message - The String commit message. + # :name - The String author full name. + # :email - The String email address. + # :parent - Optional Grit::Commit parent to this update. + # :tree - Optional String SHA of the tree to create the + # index from. + # :committer - Optional Gollum::Committer instance. If provided, + # assume that this operation is part of batch of + # updates and the commit happens later. # - # Returns the String SHA1 of the newly written version. + # Returns the String SHA1 of the newly written version, or the + # Gollum::Committer instance if this is part of a batch update. def update_page(page, name, format, data, commit = {}) name ||= page.name format ||= page.format dir = ::File.dirname(page.path) dir = '' if dir == '.' - index = nil - sha1 = commit_index(commit) do |idx| - index = idx - if page.name == name && page.format == format - index.add(page.path, normalize(data)) - else - index.delete(page.path) - add_to_index(index, dir, name, format, data, :allow_same_ext) - end + multi_commit = false + + committer = if obj = commit[:committer] + multi_commit = true + obj + else + Committer.new(self, commit) end - @access.refresh - update_working_dir(index, dir, page.name, page.format) - update_working_dir(index, dir, name, format) + if page.name == name && page.format == format + committer.add(page.path, normalize(data)) + else + committer.delete(page.path) + committer.add_to_index(dir, name, format, data, :allow_same_ext) + end - sha1 + committer.after_commit do |index| + @access.refresh + index.update_working_dir(dir, page.name, page.format) + index.update_working_dir(dir, name, format) + end + + multi_commit ? committer : committer.commit end # Public: Delete a page. # # page - The Gollum::Page to delete. # commit - The commit Hash details: - # :message - The String commit message. - # :name - The String author full name. - # :email - The String email address. + # :message - The String commit message. + # :name - The String author full name. + # :email - The String email address. + # :parent - Optional Grit::Commit parent to this update. + # :tree - Optional String SHA of the tree to create the + # index from. + # :committer - Optional Gollum::Committer instance. If provided, + # assume that this operation is part of batch of + # updates and the commit happens later. # - # Returns the String SHA1 of the newly written version. + # Returns the String SHA1 of the newly written version, or the + # Gollum::Committer instance if this is part of a batch update. def delete_page(page, commit) - index = nil - sha1 = commit_index(commit) do |idx| - index = idx - index.delete(page.path) + multi_commit = false + + committer = if obj = commit[:committer] + multi_commit = true + obj + else + Committer.new(self, commit) + end + + committer.delete(page.path) + + committer.after_commit do |index| + dir = ::File.dirname(page.path) + dir = '' if dir == '.' + + @access.refresh + index.update_working_dir(dir, page.name, page.format) end - dir = ::File.dirname(page.path) - dir = '' if dir == '.' - - @access.refresh - update_working_dir(index, dir, page.name, page.format) - - sha1 + multi_commit ? committer : committer.commit end # Public: Applies a reverse diff for a given page. If only 1 SHA is given, @@ -274,6 +316,7 @@ module Gollum # :message - The String commit message. # :name - The String author full name. # :email - The String email address. + # :parent - Optional Grit::Commit parent to this update. # # Returns a String SHA1 of the new commit, or nil if the reverse diff does # not apply. @@ -283,41 +326,39 @@ module Gollum sha2 = nil end - pcommit = @repo.commit('master') - patch = full_reverse_diff_for(page, sha1, sha2) - commit[:parent] = [pcommit] - commit[:tree] = @repo.git.apply_patch(pcommit.sha, patch) - return false unless commit[:tree] + patch = full_reverse_diff_for(page, sha1, sha2) + committer = Committer.new(self, commit) + parent = committer.parents[0] + committer.options[:tree] = @repo.git.apply_patch(parent.sha, patch) + return false unless committer.options[:tree] + committer.after_commit do |index| + @access.refresh - index = nil - sha1 = commit_index(commit) { |i| index = i } - @access.refresh - - files = [] - if page - files << [page.path, page.name, page.format] - else - # Grit::Diff can't parse reverse diffs.... yet - lines = patch.split("\n") - while line = lines.shift - if line =~ %r{^diff --git b/.+? a/(.+)$} - path = $1 - ext = ::File.extname(path) - name = ::File.basename(path, ext) - if format = ::Gollum::Page.format_for(ext) - files << [path, name, format] + files = [] + if page + files << [page.path, page.name, page.format] + else + # Grit::Diff can't parse reverse diffs.... yet + patch.each_line do |line| + if line =~ %r{^diff --git b/.+? a/(.+)$} + path = $1 + ext = ::File.extname(path) + name = ::File.basename(path, ext) + if format = ::Gollum::Page.format_for(ext) + files << [path, name, format] + end end end end + + files.each do |(path, name, format)| + dir = ::File.dirname(path) + dir = '' if dir == '.' + index.update_working_dir(dir, name, format) + end end - files.each do |(path, name, format)| - dir = ::File.dirname(path) - dir = '' if dir == '.' - update_working_dir(index, dir, name, format) - end - - sha1 + committer.commit end # Public: Applies a reverse diff to the repo. If only 1 SHA is given, @@ -464,38 +505,6 @@ module Gollum @page_class.cname(name) + '.' + ext end - # Update the given file in the repository's working directory if there - # is a working directory present. - # - # index - The Grit::Index with which to sync. - # dir - The String directory in which the file lives. - # name - The String name of the page (may be in human format). - # format - The Symbol format of the page. - # - # Returns nothing. - def update_working_dir(index, dir, name, format) - unless @repo.bare - if @page_file_dir - dir = dir.size.zero? ? @page_file_dir : File.join(dir, @page_file_dir) - end - - path = - if dir == '' - page_file_name(name, format) - else - ::File.join(dir, page_file_name(name, format)) - end - - Dir.chdir(::File.join(@repo.path, '..')) do - if file_path_scheduled_for_deletion?(index.tree, path) - @repo.git.rm({'f' => true}, '--', path) - else - @repo.git.checkout({}, 'HEAD', '--', path) - end - end - end - end - # Fill an array with a list of pages. # # ref - A String ref that is either a commit SHA or references one. @@ -510,122 +519,6 @@ module Gollum end end - # Determine if a given file is scheduled to be deleted in the next commit - # for the given Index. - # - # map - The Hash map: - # key - The String directory or filename. - # val - The Hash submap or the String contents of the file. - # path - The String path of the file including extension. - # - # Returns the Boolean response. - def file_path_scheduled_for_deletion?(map, path) - parts = path.split('/') - if parts.size == 1 - deletions = map.keys.select { |k| !map[k] } - deletions.any? { |d| d == parts.first } - else - part = parts.shift - if rest = map[part] - file_path_scheduled_for_deletion?(rest, parts.join('/')) - else - false - end - end - end - - # Determine if a given page (regardless of format) is scheduled to be - # deleted in the next commit for the given Index. - # - # map - The Hash map: - # key - The String directory or filename. - # val - The Hash submap or the String contents of the file. - # path - The String path of the page file. This may include the format - # extension in which case it will be ignored. - # - # Returns the Boolean response. - def page_path_scheduled_for_deletion?(map, path) - parts = path.split('/') - if parts.size == 1 - deletions = map.keys.select { |k| !map[k] } - downfile = parts.first.downcase.sub(/\.\w+$/, '') - deletions.any? { |d| d.downcase.sub(/\.\w+$/, '') == downfile } - else - part = parts.shift - if rest = map[part] - page_path_scheduled_for_deletion?(rest, parts.join('/')) - else - false - end - end - end - - # Adds a page to the given Index. - # - # index - The Grit::Index to which the page will be added. - # dir - The String subdirectory of the Gollum::Page without any - # prefix or suffix slashes (e.g. "foo/bar"). - # name - The String Gollum::Page name. - # format - The Symbol Gollum::Page format. - # data - The String wiki data to store in the tree map. - # allow_same_ext - A Boolean determining if the tree map allows the same - # filename with the same extension. - # - # Raises Gollum::DuplicatePageError if a matching filename already exists. - # This way, pages are not inadvertently overwritten. - # - # Returns nothing (modifies the Index in place). - def add_to_index(index, dir, name, format, data, allow_same_ext = false) - path = page_file_name(name, format) - - dir = '/' if dir.strip.empty? - - fullpath = ::File.join(*[@page_file_dir, dir, path].compact) - fullpath = fullpath[1..-1] if fullpath =~ /^\// - - if index.current_tree && tree = index.current_tree / dir - downpath = path.downcase.sub(/\.\w+$/, '') - - tree.blobs.each do |blob| - next if page_path_scheduled_for_deletion?(index.tree, fullpath) - file = blob.name.downcase.sub(/\.\w+$/, '') - file_ext = ::File.extname(blob.name).sub(/^\./, '') - if downpath == file && !(allow_same_ext && file_ext == ext) - raise DuplicatePageError.new(dir, blob.name, path) - end - end - end - - index.add(fullpath, normalize(data)) - end - - # Commits to the repo. This is a common method used by Gollum for - # creating, updating, and deleting pages. There are typically three steps: - # building an index with the current tree, yielding the index for - # modification, and then writing the commit. - # - # options - Hash of option - # - # Returns the String SHA of the new Commit. - def commit_index(options = {}) - normalize_commit(options) - parents = [options[:parent] || @repo.commit('master')] - parents.flatten! - parents.compact! - index = self.repo.index - if tree = options[:tree] - index.read_tree(tree) - elsif parent = parents[0] - index.read_tree(parent.tree.id) - end - yield index if block_given? - - options[:name] = default_committer_name if options[:name].to_s.empty? - options[:email] = default_committer_email if options[:email].to_s.empty? - actor = Grit::Actor.new(options[:name], options[:email]) - index.commit(options[:message], parents, actor) - end - # Creates a reverse diff for the given SHAs on the given Gollum::Page. # # page - The Gollum::Page to scope the patch to, or a String Path. @@ -654,20 +547,6 @@ module Gollum full_reverse_diff_for(nil, sha1, sha2) end - # Ensures a commit hash has all the required fields for a commit. - # - # commit - The commit Hash details: - # :message - The String commit message. - # :name - The String author full name. - # :email - The String email address. - # - # Returns the commit Hash - def normalize_commit(commit = {}) - commit[:name] = default_committer_name if commit[:name].to_s.empty? - commit[:email] = default_committer_email if commit[:email].to_s.empty? - commit - end - # Gets the default name for commits. # # Returns the String name. diff --git a/test/test_committer.rb b/test/test_committer.rb new file mode 100644 index 00000000..f87a05a4 --- /dev/null +++ b/test/test_committer.rb @@ -0,0 +1,27 @@ +require File.expand_path(File.join(File.dirname(__FILE__), "helper")) + +context "Wiki" do + setup do + @wiki = Gollum::Wiki.new(testpath("examples/lotr.git")) + end + + test "normalizes commit hash" do + commit = {:message => 'abc'} + name = @wiki.repo.config['user.name'] + email = @wiki.repo.config['user.email'] + committer = Gollum::Committer.new(@wiki, commit) + assert_equal name, committer.actor.name + assert_equal email, committer.actor.email + + commit[:name] = 'bob' + commit[:email] = '' + committer = Gollum::Committer.new(@wiki, commit) + assert_equal 'bob', committer.actor.name + assert_equal email, committer.actor.email + + commit[:email] = 'foo@bar.com' + committer = Gollum::Committer.new(@wiki, commit) + assert_equal 'bob', committer.actor.name + assert_equal 'foo@bar.com', committer.actor.email + end +end \ No newline at end of file diff --git a/test/test_wiki.rb b/test/test_wiki.rb index 1f862dd0..4ac9aa4e 100644 --- a/test/test_wiki.rb +++ b/test/test_wiki.rb @@ -44,23 +44,6 @@ context "Wiki" do assert_equal 4, @wiki.size end - test "normalizes commit hash" do - commit = {:message => 'abc'} - name = @wiki.repo.config['user.name'] - email = @wiki.repo.config['user.email'] - assert_equal({:message => 'abc', :name => name, :email => email}, - @wiki.normalize_commit(commit.dup)) - - commit[:name] = 'bob' - commit[:email] = '' - assert_equal({:message => 'abc', :name => 'bob', :email => email}, - @wiki.normalize_commit(commit.dup)) - - commit[:email] = 'foo@bar.com' - assert_equal({:message => 'abc', :name => 'bob', :email => 'foo@bar.com'}, - @wiki.normalize_commit(commit.dup)) - end - test "text_data" do wiki = Gollum::Wiki.new(testpath("examples/yubiwa.git")) if String.instance_methods.include?(:encoding)