diff --git a/CHANGELOG b/CHANGELOG
index 0f9711adae4..3f0bfda4868 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,12 +1,21 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.2.0 (unreleased)
+ - Fix duplicate repositories in GitHub import page (Stan Hu)
- Show last project commit to default branch on project home page
- Highlight comment based on anchor in URL
+ - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw)
+ - Improved performance of sorting milestone issues
+ - Allow users to select the Files view as default project view (Cristian Bica)
v 8.1.0 (unreleased)
+ - Send an email to admin email when a user is reported for spam (Jonathan Rochkind)
+ - Show notifications button when user is member of group rather than project (Grzegorz Bizon)
+ - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge.
+ - Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu)
- Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu)
- Speed up load times of issue detail pages by roughly 1.5x
+ - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg)
- Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu)
- Make diff file view easier to use on mobile screens (Stan Hu)
- Improved performance of finding users by username or Email address
@@ -59,6 +68,7 @@ v 8.1.0 (unreleased)
- Fix position of hamburger in header for smaller screens (Han Loong Liauw)
- Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji)
- Persist filters when sorting on admin user page (Jerry Lukins)
+ - Allow dashboard and group issues/MRs to be filtered by label
- Add spellcheck=false to certain input fields
- Invalidate stored service password if the endpoint URL is changed
- Project names are not fully shown if group name is too big, even on group page view
@@ -68,6 +78,12 @@ v 8.1.0 (unreleased)
- Hide passwords from services API (Alex Lossent)
- Fix: Images cannot show when projects' path was changed
- Let gitlab-git-http-server generate and serve 'git archive' downloads
+ - Optimize query when filtering on issuables (Zeger-Jan van de Weg)
+ - Fix padding of outdated discussion item.
+
+v 8.0.5
+ - Correct lookup-by-email for LDAP logins
+ - Fix loading spinner sometimes not being hidden on Merge Request tab switches
v 8.0.4
- Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu)
diff --git a/GITLAB_GIT_HTTP_SERVER_VERSION b/GITLAB_GIT_HTTP_SERVER_VERSION
new file mode 100644
index 00000000000..0d91a54c7d4
--- /dev/null
+++ b/GITLAB_GIT_HTTP_SERVER_VERSION
@@ -0,0 +1 @@
+0.3.0
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 32d219d4d60..5949a0fd5ad 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -65,3 +65,48 @@
line-height: 42px;
}
}
+
+.cover-block {
+ text-align: center;
+ background: #f7f8fa;
+ margin: -$gl-padding;
+ margin-bottom: 0;
+ padding: 44px $gl-padding;
+ border-bottom: 1px solid $border-color;
+ position: relative;
+
+ .avatar-holder {
+ margin-bottom: 16px;
+
+ .avatar, .identicon {
+ margin: 0 auto;
+ float: none;
+ }
+
+ .identicon {
+ @include border-radius(50%);
+ }
+ }
+
+ .cover-title {
+ color: $gl-header-color;
+ margin: 0;
+ font-size: 23px;
+ font-weight: normal;
+ margin: 16px 0 5px 0;
+ color: #4c4e54;
+ font-size: 23px;
+ line-height: 1.1;
+ }
+
+ .cover-desc {
+ padding: 0 $gl-padding;
+ color: $gl-text-color;
+ }
+
+ .cover-controls {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 9dd77747884..8742d1c39b3 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -10,6 +10,10 @@
border-bottom: 1px solid #E7E9EE;
margin-bottom: 1em;
+ &.readme-holder {
+ border-bottom: 0;
+ }
+
table {
@extend .table;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 1857c1659aa..e6d1cca9f7a 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -18,7 +18,6 @@
font-family: $monospace_font;
white-space: pre;
word-wrap: normal;
- padding: 1px 2px;
}
kbd {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 4bf58cb4a59..41c069f0ad3 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -132,6 +132,11 @@ form.edit-issue {
}
}
+.issue-closed-by-widget {
+ padding: 16px 0;
+ margin: 0px;
+}
+
.issue-form .select2-container {
width: 250px !important;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index abb03b07f51..1980fe0d458 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -30,7 +30,6 @@ ul.notes {
.discussion-header,
.note-header {
@extend .cgray;
- padding-bottom: 15px;
a:hover {
text-decoration: none;
@@ -75,6 +74,10 @@ ul.notes {
}
}
+ .discussion-body {
+ padding-top: 15px;
+ }
+
.discussion {
overflow: hidden;
display: block;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8e4f0eb2b25..b7391e5303b 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -47,3 +47,9 @@
}
}
}
+
+.calendar-hint {
+ margin-top: -12px;
+ float: right;
+ font-size: 12px;
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 48b87750264..bc62532f8a0 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -544,5 +544,5 @@ pre.light-well {
}
.project-show-readme .readme-holder {
- padding: 7px;
+ border-top: 0;
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index dadd86e88cc..ace371d7695 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -4,14 +4,6 @@
margin-right: -$gl-padding;
}
- .tree_progress {
- display: none;
- margin: 20px;
- &.loading {
- display: block;
- }
- }
-
.tree-table {
margin-bottom: 0;
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 65dbd5ef551..2f4054eaa11 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -9,6 +9,10 @@ class AbuseReportsController < ApplicationController
@abuse_report.reporter = current_user
if @abuse_report.save
+ if current_application_settings.admin_notification_email.present?
+ AbuseReportMailer.delay.notify(@abuse_report.id)
+ end
+
message = "Thank you for your report. A GitLab administrator will look into it shortly."
redirect_to root_path, notice: message
else
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 7c134d2ec9b..039f18f23e0 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -55,6 +55,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_snippet_visibility,
:restricted_signup_domains_raw,
:version_check_enabled,
+ :admin_notification_email,
:user_oauth_applications,
restricted_visibility_levels: [],
import_sources: []
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index 7777aa18031..809b44387ba 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,12 +1,17 @@
module Ci
class ProjectsController < Ci::ApplicationController
- before_action :project
- before_action :authenticate_user!, except: [:build, :badge]
- before_action :authorize_access_project!, except: [:badge]
+ before_action :project, except: [:index]
+ before_action :authenticate_user!, except: [:index, :build, :badge]
+ before_action :authorize_access_project!, except: [:index, :badge]
before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml]
before_action :no_cache, only: [:badge]
protect_from_forgery
+ def show
+ # Temporary compatibility with CI badges pointing to CI project page
+ redirect_to namespace_project_path(project.gl_project.namespace, project.gl_project)
+ end
+
# Project status badge
# Image with build status for sha or ref
def badge
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index aae77d384c6..67bf4190e7e 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -11,10 +11,6 @@ class Import::GithubController < Import::BaseController
def status
@repos = client.repos
- client.orgs.each do |org|
- @repos += client.org_repos(org.login)
- end
-
@already_added_projects = current_user.created_projects.where(import_type: "github")
already_added_projects_names = @already_added_projects.pluck(:import_source)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 97485c101fb..cc8321d97ad 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -14,6 +14,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow issues bulk update
before_action :authorize_admin_issues!, only: [:bulk_update]
+ # Cross-reference merge requests
+ before_action :closed_by_merge_requests, only: [:show]
+
respond_to :html
def index
@@ -112,6 +115,10 @@ class Projects::IssuesController < Projects::ApplicationController
render nothing: true
end
+ def closed_by_merge_requests
+ @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
+ end
+
protected
def issue
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 98df6984bf7..0d9c5461959 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -259,7 +259,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commits = @merge_request.commits
@merge_request_diff = @merge_request.merge_request_diff
-
+
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 86f4a02a6e9..15506bd677a 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -75,11 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def sort_issues
- @issues = @milestone.issues.where(id: params['sortable_issue'])
- @issues.each do |issue|
- issue.position = params['sortable_issue'].index(issue.id.to_s) + 1
- issue.save
- end
+ @milestone.sort_issues(params['sortable_issue'].map(&:to_i))
render json: { saved: true }
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 213c2a7173b..82119022cf9 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,11 +1,14 @@
class ProjectsController < ApplicationController
+ include ExtractsPath
+
prepend_before_filter :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
+ before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive]
+ before_action :authorize_admin_project!, only: [:edit, :update]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
@@ -56,6 +59,8 @@ class ProjectsController < ApplicationController
end
def transfer
+ return access_denied! unless can?(current_user, :change_namespace, @project)
+
namespace = Namespace.find_by(id: params[:new_namespace_id])
::Projects::TransferService.new(project, current_user).execute(namespace)
@@ -64,6 +69,15 @@ class ProjectsController < ApplicationController
end
end
+ def remove_fork
+ return access_denied! unless can?(current_user, :remove_fork_project, @project)
+
+ if @project.forked?
+ @project.forked_project_link.destroy
+ flash[:notice] = 'The fork relationship has been removed.'
+ end
+ end
+
def activity
respond_to do |format|
format.html
@@ -87,7 +101,7 @@ class ProjectsController < ApplicationController
render 'projects/empty'
else
if current_user
- @membership = @project.project_member_by_id(current_user.id)
+ @membership = @project.team.find_member(current_user.id)
end
render :show
@@ -139,6 +153,7 @@ class ProjectsController < ApplicationController
def archive
return access_denied! unless can?(current_user, :archive_project, @project)
+
@project.archive!
respond_to do |format|
@@ -148,6 +163,7 @@ class ProjectsController < ApplicationController
def unarchive
return access_denied! unless can?(current_user, :archive_project, @project)
+
@project.unarchive!
respond_to do |format|
@@ -225,4 +241,14 @@ class ProjectsController < ApplicationController
render "go_import", layout: false
end
+
+ def repo_exists?
+ project.repository_exists? && !project.empty_repo?
+ end
+
+ # Override get_id from ExtractsPath, which returns the branch and file path
+ # for the blob/tree, which in this case is just the root of the default branch.
+ def get_id
+ project.repository.root_ref
+ end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 97c7e74c294..c407dfc163a 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -53,15 +53,36 @@ class IssuableFinder
end
end
+ def project?
+ params[:project_id].present?
+ end
+
def project
return @project if defined?(@project)
- @project =
- if params[:project_id].present?
- Project.find(params[:project_id])
- else
- nil
- end
+ if project?
+ @project = Project.find(params[:project_id])
+
+ unless Ability.abilities.allowed?(current_user, :read_project, @project)
+ @project = nil
+ end
+ else
+ @project = nil
+ end
+
+ @project
+ end
+
+ def projects
+ return @projects if defined?(@projects)
+
+ if project?
+ project
+ elsif current_user && params[:authorized_only].presence && !current_user_related?
+ current_user.authorized_projects
+ else
+ ProjectsFinder.new.execute(current_user)
+ end
end
def search
@@ -72,7 +93,7 @@ class IssuableFinder
params[:milestone_title].present?
end
- def no_milestones?
+ def filter_by_no_milestone?
milestones? && params[:milestone_title] == Milestone::None.title
end
@@ -81,12 +102,22 @@ class IssuableFinder
@milestones =
if milestones?
- Milestone.where(title: params[:milestone_title])
+ scope = Milestone.where(project_id: projects)
+
+ scope.where(title: params[:milestone_title])
else
nil
end
end
+ def labels?
+ params[:label_name].present?
+ end
+
+ def filter_by_no_label?
+ labels? && params[:label_name] == Label::None.title
+ end
+
def assignee?
params[:assignee_id].present?
end
@@ -120,19 +151,7 @@ class IssuableFinder
private
def init_collection
- table_name = klass.table_name
-
- if project
- if Ability.abilities.allowed?(current_user, :read_project, project)
- project.send(table_name)
- else
- []
- end
- elsif current_user && params[:authorized_only].presence && !current_user_related?
- klass.of_projects(current_user.authorized_projects).references(:project)
- else
- klass.of_projects(ProjectsFinder.new.execute(current_user)).references(:project)
- end
+ klass.all
end
def by_scope(items)
@@ -170,7 +189,12 @@ class IssuableFinder
end
def by_project(items)
- items = items.of_projects(project.id) if project
+ items =
+ if projects
+ items.of_projects(projects).references(:project)
+ else
+ items.none
+ end
items
end
@@ -185,18 +209,6 @@ class IssuableFinder
items.sort(params[:sort])
end
- def by_milestone(items)
- if milestones?
- if no_milestones?
- items = items.where(milestone_id: [-1, nil])
- else
- items = items.where(milestone_id: milestones.try(:pluck, :id))
- end
- end
-
- items
- end
-
def by_assignee(items)
if assignee?
items = items.where(assignee_id: assignee.try(:id))
@@ -213,20 +225,36 @@ class IssuableFinder
items
end
- def by_label(items)
- if params[:label_name].present?
- if params[:label_name] == Label::None.title
- item_ids = LabelLink.where(target_type: klass.name).pluck(:target_id)
+ def by_milestone(items)
+ if milestones?
+ if filter_by_no_milestone?
+ items = items.where(milestone_id: [-1, nil])
+ else
+ items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
- items = items.where('id NOT IN (?)', item_ids)
+ if projects
+ items = items.where(milestones: { project_id: projects })
+ end
+ end
+ end
+
+ items
+ end
+
+ def by_label(items)
+ if labels?
+ if filter_by_no_label?
+ items = items.
+ joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
+ where(label_links: { id: nil })
else
label_names = params[:label_name].split(",")
- item_ids = LabelLink.joins(:label).
- where('labels.title in (?)', label_names).
- where(target_type: klass.name).pluck(:target_id)
+ items = items.joins(:labels).where(labels: { title: label_names })
- items = items.where(id: item_ids)
+ if projects
+ items = items.where(labels: { project_id: projects })
+ end
end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 12b87dca798..65813482120 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -19,7 +19,8 @@ module GitlabMarkdownHelper
escape_once(body)
end
- gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: current_user)
+ user = current_user if defined?(current_user)
+ gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
@@ -45,29 +46,39 @@ module GitlabMarkdownHelper
end
def markdown(text, context = {})
+ return "" unless text.present?
+
context.reverse_merge!(
- current_user: current_user,
path: @path,
+ pipeline: :default,
project: @project,
project_wiki: @project_wiki,
ref: @ref
)
- Gitlab::Markdown.render(text, context)
+ user = current_user if defined?(current_user)
+
+ html = Gitlab::Markdown.render(text, context)
+ Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user)
end
# TODO (rspeicher): Remove all usages of this helper and just call `markdown`
# with a custom pipeline depending on the content being rendered
def gfm(text, options = {})
+ return "" unless text.present?
+
options.reverse_merge!(
- current_user: current_user,
path: @path,
+ pipeline: :default,
project: @project,
project_wiki: @project_wiki,
ref: @ref
)
- Gitlab::Markdown.gfm(text, options)
+ user = current_user if defined?(current_user)
+
+ html = Gitlab::Markdown.gfm(text, options)
+ Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user)
end
def asciidoc(text)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 6ddb37cd0dc..fda18e7b316 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -83,6 +83,10 @@ module IssuesHelper
end
end
+ def merge_requests_sentence(merge_requests)
+ merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ')
+ end
+
# Required for Gitlab::Markdown::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 66b18eea699..ee04ace35d0 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -92,11 +92,19 @@ module LabelsHelper
end
end
- def project_labels_options(project)
- labels = project.labels.to_a
- labels.unshift(Label::None)
- labels.unshift(Label::Any)
- options_from_collection_for_select(labels, 'name', 'title', params[:label_name])
+ def projects_labels_options
+ labels =
+ if @project
+ @project.labels
+ else
+ Label.where(project_id: @projects)
+ end
+
+ grouped_labels = Labels::GroupService.new(labels).execute
+ grouped_labels.unshift(Label::None)
+ grouped_labels.unshift(Label::Any)
+
+ options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end
# Required for Gitlab::Markdown::LabelReferenceFilter
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 4710171ebaa..c73cb3028ee 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -34,7 +34,8 @@ module PreferencesHelper
def project_view_choices
[
['Readme (default)', :readme],
- ['Activity view', :activity]
+ ['Activity view', :activity],
+ ['Files view', :files]
]
end
@@ -46,8 +47,7 @@ module PreferencesHelper
Gitlab::ColorSchemes.for_user(current_user).css_class
end
- def prefer_readme?
- !current_user ||
- current_user.project_view == 'readme'
+ def default_project_view
+ current_user ? current_user.project_view : 'readme'
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index dbadbb74549..5301c2ccf76 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -70,6 +70,10 @@ module ProjectsHelper
"You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
end
+ def remove_fork_project_message(project)
+ "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}. Are you ABSOLUTELY sure?"
+ end
+
def project_nav_tabs
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
@@ -113,7 +117,7 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if can?(current_user, :read_build, project)
+ if project.gitlab_ci? && can?(current_user, :read_build, project)
nav_tabs << :builds
end
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
new file mode 100644
index 00000000000..f0c41f69a5c
--- /dev/null
+++ b/app/mailers/abuse_report_mailer.rb
@@ -0,0 +1,12 @@
+class AbuseReportMailer < BaseMailer
+ include Gitlab::CurrentSettings
+
+ def notify(abuse_report_id)
+ @abuse_report = AbuseReport.find(abuse_report_id)
+
+ mail(
+ to: current_application_settings.admin_notification_email,
+ subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
+ )
+ end
+end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 38bc2086683..b72178fa126 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -189,7 +189,8 @@ class Ability
:change_visibility_level,
:rename_project,
:remove_project,
- :archive_project
+ :archive_project,
+ :remove_fork_project
]
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index c8841178e93..05430c2ee18 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -44,6 +44,10 @@ class ApplicationSetting < ActiveRecord::Base
allow_blank: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
+ validates :admin_notification_email,
+ allow_blank: true,
+ email: true
+
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
diff --git a/app/models/commit.rb b/app/models/commit.rb
index d5c50013525..23b5e38336c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -2,13 +2,13 @@ class Commit
extend ActiveModel::Naming
include ActiveModel::Conversion
- include Mentionable
include Participable
+ include Mentionable
include Referable
include StaticModel
attr_mentionable :safe_message
- participant :author, :committer, :notes, :mentioned_users
+ participant :author, :committer, :notes
attr_accessor :project
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 0e8bcc1a4ec..5e964f04ef5 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,8 +6,8 @@
#
module Issuable
extend ActiveSupport::Concern
- include Mentionable
include Participable
+ include Mentionable
included do
belongs_to :author, class_name: "User"
@@ -47,8 +47,7 @@ module Issuable
prefix: true
attr_mentionable :title, :description
-
- participant :author, :assignee, :notes_with_associations, :mentioned_users
+ participant :author, :assignee, :notes_with_associations
end
module ClassMethods
@@ -86,6 +85,10 @@ module Issuable
assignee_id_changed?
end
+ def open?
+ opened? || reopened?
+ end
+
#
# Votes
#
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index b34def66d2e..193c91f1742 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -20,6 +20,12 @@ module Mentionable
end
end
+ included do
+ if self < Participable
+ participant ->(current_user) { mentioned_users(current_user, load_lazy_references: false) }
+ end
+ end
+
# Returns the text used as the body of a Note when this object is referenced
#
# By default this will be the class name and the result of calling
@@ -41,22 +47,22 @@ module Mentionable
self
end
- def all_references(current_user = self.author, text = self.mentionable_text)
- ext = Gitlab::ReferenceExtractor.new(self.project, current_user)
+ def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
+ ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references)
ext.analyze(text)
ext
end
- def mentioned_users(current_user = nil)
- all_references(current_user).users.uniq
+ def mentioned_users(current_user = nil, load_lazy_references: true)
+ all_references(current_user, load_lazy_references: load_lazy_references).users
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
- def referenced_mentionables(current_user = self.author, text = self.mentionable_text)
+ def referenced_mentionables(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
return [] if text.blank?
- refs = all_references(current_user, text)
- (refs.issues + refs.merge_requests + refs.commits).uniq - [local_reference]
+ refs = all_references(current_user, text, load_lazy_references: load_lazy_references)
+ (refs.issues + refs.merge_requests + refs.commits) - [local_reference]
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index ffc874357fd..85367f89f4f 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -12,7 +12,7 @@
#
# # ...
#
-# participant :author, :assignee, :mentioned_users, :notes
+# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) }
# end
#
# issue = Issue.last
@@ -27,7 +27,7 @@ module Participable
module ClassMethods
def participant(*attrs)
- participant_attrs.concat(attrs.map(&:to_s))
+ participant_attrs.concat(attrs)
end
def participant_attrs
@@ -37,33 +37,39 @@ module Participable
# Be aware that this method makes a lot of sql queries.
# Save result into variable if you are going to reuse it inside same request
- def participants(current_user = self.author)
- self.class.participant_attrs.flat_map do |attr|
- meth = method(attr)
-
+ def participants(current_user = self.author, load_lazy_references: true)
+ participants = self.class.participant_attrs.flat_map do |attr|
value =
- if meth.arity == 1 || meth.arity == -1
- meth.call(current_user)
+ if attr.respond_to?(:call)
+ instance_exec(current_user, &attr)
else
- meth.call
+ send(attr)
end
participants_for(value, current_user)
- end.compact.uniq.select do |user|
- user.can?(:read_project, self.project)
+ end.compact.uniq
+
+ if load_lazy_references
+ participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq
+
+ participants.select! do |user|
+ user.can?(:read_project, project)
+ end
end
+
+ participants
end
private
def participants_for(value, current_user = nil)
case value
- when User
+ when User, Gitlab::Markdown::ReferenceFilter::LazyReference
[value]
when Enumerable, ActiveRecord::Relation
value.flat_map { |v| participants_for(v, current_user) }
when Participable
- value.participants(current_user)
+ value.participants(current_user, load_lazy_references: false)
end
end
end
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
new file mode 100644
index 00000000000..0fc39cb8771
--- /dev/null
+++ b/app/models/group_label.rb
@@ -0,0 +1,9 @@
+class GroupLabel
+ attr_accessor :title, :labels
+ alias_attribute :name, :title
+
+ def initialize(title, labels)
+ @title = title
+ @labels = labels
+ end
+end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 1dd2be68ebf..91844da62e2 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -1,5 +1,5 @@
class GroupMilestone
-
+ attr_accessor :title, :milestones
alias_attribute :name, :title
def initialize(title, milestones)
@@ -7,18 +7,10 @@ class GroupMilestone
@milestones = milestones
end
- def title
- @title
- end
-
def safe_title
@title.parameterize
end
-
- def milestones
- @milestones
- end
-
+
def projects
milestones.map { |milestone| milestone.project }
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index fc7e9abe29e..72183108033 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -95,4 +95,14 @@ class Issue < ActiveRecord::Base
def source_project
project
end
+
+ # From all notes on this issue, we'll select the system notes about linked
+ # merge requests. Of those, the MRs closing `self` are returned.
+ def closed_by_merge_requests(current_user = nil)
+ return [] unless open?
+
+ notes.system.flat_map do |note|
+ note.all_references(current_user).merge_requests
+ end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c83b15c7d39..0042b95c4f1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -222,10 +222,6 @@ class MergeRequest < ActiveRecord::Base
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
- def open?
- opened? || reopened?
- end
-
def work_in_progress?
!!(title =~ /\A\[?WIP\]?:? /i)
end
@@ -294,6 +290,10 @@ class MergeRequest < ActiveRecord::Base
target_project
end
+ def closes_issue?(issue)
+ closes_issues.include?(issue)
+ end
+
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 84acba30b6b..2ff16e2825c 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -105,4 +105,36 @@ class Milestone < ActiveRecord::Base
def author_id
nil
end
+
+ # Sorts the issues for the given IDs.
+ #
+ # This method runs a single SQL query using a CASE statement to update the
+ # position of all issues in the current milestone (scoped to the list of IDs).
+ #
+ # Given the ids [10, 20, 30] this method produces a SQL query something like
+ # the following:
+ #
+ # UPDATE issues
+ # SET position = CASE
+ # WHEN id = 10 THEN 1
+ # WHEN id = 20 THEN 2
+ # WHEN id = 30 THEN 3
+ # ELSE position
+ # END
+ # WHERE id IN (10, 20, 30);
+ #
+ # This method expects that the IDs given in `ids` are already Fixnums.
+ def sort_issues(ids)
+ pairs = []
+
+ ids.each_with_index do |id, index|
+ pairs << id
+ pairs << index + 1
+ end
+
+ conditions = 'WHEN id = ? THEN ? ' * ids.length
+
+ issues.where(id: ids).
+ update_all(["position = CASE #{conditions} ELSE position END", *pairs])
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index ebbdd2f33a1..0b3aa30abd7 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -22,14 +22,14 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Note < ActiveRecord::Base
- include Mentionable
include Gitlab::CurrentSettings
include Participable
+ include Mentionable
default_value_for :system, false
attr_mentionable :note
- participant :author, :mentioned_users
+ participant :author
belongs_to :project
belongs_to :noteable, polymorphic: true
diff --git a/app/models/user.rb b/app/models/user.rb
index 17ccb3b8788..7e4321d5376 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -183,7 +183,7 @@ class User < ActiveRecord::Base
# User's Project preference
# Note: When adding an option, it MUST go on the end of the array.
- enum project_view: [:readme, :activity]
+ enum project_view: [:readme, :activity, :files]
alias_attribute :private_token, :authentication_token
@@ -706,12 +706,15 @@ class User < ActiveRecord::Base
end
def toggle_star(project)
- user_star_project = users_star_projects.
- where(project: project, user: self).take
- if user_star_project
- user_star_project.destroy
- else
- UsersStarProject.create!(project: project, user: self)
+ UsersStarProject.transaction do
+ user_star_project = users_star_projects.
+ where(project: project, user: self).lock(true).first
+
+ if user_star_project
+ user_star_project.destroy
+ else
+ UsersStarProject.create!(project: project, user: self)
+ end
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index e54044365b9..3de7bb9dcaa 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -79,7 +79,7 @@ class GitPushService
authors = Hash.new do |hash, commit|
email = commit.author_email
- return hash[email] if hash.has_key?(email)
+ next hash[email] if hash.has_key?(email)
hash[email] = commit_user(commit)
end
diff --git a/app/services/labels/group_service.rb b/app/services/labels/group_service.rb
new file mode 100644
index 00000000000..b26cee24d56
--- /dev/null
+++ b/app/services/labels/group_service.rb
@@ -0,0 +1,26 @@
+module Labels
+ class GroupService < ::BaseService
+ def initialize(project_labels)
+ @project_labels = project_labels.group_by(&:title)
+ end
+
+ def execute
+ build(@project_labels)
+ end
+
+ def label(title)
+ if title
+ group_label = @project_labels[title].group_by(&:title)
+ build(group_label).first
+ else
+ nil
+ end
+ end
+
+ private
+
+ def build(label)
+ label.map { |title, labels| GroupLabel.new(title, labels) }
+ end
+ end
+end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index aceb8cb9021..8f25c5e2496 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -6,6 +6,7 @@ module MergeRequests
#
class PostMergeService < MergeRequests::BaseService
def execute(merge_request)
+ close_issues(merge_request)
merge_request.mark_as_merged
create_merge_event(merge_request, current_user)
create_note(merge_request)
@@ -15,6 +16,15 @@ module MergeRequests
private
+ def close_issues(merge_request)
+ return unless merge_request.target_branch == project.default_branch
+
+ closed_issues = merge_request.closes_issues(current_user)
+ closed_issues.each do |issue|
+ Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ end
+ end
+
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
end
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
new file mode 100644
index 00000000000..619533e09a7
--- /dev/null
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -0,0 +1,11 @@
+%p
+ #{link_to @abuse_report.user.name, user_url(@abuse_report.user)}
+ (@#{@abuse_report.user.username}) was reported for abuse by
+ #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
+ (@#{@abuse_report.reporter.username}).
+
+%blockquote
+ = @abuse_report.message
+
+%p
+ = link_to "View details", abuse_reports_url
diff --git a/app/views/abuse_report_mailer/notify.text.haml b/app/views/abuse_report_mailer/notify.text.haml
new file mode 100644
index 00000000000..7dacf857035
--- /dev/null
+++ b/app/views/abuse_report_mailer/notify.text.haml
@@ -0,0 +1,5 @@
+#{@abuse_report.user.name} (@#{@abuse_report.user.username}) was reported for abuse by #{@abuse_report.reporter.name} (@#{@abuse_report.reporter.username}).
+\
+> #{@abuse_report.message}
+\
+View details: #{admin_abuse_reports_url}
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index a36ae0b766c..7a78526e09a 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -47,6 +47,12 @@
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Version check enabled
+ .form-group
+ = f.label :admin_notification_email, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :admin_notification_email, class: 'form-control'
+ .help-block
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
%fieldset
%legend Account and Limit Settings
diff --git a/app/views/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
similarity index 100%
rename from app/views/users/_profile.html.haml
rename to app/views/admin/users/_profile.html.haml
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 231bcb0426f..0848504b7a6 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -14,7 +14,7 @@
%strong
= link_to user_path(@user) do
= @user.username
- = render 'users/profile', user: @user
+ = render 'admin/users/profile', user: @user
.panel.panel-default
.panel-heading
diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml
new file mode 100644
index 00000000000..9c2290bc4a5
--- /dev/null
+++ b/app/views/ci/projects/index.html.haml
@@ -0,0 +1,20 @@
+.wiki
+ %h1
+ GitLab CI is now integrated in GitLab UI
+ %h2 For existing projects
+
+ %p
+ Check the following pages to find the CI status you're looking for:
+
+ %ul
+ %li Projects page - shows CI status for each project.
+ %li Project commits page - show CI status for each commit.
+
+
+
+ %h2 For new projects
+
+ %p
+ If you want to enable CI for a new project it is easy as adding
+ = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html"
+ file to your repository
diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml
index 4259f64c191..fad65310021 100644
--- a/app/views/events/_event_issue.atom.haml
+++ b/app/views/events/_event_issue.atom.haml
@@ -1,3 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- - if issue.description.present?
- = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project)
+ = markdown(issue.description, pipeline: :atom, project: issue.project)
diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml
index e8ed13df783..19bdc7b9ca5 100644
--- a/app/views/events/_event_merge_request.atom.haml
+++ b/app/views/events/_event_merge_request.atom.haml
@@ -1,3 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- - if merge_request.description.present?
- = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project)
+ = markdown(merge_request.description, pipeline: :atom, project: merge_request.project)
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index cfbfba50202..b730ebbd5f9 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- = markdown(note.note, xhtml: true, reference_only_path: false, project: note.project)
+ = markdown(note.note, pipeline: :atom, project: note.project)
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index 3625cb49d8b..b271b9daff1 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -6,7 +6,7 @@
%i
at
= commit[:timestamp].to_time.to_s(:short)
- %blockquote= markdown(escape_once(commit[:message]), xhtml: true, reference_only_path: false, project: event.project)
+ %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project)
- if event.commits_count > 15
%p
%i
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 2f7d7e86f56..854cda57c39 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -41,4 +41,8 @@
#{link_to "view it on GitLab", @target_url}.
- else
#{link_to "View it on GitLab", @target_url}
+ %br
+ You're receiving this email because of your account on #{link_to Gitlab.config.gitlab.host, root_url}.
+ If you'd like to receive fewer emails, you can adjust your notification settings.
+
= email_action @target_url
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
index 3fd4b04ac84..00cb4aa24cc 100644
--- a/app/views/notify/_note_message.html.haml
+++ b/app/views/notify/_note_message.html.haml
@@ -1,2 +1,2 @@
%div
- = markdown(@note.note, reference_only_path: false)
+ = markdown(@note.note, pipeline: :email)
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index 53a068be52e..d3b799fca23 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,5 +1,5 @@
-if @issue.description
- = markdown(@issue.description, reference_only_path: false)
+ = markdown(@issue.description, pipeline: :email)
- if @issue.assignee_id.present?
%p
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 5b7dd117c16..90ebdfc3fe2 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -6,4 +6,4 @@
Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name}
-if @merge_request.description
- = markdown(@merge_request.description, reference_only_path: false)
+ = markdown(@merge_request.description, pipeline: :email)
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 01e285a8dfa..cc41d7dd813 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -38,7 +38,7 @@
.col-sm-10
= f.select :layout, layout_choices, {}, class: 'form-control'
.help-block
- Choose between fixed (max. 1200px) and fluid (100%) application layout
+ Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group
= f.label :dashboard, class: 'control-label' do
Default Dashboard
@@ -52,6 +52,6 @@
.col-sm-10
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block
- Choose what content you want to see when visit project page
+ Choose what content you want to see on a project's home page.
.panel-footer
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index c2683bc6219..012858f70b4 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,4 +1,3 @@
-= render 'projects/last_push'
.gray-content-block.activity-filter-block
- if current_user
.pull-right
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
new file mode 100644
index 00000000000..fa978325ddd
--- /dev/null
+++ b/app/views/projects/_files.html.haml
@@ -0,0 +1,6 @@
+#tree-holder.tree-holder.clearfix
+ .gray-content-block.second-block
+ = render 'projects/tree/tree_header', tree: @tree
+
+ = render 'projects/tree/tree_content', tree: @tree
+
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 5bc1999ec9d..0a1cecfdcdf 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -1,12 +1,11 @@
- if readme = @repository.readme
- %article.readme-holder#README
- .clearfix
- .pull-right
-
- - if can?(current_user, :push_code, @project)
- = link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do
- %i.fa-align.fa.fa-pencil
- .wiki
+ %article.file-holder.readme-holder
+ .file-title
+ = blob_icon readme.mode, readme.name
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
+ %strong
+ = readme.name
+ .file-content.wiki
= cache(readme_cache_key) do
= render_readme(readme)
- else
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 555ed76426d..69fa4ad37c4 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,4 +1,6 @@
- page_title "Activity"
- header_title project_title(@project, "Activity", activity_project_path(@project))
+= render 'projects/last_push'
+
= render 'projects/activity'
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index a1ae1397584..42f632b38ef 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,19 +1,22 @@
-%ul.breadcrumb.repo-breadcrumb
- %li
- %i.fa.fa-angle-right
- = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
- = @project.path
- - tree_breadcrumbs(@tree, 6) do |title, path|
+.gray-content-block.top-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'blob', path: @path
+
+ %ul.breadcrumb.repo-breadcrumb
%li
- - if path
- - if path.end_with?(@path)
- = link_to namespace_project_blob_path(@project.namespace, @project, path) do
- %strong
- = truncate(title, length: 40)
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
+ = @project.path
+ - tree_breadcrumbs(@tree, 6) do |title, path|
+ %li
+ - if path
+ - if path.end_with?(@path)
+ = link_to namespace_project_blob_path(@project.namespace, @project, path) do
+ %strong
+ = truncate(title, length: 40)
+ - else
+ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- else
- = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- - else
- = link_to title, '#'
+ = link_to title, '#'
%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index fa4be4a1bc4..f52b89f6921 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -3,9 +3,6 @@
= render 'projects/last_push'
-%div.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'blob', path: @path
-
%div#tree-holder.tree-holder
= render 'blob', blob: @blob
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
index 3bc2daeec4e..0c298844912 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -1,14 +1,20 @@
-- return unless @membership
+- case @membership
+- when ProjectMember
+ = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do
+ = hidden_field_tag :notification_type, 'project'
+ = hidden_field_tag :notification_id, @membership.id
+ = hidden_field_tag :notification_level
+ %span.dropdown
+ %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"}
+ = icon('bell')
+ = notification_label(@membership)
+ = icon('angle-down')
+ %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
+ - Notification.project_notification_levels.each do |level|
+ = notification_list_item(level, @membership)
-= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do
- = hidden_field_tag :notification_type, 'project'
- = hidden_field_tag :notification_id, @membership.id
- = hidden_field_tag :notification_level
- %span.dropdown
- %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"}
- = icon('bell')
- = notification_label(@membership)
- = icon('angle-down')
- %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- - Notification.project_notification_levels.each do |level|
- = notification_list_item(level, @membership)
+- when GroupMember
+ .btn.btn-new.disabled.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
+ = icon('bell')
+ = notification_label(@membership)
+ = icon('angle-down')
diff --git a/app/views/projects/ci_services/index.html.haml b/app/views/projects/ci_services/index.html.haml
index c78b21884a3..c164b2d4bc0 100644
--- a/app/views/projects/ci_services/index.html.haml
+++ b/app/views/projects/ci_services/index.html.haml
@@ -6,7 +6,7 @@
%tr
%th
%th Service
- %th Desription
+ %th Description
%th Last edit
- @services.sort_by(&:title).each do |service|
%tr
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 1882a82fba5..afbf88b5507 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -189,6 +189,21 @@
- else
.nothing-here-block Only the project owner can transfer a project
+ - if @project.forked?
+ - if can?(current_user, :remove_fork_project, @project)
+ = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f|
+ .panel.panel-default.panel.panel-danger
+ .panel-heading Remove fork relationship
+ .panel-body
+ %p
+ This will remove the fork relationship to source project
+ #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}.
+ %br
+ %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
+ = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) }
+ - else
+ .nothing-here-block Only the project owner can remove the fork relationship.
+
- if can?(current_user, :remove_project, @project)
.panel.panel-default.panel.panel-danger
.panel-heading Remove project
@@ -201,7 +216,8 @@
= button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
- else
- .nothing-here-block Only project owner can remove a project
+ .nothing-here-block Only the project owner can remove a project.
+
.save-project-loader.hide
.center
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index f8f2e192e29..92a87690c54 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -17,6 +17,6 @@
This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git.
%br
The import will time out after 4 minutes. For big repositories, use a clone/push combination.
- For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}
+ For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}
.form-actions
= f.submit 'Start import', class: "btn btn-create", tabindex: 4
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
new file mode 100644
index 00000000000..aef352029d0
--- /dev/null
+++ b/app/views/projects/issues/_closed_by_box.html.haml
@@ -0,0 +1,3 @@
+.issue-closed-by-widget
+ = icon('check')
+ This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests.sort))} is accepted.
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 5cb814c9ea8..f01bf2505da 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -46,6 +46,7 @@
= markdown(@issue.description)
%textarea.hidden.js-task-list-field
= @issue.description
-
+ - if @closed_by_merge_requests.present?
+ = render 'projects/issues/closed_by_box'
.issue-discussion
= render 'projects/issues/discussion'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 68dda1424cf..10efb811939 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,44 +1,43 @@
-- if @merge_request.has_ci?
- - ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha)
- - if ci_commit
- - status = ci_commit.status
- .mr-widget-heading
- .ci_widget{class: "ci-#{status}"}
- = ci_status_icon(ci_commit)
+- ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha)
+- if ci_commit
+ - status = ci_commit.status
+ .mr-widget-heading
+ .ci_widget{class: "ci-#{status}"}
+ = ci_status_icon(ci_commit)
+ %span CI build #{status}
+ for #{@merge_request.last_commit_short_sha}.
+ %span.ci-coverage
+ = link_to "View build details", ci_status_path(ci_commit)
+
+- elsif @merge_request.has_ci?
+ - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
+ - # Remove in later versions when services like Jenkins will set CI status via Commit status API
+ .mr-widget-heading
+ - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
+ .ci_widget{class: "ci-#{status}", style: "display:none"}
+ - if status == :success
+ - status = "passed"
+ = icon("check-circle")
+ - else
+ = icon("circle")
%span CI build #{status}
for #{@merge_request.last_commit_short_sha}.
%span.ci-coverage
- = link_to "View build details", ci_status_path(ci_commit)
+ - if ci_build_details_path(@merge_request)
+ = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
- - else
- - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
- - # Remove in later versions when services like Jenkins will set CI status via Commit status API
- .mr-widget-heading
- - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
- .ci_widget{class: "ci-#{status}", style: "display:none"}
- - if status == :success
- - status = "passed"
- = icon("check-circle")
- - else
- = icon("circle")
- %span CI build #{status}
- for #{@merge_request.last_commit_short_sha}.
- %span.ci-coverage
- - if ci_build_details_path(@merge_request)
- = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
+ .ci_widget
+ = icon("spinner spin")
+ Checking CI status for #{@merge_request.last_commit_short_sha}…
- .ci_widget
- = icon("spinner spin")
- Checking CI status for #{@merge_request.last_commit_short_sha}…
+ .ci_widget.ci-not_found{style: "display:none"}
+ = icon("times-circle")
+ Could not find CI status for #{@merge_request.last_commit_short_sha}.
- .ci_widget.ci-not_found{style: "display:none"}
- = icon("times-circle")
- Could not find CI status for #{@merge_request.last_commit_short_sha}.
+ .ci_widget.ci-error{style: "display:none"}
+ = icon("times-circle")
+ Could not connect to the CI server. Please check your settings and try again.
- .ci_widget.ci-error{style: "display:none"}
- = icon("times-circle")
- Could not connect to the CI server. Please check your settings and try again.
-
- :coffeescript
- $ ->
- merge_request_widget.getCiStatus()
+ :coffeescript
+ $ ->
+ merge_request_widget.getCiStatus()
diff --git a/app/views/projects/remove_fork.js.haml b/app/views/projects/remove_fork.js.haml
new file mode 100644
index 00000000000..17b9fecfeb1
--- /dev/null
+++ b/app/views/projects/remove_fork.js.haml
@@ -0,0 +1,2 @@
+:plain
+ location.href = "#{edit_namespace_project_path(@project.namespace, @project)}";
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index e20b1fc49c0..585caf674c9 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -7,8 +7,7 @@
= render 'shared/no_ssh'
= render 'shared/no_password'
-- if prefer_readme?
- = render 'projects/last_push'
+= render 'projects/last_push'
= render "home_panel"
@@ -28,7 +27,7 @@
= link_to project_files_path(@project) do
= repository_size
- - if !prefer_readme? && @repository.readme
+ - if default_project_view != 'readme' && @repository.readme
%li
= link_to 'Readme', readme_path(@project)
@@ -68,14 +67,8 @@
.content-block.second-block.white
= render 'projects/last_commit', commit: @repository.commit, project: @project
-%section
- - if prefer_readme?
- .project-show-readme
- = render 'projects/readme'
- - else
- .project-show-activity
- = render 'projects/activity'
-
+%div{class: "project-show-#{default_project_view}"}
+ = render default_project_view
- if current_user
- access = user_max_access_in_project(current_user, @project)
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index 02ecbade219..2ddc5d504fa 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -4,5 +4,5 @@
%span.str-truncated
= link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name))
%td.tree_time_ago.cgray
- = render 'spinner'
+ = render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 7e9af19c8ba..3c5edf4b033 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,8 +1,8 @@
-%article.file-holder.readme-holder#README
+%article.file-holder.readme-holder
.file-title
- = link_to '#README' do
+ = blob_icon readme.mode, readme.name
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
%strong
- %i.fa.fa-file
= readme.name
.file-content.wiki
= render_readme(readme)
diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree_content.html.haml
similarity index 52%
rename from app/views/projects/tree/_tree.html.haml
rename to app/views/projects/tree/_tree_content.html.haml
index 7ff48e32e60..ed1f61e9077 100644
--- a/app/views/projects/tree/_tree.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,35 +1,4 @@
-.gray-content-block
- %ul.breadcrumb.repo-breadcrumb
- %li
- = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
- = @project.path
- - tree_breadcrumbs(tree, 6) do |title, path|
- %li
- - if path
- = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- - else
- = link_to title, '#'
- - if allowed_tree_edit?
- %li
- %span.dropdown
- %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
- = icon('plus')
- %ul.dropdown-menu
- %li
- = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do
- = icon('pencil fw')
- Create file
- %li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
- = icon('file fw')
- Upload file
- %li.divider
- %li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
- = icon('folder fw')
- New directory
-
-%div#tree-content-holder.tree-content-holder
+%div.tree-content-holder
.tree-table-holder
%table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
%thead
@@ -60,8 +29,6 @@
- if tree.readme
= render "projects/tree/readme", readme: tree.readme
-%div.tree_progress
-
- if allowed_tree_edit?
= render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
= render 'projects/blob/new_dir'
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
new file mode 100644
index 00000000000..1115ca6b4ca
--- /dev/null
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -0,0 +1,32 @@
+.tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'tree', path: @path
+
+%ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
+ = @project.path
+ - tree_breadcrumbs(tree, 6) do |title, path|
+ %li
+ - if path
+ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
+ - else
+ = link_to title, '#'
+ - if allowed_tree_edit?
+ %li
+ %span.dropdown
+ %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ = icon('plus')
+ %ul.dropdown-menu
+ %li
+ = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do
+ = icon('pencil fw')
+ Create file
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
+ = icon('file fw')
+ Upload file
+ %li.divider
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
+ = icon('folder fw')
+ New directory
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index e87138bf980..cf65057e704 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -5,5 +5,5 @@
- path = flatten_tree(tree_item)
= link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
%td.tree_time_ago.cgray
- = render 'spinner'
+ = render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index dec4677f830..ec14bd7f65a 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -6,12 +6,12 @@
= render 'projects/last_push'
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'tree', path: @path
-
- if can? current_user, :download_code, @project
.tree-download-holder
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true
#tree-holder.tree-holder.clearfix
- = render "tree", tree: @tree
+ .gray-content-block.top-block
+ = render 'projects/tree/tree_header', tree: @tree
+
+ = render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index b23b2f0d5eb..2e4aab36301 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -6,7 +6,7 @@
type: 'button', |
class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
:"data-clone" => project.ssh_url_to_repo, |
- :"data-title" => "Add an SSH key to your profile
to pull or push via SSH",
+ :"data-title" => "Add an SSH key to your profile
to pull or push via SSH.",
:"data-html" => "true",
:"data-container" => "body"}
SSH
@@ -15,7 +15,7 @@
type: 'button', |
class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
:"data-clone" => project.http_url_to_repo, |
- :"data-title" => "Set a password on your account
to pull or push via #{gitlab_config.protocol.upcase}",
+ :"data-title" => "Set a password on your account
to pull or push via #{gitlab_config.protocol.upcase}.",
:"data-html" => "true",
:"data-container" => "body"}
= gitlab_config.protocol.upcase
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 8f16773077e..0e4e9c0987a 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -42,11 +42,10 @@
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Milestone'})
- - if @project
- .filter-item.inline.labels-filter
- = select_tag('label_name', project_labels_options(@project),
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Label'})
+ .filter-item.inline.labels-filter
+ = select_tag('label_name', projects_labels_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Label'})
.pull-right
= render 'shared/sort_dropdown'
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 922b0c6cebf..7f29918dba3 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -1,7 +1,3 @@
-%h4
- Contributions calendar
- .pull-right
- %small Issues, merge requests and push events
#cal-heatmap.calendar
:javascript
new Calendar(
@@ -10,3 +6,5 @@
#{@starting_month},
'#{user_calendar_activities_path}'
);
+
+.calendar-hint Summary of issues, merge requests and push events
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 2a64708d07c..4ea4a1f92c2 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -6,47 +6,72 @@
= render 'shared/show_aside'
-.row
+.cover-block
+ .avatar-holder
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+ .cover-title
+ = @user.name
+
+ .cover-desc
+ %span
+ @#{@user.username}.
+ - if @user.bio.present?
+ %span
+ #{@user.bio}.
+ %span
+ Member since #{@user.created_at.stamp("Aug 21, 2011")}
+
+ .cover-desc
+ - unless @user.public_email.blank?
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ ·
+ = link_to "Skype", "skype:#{@user.skype}"
+ - unless @user.linkedin.blank?
+ ·
+ = link_to "LinkedIn", "http://www.linkedin.com/in/#{@user.linkedin}"
+ - unless @user.twitter.blank?
+ ·
+ = link_to "Twitter", "http://www.twitter.com/#{@user.twitter}"
+ - unless @user.website_url.blank?
+ ·
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ ·
+ = @user.location
+
+
+ .cover-controls
+ - if @user == current_user
+ = link_to profile_path, class: 'btn btn-gray' do
+ = icon('pencil')
+ - elsif current_user
+ .report-abuse
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
+ = icon('exclamation-circle')
+
+.gray-content-block.second-block
+ .user-calendar
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
+
+
+.row.prepend-top-20
%section.col-md-7
- .header-with-avatar
- = link_to avatar_icon(@user, 400), target: '_blank' do
- = image_tag avatar_icon(@user, 90), class: "avatar avatar-tile s90", alt: ''
- %h3
- = @user.name
- - if @user == current_user
- .pull-right.hidden-xs
- = link_to profile_path, class: 'btn btn-sm' do
- = icon('user')
- Profile settings
- - elsif current_user
- .report_abuse.pull-right
- - if @user.abuse_report
- %span#report_abuse_btn.light.btn.btn-sm.btn-close{title: 'Already reported for abuse', data: {toggle: 'tooltip', placement: 'right', container: 'body'}}
- = icon('exclamation-circle')
- - else
- %a.light.btn.btn-sm{href: new_abuse_report_path(user_id: @user.id), title: 'Report abuse', data: {toggle: 'tooltip', placement: 'right', container: 'body'}}
- = icon('exclamation-circle')
-
- .username
- @#{@user.username}
- .description
- - if @user.bio.present?
- = @user.bio
-
- .clearfix
-
- if @groups.any?
.prepend-top-20
%h4 Groups
= render 'groups', groups: @groups
%hr
- .hidden-xs
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
- %hr
%h4
User Activity
@@ -59,7 +84,6 @@
.content_list
= spinner
%aside.col-md-5
- = render 'profile', user: @user
= render 'projects', projects: @projects, contributed_projects: @contributed_projects
:coffeescript
diff --git a/config/routes.rb b/config/routes.rb
index 3dbe2c4dfcc..f6812c9280a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -378,6 +378,7 @@ Gitlab::Application.routes.draw do
[:new, :create, :index], path: "/") do
member do
put :transfer
+ delete :remove_fork
post :archive
post :unarchive
post :toggle_star
diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb
index 378354efd5a..03da29c4c68 100644
--- a/db/fixtures/development/05_users.rb
+++ b/db/fixtures/development/05_users.rb
@@ -1,5 +1,5 @@
Gitlab::Seeder.quiet do
- (2..20).each do |i|
+ 20.times do |i|
begin
User.create!(
username: FFaker::Internet.user_name,
@@ -15,7 +15,7 @@ Gitlab::Seeder.quiet do
end
end
- (1..5).each do |i|
+ 5.times do |i|
begin
User.create!(
username: "user#{i}",
diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb
index a43116829d9..e028ac82ba3 100644
--- a/db/fixtures/development/07_milestones.rb
+++ b/db/fixtures/development/07_milestones.rb
@@ -1,6 +1,6 @@
Gitlab::Seeder.quiet do
Project.all.each do |project|
- (1..5).each do |i|
+ 5.times do |i|
milestone_params = {
title: "v#{i}.0",
description: FFaker::Lorem.sentence,
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index c636e96381c..4fa572fca9b 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -1,6 +1,6 @@
Gitlab::Seeder.quiet do
Project.all.each do |project|
- (1..10).each do |i|
+ 10.times do
issue_params = {
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentence,
diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb
index 3bd4b442ade..74898544a69 100644
--- a/db/fixtures/development/12_snippets.rb
+++ b/db/fixtures/development/12_snippets.rb
@@ -22,7 +22,7 @@ class Member < ActiveRecord::Base
end
eos
- (1..50).each do |i|
+ 50.times do |i|
user = User.all.sample
PersonalSnippet.seed(:id, [{
diff --git a/db/migrate/20151008143519_add_admin_notification_email_setting.rb b/db/migrate/20151008143519_add_admin_notification_email_setting.rb
new file mode 100644
index 00000000000..0bb581efe2c
--- /dev/null
+++ b/db/migrate/20151008143519_add_admin_notification_email_setting.rb
@@ -0,0 +1,5 @@
+class AddAdminNotificationEmailSetting < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :admin_notification_email, :string
+ end
+end
diff --git a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb
new file mode 100644
index 00000000000..7f1af1c7583
--- /dev/null
+++ b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb
@@ -0,0 +1,9 @@
+class AddCiBuildsAndProjectsIndexes < ActiveRecord::Migration
+ def change
+ add_index :ci_projects, :gitlab_id
+ add_index :ci_projects, :shared_runners_enabled
+
+ add_index :ci_builds, :type
+ add_index :ci_builds, :status
+ end
+end
diff --git a/db/migrate/20151016195706_add_notes_line_code_index.rb b/db/migrate/20151016195706_add_notes_line_code_index.rb
new file mode 100644
index 00000000000..aeeb1a759fa
--- /dev/null
+++ b/db/migrate/20151016195706_add_notes_line_code_index.rb
@@ -0,0 +1,5 @@
+class AddNotesLineCodeIndex < ActiveRecord::Migration
+ def change
+ add_index :notes, :line_code
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 885756fef12..b05fa708775 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20151016131433) do
+ActiveRecord::Schema.define(version: 20151016195706) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -46,6 +46,7 @@ ActiveRecord::Schema.define(version: 20151016131433) do
t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
t.text "help_page_text"
+ t.string "admin_notification_email"
end
create_table "audit_events", force: true do |t|
@@ -115,6 +116,8 @@ ActiveRecord::Schema.define(version: 20151016131433) do
add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+ add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
+ add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree
create_table "ci_commits", force: true do |t|
t.integer "project_id"
@@ -190,6 +193,9 @@ ActiveRecord::Schema.define(version: 20151016131433) do
t.text "generated_yaml_config"
end
+ add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree
+ add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree
+
create_table "ci_runner_projects", force: true do |t|
t.integer "runner_id", null: false
t.integer "project_id", null: false
@@ -530,6 +536,7 @@ ActiveRecord::Schema.define(version: 20151016131433) do
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
+ add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
diff --git a/doc/install/installation.md b/doc/install/installation.md
index b154d280471..2e9ac7393e3 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -325,6 +325,7 @@ GitLab Shell is an SSH access and repository management software developed speci
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
cd gitlab-git-http-server
+ sudo -u git -H git checkout 0.3.0
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 7ad4935e839..305017b7048 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -84,6 +84,7 @@ Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
cd gitlab-git-http-server
+sudo -u git -H git checkout 0.2.14
sudo -u git -H make
```
diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb
index 56652ff6f05..499accb0b08 100644
--- a/features/steps/abuse_reports.rb
+++ b/features/steps/abuse_reports.rb
@@ -23,7 +23,7 @@ class Spinach::Features::AbuseReports < Spinach::FeatureSteps
end
step 'I should see a red "Report abuse" button' do
- expect(find(:css, '.report_abuse')).to have_selector(:css, 'span.btn-close')
+ expect(page).to have_button("Already reported for abuse")
end
def user_mike
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 15f77734cb2..d76891d5bde 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -86,13 +86,13 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see project "Forum" README' do
- page.within('#README') do
+ page.within('.readme-holder') do
expect(page).to have_content 'Sample repo for testing gitlab features'
end
end
step 'I should see project "Shop" README' do
- page.within('#README') do
+ page.within('.readme-holder') do
expect(page).to have_content 'testme'
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index c2fb36b4143..67ee66a2058 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -246,8 +246,8 @@ module API
# Example Request:
# DELETE /projects/:id/fork
delete ":id/fork" do
- authenticated_as_admin!
- unless user_project.forked_project_link.nil?
+ authorize! :remove_fork_project, user_project
+ if user_project.forked?
user_project.forked_project_link.destroy
end
end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index 32a368c2e2b..b082bfc434b 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -7,6 +7,14 @@ module Gitlab
module Markdown
# Convert a Markdown String into an HTML-safe String of HTML
#
+ # Note that while the returned HTML will have been sanitized of dangerous
+ # HTML, it may post a risk of information leakage if it's not also passed
+ # through `post_process`.
+ #
+ # Also note that the returned String is always HTML, not XHTML. Views
+ # requiring XHTML, such as Atom feeds, need to call `post_process` on the
+ # result, providing the appropriate `pipeline` option.
+ #
# markdown - Markdown String
# context - Hash of context options passed to our HTML Pipeline
#
@@ -31,6 +39,33 @@ module Gitlab
renderer.render(markdown)
end
+ # Perform post-processing on an HTML String
+ #
+ # This method is used to perform state-dependent changes to a String of
+ # HTML, such as removing references that the current user doesn't have
+ # permission to make (`RedactorFilter`).
+ #
+ # html - String to process
+ # options - Hash of options to customize output
+ # :pipeline - Symbol pipeline type
+ # :project - Project
+ # :user - User object
+ #
+ # Returns an HTML-safe String
+ def self.post_process(html, options)
+ context = {
+ project: options[:project],
+ current_user: options[:user]
+ }
+ doc = post_processor.to_document(html, context)
+
+ if options[:pipeline] == :atom
+ doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
+ else
+ doc.to_html
+ end.html_safe
+ end
+
# Provide autoload paths for filters to prevent a circular dependency error
autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter'
autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter'
@@ -41,6 +76,7 @@ module Gitlab
autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter'
autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter'
autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter'
+ autoload :RedactorFilter, 'gitlab/markdown/redactor_filter'
autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
@@ -50,26 +86,20 @@ module Gitlab
autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter'
autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter'
- # Public: Parse the provided text with GitLab-Flavored Markdown
+ # Public: Parse the provided HTML with GitLab-Flavored Markdown
#
- # text - the source text
- # options - A Hash of options used to customize output (default: {}):
- # :xhtml - output XHTML instead of HTML
- # :reference_only_path - Use relative path for reference links
- def self.gfm(text, options = {})
- return text if text.nil?
-
- # Duplicate the string so we don't alter the original, then call to_str
- # to cast it back to a String instead of a SafeBuffer. This is required
- # for gsub calls to work as we need them to.
- text = text.dup.to_str
-
- options.reverse_merge!(
- xhtml: false,
- reference_only_path: true,
- project: options[:project],
- current_user: options[:current_user]
- )
+ # html - HTML String
+ # options - A Hash of options used to customize output (default: {})
+ # :no_header_anchors - Disable header anchors in TableOfContentsFilter
+ # :path - Current path String
+ # :pipeline - Symbol pipeline type
+ # :project - Current Project object
+ # :project_wiki - Current ProjectWiki object
+ # :ref - Current ref String
+ #
+ # Returns an HTML-safe String
+ def self.gfm(html, options = {})
+ return '' unless html.present?
@pipeline ||= HTML::Pipeline.new(filters)
@@ -78,41 +108,36 @@ module Gitlab
pipeline: options[:pipeline],
# EmojiFilter
- asset_root: Gitlab.config.gitlab.base_url,
asset_host: Gitlab::Application.config.asset_host,
-
- # TableOfContentsFilter
- no_header_anchors: options[:no_header_anchors],
+ asset_root: Gitlab.config.gitlab.base_url,
# ReferenceFilter
- current_user: options[:current_user],
- only_path: options[:reference_only_path],
- project: options[:project],
+ only_path: only_path_pipeline?(options[:pipeline]),
+ project: options[:project],
# RelativeLinkFilter
+ project_wiki: options[:project_wiki],
ref: options[:ref],
requested_path: options[:path],
- project_wiki: options[:project_wiki]
+
+ # TableOfContentsFilter
+ no_header_anchors: options[:no_header_anchors]
}
- result = @pipeline.call(text, context)
-
- save_options = 0
- if options[:xhtml]
- save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML
- end
-
- text = result[:output].to_html(save_with: save_options)
-
- text.html_safe
+ @pipeline.to_html(html, context).html_safe
end
private
- def self.renderer
- @markdown ||= begin
- renderer = Redcarpet::Render::HTML.new
- Redcarpet::Markdown.new(renderer, redcarpet_options)
+ # Check if a pipeline enables the `only_path` context option
+ #
+ # Returns Boolean
+ def self.only_path_pipeline?(pipeline)
+ case pipeline
+ when :atom, :email
+ false
+ else
+ true
end
end
@@ -130,6 +155,17 @@ module Gitlab
}.freeze
end
+ def self.renderer
+ @markdown ||= begin
+ renderer = Redcarpet::Render::HTML.new
+ Redcarpet::Markdown.new(renderer, redcarpet_options)
+ end
+ end
+
+ def self.post_processor
+ @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter])
+ end
+
# Filters used in our pipeline
#
# SanitizationFilter should come first so that all generated reference HTML
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
index bb496135d92..e070edae0a4 100644
--- a/lib/gitlab/markdown/commit_range_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -26,6 +26,18 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ project = Project.find(node.attr("data-project")) rescue nil
+ return unless project
+
+ id = node.attr("data-commit-range")
+ range = CommitRange.new(id, project)
+
+ return unless range.valid_commits?
+
+ { commit_range: range }
+ end
+
def initialize(*args)
super
@@ -53,13 +65,11 @@ module Gitlab
range = CommitRange.new(id, project)
if range.valid_commits?
- push_result(:commit_range, range)
-
url = url_for_commit_range(project, range)
title = range.reference_title
klass = reference_class(:commit_range)
- data = data_attribute(project.id)
+ data = data_attribute(project: project.id, commit_range: id)
project_ref += '@' if project_ref
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
index fcbb2e936a5..8cdbeb1f9cf 100644
--- a/lib/gitlab/markdown/commit_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -26,6 +26,18 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ project = Project.find(node.attr("data-project")) rescue nil
+ return unless project
+
+ id = node.attr("data-commit")
+ commit = commit_from_ref(project, id)
+
+ return unless commit
+
+ { commit: commit }
+ end
+
def call
replace_text_nodes_matching(Commit.reference_pattern) do |content|
commit_link_filter(content)
@@ -39,17 +51,15 @@ module Gitlab
# Returns a String with commit references replaced with links. All links
# have `gfm` and `gfm-commit` class names attached for styling.
def commit_link_filter(text)
- self.class.references_in(text) do |match, commit_ref, project_ref|
+ self.class.references_in(text) do |match, id, project_ref|
project = self.project_from_ref(project_ref)
- if commit = commit_from_ref(project, commit_ref)
- push_result(:commit, commit)
-
+ if commit = self.class.commit_from_ref(project, id)
url = url_for_commit(project, commit)
title = escape_once(commit.link_title)
klass = reference_class(:commit)
- data = data_attribute(project.id)
+ data = data_attribute(project: project.id, commit: id)
project_ref += '@' if project_ref
@@ -62,9 +72,9 @@ module Gitlab
end
end
- def commit_from_ref(project, commit_ref)
+ def self.commit_from_ref(project, id)
if project && project.valid_repo?
- project.commit(commit_ref)
+ project.commit(id)
end
end
diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb
index 855748fdccc..6ab04a584b0 100644
--- a/lib/gitlab/markdown/cross_project_reference.rb
+++ b/lib/gitlab/markdown/cross_project_reference.rb
@@ -13,18 +13,11 @@ module Gitlab
#
# ref - String reference.
#
- # Returns a Project, or nil if the reference can't be accessed
+ # Returns a Project, or nil if the reference can't be found
def project_from_ref(ref)
return context[:project] unless ref
- other = Project.find_with_namespace(ref)
- return nil unless other && user_can_reference_project?(other)
-
- other
- end
-
- def user_can_reference_project?(project, user = context[:current_user])
- Ability.abilities.allowed?(user, :read_project, project)
+ Project.find_with_namespace(ref)
end
end
end
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb
index f7c43e1ca89..8f86f13976a 100644
--- a/lib/gitlab/markdown/external_issue_reference_filter.rb
+++ b/lib/gitlab/markdown/external_issue_reference_filter.rb
@@ -47,8 +47,9 @@ module Gitlab
title = escape_once("Issue in #{project.external_issue_tracker.title}")
klass = reference_class(:issue)
+ data = data_attribute(project: project.id)
- %(#{match})
end
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb
index 01320f80796..481d282f7b1 100644
--- a/lib/gitlab/markdown/issue_reference_filter.rb
+++ b/lib/gitlab/markdown/issue_reference_filter.rb
@@ -27,6 +27,10 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ { issue: LazyReference.new(Issue, node.attr("data-issue")) }
+ end
+
def call
replace_text_nodes_matching(Issue.reference_pattern) do |content|
issue_link_filter(content)
@@ -45,13 +49,11 @@ module Gitlab
project = self.project_from_ref(project_ref)
if project && issue = project.get_issue(id)
- push_result(:issue, issue)
-
url = url_for_issue(id, project, only_path: context[:only_path])
title = escape_once("Issue: #{issue.title}")
klass = reference_class(:issue)
- data = data_attribute(project.id)
+ data = data_attribute(project: project.id, issue: issue.id)
%(#{render_colored_label(label)})
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
index ecbd263d0e0..5bc63269808 100644
--- a/lib/gitlab/markdown/merge_request_reference_filter.rb
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -27,6 +27,10 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ { merge_request: LazyReference.new(MergeRequest, node.attr("data-merge-request")) }
+ end
+
def call
replace_text_nodes_matching(MergeRequest.reference_pattern) do |content|
merge_request_link_filter(content)
@@ -45,11 +49,9 @@ module Gitlab
project = self.project_from_ref(project_ref)
if project && merge_request = project.merge_requests.find_by(iid: id)
- push_result(:merge_request, merge_request)
-
title = escape_once("Merge Request: #{merge_request.title}")
klass = reference_class(:merge_request)
- data = data_attribute(project.id)
+ data = data_attribute(project: project.id, merge_request: merge_request.id)
url = url_for_merge_request(merge_request, project)
diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb
new file mode 100644
index 00000000000..a1f3a8a8ebf
--- /dev/null
+++ b/lib/gitlab/markdown/redactor_filter.rb
@@ -0,0 +1,40 @@
+require 'gitlab/markdown'
+require 'html/pipeline/filter'
+
+module Gitlab
+ module Markdown
+ # HTML filter that removes references to records that the current user does
+ # not have permission to view.
+ #
+ # Expected to be run in its own post-processing pipeline.
+ #
+ class RedactorFilter < HTML::Pipeline::Filter
+ def call
+ doc.css('a.gfm').each do |node|
+ unless user_can_reference?(node)
+ node.replace(node.text)
+ end
+ end
+
+ doc
+ end
+
+ private
+
+ def user_can_reference?(node)
+ if node.has_attribute?('data-reference-filter')
+ reference_type = node.attr('data-reference-filter')
+ reference_filter = reference_type.constantize
+
+ reference_filter.user_can_reference?(current_user, node, context)
+ else
+ true
+ end
+ end
+
+ def current_user
+ context[:current_user]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb
index 9b293c957d6..adaca78ba27 100644
--- a/lib/gitlab/markdown/reference_filter.rb
+++ b/lib/gitlab/markdown/reference_filter.rb
@@ -11,30 +11,57 @@ module Gitlab
# Context options:
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
- #
- # Results:
- # :references - A Hash of references that were found and replaced.
class ReferenceFilter < HTML::Pipeline::Filter
- def initialize(*args)
- super
+ LazyReference = Struct.new(:klass, :ids) do
+ def self.load(refs)
+ lazy_references, values = refs.partition { |ref| ref.is_a?(self) }
+
+ lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs|
+ ids = refs.flat_map(&:ids)
+ klass.where(id: ids)
+ end
- result[:references] = Hash.new { |hash, type| hash[type] = [] }
+ values + lazy_values
+ end
+
+ def load
+ self.klass.where(id: self.ids)
+ end
+ end
+
+ def self.user_can_reference?(user, node, context)
+ if node.has_attribute?('data-project')
+ project_id = node.attr('data-project').to_i
+ return true if project_id == context[:project].try(:id)
+
+ project = Project.find(project_id) rescue nil
+ Ability.abilities.allowed?(user, :read_project, project)
+ else
+ true
+ end
+ end
+
+ def self.referenced_by(node)
+ raise NotImplementedError, "#{self} does not implement #{__method__}"
end
# Returns a data attribute String to attach to a reference link
#
- # id - Object ID
- # type - Object type (default: :project)
+ # attributes - Hash, where the key becomes the data attribute name and the
+ # value is the data attribute value
#
# Examples:
#
- # data_attribute(1) # => "data-project-id=\"1\""
- # data_attribute(2, :user) # => "data-user-id=\"2\""
- # data_attribute(3, :group) # => "data-group-id=\"3\""
+ # data_attribute(project: 1, issue: 2)
+ # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ #
+ # data_attribute(project: 3, merge_request: 4)
+ # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
#
# Returns a String
- def data_attribute(id, type = :project)
- %Q(data-#{type}-id="#{id}")
+ def data_attribute(attributes = {})
+ attributes[:reference_filter] = self.class.name
+ attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
end
def escape_once(html)
@@ -59,16 +86,6 @@ module Gitlab
context[:project]
end
- # Add a reference to the pipeline's result Hash
- #
- # type - Singular Symbol reference type (e.g., :issue, :user, etc.)
- # values - One or more Objects to add
- def push_result(type, *values)
- return if values.empty?
-
- result[:references][type].push(*values)
- end
-
def reference_class(type)
"gfm gfm-#{type}"
end
@@ -85,7 +102,7 @@ module Gitlab
# Yields the current node's String contents. The result of the block will
# replace the node's existing content and update the current document.
#
- # Returns the updated Nokogiri::XML::Document object.
+ # Returns the updated Nokogiri::HTML::DocumentFragment object.
def replace_text_nodes_matching(pattern)
return doc if project.nil?
diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb
new file mode 100644
index 00000000000..00f983675e6
--- /dev/null
+++ b/lib/gitlab/markdown/reference_gatherer_filter.rb
@@ -0,0 +1,63 @@
+require 'gitlab/markdown'
+require 'html/pipeline/filter'
+
+module Gitlab
+ module Markdown
+ # HTML filter that gathers all referenced records that the current user has
+ # permission to view.
+ #
+ # Expected to be run in its own post-processing pipeline.
+ #
+ class ReferenceGathererFilter < HTML::Pipeline::Filter
+ def initialize(*)
+ super
+
+ result[:references] ||= Hash.new { |hash, type| hash[type] = [] }
+ end
+
+ def call
+ doc.css('a.gfm').each do |node|
+ gather_references(node)
+ end
+
+ load_lazy_references unless context[:load_lazy_references] == false
+
+ doc
+ end
+
+ private
+
+ def gather_references(node)
+ return unless node.has_attribute?('data-reference-filter')
+
+ reference_type = node.attr('data-reference-filter')
+ reference_filter = reference_type.constantize
+
+ return if context[:reference_filter] && reference_filter != context[:reference_filter]
+
+ return unless reference_filter.user_can_reference?(current_user, node, context)
+
+ references = reference_filter.referenced_by(node)
+ return unless references
+
+ references.each do |type, values|
+ Array.wrap(values).each do |value|
+ result[:references][type] << value
+ end
+ end
+ end
+
+ # Will load all references of one type using one query.
+ def load_lazy_references
+ refs = result[:references]
+ refs.each do |type, values|
+ refs[type] = ReferenceFilter::LazyReference.load(values)
+ end
+ end
+
+ def current_user
+ context[:current_user]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
index e2cf89cb1d8..f783f951711 100644
--- a/lib/gitlab/markdown/snippet_reference_filter.rb
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -27,6 +27,10 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ { snippet: LazyReference.new(Snippet, node.attr("data-snippet")) }
+ end
+
def call
replace_text_nodes_matching(Snippet.reference_pattern) do |content|
snippet_link_filter(content)
@@ -45,11 +49,9 @@ module Gitlab
project = self.project_from_ref(project_ref)
if project && snippet = project.snippets.find_by(id: id)
- push_result(:snippet, snippet)
-
title = escape_once("Snippet: #{snippet.title}")
klass = reference_class(:snippet)
- data = data_attribute(project.id)
+ data = data_attribute(project: project.id, snippet: snippet.id)
url = url_for_snippet(snippet, project)
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
index 6f436ea7167..2a594e1662e 100644
--- a/lib/gitlab/markdown/user_reference_filter.rb
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -23,6 +23,31 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ if node.has_attribute?('data-group')
+ group = Group.find(node.attr('data-group')) rescue nil
+ return unless group
+
+ { user: group.users }
+ elsif node.has_attribute?('data-user')
+ { user: LazyReference.new(User, node.attr('data-user')) }
+ elsif node.has_attribute?('data-project')
+ project = Project.find(node.attr('data-project')) rescue nil
+ return unless project
+
+ { user: project.team.members.flatten }
+ end
+ end
+
+ def self.user_can_reference?(user, node, context)
+ if node.has_attribute?('data-group')
+ group = Group.find(node.attr('data-group')) rescue nil
+ Ability.abilities.allowed?(user, :read_group, group)
+ else
+ super
+ end
+ end
+
def call
replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content)
@@ -61,14 +86,12 @@ module Gitlab
def link_to_all
project = context[:project]
- # FIXME (rspeicher): Law of Demeter
- push_result(:user, *project.team.members.flatten)
-
url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path])
+ data = data_attribute(project: project.id)
text = User.reference_prefix + 'all'
- %(#{text})
+ %(#{text})
end
def link_to_namespace(namespace)
@@ -80,30 +103,20 @@ module Gitlab
end
def link_to_group(group, namespace)
- return unless user_can_reference_group?(namespace)
-
- push_result(:user, *namespace.users)
-
url = urls.group_url(group, only_path: context[:only_path])
- data = data_attribute(namespace.id, :group)
+ data = data_attribute(group: namespace.id)
text = Group.reference_prefix + group
%(#{text})
end
def link_to_user(user, namespace)
- push_result(:user, namespace.owner)
-
url = urls.user_url(user, only_path: context[:only_path])
- data = data_attribute(namespace.owner_id, :user)
+ data = data_attribute(user: namespace.owner_id)
text = User.reference_prefix + user
%(#{text})
end
-
- def user_can_reference_group?(group)
- Ability.abilities.allowed?(context[:current_user], :read_group, group)
- end
end
end
end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 30497e274c2..da8df8a3025 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -3,11 +3,12 @@ require 'gitlab/markdown'
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
- attr_accessor :project, :current_user
+ attr_accessor :project, :current_user, :load_lazy_references
- def initialize(project, current_user = nil)
+ def initialize(project, current_user = nil, load_lazy_references: true)
@project = project
@current_user = current_user
+ @load_lazy_references = load_lazy_references
end
def analyze(text)
@@ -26,9 +27,9 @@ module Gitlab
def references
@references ||= Hash.new do |references, type|
type = type.to_sym
- return references[type] if references.has_key?(type)
+ next references[type] if references.has_key?(type)
- references[type] = pipeline_result(type).uniq
+ references[type] = pipeline_result(type)
end
end
@@ -41,21 +42,32 @@ module Gitlab
def pipeline_result(filter_type)
return [] if @text.blank?
- klass = filter_type.to_s.camelize + 'ReferenceFilter'
+ klass = "#{filter_type.to_s.camelize}ReferenceFilter"
filter = Gitlab::Markdown.const_get(klass)
context = {
project: project,
current_user: current_user,
+
# We don't actually care about the links generated
only_path: true,
- ignore_blockquotes: true
+ ignore_blockquotes: true,
+
+ # ReferenceGathererFilter
+ load_lazy_references: false,
+ reference_filter: filter
}
- pipeline = HTML::Pipeline.new([filter], context)
+ pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context)
result = pipeline.call(@text)
- result[:references][filter_type]
+ values = result[:references][filter_type].uniq
+
+ if @load_lazy_references
+ values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq
+ end
+
+ values
end
end
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 606bf241db7..2e73f792a9d 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -335,7 +335,7 @@ namespace :gitlab do
print "Redis version >= #{min_redis_version}? ... "
redis_version = run(%W(redis-cli --version))
- redis_version = redis_version.try(:match, /redis-cli (.*)/)
+ redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
puts "yes".green
diff --git a/spec/benchmarks/models/milestone_spec.rb b/spec/benchmarks/models/milestone_spec.rb
new file mode 100644
index 00000000000..a94afc4c40d
--- /dev/null
+++ b/spec/benchmarks/models/milestone_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Milestone, benchmark: true do
+ describe '#sort_issues' do
+ let(:milestone) { create(:milestone) }
+
+ let(:issue1) { create(:issue, milestone: milestone) }
+ let(:issue2) { create(:issue, milestone: milestone) }
+ let(:issue3) { create(:issue, milestone: milestone) }
+
+ let(:issue_ids) { [issue3.id, issue2.id, issue1.id] }
+
+ benchmark_subject { milestone.sort_issues(issue_ids) }
+
+ it { is_expected.to iterate_per_second(500) }
+ end
+end
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
new file mode 100644
index 00000000000..0faab8d7ff0
--- /dev/null
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe AbuseReportsController do
+ let(:reporter) { create(:user) }
+ let(:user) { create(:user) }
+ let(:message) { "This user is a spammer" }
+
+ before do
+ sign_in(reporter)
+ end
+
+ describe "POST create" do
+ context "with admin notification email set" do
+ let(:admin_email) { "admin@example.com"}
+
+ before(:each) do
+ stub_application_setting(admin_notification_email: admin_email)
+ end
+
+ it "sends a notification email" do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to).to eq([admin_email])
+ expect(email.subject).to include(user.username)
+ expect(email.text_part.body).to include(message)
+ end
+
+ it "saves the abuse report" do
+ expect do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+ end.to change { AbuseReport.count }.by(1)
+ end
+ end
+
+ context "without admin notification email set" do
+ before(:each) do
+ stub_application_setting(admin_notification_email: nil)
+ end
+
+ it "does not send a notification email" do
+ expect do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+ end.not_to change { ActionMailer::Base.deliveries.count }
+ end
+
+ it "saves the abuse report" do
+ expect do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+ end.to change { AbuseReport.count }.by(1)
+ end
+ end
+ end
+
+end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 766be578f7f..bbf8adef534 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -41,7 +41,7 @@ describe Import::GithubController do
it "assigns variables" do
@project = create(:project, import_type: 'github', creator_id: user.id)
- stub_client(repos: [@repo], orgs: [@org], org_repos: [@org_repo])
+ stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo])
get :status
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 21beaf37fce..4460bf12f96 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -22,6 +22,34 @@ describe ProjectsController do
end
end
+ context "rendering default project view" do
+ render_views
+
+ it "renders the activity view" do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(user).to receive(:project_view).and_return('activity')
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(response).to render_template('_activity')
+ end
+
+ it "renders the readme view" do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(user).to receive(:project_view).and_return('readme')
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(response).to render_template('_readme')
+ end
+
+ it "renders the files view" do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(user).to receive(:project_view).and_return('files')
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(response).to render_template('_files')
+ end
+ end
+
context "when requested with case sensitive namespace and project path" do
it "redirects to the normalized path for case mismatch" do
get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
@@ -62,4 +90,50 @@ describe ProjectsController do
expect(user.starred?(public_project)).to be_falsey
end
end
+
+ describe "DELETE remove_fork" do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'with forked project' do
+ let(:project_fork) { create(:project, namespace: user.namespace) }
+
+ before do
+ create(:forked_project_link, forked_to_project: project_fork)
+ end
+
+ it 'should remove fork from project' do
+ delete(:remove_fork,
+ namespace_id: project_fork.namespace.to_param,
+ id: project_fork.to_param, format: :js)
+
+ expect(project_fork.forked?).to be_falsey
+ expect(flash[:notice]).to eq('The fork relationship has been removed.')
+ expect(response).to render_template(:remove_fork)
+ end
+ end
+
+ context 'when project not forked' do
+ let(:unforked_project) { create(:project, namespace: user.namespace) }
+
+ it 'should do nothing if project was not forked' do
+ delete(:remove_fork,
+ namespace_id: unforked_project.namespace.to_param,
+ id: unforked_project.to_param, format: :js)
+
+ expect(flash[:notice]).to be_nil
+ expect(response).to render_template(:remove_fork)
+ end
+ end
+ end
+
+ it "does nothing if user is not signed in" do
+ delete(:remove_fork,
+ namespace_id: project.namespace.to_param,
+ id: project.to_param, format: :js)
+ expect(response.status).to eq(401)
+ end
+ end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index c557a1061af..fdd8cf07b12 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -220,7 +220,7 @@ describe 'GitLab Markdown', feature: true do
end
end
- # `markdown` calls these two methods
+ # Fake a `current_user` helper
def current_user
@feat.user
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index aac93b17a38..09fcff2444a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -34,6 +34,27 @@ feature 'Project', feature: true do
end
end
+ describe 'remove forked relationship', js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ before do
+ login_with user
+ create(:forked_project_link, forked_to_project: project)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ it 'should remove fork' do
+ expect(page).to have_content 'Remove fork relationship'
+
+ remove_with_confirm('Remove fork relationship', project.path)
+
+ expect(page).to have_content 'The fork relationship has been removed.'
+ expect(project.forked?).to be_falsey
+ expect(page).not_to have_content 'Remove fork relationship'
+ end
+ end
+
describe 'removal', js: true do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
@@ -45,13 +66,13 @@ feature 'Project', feature: true do
end
it 'should remove project' do
- expect { remove_project }.to change {Project.count}.by(-1)
+ expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
end
end
- def remove_project
- click_button "Remove project"
- fill_in 'confirm_name_input', with: project.path
+ def remove_with_confirm(button_text, confirm_with)
+ click_button button_text
+ fill_in 'confirm_name_input', with: confirm_with
click_button 'Confirm'
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 20ae29e2bd3..762ec25c4f5 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -11,12 +11,15 @@ describe GitlabMarkdownHelper do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:snippet) { create(:project_snippet, project: project) }
- # Helper expects a current_user method.
- let(:current_user) { user }
-
before do
+ # Ensure the generated reference links aren't redacted
+ project.team << [user, :master]
+
# Helper expects a @project instance variable
- @project = project
+ helper.instance_variable_set(:@project, project)
+
+ # Stub the `current_user` helper
+ allow(helper).to receive(:current_user).and_return(user)
end
describe "#markdown" do
@@ -25,23 +28,23 @@ describe GitlabMarkdownHelper do
it "should link to the merge request" do
expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
- expect(markdown(actual)).to match(expected)
+ expect(helper.markdown(actual)).to match(expected)
end
it "should link to the commit" do
expected = namespace_project_commit_path(project.namespace, project, commit)
- expect(markdown(actual)).to match(expected)
+ expect(helper.markdown(actual)).to match(expected)
end
it "should link to the issue" do
expected = namespace_project_issue_path(project.namespace, project, issue)
- expect(markdown(actual)).to match(expected)
+ expect(helper.markdown(actual)).to match(expected)
end
end
describe "override default project" do
let(:actual) { issue.to_reference }
- let(:second_project) { create(:project) }
+ let(:second_project) { create(:project, :public) }
let(:second_issue) { create(:issue, project: second_project) }
it 'should link to the issue' do
@@ -56,7 +59,7 @@ describe GitlabMarkdownHelper do
let(:issues) { create_list(:issue, 2, project: project) }
it 'should handle references nested in links with all the text' do
- actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
+ actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup
@@ -86,7 +89,7 @@ describe GitlabMarkdownHelper do
end
it 'should forward HTML options' do
- actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
+ actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
doc = Nokogiri::HTML.parse(actual)
expect(doc.css('a')).to satisfy do |v|
@@ -97,13 +100,13 @@ describe GitlabMarkdownHelper do
it "escapes HTML passed in as the body" do
actual = "This is a