Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.1 on 2022-01-26 at 16:00 UTC+1 (a few minutes of down time)

projects_helper.rb 16 KB
Newer Older
randx's avatar
randx committed
1
module ProjectsHelper
2
  def link_to_project(project)
Josh Frye's avatar
Escape!    
Josh Frye committed
3
    link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
4
      title = content_tag(:span, project.name, class: 'project-name')
5
6

      if project.namespace
7
        namespace = content_tag(:span, "#{project.namespace.human_name} / ", class: 'namespace-name')
8
9
10
11
12
        title = namespace + title
      end

      title
    end
13
  end
randx's avatar
randx committed
14

Jacob Schatz's avatar
Jacob Schatz committed
15
  def link_to_member_avatar(author, opts = {})
16
    default_opts = { size: 16, lazy_load: false }
Jacob Schatz's avatar
Jacob Schatz committed
17
    opts = default_opts.merge(opts)
18
19

    classes = %W[avatar avatar-inline s#{opts[:size]}]
Maxim Rydkin's avatar
Maxim Rydkin committed
20
21
    classes << opts[:avatar_class] if opts[:avatar_class]

22
    avatar = avatar_icon_for_user(author, opts[:size])
23
24
25
    src = opts[:lazy_load] ? nil : avatar

    image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
Jacob Schatz's avatar
Jacob Schatz committed
26
27
  end

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  def author_content_tag(author, opts = {})
    default_opts = { author_class: 'author', tooltip: false, by_username: false }
    opts = default_opts.merge(opts)

    has_tooltip = !opts[:by_username] && opts[:tooltip]

    username = opts[:by_username] ? author.to_reference : author.name
    name_tag_options = { class: [opts[:author_class]] }

    if has_tooltip
      name_tag_options[:title] = author.to_reference
      name_tag_options[:data] = { placement: 'top' }
      name_tag_options[:class] << 'has-tooltip'
    end

    content_tag(:span, sanitize(username), name_tag_options)
  end

Phil Hughes's avatar
Phil Hughes committed
46
  def link_to_member(project, author, opts = {}, &block)
47
    default_opts = { avatar: true, name: true, title: ":name" }
48
49
    opts = default_opts.merge(opts)

50
51
    return "(deleted)" unless author

52
    author_html = ""
53

54
    # Build avatar image tag
55
    author_html << link_to_member_avatar(author, opts) if opts[:avatar]
56

57
    # Build name span tag
58
    author_html << author_content_tag(author, opts) if opts[:name]
59

Phil Hughes's avatar
Phil Hughes committed
60
61
    author_html << capture(&block) if block

62
    author_html = author_html.html_safe
63

64
    if opts[:name]
65
      link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
66
    else
67
      title = opts[:title].sub(":name", sanitize(author.name))
68
      link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' }).html_safe
69
    end
randx's avatar
randx committed
70
  end
71

72
  def project_title(project)
73
74
    namespace_link =
      if project.group
Phil Hughes's avatar
Phil Hughes committed
75
        group_title(project.group, nil, nil)
76
77
78
      else
        owner = project.namespace.owner
        link_to(simple_sanitize(owner.name), user_path(owner))
79
      end
80

Phil Hughes's avatar
Phil Hughes committed
81
    project_link = link_to project_path(project) do
Phil Hughes's avatar
Phil Hughes committed
82
      output =
83
        if project.avatar_url && !Rails.env.test?
84
          project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15)
Phil Hughes's avatar
Phil Hughes committed
85
86
87
88
        else
          ""
        end

Phil Hughes's avatar
Phil Hughes committed
89
      output << content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")
Phil Hughes's avatar
Phil Hughes committed
90
91
      output.html_safe
    end
92

Phil Hughes's avatar
Phil Hughes committed
93
94
    namespace_link = breadcrumb_list_item(namespace_link) unless project.group
    project_link = breadcrumb_list_item project_link
Phil Hughes's avatar
Phil Hughes committed
95

Phil Hughes's avatar
Phil Hughes committed
96
    "#{namespace_link} #{project_link}".html_safe
97
  end
98
99

  def remove_project_message(project)
100
101
    _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
      { project_full_name: project.full_name }
102
  end
103

104
  def transfer_project_message(project)
105
106
    _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") %
      { project_full_name: project.full_name }
107
108
  end

109
  def remove_fork_project_message(project)
110
    _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") %
111
112
113
114
115
116
117
118
119
      { forked_from_project: fork_source_name(project) }
  end

  def fork_source_name(project)
    if @project.fork_source
      @project.fork_source.full_name
    else
      @project.fork_network&.deleted_root_project_name
    end
120
121
  end

122
123
124
125
  def project_nav_tabs
    @nav_tabs ||= get_project_nav_tabs(@project, current_user)
  end

126
127
128
129
130
131
  def project_search_tabs?(tab)
    abilities = Array(search_tab_ability_map[tab])

    abilities.any? { |ability| can?(current_user, ability, @project) }
  end

132
133
134
135
  def project_nav_tab?(name)
    project_nav_tabs.include? name
  end

Douwe Maan's avatar
Douwe Maan committed
136
  def project_for_deploy_key(deploy_key)
137
    if deploy_key.has_access_to?(@project)
Douwe Maan's avatar
Douwe Maan committed
138
139
      @project
    else
Lin Jen-Shin's avatar
Lin Jen-Shin committed
140
141
142
      deploy_key.projects.find do |project|
        can?(current_user, :read_project, project)
      end
Douwe Maan's avatar
Douwe Maan committed
143
144
145
    end
  end

Valery Sizov's avatar
Valery Sizov committed
146
147
148
  def can_change_visibility_level?(project, current_user)
    return false unless can?(current_user, :change_visibility_level, project)

149
150
    if project.fork_source
      project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE
Valery Sizov's avatar
Valery Sizov committed
151
152
153
154
155
    else
      true
    end
  end

156
  def last_push_event
157
    current_user&.recent_push(@project)
158
159
  end

Felipe Artur's avatar
Felipe Artur committed
160
161
162
163
  def project_feature_access_select(field)
    # Don't show option "everyone with access" if project is private
    options = project_feature_options

164
165
    level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend

Felipe Artur's avatar
Felipe Artur committed
166
    if @project.private?
167
168
      disabled_option = ProjectFeature::ENABLED
      highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
Felipe Artur's avatar
Felipe Artur committed
169
170
    end

171
    options = options_for_select(
172
      options.invert,
173
      selected: highest_available_option || level,
174
175
      disabled: disabled_option
    )
176

177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
    content_tag :div, class: "select-wrapper" do
      concat(
        content_tag(
          :select,
          options,
          name: "project[project_feature_attributes][#{field}]",
          id: "project_project_feature_attributes_#{field}",
          class: "pull-right form-control select-control #{repo_children_classes(field)} ",
          data: { field: field }
        )
      )
      concat(
        icon('chevron-down')
      )
    end.html_safe
Felipe Artur's avatar
Felipe Artur committed
192
193
  end

194
  def link_to_autodeploy_doc
195
    link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank'
196
197
  end

Valery Sizov's avatar
Valery Sizov committed
198
  def autodeploy_flash_notice(branch_name)
199
200
201
    translation = _("Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}") %
      { branch_name: truncate(sanitize(branch_name)), link_to_autodeploy_doc: link_to_autodeploy_doc }
    translation.html_safe
Valery Sizov's avatar
Valery Sizov committed
202
203
  end

204
  def project_list_cache_key(project)
205
    key = [
206
      project.route.cache_key,
207
208
209
      project.cache_key,
      controller.controller_name,
      controller.action_name,
210
      Gitlab::CurrentSettings.cache_key,
211
      "cross-project:#{can?(current_user, :read_cross_project)}",
212
      'v2.5'
213
214
    ]

215
216
217
218
219
    key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?

    key
  end

220
  def load_pipeline_status(projects)
221
222
    Gitlab::Cache::Ci::ProjectPipelineStatus
      .load_in_batch_for_projects(projects)
223
224
  end

225
226
227
228
229
230
  def show_no_ssh_key_message?
    cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
  end

  def show_no_password_message?
    cookies[:hide_no_password_message].blank? && !current_user.hide_no_password &&
231
      current_user.require_extra_setup_for_git_auth?
232
233
234
  end

  def link_to_set_password
235
    if current_user.require_password_creation_for_git?
236
237
238
239
240
241
      link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path
    else
      link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path
    end
  end

242
243
244
245
246
247
248
249
250
  # Returns true if any projects are present.
  #
  # If the relation has a LIMIT applied we'll cast the relation to an Array
  # since repeated any? checks would otherwise result in multiple COUNT queries
  # being executed.
  #
  # If no limit is applied we'll just issue a COUNT since the result set could
  # be too large to load into memory.
  def any_projects?(projects)
251
252
    return projects.any? if projects.is_a?(Array)

253
254
255
256
257
258
259
    if projects.limit_value
      projects.to_a.any?
    else
      projects.except(:offset).any?
    end
  end

260
261
  def show_projects?(projects, params)
    !!(params[:personal] || params[:name] || any_projects?(projects))
262
263
  end

264
265
266
267
268
269
270
271
272
273
274
  def push_to_create_project_command(user = current_user)
    repository_url =
      if Gitlab::CurrentSettings.current_application_settings.enabled_git_access_protocol == 'http'
        user_url(user)
      else
        Gitlab.config.gitlab_shell.ssh_path_prefix + user.username
      end

    "git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)"
  end

275
276
  private

277
278
279
280
281
282
283
284
285
286
  def repo_children_classes(field)
    needs_repo_check = [:merge_requests_access_level, :builds_access_level]
    return unless needs_repo_check.include?(field)

    classes = "project-repo-select js-repo-select"
    classes << " disabled" unless @project.feature_available?(:repository, current_user)

    classes
  end

287
  def get_project_nav_tabs(project, current_user)
288
    nav_tabs = [:home]
289

290
    if !project.empty_repo? && can?(current_user, :download_code, project)
291
      nav_tabs << [:files, :commits, :network, :graphs, :forks]
292
293
    end

294
    if project.repo_exists? && can?(current_user, :read_merge_request, project)
295
296
297
      nav_tabs << :merge_requests
    end

298
    if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
299
      nav_tabs << :container_registry
Kamil Trzcinski's avatar
Kamil Trzcinski committed
300
301
    end

302
303
304
305
    if project.builds_enabled? && can?(current_user, :read_pipeline, project)
      nav_tabs << :pipelines
    end

306
307
308
309
    if project.external_issue_tracker
      nav_tabs << :external_issue_tracker
    end

310
311
312
313
314
315
316
317
318
319
320
    tab_ability_map.each do |tab, ability|
      if can?(current_user, ability, project)
        nav_tabs << tab
      end
    end

    nav_tabs.flatten
  end

  def tab_ability_map
    {
321
322
323
324
325
      environments:     :read_environment,
      milestones:       :read_milestone,
      snippets:         :read_project_snippet,
      settings:         :admin_project,
      builds:           :read_build,
326
      clusters:         :read_cluster,
327
328
329
330
      labels:           :read_label,
      issues:           :read_issue,
      project_members:  :read_project_member,
      wiki:             :read_wiki
331
    }
332
  end
333

334
335
336
337
338
339
340
  def search_tab_ability_map
    @search_tab_ability_map ||= tab_ability_map.merge(
      blobs:          :download_code,
      commits:        :download_code,
      merge_requests: :read_merge_request,
      notes:          [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
    )
341
  end
342

343
344
  def project_lfs_status(project)
    if project.lfs_enabled?
345
      content_tag(:span, class: 'lfs-enabled') do
346
        s_('LFSStatus|Enabled')
347
348
      end
    else
349
      content_tag(:span, class: 'lfs-disabled') do
350
        s_('LFSStatus|Disabled')
351
352
353
354
      end
    end
  end

355
356
  def git_user_name
    if current_user
357
      current_user.name.gsub('"', '\"')
358
    else
359
      _("Your name")
360
361
362
363
364
365
366
367
368
369
    end
  end

  def git_user_email
    if current_user
      current_user.email
    else
      "your@email.com"
    end
  end
370

371
  def default_url_to_repo(project = @project)
372
373
    case default_clone_protocol
    when 'ssh'
374
375
      project.ssh_url_to_repo
    else
376
      project.http_url_to_repo
377
    end
378
  end
379

380
  def default_clone_protocol
381
382
    if allowed_protocols_present?
      enabled_protocol
383
    else
384
385
386
387
388
      if !current_user || current_user.require_ssh_key?
        gitlab_config.protocol
      else
        'ssh'
      end
389
    end
390
  end
391
392
393

  def project_last_activity(project)
    if project.last_activity_at
394
      time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
395
    else
396
      s_("ProjectLastActivity|Never")
397
398
    end
  end
399

400
401
402
403
  def koding_project_url(project = nil, branch = nil, sha = nil)
    if project
      import_path = "/Home/Stacks/import"

404
      repo = project.full_path
405
406
407
408
409
      branch ||= project.default_branch
      sha ||= project.commit.short_id

      path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"

410
      return URI.join(Gitlab::CurrentSettings.koding_url, path).to_s
411
412
    end

413
    Gitlab::CurrentSettings.koding_url
414
415
  end

416
417
  def project_wiki_path_with_version(proj, page, version, is_newest)
    url_params = is_newest ? {} : { version_id: version }
418
    project_wiki_path(proj, page, url_params)
419
  end
420

Valery Sizov's avatar
Valery Sizov committed
421
422
423
424
425
426
427
428
429
430
  def project_status_css_class(status)
    case status
    when "started"
      "active"
    when "failed"
      "danger"
    when "finished"
      "success"
    end
  end
431

432
  def readme_cache_key
433
    sha = @project.commit.try(:sha) || 'nil'
434
    [@project.full_path, sha, "readme"].join('-')
435
  end
436

437
438
439
  def current_ref
    @ref || @repository.try(:root_ref)
  end
440

441
  def sanitize_repo_path(project, message)
442
443
    return '' unless message.present?

444
445
446
447
    exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
    filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")

    filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
448
  end
Felipe Artur's avatar
Felipe Artur committed
449
450
451

  def project_feature_options
    {
452
453
454
      ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'),
      ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'),
      ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access')
Felipe Artur's avatar
Felipe Artur committed
455
456
    }
  end
457
458
459
460

  def project_child_container_class(view_path)
    view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
  end
461
462
463
464

  def project_issues(project)
    IssuesFinder.new(current_user, project_id: project.id).execute
  end
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
465
466

  def visibility_select_options(project, selected_level)
467
468
469
470
    level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options|
      next if restricted_levels.include?(level)

      level_options << [
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
471
472
473
474
475
        visibility_level_label(level),
        { data: { description: visibility_level_description(level, project) } },
        level
      ]
    end
476
477
478
479
480
481
482

    options_for_select(level_options, selected_level)
  end

  def restricted_levels
    return [] if current_user.admin?

483
    Gitlab::CurrentSettings.restricted_visibility_levels || []
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
484
  end
485

486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
  def project_permissions_settings(project)
    feature = project.project_feature
    {
      visibilityLevel: project.visibility_level,
      requestAccessEnabled: !!project.request_access_enabled,
      issuesAccessLevel: feature.issues_access_level,
      repositoryAccessLevel: feature.repository_access_level,
      mergeRequestsAccessLevel: feature.merge_requests_access_level,
      buildsAccessLevel: feature.builds_access_level,
      wikiAccessLevel: feature.wiki_access_level,
      snippetsAccessLevel: feature.snippets_access_level,
      containerRegistryEnabled: !!project.container_registry_enabled,
      lfsEnabled: !!project.lfs_enabled
    }
  end

  def project_permissions_panel_data(project)
    data = {
      currentSettings: project_permissions_settings(project),
      canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
      allowedVisibilityOptions: project_allowed_visibility_levels(project),
      visibilityHelpPath: help_page_path('public_access/public_access'),
      registryAvailable: Gitlab.config.registry.enabled,
      registryHelpPath: help_page_path('user/project/container_registry'),
      lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
      lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
    }

    data.to_json.html_safe
  end

  def project_allowed_visibility_levels(project)
    Gitlab::VisibilityLevel.values.select do |level|
      project.visibility_level_allowed?(level) && !restricted_levels.include?(level)
    end
  end

523
524
525
526
527
528
529
  def find_file_path
    return unless @project && !@project.empty_repo?

    ref = @ref || @project.repository.root_ref

    project_find_file_path(@project, ref)
  end
530
531
532
533

  def can_show_last_commit_in_list?(project)
    can?(current_user, :read_cross_project) && project.commit
  end
Rob Watson's avatar
Rob Watson committed
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551

  def pages_https_only_disabled?
    !@project.pages_domains.all?(&:https?)
  end

  def pages_https_only_title
    return unless pages_https_only_disabled?

    "You must enable HTTPS for all your domains first"
  end

  def pages_https_only_label_class
    if pages_https_only_disabled?
      "list-label disabled"
    else
      "list-label"
    end
  end
randx's avatar
randx committed
552
end