gitlab-org--gitlab-foss/app/helpers/projects_helper.rb

553 lines
16 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2012-09-07 16:57:13 +00:00
module ProjectsHelper
def link_to_project(project)
2016-03-11 20:53:27 +00:00
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
2014-04-11 22:00:58 +00:00
title = content_tag(:span, project.name, class: 'project-name')
2012-12-09 08:56:15 +00:00
if project.namespace
2013-10-10 07:53:49 +00:00
namespace = content_tag(:span, "#{project.namespace.human_name} / ", class: 'namespace-name')
2012-12-09 08:56:15 +00:00
title = namespace + title
end
title
end
end
2012-10-29 21:45:11 +00:00
2016-01-29 01:36:48 +00:00
def link_to_member_avatar(author, opts = {})
default_opts = { size: 16, lazy_load: false }
2016-01-29 01:36:48 +00:00
opts = default_opts.merge(opts)
classes = %W[avatar avatar-inline s#{opts[:size]}]
2017-09-05 20:17:53 +00:00
classes << opts[:avatar_class] if opts[:avatar_class]
avatar = avatar_icon_for_user(author, opts[:size])
src = opts[:lazy_load] ? nil : avatar
image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
2016-01-29 01:36:48 +00:00
end
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
# NOTE: ActionView::Helpers::TagHelper#content_tag HTML escapes username
content_tag(:span, username, name_tag_options)
end
def link_to_member(project, author, opts = {}, &block)
default_opts = { avatar: true, name: true, title: ":name" }
opts = default_opts.merge(opts)
return "(deleted)" unless author
author_html = []
# Build avatar image tag
author_html << link_to_member_avatar(author, opts) if opts[:avatar]
# Build name span tag
author_html << author_content_tag(author, opts) if opts[:name]
author_html << capture(&block) if block
author_html = author_html.join.html_safe
if opts[:name]
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
else
title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author-link has-tooltip", title: title, data: { container: 'body' }).html_safe
end
2012-10-29 21:45:11 +00:00
end
2012-12-12 10:02:29 +00:00
def project_title(project)
namespace_link =
if project.group
group_title(project.group, nil, nil)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
end
2017-09-06 11:19:03 +00:00
project_link = link_to project_path(project) do
icon = project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) if project.avatar_url && !Rails.env.test?
[icon, content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")].join.html_safe
2017-06-30 16:52:11 +00:00
end
2017-09-06 11:19:03 +00:00
namespace_link = breadcrumb_list_item(namespace_link) unless project.group
project_link = breadcrumb_list_item project_link
2017-09-06 11:19:03 +00:00
"#{namespace_link} #{project_link}".html_safe
2012-12-12 10:02:29 +00:00
end
2013-05-20 11:22:18 +00:00
def remove_project_message(project)
_("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_full_name: project.full_name }
2013-05-20 11:22:18 +00:00
end
2013-06-13 19:58:27 +00:00
def transfer_project_message(project)
_("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") %
{ project_full_name: project.full_name }
end
def remove_fork_project_message(project)
_("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") %
{ 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
end
2013-06-13 19:58:27 +00:00
def project_nav_tabs
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
def project_search_tabs?(tab)
abilities = Array(search_tab_ability_map[tab])
abilities.any? { |ability| can?(current_user, ability, @project) }
end
2013-06-13 19:58:27 +00:00
def project_nav_tab?(name)
project_nav_tabs.include? name
end
2015-04-03 10:22:44 +00:00
def project_for_deploy_key(deploy_key)
if deploy_key.has_access_to?(@project)
2015-04-03 10:22:44 +00:00
@project
else
2016-11-17 19:56:35 +00:00
deploy_key.projects.find do |project|
can?(current_user, :read_project, project)
end
2015-04-03 10:22:44 +00:00
end
end
2015-07-06 12:38:43 +00:00
def can_change_visibility_level?(project, current_user)
return false unless can?(current_user, :change_visibility_level, project)
if project.fork_source
project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE
2015-07-06 12:38:43 +00:00
else
true
end
end
def last_push_event
Rework how recent push events are retrieved Whenever you push to a branch GitLab will show a button to create a merge request (should one not exist already). The underlying code to display this data was quite inefficient. For example, it involved multiple slow queries just to figure out what the most recent push event was. This commit changes the way this data is retrieved so it's much faster. This is achieved by caching the ID of the last push event on every push, which is then retrieved when loading certain pages. Database queries are only executed if necessary and the cached data is removed automatically once a merge request has been created, or 2 hours after being stored. A trade-off of this approach is that we _only_ track the last event. Previously if you were to push to branch A and B then create a merge request for branch B we'd still show the widget for branch A. As of this commit this is no longer the case, instead we will only show the widget for the branch you pushed to most recently. Once a merge request exists the widget is no longer displayed. Alternative solutions are either too complex and/or too slow, hence the decision was made to settle for this trade-off. Performance Impact ------------------ In the best case scenario (= a user didn't push anything for more than 2 hours) we perform a single Redis GET per page. Should there be cached data we will run a single (and lightweight) SQL query to get the event data from the database. If a merge request already exists we will run an additional DEL to remove the cache key. The difference in response timings can vary a bit per project. On GitLab.com the 99th percentile of time spent in User#recent_push hovers between 100 milliseconds and 1 second, while the mean hovers around 50 milliseconds. With the changes in this MR the expected time spent in User#recent_push is expected to be reduced down to just a few milliseconds. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/35990
2017-09-01 10:50:14 +00:00
current_user&.recent_push(@project)
end
def link_to_autodeploy_doc
2017-06-07 20:13:44 +00:00
link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank'
end
2017-02-07 15:59:38 +00:00
def autodeploy_flash_notice(branch_name)
2017-06-07 20:13:44 +00:00
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
2017-02-07 15:59:38 +00:00
end
2017-03-16 09:53:48 +00:00
def project_list_cache_key(project)
key = [
project.route.cache_key,
project.cache_key,
project.last_activity_date,
controller.controller_name,
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
max_project_member_access_cache_key(project),
'v2.6'
]
2017-03-16 09:53:48 +00:00
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
key
end
def load_pipeline_status(projects)
2017-06-21 13:48:12 +00:00
Gitlab::Cache::Ci::ProjectPipelineStatus
.load_in_batch_for_projects(projects)
end
def show_no_ssh_key_message?
Gitlab::CurrentSettings.user_show_add_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 &&
current_user.require_extra_setup_for_git_auth?
end
def show_auto_devops_implicitly_enabled_banner?(project)
cookie_key = "hide_auto_devops_implicitly_enabled_banner_#{project.id}"
project.has_auto_devops_implicitly_enabled? &&
cookies[cookie_key.to_sym].blank? &&
(project.owner == current_user || project.team.maintainer?(current_user))
end
def link_to_set_password
if current_user.require_password_creation_for_git?
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
# 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.
# rubocop: disable CodeReuse/ActiveRecord
def any_projects?(projects)
return projects.any? if projects.is_a?(Array)
if projects.limit_value
projects.to_a.any?
else
projects.except(:offset).any?
end
end
# rubocop: enable CodeReuse/ActiveRecord
def show_projects?(projects, params)
!!(params[:personal] || params[:name] || any_projects?(projects))
end
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
2018-06-05 10:10:34 +00:00
def show_xcode_link?(project = @project)
browser.platform.mac? && project.repository.xcode_project?
end
def xcode_uri_to_repo(project = @project)
"xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
end
def legacy_render_context(params)
params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
end
2013-06-13 19:58:27 +00:00
private
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
2013-06-13 19:58:27 +00:00
if !project.empty_repo? && can?(current_user, :download_code, project)
nav_tabs << [:files, :commits, :network, :graphs, :forks]
2013-06-13 19:58:27 +00:00
end
if project.repo_exists? && can?(current_user, :read_merge_request, project)
2013-06-13 19:58:27 +00:00
nav_tabs << :merge_requests
end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
2016-05-09 19:34:10 +00:00
nav_tabs << :container_registry
2016-04-18 12:14:40 +00:00
end
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
nav_tabs << :operations
end
if project.external_issue_tracker
nav_tabs << :external_issue_tracker
end
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
{
environments: :read_environment,
milestones: :read_milestone,
snippets: :read_project_snippet,
settings: :admin_project,
builds: :read_build,
clusters: :read_cluster,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
wiki: :read_wiki
}
end
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]
)
2013-06-13 19:58:27 +00:00
end
def project_lfs_status(project)
if project.lfs_enabled?
content_tag(:span, class: 'lfs-enabled') do
2017-06-07 20:13:44 +00:00
s_('LFSStatus|Enabled')
end
else
content_tag(:span, class: 'lfs-disabled') do
2017-06-07 20:13:44 +00:00
s_('LFSStatus|Disabled')
end
end
end
def git_user_name
if current_user
2017-09-13 12:39:50 +00:00
current_user.name.gsub('"', '\"')
else
2017-06-07 20:13:44 +00:00
_("Your name")
end
end
def git_user_email
if current_user
current_user.email
else
"your@email.com"
end
end
def default_url_to_repo(project = @project)
case default_clone_protocol
when 'ssh'
project.ssh_url_to_repo
else
project.http_url_to_repo
end
end
2018-09-06 07:27:39 +00:00
def default_clone_label
_("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase }
end
def default_clone_protocol
if allowed_protocols_present?
enabled_protocol
else
2018-06-20 16:38:42 +00:00
extra_default_clone_protocol
end
end
def extra_default_clone_protocol
if !current_user || current_user.require_ssh_key?
gitlab_config.protocol
else
'ssh'
end
end
def project_last_activity(project)
if project.last_activity_at
time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
else
2017-06-07 20:13:44 +00:00
s_("ProjectLastActivity|Never")
end
end
def project_wiki_path_with_version(proj, page, version, is_newest)
url_params = is_newest ? {} : { version_id: version }
project_wiki_path(proj, page, url_params)
end
2014-12-31 13:07:48 +00:00
def project_status_css_class(status)
case status
when "started"
2018-06-06 01:09:58 +00:00
"table-active"
2014-12-31 13:07:48 +00:00
when "failed"
2018-06-06 01:09:58 +00:00
"table-danger"
2014-12-31 13:07:48 +00:00
when "finished"
2018-06-06 01:09:58 +00:00
"table-success"
2014-12-31 13:07:48 +00:00
end
end
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.full_path, sha, "readme"].join('-')
end
def current_ref
@ref || @repository.try(:root_ref)
end
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
def project_issues(project)
IssuesFinder.new(current_user, project_id: project.id).execute
end
def restricted_levels
return [] if current_user.admin?
Gitlab::CurrentSettings.restricted_visibility_levels || []
end
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,
pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled
}
end
def project_permissions_panel_data(project)
{
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,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs'),
pagesAvailable: Gitlab.config.pages.enabled,
pagesAccessControlEnabled: Gitlab.config.pages.access_control,
pagesHelpPath: help_page_path('user/project/pages/index.md')
}
end
def project_permissions_panel_data_json(project)
project_permissions_panel_data(project).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
def find_file_path
return unless @project && !@project.empty_repo?
ref = @ref || @project.repository.root_ref
project_find_file_path(@project, ref)
end
def can_show_last_commit_in_list?(project)
can?(current_user, :read_cross_project) && project.commit
end
2018-01-03 08:07:03 +00:00
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
2018-06-20 16:38:42 +00:00
def sidebar_projects_paths
%w[
projects#show
projects#activity
cycle_analytics#show
]
end
2018-06-20 16:38:42 +00:00
def sidebar_settings_paths
%w[
projects#edit
project_members#index
integrations#show
services#edit
repository#show
ci_cd#show
badges#index
pages#show
]
end
def sidebar_repository_paths
%w[
tree
blob
blame
edit_tree
new_tree
find_file
commit
commits
compare
projects/repositories
tags
branches
releases
graphs
network
]
end
2018-11-08 07:02:44 +00:00
def sidebar_operations_paths
%w[
environments
clusters
user
gcp
]
end
2012-09-07 16:57:13 +00:00
end