Merge branch 'master' into jrochkind/gitlab-ce-fix_2839_send_abuse_report_notify

[ci skip]
This commit is contained in:
Douwe Maan 2015-10-19 13:48:52 +02:00
commit 47194545c7
60 changed files with 579 additions and 182 deletions

View file

@ -3,12 +3,17 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.2.0 (unreleased) v 8.2.0 (unreleased)
- Show last project commit to default branch on project home page - Show last project commit to default branch on project home page
- Highlight comment based on anchor in URL - 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) v 8.1.0 (unreleased)
- Send an email to admin email when a user is reported for spam (Jonathan Rochkind) - Send an email to admin email when a user is reported for spam (Jonathan Rochkind)
- 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 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) - 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 - 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) - 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) - Make diff file view easier to use on mobile screens (Stan Hu)
- Improved performance of finding users by username or Email address - Improved performance of finding users by username or Email address

View file

@ -65,3 +65,48 @@
line-height: 42px; 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;
}
}

View file

@ -10,6 +10,10 @@
border-bottom: 1px solid #E7E9EE; border-bottom: 1px solid #E7E9EE;
margin-bottom: 1em; margin-bottom: 1em;
&.readme-holder {
border-bottom: 0;
}
table { table {
@extend .table; @extend .table;
} }

View file

@ -18,7 +18,6 @@
font-family: $monospace_font; font-family: $monospace_font;
white-space: pre; white-space: pre;
word-wrap: normal; word-wrap: normal;
padding: 1px 2px;
} }
kbd { kbd {

View file

@ -132,6 +132,11 @@ form.edit-issue {
} }
} }
.issue-closed-by-widget {
padding: 16px 0;
margin: 0px;
}
.issue-form .select2-container { .issue-form .select2-container {
width: 250px !important; width: 250px !important;
} }

View file

@ -47,3 +47,9 @@
} }
} }
} }
.calendar-hint {
margin-top: -12px;
float: right;
font-size: 12px;
}

View file

@ -544,5 +544,5 @@ pre.light-well {
} }
.project-show-readme .readme-holder { .project-show-readme .readme-holder {
padding: 7px; border-top: 0;
} }

View file

@ -4,14 +4,6 @@
margin-right: -$gl-padding; margin-right: -$gl-padding;
} }
.tree_progress {
display: none;
margin: 20px;
&.loading {
display: block;
}
}
.tree-table { .tree-table {
margin-bottom: 0; margin-bottom: 0;

View file

@ -14,6 +14,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow issues bulk update # Allow issues bulk update
before_action :authorize_admin_issues!, only: [: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 respond_to :html
def index def index
@ -112,6 +115,10 @@ class Projects::IssuesController < Projects::ApplicationController
render nothing: true render nothing: true
end end
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
protected protected
def issue def issue

View file

@ -259,7 +259,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commits = @merge_request.commits @commits = @merge_request.commits
@merge_request_diff = @merge_request.merge_request_diff @merge_request_diff = @merge_request.merge_request_diff
if @merge_request.locked_long_ago? if @merge_request.locked_long_ago?
@merge_request.unlock_mr @merge_request.unlock_mr
@merge_request.close @merge_request.close

View file

@ -75,11 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def sort_issues def sort_issues
@issues = @milestone.issues.where(id: params['sortable_issue']) @milestone.sort_issues(params['sortable_issue'].map(&:to_i))
@issues.each do |issue|
issue.position = params['sortable_issue'].index(issue.id.to_s) + 1
issue.save
end
render json: { saved: true } render json: { saved: true }
end end

View file

@ -1,11 +1,14 @@
class ProjectsController < ApplicationController class ProjectsController < ApplicationController
include ExtractsPath
prepend_before_filter :render_go_import, only: [:show] prepend_before_filter :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity] skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create] before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create] before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize # 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] before_action :event_filter, only: [:show, :activity]
layout :determine_layout layout :determine_layout
@ -56,6 +59,8 @@ class ProjectsController < ApplicationController
end end
def transfer def transfer
return access_denied! unless can?(current_user, :change_namespace, @project)
namespace = Namespace.find_by(id: params[:new_namespace_id]) namespace = Namespace.find_by(id: params[:new_namespace_id])
::Projects::TransferService.new(project, current_user).execute(namespace) ::Projects::TransferService.new(project, current_user).execute(namespace)
@ -64,6 +69,15 @@ class ProjectsController < ApplicationController
end end
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 def activity
respond_to do |format| respond_to do |format|
format.html format.html
@ -139,6 +153,7 @@ class ProjectsController < ApplicationController
def archive def archive
return access_denied! unless can?(current_user, :archive_project, @project) return access_denied! unless can?(current_user, :archive_project, @project)
@project.archive! @project.archive!
respond_to do |format| respond_to do |format|
@ -148,6 +163,7 @@ class ProjectsController < ApplicationController
def unarchive def unarchive
return access_denied! unless can?(current_user, :archive_project, @project) return access_denied! unless can?(current_user, :archive_project, @project)
@project.unarchive! @project.unarchive!
respond_to do |format| respond_to do |format|
@ -225,4 +241,14 @@ class ProjectsController < ApplicationController
render "go_import", layout: false render "go_import", layout: false
end 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 end

View file

@ -83,6 +83,10 @@ module IssuesHelper
end end
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 # Required for Gitlab::Markdown::IssueReferenceFilter
module_function :url_for_issue module_function :url_for_issue
end end

View file

@ -34,7 +34,8 @@ module PreferencesHelper
def project_view_choices def project_view_choices
[ [
['Readme (default)', :readme], ['Readme (default)', :readme],
['Activity view', :activity] ['Activity view', :activity],
['Files view', :files]
] ]
end end
@ -46,8 +47,7 @@ module PreferencesHelper
Gitlab::ColorSchemes.for_user(current_user).css_class Gitlab::ColorSchemes.for_user(current_user).css_class
end end
def prefer_readme? def default_project_view
!current_user || current_user ? current_user.project_view : 'readme'
current_user.project_view == 'readme'
end end
end end

View file

@ -70,6 +70,10 @@ module ProjectsHelper
"You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?" "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
end 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 def project_nav_tabs
@nav_tabs ||= get_project_nav_tabs(@project, current_user) @nav_tabs ||= get_project_nav_tabs(@project, current_user)
end end

View file

@ -189,7 +189,8 @@ class Ability
:change_visibility_level, :change_visibility_level,
:rename_project, :rename_project,
:remove_project, :remove_project,
:archive_project :archive_project,
:remove_fork_project
] ]
end end

View file

@ -85,6 +85,10 @@ module Issuable
assignee_id_changed? assignee_id_changed?
end end
def open?
opened? || reopened?
end
# #
# Votes # Votes
# #

View file

@ -95,4 +95,14 @@ class Issue < ActiveRecord::Base
def source_project def source_project
project project
end 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 end

View file

@ -222,10 +222,6 @@ class MergeRequest < ActiveRecord::Base
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end end
def open?
opened? || reopened?
end
def work_in_progress? def work_in_progress?
!!(title =~ /\A\[?WIP\]?:? /i) !!(title =~ /\A\[?WIP\]?:? /i)
end end
@ -294,6 +290,10 @@ class MergeRequest < ActiveRecord::Base
target_project target_project
end 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. # Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author) def closes_issues(current_user = self.author)
if target_branch == project.default_branch if target_branch == project.default_branch

View file

@ -105,4 +105,36 @@ class Milestone < ActiveRecord::Base
def author_id def author_id
nil nil
end 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 end

View file

@ -183,7 +183,7 @@ class User < ActiveRecord::Base
# User's Project preference # User's Project preference
# Note: When adding an option, it MUST go on the end of the array. # 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 alias_attribute :private_token, :authentication_token

View file

@ -79,7 +79,7 @@ class GitPushService
authors = Hash.new do |hash, commit| authors = Hash.new do |hash, commit|
email = commit.author_email 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) hash[email] = commit_user(commit)
end end

View file

@ -6,6 +6,7 @@ module MergeRequests
# #
class PostMergeService < MergeRequests::BaseService class PostMergeService < MergeRequests::BaseService
def execute(merge_request) def execute(merge_request)
close_issues(merge_request)
merge_request.mark_as_merged merge_request.mark_as_merged
create_merge_event(merge_request, current_user) create_merge_event(merge_request, current_user)
create_note(merge_request) create_note(merge_request)
@ -15,6 +16,15 @@ module MergeRequests
private 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) def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user) EventCreateService.new.merge_mr(merge_request, current_user)
end end

View file

@ -14,7 +14,7 @@
%strong %strong
= link_to user_path(@user) do = link_to user_path(@user) do
= @user.username = @user.username
= render 'users/profile', user: @user = render 'admin/users/profile', user: @user
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading

View file

@ -41,4 +41,8 @@
#{link_to "view it on GitLab", @target_url}. #{link_to "view it on GitLab", @target_url}.
- else - else
#{link_to "View it on GitLab", @target_url} #{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 = email_action @target_url

View file

@ -38,7 +38,7 @@
.col-sm-10 .col-sm-10
= f.select :layout, layout_choices, {}, class: 'form-control' = f.select :layout, layout_choices, {}, class: 'form-control'
.help-block .help-block
Choose between fixed (max. 1200px) and fluid (100%) application layout Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group .form-group
= f.label :dashboard, class: 'control-label' do = f.label :dashboard, class: 'control-label' do
Default Dashboard Default Dashboard
@ -52,6 +52,6 @@
.col-sm-10 .col-sm-10
= f.select :project_view, project_view_choices, {}, class: 'form-control' = f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block .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 .panel-footer
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'

View file

@ -1,4 +1,3 @@
= render 'projects/last_push'
.gray-content-block.activity-filter-block .gray-content-block.activity-filter-block
- if current_user - if current_user
.pull-right .pull-right

View file

@ -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

View file

@ -1,12 +1,11 @@
- if readme = @repository.readme - if readme = @repository.readme
%article.readme-holder#README %article.file-holder.readme-holder
.clearfix .file-title
.pull-right = blob_icon readme.mode, readme.name
&nbsp; = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
- if can?(current_user, :push_code, @project) %strong
= link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do = readme.name
%i.fa-align.fa.fa-pencil .file-content.wiki
.wiki
= cache(readme_cache_key) do = cache(readme_cache_key) do
= render_readme(readme) = render_readme(readme)
- else - else

View file

@ -1,4 +1,6 @@
- page_title "Activity" - page_title "Activity"
- header_title project_title(@project, "Activity", activity_project_path(@project)) - header_title project_title(@project, "Activity", activity_project_path(@project))
= render 'projects/last_push'
= render 'projects/activity' = render 'projects/activity'

View file

@ -1,19 +1,22 @@
%ul.breadcrumb.repo-breadcrumb .gray-content-block.top-block
%li .tree-ref-holder
%i.fa.fa-angle-right = render 'shared/ref_switcher', destination: 'blob', path: @path
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path %ul.breadcrumb.repo-breadcrumb
- tree_breadcrumbs(@tree, 6) do |title, path|
%li %li
- if path = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
- if path.end_with?(@path) = @project.path
= link_to namespace_project_blob_path(@project.namespace, @project, path) do - tree_breadcrumbs(@tree, 6) do |title, path|
%strong %li
= truncate(title, length: 40) - 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 - else
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) = link_to title, '#'
- else
= link_to title, '#'
%ul.blob-commit-info.hidden-xs %ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)

View file

@ -3,9 +3,6 @@
= render 'projects/last_push' = render 'projects/last_push'
%div.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
%div#tree-holder.tree-holder %div#tree-holder.tree-holder
= render 'blob', blob: @blob = render 'blob', blob: @blob

View file

@ -6,7 +6,7 @@
%tr %tr
%th %th
%th Service %th Service
%th Desription %th Description
%th Last edit %th Last edit
- @services.sort_by(&:title).each do |service| - @services.sort_by(&:title).each do |service|
%tr %tr

View file

@ -189,6 +189,21 @@
- else - else
.nothing-here-block Only the project owner can transfer a project .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) - if can?(current_user, :remove_project, @project)
.panel.panel-default.panel.panel-danger .panel.panel-default.panel.panel-danger
.panel-heading Remove project .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) } = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
- else - 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 .save-project-loader.hide
.center .center

View file

@ -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.

View file

@ -46,6 +46,7 @@
= markdown(@issue.description) = markdown(@issue.description)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @issue.description = @issue.description
- if @closed_by_merge_requests.present?
= render 'projects/issues/closed_by_box'
.issue-discussion .issue-discussion
= render 'projects/issues/discussion' = render 'projects/issues/discussion'

View file

@ -0,0 +1,2 @@
:plain
location.href = "#{edit_namespace_project_path(@project.namespace, @project)}";

View file

@ -7,8 +7,7 @@
= render 'shared/no_ssh' = render 'shared/no_ssh'
= render 'shared/no_password' = render 'shared/no_password'
- if prefer_readme? = render 'projects/last_push'
= render 'projects/last_push'
= render "home_panel" = render "home_panel"
@ -28,7 +27,7 @@
= link_to project_files_path(@project) do = link_to project_files_path(@project) do
= repository_size = repository_size
- if !prefer_readme? && @repository.readme - if default_project_view != 'readme' && @repository.readme
%li %li
= link_to 'Readme', readme_path(@project) = link_to 'Readme', readme_path(@project)
@ -68,14 +67,8 @@
.content-block.second-block.white .content-block.second-block.white
= render 'projects/last_commit', commit: @repository.commit, project: @project = render 'projects/last_commit', commit: @repository.commit, project: @project
%section %div{class: "project-show-#{default_project_view}"}
- if prefer_readme? = render default_project_view
.project-show-readme
= render 'projects/readme'
- else
.project-show-activity
= render 'projects/activity'
- if current_user - if current_user
- access = user_max_access_in_project(current_user, @project) - access = user_max_access_in_project(current_user, @project)

View file

@ -4,5 +4,5 @@
%span.str-truncated %span.str-truncated
= link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)) = 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 %td.tree_time_ago.cgray
= render 'spinner' = render 'projects/tree/spinner'
%td.hidden-xs.tree_commit %td.hidden-xs.tree_commit

View file

@ -1,8 +1,8 @@
%article.file-holder.readme-holder#README %article.file-holder.readme-holder
.file-title .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 %strong
%i.fa.fa-file
= readme.name = readme.name
.file-content.wiki .file-content.wiki
= render_readme(readme) = render_readme(readme)

View file

@ -1,35 +1,4 @@
.gray-content-block %div.tree-content-holder
%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
.tree-table-holder .tree-table-holder
%table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
%thead %thead
@ -60,8 +29,6 @@
- if tree.readme - if tree.readme
= render "projects/tree/readme", readme: tree.readme = render "projects/tree/readme", readme: tree.readme
%div.tree_progress
- if allowed_tree_edit? - 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/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' = render 'projects/blob/new_dir'

View file

@ -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

View file

@ -5,5 +5,5 @@
- path = flatten_tree(tree_item) - path = flatten_tree(tree_item)
= link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)) = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
%td.tree_time_ago.cgray %td.tree_time_ago.cgray
= render 'spinner' = render 'projects/tree/spinner'
%td.hidden-xs.tree_commit %td.hidden-xs.tree_commit

View file

@ -6,12 +6,12 @@
= render 'projects/last_push' = render 'projects/last_push'
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
- if can? current_user, :download_code, @project - if can? current_user, :download_code, @project
.tree-download-holder .tree-download-holder
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true = 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 #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

View file

@ -1,7 +1,3 @@
%h4
Contributions calendar
.pull-right
%small Issues, merge requests and push events
#cal-heatmap.calendar #cal-heatmap.calendar
:javascript :javascript
new Calendar( new Calendar(
@ -10,3 +6,5 @@
#{@starting_month}, #{@starting_month},
'#{user_calendar_activities_path}' '#{user_calendar_activities_path}'
); );
.calendar-hint Summary of issues, merge requests and push events

View file

@ -6,47 +6,72 @@
= render 'shared/show_aside' = 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?
&middot;
= link_to "Skype", "skype:#{@user.skype}"
- unless @user.linkedin.blank?
&middot;
= link_to "LinkedIn", "http://www.linkedin.com/in/#{@user.linkedin}"
- unless @user.twitter.blank?
&middot;
= link_to "Twitter", "http://www.twitter.com/#{@user.twitter}"
- unless @user.website_url.blank?
&middot;
= link_to @user.short_website_url, @user.full_website_url
- unless @user.location.blank?
&middot;
= @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 %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? - if @groups.any?
.prepend-top-20 .prepend-top-20
%h4 Groups %h4 Groups
= render 'groups', groups: @groups = render 'groups', groups: @groups
%hr %hr
.hidden-xs
.user-calendar
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
%hr
%h4 %h4
User Activity User Activity
@ -59,7 +84,6 @@
.content_list .content_list
= spinner = spinner
%aside.col-md-5 %aside.col-md-5
= render 'profile', user: @user
= render 'projects', projects: @projects, contributed_projects: @contributed_projects = render 'projects', projects: @projects, contributed_projects: @contributed_projects
:coffeescript :coffeescript

View file

@ -378,6 +378,7 @@ Gitlab::Application.routes.draw do
[:new, :create, :index], path: "/") do [:new, :create, :index], path: "/") do
member do member do
put :transfer put :transfer
delete :remove_fork
post :archive post :archive
post :unarchive post :unarchive
post :toggle_star post :toggle_star

View file

@ -23,7 +23,7 @@ class Spinach::Features::AbuseReports < Spinach::FeatureSteps
end end
step 'I should see a red "Report abuse" button' do 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 end
def user_mike def user_mike

View file

@ -86,13 +86,13 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end end
step 'I should see project "Forum" README' do 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' expect(page).to have_content 'Sample repo for testing gitlab features'
end end
end end
step 'I should see project "Shop" README' do step 'I should see project "Shop" README' do
page.within('#README') do page.within('.readme-holder') do
expect(page).to have_content 'testme' expect(page).to have_content 'testme'
end end
end end

View file

@ -246,8 +246,8 @@ module API
# Example Request: # Example Request:
# DELETE /projects/:id/fork # DELETE /projects/:id/fork
delete ":id/fork" do delete ":id/fork" do
authenticated_as_admin! authorize! :remove_fork_project, user_project
unless user_project.forked_project_link.nil? if user_project.forked?
user_project.forked_project_link.destroy user_project.forked_project_link.destroy
end end
end end

View file

@ -27,7 +27,7 @@ module Gitlab
def references def references
@references ||= Hash.new do |references, type| @references ||= Hash.new do |references, type|
type = type.to_sym 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) references[type] = pipeline_result(type)
end end

View file

@ -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

View file

@ -22,6 +22,34 @@ describe ProjectsController do
end end
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 context "when requested with case sensitive namespace and project path" do
it "redirects to the normalized path for case mismatch" do it "redirects to the normalized path for case mismatch" do
get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase 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 expect(user.starred?(public_project)).to be_falsey
end end
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 end

View file

@ -34,6 +34,27 @@ feature 'Project', feature: true do
end end
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 describe 'removal', js: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) } let(:project) { create(:project, namespace: user.namespace) }
@ -45,13 +66,13 @@ feature 'Project', feature: true do
end end
it 'should remove project' do 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
end end
def remove_project def remove_with_confirm(button_text, confirm_with)
click_button "Remove project" click_button button_text
fill_in 'confirm_name_input', with: project.path fill_in 'confirm_name_input', with: confirm_with
click_button 'Confirm' click_button 'Confirm'
end end
end end

View file

@ -117,4 +117,14 @@ describe IssuesHelper do
end end
end end
describe "#merge_requests_sentence" do
subject { merge_requests_sentence(merge_requests)}
let(:merge_requests) do
[ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
build(:merge_request, iid: 3)]
end
it { is_expected.to eq("!1, !2, or !3") }
end
end end

View file

@ -68,7 +68,6 @@ describe Issue, "Issuable" do
end end
end end
describe "#to_hook_data" do describe "#to_hook_data" do
let(:hook_data) { issue.to_hook_data(user) } let(:hook_data) { issue.to_hook_data(user) }

View file

@ -68,6 +68,43 @@ describe Issue do
end end
end end
describe '#closed_by_merge_requests' do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project, state: "opened")}
let(:closed_issue) { build(:issue, project: project, state: "closed")}
let(:mr) do
opts = {
title: 'Awesome merge_request',
description: "Fixes #{issue.to_reference}",
source_branch: 'feature',
target_branch: 'master'
}
MergeRequests::CreateService.new(project, project.owner, opts).execute
end
let(:closed_mr) do
opts = {
title: 'Awesome merge_request 2',
description: "Fixes #{issue.to_reference}",
source_branch: 'feature',
target_branch: 'master',
state: 'closed'
}
MergeRequests::CreateService.new(project, project.owner, opts).execute
end
it 'returns the merge request to close this issue' do
allow(mr).to receive(:closes_issue?).with(issue).and_return(true)
expect(issue.closed_by_merge_requests).to eq([mr])
end
it "returns an empty array when the current issue is closed already" do
expect(closed_issue.closed_by_merge_requests).to eq([])
end
end
it_behaves_like 'an editable mentionable' do it_behaves_like 'an editable mentionable' do
subject { create(:issue) } subject { create(:issue) }

View file

@ -140,4 +140,32 @@ describe Milestone do
end end
end end
describe '#sort_issues' do
let(:milestone) { create(:milestone) }
let(:issue1) { create(:issue, milestone: milestone, position: 1) }
let(:issue2) { create(:issue, milestone: milestone, position: 2) }
let(:issue3) { create(:issue, milestone: milestone, position: 3) }
let(:issue4) { create(:issue, position: 42) }
it 'sorts the given issues' do
milestone.sort_issues([issue3.id, issue2.id, issue1.id])
issue1.reload
issue2.reload
issue3.reload
expect(issue1.position).to eq(3)
expect(issue2.position).to eq(2)
expect(issue3.position).to eq(1)
end
it 'ignores issues not part of the milestone' do
milestone.sort_issues([issue3.id, issue2.id, issue1.id, issue4.id])
issue4.reload
expect(issue4.position).to eq(42)
end
end
end end

View file

@ -606,28 +606,42 @@ describe API::API, api: true do
describe 'DELETE /projects/:id/fork' do describe 'DELETE /projects/:id/fork' do
it "shouldn't available for non admin users" do it "shouldn't be visible to users outside group" do
delete api("/projects/#{project_fork_target.id}/fork", user) delete api("/projects/#{project_fork_target.id}/fork", user)
expect(response.status).to eq(403) expect(response.status).to eq(404)
end end
it 'should make forked project unforked' do context 'when users belong to project group' do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) let(:project_fork_target) { create(:project, group: create(:group)) }
project_fork_target.reload
expect(project_fork_target.forked_from_project).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
delete api("/projects/#{project_fork_target.id}/fork", admin)
expect(response.status).to eq(200)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_nil
expect(project_fork_target.forked?).not_to be_truthy
end
it 'should be idempotent if not forked' do before do
expect(project_fork_target.forked_from_project).to be_nil project_fork_target.group.add_owner user
delete api("/projects/#{project_fork_target.id}/fork", admin) project_fork_target.group.add_developer user2
expect(response.status).to eq(200) end
expect(project_fork_target.reload.forked_from_project).to be_nil
it 'should be forbidden to non-owner users' do
delete api("/projects/#{project_fork_target.id}/fork", user2)
expect(response.status).to eq(403)
end
it 'should make forked project unforked' do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
project_fork_target.reload
expect(project_fork_target.forked_from_project).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
delete api("/projects/#{project_fork_target.id}/fork", admin)
expect(response.status).to eq(200)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_nil
expect(project_fork_target.forked?).not_to be_truthy
end
it 'should be idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
delete api("/projects/#{project_fork_target.id}/fork", admin)
expect(response.status).to eq(200)
expect(project_fork_target.reload.forked_from_project).to be_nil
end
end end
end end
end end