# ~*~ encoding: utf-8 ~*~
require 'pathname'

module Gollum
  class Wiki
    include Pagination

    class << self
      # Sets the default ref for the wiki.
      attr_writer :default_refs

      # Sets the default name for commits.
      attr_writer :default_committer_name

      # Sets the default email for commits.
      attr_writer :default_committer_email

      # Hash for setting different default wiki options
      # These defaults can be overridden by options passed directly to initialize()
      attr_writer :default_options

      def find_default_ref(repo)
        repo.find_branch(self.default_refs) || Gollum::Git.global_default_branch || self.default_refs.first
      end

      def default_refs
        @default_refs || ['master', 'main']
      end

      def default_committer_name
        @default_committer_name || 'Anonymous'
      end

      def default_committer_email
        @default_committer_email || 'anon@anonymous.invalid'
      end

      def default_options
        @default_options || {}
      end
    end

    # Whether or not the wiki's repository is bare (doesn't have a working directory)
    attr_reader :repo_is_bare

    # The String path to the repository
    attr_reader :path

    # The String base path to prefix to internal links. For example, when set
    # to "/wiki", the page "Hobbit" will be linked as "/wiki/Hobbit". Defaults
    # to "/".
    attr_reader :base_path

    # Gets the String ref in which all page files reside.
    attr_reader :ref

    # Gets the String directory in which all page files reside.
    attr_reader :page_file_dir

    # Injects custom css from custom.css in root repo.
    # Defaults to false
    attr_reader :css

    # Whether or not content is editable.
    # Defaults to true
    attr_reader :allow_editing

    # Sets page title to value of first h1
    # Defaults to false
    attr_reader :h1_title

    # Whether or not to render a page's metadata on the page
    # Defaults to true
    attr_reader :display_metadata

    # Gets the custom index page for / and subdirs (e.g. foo/)
    attr_reader :index_page

    # Gets side on which the sidebar should be shown
    attr_reader :bar_side

    # An array of symbols which refer to classes under Gollum::Filter,
    # each of which is an element in the "filtering chain".  See
    # the documentation for Gollum::Filter for more on how this chain
    # works, and what filter classes need to implement.
    attr_reader :filter_chain

    # Global metadata to be merged into the metadata for each page
    attr_reader :metadata

    # Public: Initialize a new Gollum Repo.
    #
    # path    - The String path to the Git repository that holds the Gollum
    #           site.
    # options - Optional Hash:
    #           :universal_toc - Table of contents on all pages.  Default: false
    #           :base_path     - String base path for all Wiki links.
    #                            Default: "/"
    #           :page_file_dir - String the directory in which all page files reside
    #           :ref - String the repository ref to retrieve pages from
    #           :mathjax       - Set to false to disable mathjax.
    #           :user_icons    - Enable user icons on the history page. [gravatar, identicon, none].
    #                            Default: none
    #           :global_tag_lookup        - Enable 4.x compatibility behavior for links
    #           :hyphened_tag_lookup - Spaces in tag paths are treated as dashes (-)
    #           :case_insensitive_tag_lookup - Paths in tags are compared case_insensitively
    #           :css           - Include the custom.css file from the repo.
    #           :emoji         - Parse and interpret emoji tags (e.g. :heart:).
    #           :h1_title      - Concatenate all h1's on a page to form the
    #                            page title.
    #           :display_metadata - Whether or not to render a page's metadata on the page. Default: true
    #           :index_page    - The default page to retrieve or create if the
    #                            a directory is accessed.
    #           :bar_side      - Where the sidebar should be displayed, may be:
    #                             - :left
    #                             - :right
    #           :allow_editing - Set whether wiki content can be edited. Default: true
    #           :allow_uploads - Set to true to allow file uploads.
    #           :per_page_uploads - Whether uploads should be stored in a central
    #                            'uploads' directory, or in a directory named for
    #                            the page they were uploaded to.
    #           :filter_chain  - Override the default filter chain with your own.
    #
    # Returns a fresh Gollum::Repo.
    def initialize(path, options = {})
      options = self.class.default_options.merge(options)
      if path.is_a?(GitAccess)
        options[:access] = path
        path             = path.path
      end

      @path                 = path
      @repo_is_bare         = options.fetch :repo_is_bare, nil
      @page_file_dir        = options.fetch :page_file_dir, nil
      @page_file_dir        = Pathname.new("/#{@page_file_dir}").cleanpath.to_s[1..-1] if @page_file_dir
      @access               = options.fetch :access, GitAccess.new(path, @page_file_dir, @repo_is_bare)
      @base_path            = options.fetch :base_path, "/"
      @repo                 = @access.repo
      @ref                  = options.fetch :ref, self.class.find_default_ref(@repo)
      @universal_toc        = options.fetch :universal_toc, false
      @mathjax              = options.fetch :mathjax, false
      @global_tag_lookup    = options.fetch :global_tag_lookup, false
      @hyphened_tag_lookup  = options.fetch :hyphened_tag_lookup, false
      @case_insensitive_tag_lookup = options.fetch :case_insensitive_tag_lookup, false
      @css                  = options.fetch :css, false
      @emoji                = options.fetch :emoji, false
      @critic_markup        = options.fetch :critic_markup, false
      @h1_title             = options.fetch :h1_title, false
      @display_metadata     = options.fetch :display_metadata, true
      @index_page           = options.fetch :index_page, 'Home'
      @bar_side             = options.fetch :sidebar, :right
      @user_icons           = ['gravatar', 'identicon'].include?(options[:user_icons]) ?
          options[:user_icons] : 'none'
      @allow_editing        = options.fetch :allow_editing, true
      @allow_uploads        = options.fetch :allow_uploads, false
      @per_page_uploads     = options.fetch :per_page_uploads, false
      @metadata             = options.fetch :metadata, {}
      @filter_chain         = options.fetch :filter_chain,
                                            [:YAML, :BibTeX, :PlainText, :CriticMarkup, :TOC, :Sanitize, :RemoteCode, :Code, :Macro, :Emoji, :PlantUML, :Tags, :PandocBib, :Render]
      @filter_chain.delete(:Emoji) unless options.fetch :emoji, false
      @filter_chain.delete(:PandocBib) unless ::Gollum::MarkupRegisterUtils.using_pandoc?
      @filter_chain.delete(:CriticMarkup) unless options.fetch :critic_markup, false

      Hook.execute(:post_wiki_initialize, self)
    end

    # Public: check whether the wiki's git repo exists on the filesystem.
    #
    # Returns true if the repo exists, and false if it does not.
    def exist?
      @access.exist?
    end

    # Public: Get the formatted page for a given page name, version, and dir.
    #
    # path    - The String path to the the wiki page (may or may not include file extension).
    # version - The String version ID to find (default: @ref).
    # global_match - If true, find a File matching path's filename, but not it's directory (so anywhere in the repo)
    #
    # Returns a Gollum::Page or nil if no matching page was found.
    def page(path, version = nil, global_match = false)
      ::Gollum::Page.find(self, path, version.nil? ? @ref : version, false, global_match)
    end

    # Public: Get the static file for a given name.
    #
    # name    - The full String pathname to the file.
    # version - The String version ID to find (default: @ref).
    # try_on_disk - If true, try to return just a reference to a file
    #               that exists on the disk.
    #
    # Returns a Gollum::File or nil if no matching file was found. Note
    # that if you specify try_on_disk=true, you may or may not get a file
    # for which on_disk? is actually true.
    def file(name, version = nil, try_on_disk = false)
      ::Gollum::File.find(self, name, version.nil? ? @ref : version, try_on_disk)
    end

    # Public: Create an in-memory Page with the given data and format. This
    # is useful for previewing what content will look like before committing
    # it to the repository.
    #
    # name   - The String name of the page.
    # format - The Symbol format of the page.
    # data   - The new String contents of the page.
    #
    # Returns the in-memory Gollum::Page.
    def preview_page(name, data, format)
      ::Gollum::PreviewPage.new(self, "#{name}.#{::Gollum::Page.format_to_ext(format.to_sym)}", data, @access.commit(@ref))
    end

    # Public: Write a new version of a page to the Gollum repo root.
    #
    # path   - The String path where the page will be written.
    # 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.
    #          :parent    - Optional Gollum::Git::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, or the
    # Gollum::Committer instance if this is part of a batch update.
    def write_page(path, format, data, commit = {})
     write(merge_path_elements(nil, path, format), data, commit)
    end

    # Public: Write a new version of a file to the Gollum repo.
    #
    # path   - The String path where the file will be written.
    # 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.
    #          :parent    - Optional Gollum::Git::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, or the
    # Gollum::Committer instance if this is part of a batch update
    def write_file(name, data, commit = {})
      write(merge_path_elements(nil, name, nil), data, commit)
    end

    # Public: Write a file to the Gollum repo regardless of existing versions.
    #
    # path   - The String path where the file will be written.
    # 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.
    #          :parent    - Optional Gollum::Git::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, or the
    # Gollum::Committer instance if this is part of a batch update
    def overwrite_file(name, data, commit = {})
      write(merge_path_elements(nil, name, nil), data, commit, force_overwrite = true)
    end

    # Public: Rename an existing page without altering content.
    #
    # page   - The Gollum::Page to update.
    # rename - The String extension-less full path of the page (leading '/' is ignored).
    # commit - The commit Hash details:
    #          :message   - The String commit message.
    #          :name      - The String author full name.
    #          :email     - The String email address.
    #          :parent    - Optional Gollum::Git::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, or the
    # Gollum::Committer instance if this is part of a batch update.
    # Returns false if the operation is a NOOP.
    def rename_page(page, rename, commit = {})
      return false if page.nil?
      return false if rename.nil? or rename.empty?

      (target_dir, target_name) = ::File.split(rename)
      (source_dir, source_name) = ::File.split(page.path)
      source_name               = page.filename_stripped

      # File.split gives us relative paths with ".", commiter.add_to_index doesn't like that.
      target_dir                = '' if target_dir == '.'
      source_dir                = '' if source_dir == '.'
      target_dir                = target_dir.gsub(/^\//, '')

      # if the rename is a NOOP, abort
      if source_dir == target_dir and source_name == target_name
        return false
      end

      multi_commit = !!commit[:committer]
      committer    = multi_commit ? commit[:committer] : Committer.new(self, commit)

      committer.delete(page.path)
      committer.add_to_index(merge_path_elements(target_dir, target_name, page.format), page.raw_data)

      committer.after_commit do |index, _sha|
        @access.refresh
        index.update_working_dir(merge_path_elements(source_dir, source_name, page.format))
        index.update_working_dir(merge_path_elements(target_dir, target_name, page.format))
      end

      multi_commit ? committer : committer.commit
    end

    # Public: Update an existing page with new content. The location of the
    # page inside the repository will not change. If the given format is
    # different than the current format of the page, the filename will be
    # changed to reflect the new format.
    #
    # page   - The Gollum::Page to update.
    # name   - The String extension-less name of the page.
    # 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.
    #          :parent    - Optional Gollum::Git::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, 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      = nil if dir == '.'
      rename   = (page.name != name || page.format != format)
      new_path = ::File.join([dir, self.page_file_name(name, format)].compact) if rename

      multi_commit = !!commit[:committer]
      committer    = multi_commit ? commit[:committer] : Committer.new(self, commit)

      if !rename
        committer.add(page.path, normalize(data))
      else
        committer.delete(page.path)
        committer.add_to_index(new_path, data)
      end

      committer.after_commit do |index, _sha|
        @access.refresh
        index.update_working_dir(page.path)
        index.update_working_dir(new_path) if rename
      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.
    #          :parent    - Optional Gollum::Git::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, or the
    # Gollum::Committer instance if this is part of a batch update.
    def delete_page(page, commit)
      delete_file(page.url_path, commit)
    end

    # Public: Delete a file.
    #
    # path   - The path to the file to delete
    # commit - The commit Hash details:
    #          :message   - The String commit message.
    #          :name      - The String author full name.
    #          :email     - The String email address.
    #          :parent    - Optional Gollum::Git::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, or the
    # Gollum::Committer instance if this is part of a batch update.
    def delete_file(path, commit)
      fullpath     = ::File.join([page_file_dir, path].compact)
      multi_commit = !!commit[:committer]
      committer    = multi_commit ? commit[:committer] : Committer.new(self, commit)

      committer.delete(fullpath)

      committer.after_commit do |index, _sha|
        dir = '' if dir == '.'
        @access.refresh
        index.update_working_dir(fullpath)
      end

      multi_commit ? committer : committer.commit
    end

    # Public: Applies a reverse diff for a given page.  If only 1 SHA is given,
    # the reverse diff will be taken from its parent (^SHA...SHA).  If two SHAs
    # are given, the reverse diff is taken from SHA1...SHA2.
    #
    # page   - The Gollum::Page to delete.
    # sha1   - String SHA1 of the earlier parent if two SHAs are given,
    #          or the child.
    # sha2   - Optional String SHA1 of the child.
    # commit - The commit Hash details:
    #          :message - The String commit message.
    #          :name    - The String author full name.
    #          :email   - The String email address.
    #          :parent  - Optional Gollum::Git::Commit parent to this update.
    # Returns a String SHA1 of the new commit, or nil if the reverse diff does
    # not apply.
    def revert_page(page, sha1, sha2 = nil, commit = {})
      return false unless page
      left, right, options = parse_revert_options(sha1, sha2, commit)
      commit_and_update_paths(@repo.git.revert_path(page.path, left, right), [page.path], options)
    end

    # Public: Applies a reverse diff to the repo.  If only 1 SHA is given,
    # the reverse diff will be taken from its parent (^SHA...SHA).  If two SHAs
    # are given, the reverse diff is taken from SHA1...SHA2.
    #
    # sha1   - String SHA1 of the earlier parent if two SHAs are given,
    #          or the child.
    # sha2   - Optional String SHA1 of the child.
    # commit - The commit Hash details:
    #          :message - The String commit message.
    #          :name    - The String author full name.
    #          :email   - The String email address.
    #
    # Returns a String SHA1 of the new commit, or nil if the reverse diff does
    # not apply.
    def revert_commit(sha1, sha2 = nil, commit = {})
      left, right, options = parse_revert_options(sha1, sha2, commit)
      tree, files = repo.git.revert_commit(left, right)
      commit_and_update_paths(tree, files, options)
    end

    # Public: Lists all pages for this wiki.
    #
    # treeish - The String commit ID or ref to find  (default:  @ref)
    #
    # Returns an Array of Gollum::Page instances.
    def pages(treeish = nil)
      tree_list(treeish || @ref, true, false)
    end

    # Public: Lists all non-page files for this wiki.
    #
    # treeish - The String commit ID or ref to find  (default:  @ref)
    #
    # Returns an Array of Gollum::File instances.
    def files(treeish = nil)
      tree_list(treeish || @ref, false, true)
    end

    # Public: Returns the number of pages accessible from a commit
    #
    # ref - A String ref that is either a commit SHA or references one.
    #
    # Returns a Fixnum
    def size(ref = nil)
      tree_map_for(ref || @ref).inject(0) do |num, entry|
        num + (::Gollum::Page.valid_page_name?(entry.name) ? 1 : 0)
      end
    rescue Gollum::Git::NoSuchShaFound
      0
    end

    # Public: Search all pages for this wiki.
    #
    # query - The string to search for
    #
    # Returns an Array with Objects of page name and count of matches
    def search(query)
      options = {:path => page_file_dir, :ref => ref}
      search_terms = query.scan(/"([^"]+)"|(\S+)/).flatten.compact.map {|term| Regexp.escape(term)}
      search_terms_regex = search_terms.join('|')
      query = /^(.*(?:#{search_terms_regex}).*)$/i
      results = @repo.git.grep(search_terms, options) do |name, data|
        result = {:count => 0}
        result[:name] = extract_page_file_dir(name)
        result[:filename_count] = result[:name].scan(/#{search_terms_regex}/i).size
        result[:context] = []
        if data
          begin
            data.scan(query) do |match|
              result[:context] << match.first
              result[:count] += match.first.scan(/#{search_terms_regex}/i).size
            end
          rescue ArgumentError # https://github.com/gollum/gollum/issues/1491
            next
          end
        end
        ((result[:count] + result[:filename_count]) == 0) ? nil : result
      end
      [results, search_terms]
    end

    # Public: All of the versions that have touched the Page.
    #
    # options - The options Hash:
    #           :page_num  - The Integer page number (default: 1).
    #           :per_page  - The Integer max count of items to return.
    #
    # Returns an Array of Gollum::Git::Commit.
    def log(options = {})
      @repo.log(@ref, nil, log_pagination_options(options))
    end

    # Returns the latest changes in the wiki (globally)
    #
    # options - The options Hash:
    #           :max_count  - The Integer number of items to return.
    #
    # Returns an Array of Gollum::Git::Commit.
    def latest_changes(options={})
      options[:max_count] = 10 unless options[:max_count]
      @repo.log(@ref, page_file_dir, options)
    end

    # Public: Refreshes just the cached Git reference data.  This should
    # be called after every Gollum update.
    #
    # Returns nothing.
    def clear_cache
      @access.refresh
    end

    def redirects
      if @redirects.nil? || @redirects.stale?
        @redirects = {}.extend(::Gollum::Redirects)
        @redirects.init(self)
        @redirects.load
      end
      @redirects
    end

    def add_redirect(old_path, new_path, commit=nil)
      redirects[old_path] = new_path
      redirects.dump(commit)
    end

    def remove_redirect(path, commit=nil)
      redirects.tap{|k| k.delete(path)}
      redirects.dump(commit)
    end

    #########################################################################
    #
    # Internal Methods
    #
    #########################################################################

    # The Gollum::Git::Repo associated with the wiki.
    #
    # Returns the Gollum::Git::Repo.
    attr_reader :repo

    # The String path to the Git repository that holds the Gollum site.
    #
    # Returns the String path.
    attr_reader :path

    # Toggles display of universal table of contents
    attr_reader :universal_toc

    # Toggles mathjax.
    attr_reader :mathjax

    # Toggles user icons. Default: 'none'
    attr_reader :user_icons

    # Toggles showing all files in files view. Default is false.
    # When false, only valid pages in the git repo are displayed.
    attr_reader :show_all

    # Enable 4.x compatibility behavior for links
    attr_reader :global_tag_lookup

    # Enable 4.x compatibility for case-case_insensitive links
    attr_reader :case_insensitive_tag_lookup

    # Spaces in tag paths are treated as dashes (-)
    attr_reader :hyphened_tag_lookup

    # Toggles file upload functionality.
    attr_reader :allow_uploads

    # Toggles whether uploaded files go into 'uploads', or a directory
    # named after the page they were uploaded to.
    attr_reader :per_page_uploads

    # Normalize the data.
    #
    # data - The String data to be normalized.
    #
    # Returns the normalized data String.
    def normalize(data)
      data.gsub(/\r/, '')
    end

    # Assemble a Page's filename from its name and format.
    #
    # name   - The String name of the page (should be pre-canonicalized).
    # format - The Symbol format of the page.
    #
    # Returns the String filename.
    def page_file_name(name, format)
      format.nil? ? name : "#{name}.#{::Gollum::Page.format_to_ext(format)}"
    end

    # Fill an array with a list of pages and files in the wiki.
    #
    # ref - A String ref that is either a commit SHA or references one.
    #
    # Returns a flat Array of Gollum::Page and Gollum::File instances.
    def tree_list(ref = @ref, pages=true, files=true)
      if (sha = @access.ref_to_sha(ref))
        commit = @access.commit(sha)
        tree_map_for(sha).inject([]) do |list, entry|
          if ::Gollum::Page.valid_page_name?(entry.name)
            list << entry.page(self, commit) if pages
          elsif files && !entry.name.start_with?('_') && !::Gollum::Page.protected_files.include?(entry.name)
            list << entry.file(self, commit)
          end
          list
        end
      else
        []
      end
    end

    # Gets the default name for commits.
    #
    # Returns the String name.
    def default_committer_name
      @default_committer_name ||= \
        @repo.config['user.name'] || self.class.default_committer_name
    end

    # Gets the default email for commits.
    #
    # Returns the String email address.
    def default_committer_email
      email = @repo.config['user.email']
      email = email.delete('<>') if email
      @default_committer_email ||= email || self.class.default_committer_email
    end

    # Gets the commit object for the given ref or sha.
    #
    # ref - A string ref or SHA pointing to a valid commit.
    #
    # Returns a Gollum::Git::Commit instance.
    def commit_for(ref)
      @access.commit(ref)
    rescue Gollum::Git::NoSuchShaFound
    end

    # Finds a full listing of files and their blob SHA for a given ref.  Each
    # listing is cached based on its actual commit SHA.
    #
    # ref - A String ref that is either a commit SHA or references one.
    # ignore_page_file_dir - Boolean, if true, searches all files within the git repo, regardless of dir/subdir
    #
    # Returns an Array of BlobEntry instances.
    def tree_map_for(ref, ignore_page_file_dir = false)
      if ignore_page_file_dir && !@page_file_dir.nil?
        @root_access ||= GitAccess.new(path, nil, @repo_is_bare)
        @root_access.tree(ref)
      else
        @access.tree(ref)
      end
    rescue Gollum::Git::NoSuchShaFound
      []
    end

    def inspect
      %(#<#{self.class.name}:#{object_id} #{@repo.path}>)
    end

    # Public: Creates a Sanitize instance
    #
    # Returns a Sanitize instance.
    def sanitizer
      @sanitizer ||= Gollum::Sanitization.new(Gollum::Markup.to_xml_opts)
    end

    private

    def parse_revert_options(sha1, sha2, commit = {})
      if sha2.is_a?(Hash)
        return "#{sha1}^", sha1, sha2
      elsif sha2.nil?
        return "#{sha1}^", sha1, commit
      else
        return sha1, sha2, commit
      end
    end

    def commit_and_update_paths(tree, paths, options)
      return false unless tree
      committer = Committer.new(self, options)
      parent    = committer.parents[0]

      committer.options[:tree] = tree

      committer.after_commit do |index, _sha|
        @access.refresh

        paths.each do |path|
          index.update_working_dir(path)
        end
      end

      committer.commit
    end

    # Conjoins elements of a page or file path and prefixes the page_file_dir.
    # Throws Gollum::IllegalDirectoryPath if page_file_dir is set, and the resulting
    # path is not below it (e.g. if the dir or name contained '../')
    #
    # dir    - The String directory path
    # name   - The String name of the Page or File
    # format - The Symbol format of the page. Should be nil for Files.
    #
    # Returns a String path.  .
    def merge_path_elements(dir, name, format)
      result = ::File.join([@page_file_dir, dir, self.page_file_name(name, format)].compact)
      result = Pathname.new(result).cleanpath.to_s
      if @page_file_dir
        raise Gollum::IllegalDirectoryPath unless result.start_with?("#{@page_file_dir}/")
        result
      else
        result[0] == '/' ? result[1..-1] : result
      end
    end

    def extract_page_file_dir(path)
      @page_file_dir ? path[@page_file_dir.length+1..-1] : path
    end

    def write(path, data, commit = {}, force_overwrite = false)
      multi_commit = !!commit[:committer]
      committer    = multi_commit ? commit[:committer] : Committer.new(self, commit)
      committer.add_to_index(path, data, commit, force_overwrite)

      committer.after_commit do |index, _sha|
        @access.refresh
        index.update_working_dir(path)
      end

      multi_commit ? committer : committer.commit
    end

  end
end
