Class: Wiki

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Naming, Gitlab::Utils::Override
Includes:
Gitlab::Utils::StrongMemoize, GlobalID::Identification, HasRepository, Repositories::CanHousekeepRepository
Defined in:
app/models/wiki.rb

Direct Known Subclasses

ProjectWiki

Constant Summary collapse

MARKUPS =

rubocop:disable Style/MultilineIfModifier

{ # rubocop:disable Style/MultilineIfModifier
  markdown: {
    name: 'Markdown',
    default_extension: :md,
    extension_regex: Regexp.new('md|mkdn?|mdown|markdown', 'i'),
    created_by_user: true
  },
  rdoc: {
    name: 'RDoc',
    default_extension: :rdoc,
    extension_regex: Regexp.new('rdoc', 'i'),
    created_by_user: true
  },
  asciidoc: {
    name: 'AsciiDoc',
    default_extension: :asciidoc,
    extension_regex: Regexp.new('adoc|asciidoc', 'i'),
    created_by_user: true
  },
  org: {
    name: 'Org',
    default_extension: :org,
    extension_regex: Regexp.new('org', 'i'),
    created_by_user: true
  },
  textile: {
    name: 'Textile',
    default_extension: :textile,
    extension_regex: Regexp.new('textile', 'i')
  },
  creole: {
    name: 'Creole',
    default_extension: :creole,
    extension_regex: Regexp.new('creole', 'i')
  },
  rest: {
    name: 'reStructuredText',
    default_extension: :rst,
    extension_regex: Regexp.new('re?st(\.txt)?', 'i')
  },
  mediawiki: {
    name: 'MediaWiki',
    default_extension: :mediawiki,
    extension_regex: Regexp.new('(media)?wiki', 'i')
  },
  pod: {
    name: 'Pod',
    default_extension: :pod,
    extension_regex: Regexp.new('pod', 'i')
  },
  plaintext: {
    name: 'Plain Text',
    default_extension: :txt,
    extension_regex: Regexp.new('txt', 'i')
  }
}.freeze
VALID_USER_MARKUPS =
MARKUPS.select { |_, v| v[:created_by_user] }.freeze
ALLOWED_EXTENSIONS_REGEX =
Regexp.union(MARKUPS.map { |key, value| value[:extension_regex] }).freeze
CouldNotCreateWikiError =
Class.new(StandardError)
HOMEPAGE =
'home'
'_sidebar'
TITLE_ORDER =
'title'
CREATED_AT_ORDER =
'created_at'
DIRECTION_DESC =
'desc'
DIRECTION_ASC =
'asc'

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Utils::Override

extended, extensions, included, method_added, override, prepended, queue_verification, verify!

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods included from Repositories::CanHousekeepRepository

#increment_pushes_since_gc, #pushes_since_gc, #reset_pushes_since_gc

Methods included from HasRepository

#after_change_head_branch_does_not_exist, #after_repository_change_head, #commit, #commit_by, #commits_by, #default_branch_from_group_preferences, #default_branch_from_preferences, #empty_repo?, #http_url_to_repo, #lfs_enabled?, #lfs_http_url_to_repo, #reload_default_branch, #repo_exists?, #repository_exists?, #repository_size_checker, #root_ref?, #ssh_url_to_repo, #storage, #url_to_repo, #valid_repo?, #web_url

Methods included from Gitlab::ShellAdapter

#gitlab_shell

Methods included from Referable

#referable_inspect, #reference_link_text, #to_reference, #to_reference_base

Constructor Details

#initialize(container, user = nil) ⇒ Wiki

Returns a new instance of Wiki.

Raises:

  • (ArgumentError)

108
109
110
111
112
113
# File 'app/models/wiki.rb', line 108

def initialize(container, user = nil)
  raise ArgumentError, "user must be a User, got #{user.class}" if user && !user.is_a?(User)

  @container = container
  @user = user
end

Class Attribute Details

.container_classObject

Returns the value of attribute container_class.


96
97
98
# File 'app/models/wiki.rb', line 96

def container_class
  @container_class
end

Instance Attribute Details

#containerObject (readonly)

Returns the value of attribute container.


85
86
87
# File 'app/models/wiki.rb', line 85

def container
  @container
end

#error_messageObject (readonly)

Returns a string describing what went wrong after an operation fails.


89
90
91
# File 'app/models/wiki.rb', line 89

def error_message
  @error_message
end

#userObject (readonly)

Returns the value of attribute user.


85
86
87
# File 'app/models/wiki.rb', line 85

def user
  @user
end

Class Method Details

.find_by_id(container_id) ⇒ Object

This is needed to support repository lookup through Gitlab::GlRepository::Identifier


103
104
105
# File 'app/models/wiki.rb', line 103

def find_by_id(container_id)
  container_class.find_by_id(container_id)&.wiki
end

.for_container(container, user = nil) ⇒ Object


98
99
100
# File 'app/models/wiki.rb', line 98

def for_container(container, user = nil)
  "#{container.class.name}Wiki".constantize.new(container, user)
end

Instance Method Details

#==(other) ⇒ Object


115
116
117
# File 'app/models/wiki.rb', line 115

def ==(other)
  other.is_a?(self.class) && container == other.container
end

#after_post_receiveObject

Callbacks for background processing after wiki changes. These will be executed after any change to the wiki repository.


369
370
# File 'app/models/wiki.rb', line 369

def after_post_receive
end

#after_wiki_activityObject

Callbacks for synchronous processing after wiki changes. These will be executed after any change made through GitLab itself (web UI and API), but not for Git pushes.


364
365
# File 'app/models/wiki.rb', line 364

def after_wiki_activity
end

#capture_git_error(action, &block) ⇒ Object


381
382
383
384
385
386
387
388
389
390
391
392
# File 'app/models/wiki.rb', line 381

def capture_git_error(action, &block)
  yield block
rescue Gitlab::Git::Index::IndexError,
       Gitlab::Git::CommitError,
       Gitlab::Git::PreReceiveError,
       Gitlab::Git::CommandError,
       ArgumentError => error

  Gitlab::ErrorTracking.log_exception(error, action: action, wiki_id: id)

  false
end

#cleanupObject


377
378
379
# File 'app/models/wiki.rb', line 377

def cleanup
  @repository = nil
end

#create_page(title, content, format = :markdown, message = nil) ⇒ Object


229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'app/models/wiki.rb', line 229

def create_page(title, content, format = :markdown, message = nil)
  if Feature.enabled?(:gitaly_replace_wiki_create_page, container, type: :undefined)
    with_valid_format(format) do |default_extension|
      if file_exists_by_regex?(title)
        raise_duplicate_page_error!
      end

      capture_git_error(:created) do
        create_wiki_repository unless repository_exists?
        sanitized_path = sluggified_full_path(title, default_extension)
        repository.create_file(user, sanitized_path, content, **multi_commit_options(:created, message, title))
        repository.expire_status_cache if repository.empty?
        after_wiki_activity

        true
      rescue Gitlab::Git::Index::IndexError
        raise_duplicate_page_error!
      end
    end
  else
    commit = commit_details(:created, message, title)

    wiki.write_page(title, format.to_sym, content, commit)
    repository.expire_status_cache if repository.empty?
    after_wiki_activity

    true
  end
rescue Gitlab::Git::Wiki::DuplicatePageError => e
  @error_message = _("Duplicate page: %{error_message}" % { error_message: e.message })

  false
end

#create_wiki_repositoryObject


145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'app/models/wiki.rb', line 145

def create_wiki_repository
  repository.create_if_not_exists(default_branch)

  raise CouldNotCreateWikiError unless repository_exists?
rescue StandardError => err
  Gitlab::ErrorTracking.track_exception(err, wiki: {
    container_type: container.class.name,
    container_id: container.id,
    full_path: full_path,
    disk_path: disk_path
  })

  raise CouldNotCreateWikiError
end

#default_branchObject


353
354
355
# File 'app/models/wiki.rb', line 353

def default_branch
  super || Gitlab::Git::Wiki.default_ref(container)
end

#delete_page(page, message = nil) ⇒ Object


297
298
299
300
301
302
303
304
305
306
307
# File 'app/models/wiki.rb', line 297

def delete_page(page, message = nil)
  return unless page

  capture_git_error(:deleted) do
    repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))

    after_wiki_activity

    true
  end
end

#empty?Boolean

Returns:

  • (Boolean)

164
165
166
# File 'app/models/wiki.rb', line 164

def empty?
  !repository_exists? || list_pages(limit: 1).empty?
end

#ensure_repositoryObject


317
318
319
# File 'app/models/wiki.rb', line 317

def ensure_repository
  raise CouldNotCreateWikiError unless wiki.repository_exists?
end

#exists?Boolean

Returns:

  • (Boolean)

168
169
170
# File 'app/models/wiki.rb', line 168

def exists?
  !empty?
end

#find_file(name, version = 'HEAD', load_content: true) ⇒ Object


220
221
222
223
224
225
226
227
# File 'app/models/wiki.rb', line 220

def find_file(name, version = 'HEAD', load_content: true)
  data_limit = load_content ? -1 : 0
  blobs = repository.blobs_at([[version, name]], blob_size_limit: data_limit)

  return if blobs.empty?

  Gitlab::Git::WikiFile.new(blobs.first)
end

#find_page(title, version = nil, load_content: true) ⇒ Object

Finds a page within the repository based on a title or slug.

title - The human readable or parameterized title of

the page.

Returns an initialized WikiPage instance or nil


208
209
210
211
212
213
214
# File 'app/models/wiki.rb', line 208

def find_page(title, version = nil, load_content: true)
  page_title, page_dir = page_title_and_dir(title)

  if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content)
    WikiPage.new(self, page)
  end
end

#find_sidebar(version = nil) ⇒ Object


216
217
218
# File 'app/models/wiki.rb', line 216

def find_sidebar(version = nil)
  find_page(SIDEBAR, version)
end

#full_pathObject Also known as: path_with_namespace


345
346
347
# File 'app/models/wiki.rb', line 345

def full_path
  container.full_path + '.wiki'
end

#git_garbage_collect_worker_klassObject


373
374
375
# File 'app/models/wiki.rb', line 373

def git_garbage_collect_worker_klass
  Wikis::GitGarbageCollectWorker
end

#has_home_page?Boolean

Returns:

  • (Boolean)

160
161
162
# File 'app/models/wiki.rb', line 160

def has_home_page?
  !!find_page(HOMEPAGE)
end

#hashed_storage?Boolean

Returns:

  • (Boolean)

Raises:

  • (NotImplementedError)

340
341
342
# File 'app/models/wiki.rb', line 340

def hashed_storage?
  raise NotImplementedError
end

#hook_attrsObject


321
322
323
324
325
326
327
328
329
# File 'app/models/wiki.rb', line 321

def hook_attrs
  {
    web_url: web_url,
    git_ssh_url: ssh_url_to_repo,
    git_http_url: http_url_to_repo,
    path_with_namespace: full_path,
    default_branch: default_branch
  }
end

#idObject

This is needed in:

  • Storage::Hashed

  • Gitlab::GlRepository::RepoType#identifier_for_container

We also need an `#id` to support `build_stubbed` in tests, where the value doesn't matter.

NOTE: Wikis don't have a DB record, so this ID can be the same for two wikis in different containers and should not be expected to be unique. Use `to_global_id` instead if you need a unique ID.


129
130
131
# File 'app/models/wiki.rb', line 129

def id
  container.id
end

#list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false) ⇒ Object

Lists wiki pages of the repository.

limit - max number of pages returned by the method. sort - criterion by which the pages are sorted. direction - order of the sorted pages. load_content - option, which specifies whether the content inside the page

will be loaded.

Returns an Array of GitLab WikiPage instances or an empty Array if this Wiki has no pages.


182
183
184
185
186
187
188
189
190
191
# File 'app/models/wiki.rb', line 182

def list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false)
  wiki.list_pages(
    limit: limit,
    sort: sort,
    direction_desc: direction == DIRECTION_DESC,
    load_content: load_content
  ).map do |page|
    WikiPage.new(self, page)
  end
end

#page_title_and_dir(title) ⇒ Object


309
310
311
312
313
314
315
# File 'app/models/wiki.rb', line 309

def page_title_and_dir(title)
  return unless title

  title_array = title.split("/")
  title = title_array.pop
  [title, title_array.join("/")]
end

#pathObject


133
134
135
# File 'app/models/wiki.rb', line 133

def path
  container.path + '.wiki'
end

#repositoryObject


332
333
334
# File 'app/models/wiki.rb', line 332

def repository
  @repository ||= Gitlab::GlRepository::WIKI.repository_for(self)
end

#repository_storageObject

Raises:

  • (NotImplementedError)

336
337
338
# File 'app/models/wiki.rb', line 336

def repository_storage
  raise NotImplementedError
end

193
194
195
196
197
198
199
# File 'app/models/wiki.rb', line 193

def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options)
  pages = list_pages(**options.merge(limit: limit + 1))
  limited = pages.size > limit
  pages = pages.first(limit) if limited

  [WikiDirectory.group_pages(pages), limited]
end

#update_page(page, content:, title: nil, format: :markdown, message: nil) ⇒ Object


263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'app/models/wiki.rb', line 263

def update_page(page, content:, title: nil, format: :markdown, message: nil)
  if Feature.enabled?(:gitaly_replace_wiki_update_page, container, type: :undefined)
    with_valid_format(format) do |default_extension|
      title = title.presence || Pathname(page.path).sub_ext('').to_s

      # If the format is the same we keep the former extension. This check is for formats
      # that can have more than one extension like Markdown (.md, .markdown)
      # If we don't do this we will override the existing extension.
      extension = page.format != format.to_sym ? default_extension : File.extname(page.path).downcase[1..]

      capture_git_error(:updated) do
        repository.update_file(
          user,
          sluggified_full_path(title, extension),
          content,
          previous_path: page.path,
          **multi_commit_options(:updated, message, title))

        after_wiki_activity

        true
      end
    end
  else
    commit = commit_details(:updated, message, page.title)

    wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)

    after_wiki_activity

    true
  end
end

#wikiObject

Returns the Gitlab::Git::Wiki object.


138
139
140
141
142
143
# File 'app/models/wiki.rb', line 138

def wiki
  strong_memoize(:wiki) do
    create_wiki_repository
    Gitlab::Git::Wiki.new(repository.raw)
  end
end

#wiki_base_pathObject


357
358
359
# File 'app/models/wiki.rb', line 357

def wiki_base_path
  web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
end