wiki.rb 9.52 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
module Gitlab
  module Git
    class Wiki
      DuplicatePageError = Class.new(StandardError)

      CommitDetails = Struct.new(:name, :email, :message) do
        def to_h
          { name: name, email: email, message: message }
        end
      end
11
      PageBlob = Struct.new(:name)
12

13 14
      attr_reader :repository

15 16 17 18 19 20 21 22 23 24 25 26 27 28
      def self.default_ref
        'master'
      end

      # Initialize with a Gitlab::Git::Repository instance
      def initialize(repository)
        @repository = repository
      end

      def repository_exists?
        @repository.exists?
      end

      def write_page(name, format, content, commit_details)
29
        @repository.gitaly_migrate(:wiki_write_page) do |is_enabled|
30 31 32 33 34 35
          if is_enabled
            gitaly_write_page(name, format, content, commit_details)
          else
            gollum_write_page(name, format, content, commit_details)
          end
        end
36 37 38
      end

      def delete_page(page_path, commit_details)
39 40 41 42 43 44 45
        @repository.gitaly_migrate(:wiki_delete_page) do |is_enabled|
          if is_enabled
            gitaly_delete_page(page_path, commit_details)
          else
            gollum_delete_page(page_path, commit_details)
          end
        end
46 47 48
      end

      def update_page(page_path, title, format, content, commit_details)
49
        @repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
50 51 52 53 54 55
          if is_enabled
            gitaly_update_page(page_path, title, format, content, commit_details)
          else
            gollum_update_page(page_path, title, format, content, commit_details)
          end
        end
56 57
      end

58
      def pages(limit: nil)
59
        @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled|
60 61 62
          if is_enabled
            gitaly_get_all_pages
          else
63
            gollum_get_all_pages(limit: limit)
64 65
          end
        end
66 67 68
      end

      def page(title:, version: nil, dir: nil)
69
        @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
70 71 72 73 74
          if is_enabled
            gitaly_find_page(title: title, version: version, dir: dir)
          else
            gollum_find_page(title: title, version: version, dir: dir)
          end
75 76 77 78
        end
      end

      def file(name, version)
79 80 81 82 83 84 85
        @repository.gitaly_migrate(:wiki_find_file) do |is_enabled|
          if is_enabled
            gitaly_find_file(name, version)
          else
            gollum_find_file(name, version)
          end
        end
86 87
      end

88 89 90 91 92
      # options:
      #  :page     - The Integer page number.
      #  :per_page - The number of items per page.
      #  :limit    - Total number of items to return.
      def page_versions(page_path, options = {})
93 94
        @repository.gitaly_migrate(:wiki_page_versions) do |is_enabled|
          if is_enabled
95 96 97 98 99 100 101
            versions = gitaly_wiki_client.page_versions(page_path, options)

            # Gitaly uses gollum-lib to get the versions. Gollum defaults to 20
            # per page, but also fetches 20 if `limit` or `per_page` < 20.
            # Slicing returns an array with the expected number of items.
            slice_bound = options[:limit] || options[:per_page] || Gollum::Page.per_page
            versions[0..slice_bound]
102 103 104 105 106 107 108 109
          else
            current_page = gollum_page_by_path(page_path)

            commits_from_page(current_page, options).map do |gitlab_git_commit|
              gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id)
              Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format)
            end
          end
110 111 112
        end
      end

113 114 115 116
      def count_page_versions(page_path)
        @repository.count_commits(ref: 'HEAD', path: page_path)
      end

117
      def preview_slug(title, format)
118 119 120 121 122 123 124 125 126
        # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid
        # using Rugged through a Gollum::Wiki instance
        page_class = Gollum::Page
        page = page_class.new(nil)
        ext = page_class.format_to_ext(format.to_sym)
        name = page_class.cname(title) + '.' + ext
        blob = PageBlob.new(name)
        page.populate(blob)
        page.url_path
127 128
      end

129 130 131 132 133 134 135 136 137 138 139 140 141 142
      def page_formatted_data(title:, dir: nil, version: nil)
        version = version&.id

        @repository.gitaly_migrate(:wiki_page_formatted_data) do |is_enabled|
          if is_enabled
            gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
          else
            # We don't use #page because if wiki_find_page feature is enabled, we would
            # get a page without formatted_data.
            gollum_find_page(title: title, dir: dir, version: version)&.formatted_data
          end
        end
      end

143 144
      private

145 146 147 148 149
      # options:
      #  :page     - The Integer page number.
      #  :per_page - The number of items per page.
      #  :limit    - Total number of items to return.
      def commits_from_page(gollum_page, options = {})
150 151 152 153
        unless options[:limit]
          options[:offset] = ([1, options.delete(:page).to_i].max - 1) * Gollum::Page.per_page
          options[:limit] = (options.delete(:per_page) || Gollum::Page.per_page).to_i
        end
154 155 156

        @repository.log(ref: gollum_page.last_version.id,
                        path: gollum_page.path,
157 158
                        limit: options[:limit],
                        offset: options[:offset])
159 160
      end

161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
      def gollum_wiki
        @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
      end

      def gollum_page_by_path(page_path)
        page_name = Gollum::Page.canonicalize_filename(page_path)
        page_dir = File.split(page_path).first

        gollum_wiki.paged(page_name, page_dir)
      end

      def new_page(gollum_page)
        Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id))
      end

      def new_version(gollum_page, commit_id)
177 178 179 180 181 182 183 184 185 186 187
        Gitlab::Git::WikiPageVersion.new(version(commit_id), gollum_page&.format)
      end

      def version(commit_id)
        commit_find_proc = -> { Gitlab::Git::Commit.find(@repository, commit_id) }

        if RequestStore.active?
          RequestStore.fetch([:wiki_version_commit, commit_id]) { commit_find_proc.call }
        else
          commit_find_proc.call
        end
188 189 190 191 192 193 194
      end

      def assert_type!(object, klass)
        unless object.is_a?(klass)
          raise ArgumentError, "expected a #{klass}, got #{object.inspect}"
        end
      end
195 196 197 198 199 200 201 202 203

      def gitaly_wiki_client
        @gitaly_wiki_client ||= Gitlab::GitalyClient::WikiService.new(@repository)
      end

      def gollum_write_page(name, format, content, commit_details)
        assert_type!(format, Symbol)
        assert_type!(commit_details, CommitDetails)

204 205 206 207
        filename = File.basename(name)
        dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir

        gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
208 209 210 211 212 213

        nil
      rescue Gollum::DuplicatePageError => e
        raise Gitlab::Git::Wiki::DuplicatePageError, e.message
      end

214 215 216 217 218 219 220
      def gollum_delete_page(page_path, commit_details)
        assert_type!(commit_details, CommitDetails)

        gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
        nil
      end

221 222 223 224
      def gollum_update_page(page_path, title, format, content, commit_details)
        assert_type!(format, Symbol)
        assert_type!(commit_details, CommitDetails)

225 226 227 228 229 230 231 232 233
        page = gollum_page_by_path(page_path)
        committer = Gollum::Committer.new(page.wiki, commit_details.to_h)

        # Instead of performing two renames if the title has changed,
        # the update_page will only update the format and content and
        # the rename_page will do anything related to moving/renaming
        gollum_wiki.update_page(page, page.name, format, content, committer: committer)
        gollum_wiki.rename_page(page, title, committer: committer)
        committer.commit
234 235 236
        nil
      end

237 238 239 240 241 242 243 244 245 246 247
      def gollum_find_page(title:, version: nil, dir: nil)
        if version
          version = Gitlab::Git::Commit.find(@repository, version).id
        end

        gollum_page = gollum_wiki.page(title, version, dir)
        return unless gollum_page

        new_page(gollum_page)
      end

248 249 250 251 252 253 254 255
      def gollum_find_file(name, version)
        version ||= self.class.default_ref
        gollum_file = gollum_wiki.file(name, version)
        return unless gollum_file

        Gitlab::Git::WikiFile.new(gollum_file)
      end

256 257
      def gollum_get_all_pages(limit: nil)
        gollum_wiki.pages(limit: limit).map { |gollum_page| new_page(gollum_page) }
258 259
      end

260 261 262
      def gitaly_write_page(name, format, content, commit_details)
        gitaly_wiki_client.write_page(name, format, content, commit_details)
      end
263

264 265 266 267
      def gitaly_update_page(page_path, title, format, content, commit_details)
        gitaly_wiki_client.update_page(page_path, title, format, content, commit_details)
      end

268 269 270
      def gitaly_delete_page(page_path, commit_details)
        gitaly_wiki_client.delete_page(page_path, commit_details)
      end
271 272 273 274 275 276 277

      def gitaly_find_page(title:, version: nil, dir: nil)
        wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir)
        return unless wiki_page

        Gitlab::Git::WikiPage.new(wiki_page, version)
      end
278 279 280 281 282 283 284

      def gitaly_find_file(name, version)
        wiki_file = gitaly_wiki_client.find_file(name, version)
        return unless wiki_file

        Gitlab::Git::WikiFile.new(wiki_file)
      end
285 286 287 288 289 290

      def gitaly_get_all_pages
        gitaly_wiki_client.get_all_pages.map do |wiki_page, version|
          Gitlab::Git::WikiPage.new(wiki_page, version)
        end
      end
291 292 293
    end
  end
end