Commit 1f4dc0b1 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '8-2-stable' of gitlab.com:gitlab-org/gitlab-ce into 8-2-stable-ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 1a6353b3 b6f0eddc
...@@ -9,6 +9,8 @@ v 8.2.0 (unreleased) ...@@ -9,6 +9,8 @@ v 8.2.0 (unreleased)
v 8.3.0 (unreleased) v 8.3.0 (unreleased)
v 8.2.0 v 8.2.0
- Improved performance of finding projects and groups in various places
- Improved performance of rendering user profile pages and Atom feeds
- Fix grouping of contributors by email in graph. - Fix grouping of contributors by email in graph.
- Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
- Fix Drone CI service template not saving properly (Stan Hu) - Fix Drone CI service template not saving properly (Stan Hu)
...@@ -17,6 +19,10 @@ v 8.2.0 ...@@ -17,6 +19,10 @@ v 8.2.0
- Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu)
- Improved performance of finding users by one of their Email addresses - Improved performance of finding users by one of their Email addresses
- Add allow_failure field to commit status API (Stan Hu) - Add allow_failure field to commit status API (Stan Hu)
- Commits without .gitlab-ci.yml are marked as skipped
- Save detailed error when YAML syntax is invalid
- Since GitLab CI is enabled by default, remove enabling it by pushing .gitlab-ci.yml
- Added build artifacts
- Improved performance of replacing references in comments - Improved performance of replacing references in comments
- 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
...@@ -34,6 +40,7 @@ v 8.2.0 ...@@ -34,6 +40,7 @@ v 8.2.0
- Allow to define cache in `.gitlab-ci.yml` - Allow to define cache in `.gitlab-ci.yml`
- Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
- Remove deprecated CI events from project settings page - Remove deprecated CI events from project settings page
- Improve personal snippet access workflow (Douglas Alexandre)
- [API] Add ability to fetch the commit ID of the last commit that actually touched a file - [API] Add ability to fetch the commit ID of the last commit that actually touched a file
- Fix omniauth documentation setting for omnibus configuration (Jon Cairns) - Fix omniauth documentation setting for omnibus configuration (Jon Cairns)
- Add "New file" link to dropdown on project page - Add "New file" link to dropdown on project page
...@@ -55,7 +62,9 @@ v 8.2.0 ...@@ -55,7 +62,9 @@ v 8.2.0
- Fix trailing whitespace issue in merge request/issue title - Fix trailing whitespace issue in merge request/issue title
- Fix bug when milestone/label filter was empty for dashboard issues page - Fix bug when milestone/label filter was empty for dashboard issues page
- Add ability to create milestone in group projects from single form - Add ability to create milestone in group projects from single form
- Add option to create merge request when editing/creating a file (Dirceu Tiegs)
- Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez) - Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez)
- Add Award Emoji to issue and merge request pages
v 8.1.4 v 8.1.4
- Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id) ->
addAward: (emoji) ->
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
addAwardToEmojiBar: (emoji, custom_path = '') ->
if @exist(emoji)
if @isActive(emoji)
@decrementCounter(emoji)
else
counter = @findEmojiIcon(emoji).siblings(".counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
else
@createEmoji(emoji, custom_path)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0
isActive: (emoji) ->
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
counter.parent().removeClass("active")
@removeMeFromAuthorList(emoji)
else
award = counter.parent()
award.tooltip("destroy")
award.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors = _.without(authors, "me").join(", ")
award_block.attr("title", authors)
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors.push("me")
award_block.attr("title", authors.join(", "))
@resetTooltip(award_block)
resetTooltip: (award) ->
award.tooltip("destroy")
# "destroy" call is asynchronous, this is why we need to set timeout.
setTimeout (->
award.tooltip()
), 200
createEmoji: (emoji, custom_path) ->
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon' data-emoji='" + emoji + "'>")
nodes.push(@getImage(emoji, custom_path))
nodes.push("</div>")
nodes.push("<div class='counter'>1")
nodes.push("</div></div>")
$(".awards-controls").before(nodes.join("\n"))
$(".award").tooltip()
getImage: (emoji, custom_path) ->
if custom_path
$(".awards-menu li").first().html().replace(/emoji\/.*\.png/, custom_path)
else
$("li[data-emoji='" + emoji + "']").html()
postEmoji: (emoji, callback) ->
$.post @post_emoji_url, { note: {
note: ":" + emoji + ":"
noteable_type: @noteable_type
noteable_id: @noteable_id
}},(data) ->
if data.ok
callback.call()
findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']")
\ No newline at end of file
...@@ -23,18 +23,6 @@ class @BlobFileDropzone ...@@ -23,18 +23,6 @@ class @BlobFileDropzone
init: -> init: ->
this.on 'addedfile', (file) -> this.on 'addedfile', (file) ->
$('.dropzone-alerts').html('').hide() $('.dropzone-alerts').html('').hide()
commit_message = form.find('#commit_message')[0]
if /^Upload/.test(commit_message.placeholder)
commit_message.placeholder = 'Upload ' + file.name
return
this.on 'removedfile', (file) ->
commit_message = form.find('#commit_message')[0]
if /^Upload/.test(commit_message.placeholder)
commit_message.placeholder = 'Upload new file'
return return
...@@ -47,8 +35,9 @@ class @BlobFileDropzone ...@@ -47,8 +35,9 @@ class @BlobFileDropzone
return return
this.on 'sending', (file, xhr, formData) -> this.on 'sending', (file, xhr, formData) ->
formData.append('new_branch', form.find('#new_branch').val()) formData.append('new_branch', form.find('.js-new-branch').val())
formData.append('commit_message', form.find('#commit_message').val()) formData.append('create_merge_request', form.find('.js-create-merge-request').val())
formData.append('commit_message', form.find('.js-commit-message').val())
return return
# Override behavior of adding error underneath preview # Override behavior of adding error underneath preview
......
...@@ -9,13 +9,24 @@ $ -> ...@@ -9,13 +9,24 @@ $ ->
clipboard.on 'success', (e) -> clipboard.on 'success', (e) ->
$(e.trigger). $(e.trigger).
tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!'). tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!').
tooltip('show') tooltip('show').
one('mouseleave', -> $(this).tooltip('hide'))
# Clear the selection and blur the trigger so it loses its border # Clear the selection and blur the trigger so it loses its border
e.clearSelection() e.clearSelection()
$(e.trigger).blur() $(e.trigger).blur()
# Manually hide the tooltip after 1 second # Safari doesn't support `execCommand`, so instead we inform the user to
setTimeout(-> # copy manually.
$(e.trigger).tooltip('hide') #
, 1000) # See http://clipboardjs.com/#browser-support
clipboard.on 'error', (e) ->
if /Mac/i.test(navigator.userAgent)
title = "Press &#8984;-C to copy"
else
title = "Press Ctrl-C to copy"
$(e.trigger).
tooltip(trigger: 'manual', placement: 'auto bottom', html: true, title: title).
tooltip('show').
one('mouseleave', -> $(this).tooltip('hide'))
class @NewCommitForm
constructor: (form) ->
@newBranch = form.find('.js-new-branch')
@originalBranch = form.find('.js-original-branch')
@createMergeRequest = form.find('.js-create-merge-request')
@createMergeRequestFormGroup = form.find('.js-create-merge-request-form-group')
@renderDestination()
@newBranch.keyup @renderDestination
renderDestination: =>
different = @newBranch.val() != @originalBranch.val()
if different
@createMergeRequestFormGroup.show()
@createMergeRequest.prop('checked', true) unless @wasDifferent
else
@createMergeRequestFormGroup.hide()
@createMergeRequest.prop('checked', false)
@wasDifferent = different
...@@ -113,13 +113,16 @@ class @Notes ...@@ -113,13 +113,16 @@ class @Notes
renderNote: (note) -> renderNote: (note) ->
# render note if it not present in loaded list # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
if @isNewNote(note) if @isNewNote(note) && !note.award
@note_ids.push(note.id) @note_ids.push(note.id)
$('ul.main-notes-list'). $('ul.main-notes-list').
append(note.html). append(note.html).
syntaxHighlight() syntaxHighlight()
@initTaskList() @initTaskList()
if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
### ###
Check if note does not exists on page Check if note does not exists on page
### ###
...@@ -255,7 +258,6 @@ class @Notes ...@@ -255,7 +258,6 @@ class @Notes
### ###
addNote: (xhr, note, status) => addNote: (xhr, note, status) =>
@renderNote(note) @renderNote(note)
@updateVotes()
### ###
Called in response to the new note form being submitted Called in response to the new note form being submitted
...@@ -473,9 +475,6 @@ class @Notes ...@@ -473,9 +475,6 @@ class @Notes
form = $(e.target).closest(".js-discussion-note-form") form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form) @removeDiscussionNoteForm(form)
updateVotes: ->
true
### ###
Called after an attachment file has been selected. Called after an attachment file has been selected.
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
li { li {
padding: 3px 0px; padding: 3px 0px;
line-height: 20px;
} }
} }
.new-file { .new-file {
......
...@@ -101,3 +101,71 @@ ...@@ -101,3 +101,71 @@
background-color: $background-color; background-color: $background-color;
} }
} }
.awards {
@include clearfix;
line-height: 34px;
margin: 2px 0;
.award {
@include border-radius(5px);
border: 1px solid;
padding: 0px 10px;
float: left;
margin: 0 5px;
border-color: $border-color;
cursor: pointer;
&.active {
border-color: $border-gray-light;
background-color: $gray-light;
.counter {
font-weight: bold;
}
}
.icon {
float: left;
margin-right: 10px;
}
.counter {
float: left;
}
}
.awards-controls {
margin-left: 10px;
float: left;
.add-award {
font-size: 24px;
color: $gl-gray;
position: relative;
top: 2px;
&:hover,
&:link {
text-decoration: none;
}
}
.awards-menu {
padding: $gl-padding;
min-width: 214px;
> li {
margin: 5px;
}
}
}
.awards-menu{
li {
float: left;
margin: 3px;
}
}
}
...@@ -15,10 +15,10 @@ module Ci ...@@ -15,10 +15,10 @@ module Ci
@builds = @config_processor.builds @builds = @config_processor.builds
@status = true @status = true
end end
rescue Ci::GitlabCiYamlProcessor::ValidationError => e rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
@error = e.message @error = e.message
@status = false @status = false
rescue Exception rescue
@error = "Undefined error" @error = "Undefined error"
@status = false @status = false
end end
......
module CreatesMergeRequestForCommit
extend ActiveSupport::Concern
def new_merge_request_path
if @project.forked?
target_project = @project.forked_from_project || @project
target_branch = target_project.repository.root_ref
else
target_project = @project
target_branch = @ref
end
new_namespace_project_merge_request_path(
@project.namespace,
@project,
merge_request: {
source_project_id: @project.id,
target_project_id: target_project.id,
source_branch: @new_branch,
target_branch: target_branch
}
)
end
def create_merge_request?
params[:create_merge_request] && @new_branch != @ref
end
end
# Controller for viewing a file's blame # Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController class Projects::BlobController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include CreatesMergeRequestForCommit
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path # Raised when given an invalid file path
...@@ -22,21 +23,9 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -22,21 +23,9 @@ class Projects::BlobController < Projects::ApplicationController
end end
def create def create
result = Files::CreateService.new(@project, current_user, @commit_params).execute create_commit(Files::CreateService, success_path: after_create_path,
failure_view: :new,
if result[:status] == :success failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
flash[:notice] = "The changes have been successfully committed"
respond_to do |format|
format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }
format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render :new }
format.json { render json: { message: "failed", filePath: namespace_project_blob_path(@project.namespace, @project, @id) } }
end
end
end end
def show def show
...@@ -47,21 +36,9 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -47,21 +36,9 @@ class Projects::BlobController < Projects::ApplicationController
end end
def update def update
result = Files::UpdateService.new(@project, current_user, @commit_params).execute create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
if result[:status] == :success failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
flash[:notice] = "Your changes have been successfully committed"
respond_to do |format|
format.html { redirect_to after_edit_path }
format.json { render json: { message: "success", filePath: after_edit_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render :edit }
format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
end
end
end end
def preview def preview
...@@ -77,7 +54,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -77,7 +54,7 @@ class Projects::BlobController < Projects::ApplicationController
if result[:status] == :success if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed" flash[:notice] = "Your changes have been successfully committed"
redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch) redirect_to after_destroy_path
else else
flash[:alert] = result[:message] flash[:alert] = result[:message]
render :show render :show
...@@ -131,15 +108,51 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -131,15 +108,51 @@ class Projects::BlobController < Projects::ApplicationController
render_404 render_404
end end
def create_commit(service, success_path:, failure_view:, failure_path:)
result = service.new(@project, current_user, @commit_params).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: "success", filePath: success_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render failure_view }
format.json { render json: { message: "failed", filePath: failure_path } }
end
end
end
def after_create_path
@after_create_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @file_path))
end
end
def after_edit_path def after_edit_path
@after_edit_path ||= @after_edit_path ||=
if from_merge_request if create_merge_request?
new_merge_request_path
elsif from_merge_request && @new_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}" "#file-path-#{hexdigest(@path)}"
elsif @target_branch.present?
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
else else
namespace_project_blob_path(@project.namespace, @project, @id) namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @path))
end
end
def after_destroy_path
@after_destroy_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_tree_path(@project.namespace, @project, @new_branch)
end end
end end
...@@ -154,7 +167,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -154,7 +167,7 @@ class Projects::BlobController < Projects::ApplicationController
def editor_variables def editor_variables
@current_branch = @ref @current_branch = @ref
@target_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref @new_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref
@file_path = @file_path =
if action_name.to_s == 'create' if action_name.to_s == 'create'
...@@ -174,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -174,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController
@commit_params = { @commit_params = {
file_path: @file_path, file_path: @file_path,
current_branch: @current_branch, current_branch: @current_branch,
target_branch: @target_branch, target_branch: @new_branch,
commit_message: params[:commit_message], commit_message: params[:commit_message],
file_content: params[:content], file_content: params[:content],
file_content_encoding: params[:encoding] file_content_encoding: params[:encoding]
......
...@@ -66,7 +66,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -66,7 +66,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show def show
@participants = @issue.participants(current_user) @participants = @issue.participants(current_user)
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.with_associations.fresh @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue @noteable = @issue
respond_with(@issue) respond_with(@issue)
......
...@@ -294,7 +294,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -294,7 +294,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.inc_author.fresh @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
@discussions = Note.discussions_from_notes(@notes) @discussions = Note.discussions_from_notes(@notes)
@noteable = @merge_request @noteable = @merge_request
......
...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment] before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
def index def index
current_fetched_at = Time.now.to_i current_fetched_at = Time.now.to_i
...@@ -58,6 +58,27 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -58,6 +58,27 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
def award_toggle
noteable = note_params[:noteable_type] == "issue" ? Issue : MergeRequest
noteable = noteable.find_by!(id: note_params[:noteable_id], project: project)
data = {
author: current_user,
is_award: true,
note: note_params[:note].gsub(":", '')
}
note = noteable.notes.find_by(data)
if note
note.destroy
else
Notes::CreateService.new(project, current_user, note_params).execute
end
render json: { ok: true }
end
private private
def note def note
...@@ -111,6 +132,9 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -111,6 +132,9 @@ class Projects::NotesController < Projects::ApplicationController
id: note.id, id: note.id,
discussion_id: note.discussion_id, discussion_id: note.discussion_id,
html: note_to_html(note), html: note_to_html(note),
award: note.is_award,
emoji_path: note.is_award ? ::AwardEmoji.path_to_emoji_image(note.note) : "",
note: note.note,
discussion_html: note_to_discussion_html(note), discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note) discussion_with_diff_html: note_to_discussion_with_diff_html(note)
} }
......
# Controller for viewing a repository's file structure # Controller for viewing a repository's file structure
class Projects::TreeController < Projects::ApplicationController class Projects::TreeController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include CreatesMergeRequestForCommit
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
before_action :require_non_empty_project, except: [:new, :create] before_action :require_non_empty_project, except: [:new, :create]
...@@ -43,7 +44,7 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -43,7 +44,7 @@ class Projects::TreeController < Projects::ApplicationController
if result && result[:status] == :success if result && result[:status] == :success
flash[:notice] = "The directory has been successfully created" flash[:notice] = "The directory has been successfully created"
respond_to do |format| respond_to do |format|
format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) } format.html { redirect_to after_create_dir_path }
end end
else else
flash[:alert] = message flash[:alert] = message
...@@ -53,6 +54,8 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -53,6 +54,8 @@ class Projects::TreeController < Projects::ApplicationController
end end
end end
private
def assign_dir_vars def assign_dir_vars
@new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref
@dir_name = File.join(@path, params[:dir_name]) @dir_name = File.join(@path, params[:dir_name])
...@@ -63,4 +66,12 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -63,4 +66,12 @@ class Projects::TreeController < Projects::ApplicationController
commit_message: params[:commit_message], commit_message: params[:commit_message],
} }
end end
def after_create_dir_path
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name))
end
end
end end
class SnippetsController < ApplicationController class SnippetsController < ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
before_action :authorize_read_snippet!, only: [:show]
# Allow modify snippet # Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update] before_action :authorize_update_snippet!, only: [:edit, :update]
...@@ -79,8 +82,12 @@ class SnippetsController < ApplicationController ...@@ -79,8 +82,12 @@ class SnippetsController < ApplicationController
[Snippet::PUBLIC, Snippet::INTERNAL]). [Snippet::PUBLIC, Snippet::INTERNAL]).
find(params[:id]) find(params[:id])
else else
PersonalSnippet.are_public.find(params[:id]) PersonalSnippet.find(params[:id])
end
end end
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
end end
def authorize_update_snippet! def authorize_update_snippet!
......
...@@ -3,14 +3,11 @@ class UsersController < ApplicationController ...@@ -3,14 +3,11 @@ class UsersController < ApplicationController
before_action :set_user before_action :set_user
def show def show
@contributed_projects = contributed_projects.joined(@user). @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
reject(&:forked?)
@projects = @user.personal_projects. @projects = PersonalProjectsFinder.new(@user).execute(current_user)
where(id: authorized_projects_ids).includes(:namespace)
# Collect only groups common for both users @groups = JoinedGroupsFinder.new(@user).execute(current_user)
@groups = @user.groups & GroupsFinder.new.execute(current_user)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -53,16 +50,8 @@ class UsersController < ApplicationController ...@@ -53,16 +50,8 @@ class UsersController < ApplicationController
@user = User.find_by_username!(params[:username]) @user = User.find_by_username!(params[:username])
end end
def authorized_projects_ids
# Projects user can view
@authorized_projects_ids ||=
ProjectsFinder.new.execute(current_user).pluck(:id)
end
def contributed_projects def contributed_projects
@contributed_projects = Project. ContributedProjectsFinder.new(@user).execute(current_user)
where(id: authorized_projects_ids & @user.contributed_projects_ids).
includes(:namespace)
end end
def contributions_calendar def contributions_calendar
...@@ -73,9 +62,13 @@ class UsersController < ApplicationController ...@@ -73,9 +62,13 @@ class UsersController < ApplicationController
def load_events def load_events
# Get user activity feed for projects common for both users # Get user activity feed for projects common for both users
@events = @user.recent_events. @events = @user.recent_events.
where(project_id: authorized_projects_ids). merge(projects_for_current_user).
with_associations references(:project).
with_associations.
limit_recent(20, params[:offset])
end
@events = @events.limit(20).offset(params[:offset] || 0) def projects_for_current_user
ProjectsFinder.new.execute(current_user)
end end
end end
class ContributedProjectsFinder
def initialize(user)
@user = user
end
# Finds the projects "@user" contributed to, limited to either public projects
# or projects visible to the given user.
#
# current_user - When given the list of the projects is limited to those only
# visible by this user.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
relation.includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.contributed_projects.visible_to_user(current_user)
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_projects.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
end
def public_projects
@user.contributed_projects.public_only
end
end
class GroupsFinder class GroupsFinder
def execute(current_user, options = {}) # Finds the groups available to the given user.
all_groups(current_user) #
# current_user - The user to find the groups for.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end end
private private
def all_groups(current_user) # This method returns the groups "current_user" can see.
group_ids = if current_user def groups_visible_to_user(current_user)
if current_user.authorized_groups.any? base = groups_for_projects(public_and_internal_projects)
# User has access to groups
# union = Gitlab::SQL::Union.
# Return only: new([base.select(:id), current_user.authorized_groups.select(:id)])
# groups with public projects
# groups with internal projects Group.where("namespaces.id IN (#{union.to_sql})")
# groups with joined projects
#
Project.public_and_internal_only.pluck(:namespace_id) +
current_user.authorized_groups.pluck(:id)
else
# User has no group membership
#
# Return only:
# groups with public projects
# groups with internal projects
#
Project.public_and_internal_only.pluck(:namespace_id)
end end
else
# Not authenticated def public_groups
# groups_for_projects(public_projects)
# Return only: end
# groups with public projects
Project.public_only.pluck(:namespace_id) def groups_for_projects(projects)
Group.public_and_given_groups(projects.select(:namespace_id))
end
def public_projects
Project.unscoped.public_only
end end
Group.where("public IS TRUE OR id IN(?)", group_ids) def public_and_internal_projects
Project.unscoped.public_and_internal_only
end end
end end
# Class for finding the groups a user is a member of.
class JoinedGroupsFinder
def initialize(user = nil)
@user = user
end
# Finds the groups of the source user, optionally limited to those visible to
# the current user.
#
# current_user - If given the groups of "@user" will only include the groups
# "current_user" can also see.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end
private
# Returns the groups the user in "current_user" can see.
#
# This list includes all public/internal projects as well as the projects of
# "@user" that "current_user" also has access to.
def groups_visible_to_user(current_user)
base = @user.authorized_groups.visible_to_user(current_user)
extra = public_and_internal_groups
union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
Group.where("namespaces.id IN (#{union.to_sql})")
end
def public_groups
groups_for_projects(@user.authorized_projects.public_only)
end
def public_and_internal_groups
groups_for_projects(@user.authorized_projects.public_and_internal_only)
end
def groups_for_projects(projects)
@user.groups.public_and_given_groups(projects.select(:namespace_id))
end
end
...@@ -12,9 +12,9 @@ class NotesFinder ...@@ -12,9 +12,9 @@ class NotesFinder
when "commit" when "commit"
project.notes.for_commit_id(target_id).not_inline project.notes.for_commit_id(target_id).not_inline
when "issue" when "issue"
project.issues.find(target_id).notes.inc_author project.issues.find(target_id).notes.nonawards.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes project.snippets.find(target_id).notes
else else
......
class PersonalProjectsFinder
def initialize(user)
@user = user
end
# Finds the projects belonging to the user in "@user", limited to either
# public projects or projects visible to the given user.
#
# current_user - When given the list of projects is limited to those only
# visible by this user.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
relation.includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.personal_projects.visible_to_user(current_user)
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_and_internal_projects.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
end
def public_projects
@user.personal_projects.public_only
end
def public_and_internal_projects
@user.personal_projects.public_and_internal_only
end
end
class ProjectsFinder class ProjectsFinder
def execute(current_user, options = {}) # Returns all projects, optionally including group projects a user has access
# to.
#
# ## Examples
#
# Retrieving all public projects:
#
# ProjectsFinder.new.execute
#
# Retrieving all public/internal projects and those the given user has access
# to:
#
# ProjectsFinder.new.execute(some_user)
#
# Retrieving all public/internal projects as well as the group's projects the
# user has access to:
#
# ProjectsFinder.new.execute(some_user, group: some_group)
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil, options = {})
group = options[:group] group = options[:group]
if group if group
group_projects(current_user, group) base, extra = group_projects(current_user, group)
else
base, extra = all_projects(current_user)
end
if base and extra
union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
else else
all_projects(current_user) base
end end
end end
...@@ -13,6 +41,7 @@ class ProjectsFinder ...@@ -13,6 +41,7 @@ class ProjectsFinder
def group_projects(current_user, group) def group_projects(current_user, group)
if current_user if current_user
<<<<<<< HEAD
if group.users.include?(current_user) if group.users.include?(current_user)
# User is group member # User is group member
# #
...@@ -57,45 +86,38 @@ class ProjectsFinder ...@@ -57,45 +86,38 @@ class ProjectsFinder
) )
end end
end end
=======
[
group_projects_for_user(current_user, group),
group.projects.public_and_internal_only
]
>>>>>>> b6f0eddce552d7423869e9072a7a0706e309dbdf
else else
# Not authenticated [group.projects.public_only]
#
# Return only:
# public projects
group.projects.public_only
end end
end end
def all_projects(current_user) def all_projects(current_user)
if current_user if current_user
if current_user.authorized_projects.any? [current_user.authorized_projects, public_and_internal_projects]
# User has access to private projects
#
# Return only:
# public projects
# internal projects
# joined projects
#
Project.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
current_user.authorized_projects.pluck(:id),
Project.public_and_internal_levels
)
else else
# User has no access to private projects [Project.public_only]
# end
# Return only:
# public projects
# internal projects
#
Project.public_and_internal_only
end end
def group_projects_for_user(current_user, group)
if group.users.include?(current_user)
group.projects
else else
# Not authenticated group.projects.visible_to_user(current_user)
# end
# Return only:
# public projects
Project.public_only
end end
def public_projects
Project.unscoped.public_only
end
def public_and_internal_projects
Project.unscoped.public_and_internal_only
end end
end end
...@@ -87,6 +87,33 @@ module IssuesHelper ...@@ -87,6 +87,33 @@ module IssuesHelper
merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ')
end end
def url_to_emoji(name)
emoji_path = ::AwardEmoji.path_to_emoji_image(name)
url_to_image(emoji_path)
rescue StandardError
""
end
def emoji_author_list(notes, current_user)
list = notes.map do |note|
note.author == current_user ? "me" : note.author.username
end
list.join(", ")
end
def emoji_list
::AwardEmoji::EMOJI_LIST
end
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
"active"
else
""
end
end
# Required for Gitlab::Markdown::IssueReferenceFilter # Required for Gitlab::Markdown::IssueReferenceFilter
module_function :url_for_issue module_function :url_for_issue
end end
...@@ -8,14 +8,6 @@ module MergeRequestsHelper ...@@ -8,14 +8,6 @@ module MergeRequestsHelper
) )
end end
def new_mr_path_for_fork_from_push_event(event)
new_namespace_project_merge_request_path(
event.project.namespace,
event.project,
new_mr_from_push_event(event, event.project.forked_from_project)
)
end
def new_mr_from_push_event(event, target_project) def new_mr_from_push_event(event, target_project)
{ {
merge_request: { merge_request: {
......
class Ability class Ability
class << self class << self
def allowed(user, subject) def allowed(user, subject)
return not_auth_abilities(user, subject) if user.nil? return anonymous_abilities(user, subject) if user.nil?
return [] unless user.kind_of?(User) return [] unless user.is_a?(User)
return [] if user.blocked? return [] if user.blocked?
abilities = abilities =
...@@ -36,15 +36,25 @@ class Ability ...@@ -36,15 +36,25 @@ class Ability
] ]
end end
# List of possible abilities # List of possible abilities for anonymous user
# for non-authenticated user def anonymous_abilities(user, subject)
def not_auth_abilities(user, subject) case true
project = if subject.kind_of?(Project) when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project)
anonymous_project_abilities(subject)
when subject.is_a?(Group) || subject.respond_to?(:group)
anonymous_group_abilities(subject)
else
[]
end
end
def anonymous_project_abilities(subject)
project = if subject.is_a?(Project)
subject subject
elsif subject.respond_to?(:project)
subject.project
else else
nil subject.project
end end
if project && project.public? if project && project.public?
...@@ -64,12 +74,15 @@ class Ability ...@@ -64,12 +74,15 @@ class Ability
rules - project_disabled_features_rules(project) rules - project_disabled_features_rules(project)
else else
group = if subject.kind_of?(Group) []
end
end
def anonymous_group_abilities(subject)
group = if subject.is_a?(Group)
subject subject
elsif subject.respond_to?(:group)
subject.group
else else
nil subject.group
end end
if group && group.public_profile? if group && group.public_profile?
...@@ -78,6 +91,13 @@ class Ability ...@@ -78,6 +91,13 @@ class Ability
[] []
end end
end end
def anonymous_personal_snippet_abilities(snippet)
if snippet.public?
[:read_personal_snippet]
else
[]
end
end end
def global_abilities(user) def global_abilities(user)
...@@ -300,7 +320,7 @@ class Ability ...@@ -300,7 +320,7 @@ class Ability
end end
end end
[:note, :project_snippet, :personal_snippet].each do |name| [:note, :project_snippet].each do |name|
define_method "#{name}_abilities" do |user, subject| define_method "#{name}_abilities" do |user, subject|
rules = [] rules = []
...@@ -320,6 +340,24 @@ class Ability ...@@ -320,6 +340,24 @@ class Ability
end end
end end
def personal_snippet_abilities(user, snippet)
rules = []
if snippet.author == user
rules += [
:read_personal_snippet,
:update_personal_snippet,
:admin_personal_snippet
]
end
if snippet.public? || snippet.internal?
rules << :read_personal_snippet
end
rules
end
def group_member_abilities(user, subject) def group_member_abilities(user, subject)
rules = [] rules = []
target_user = subject.user target_user = subject.user
......
...@@ -188,13 +188,13 @@ module Ci ...@@ -188,13 +188,13 @@ module Ci
end end
def config_processor def config_processor
return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace) @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError => e rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error(e.message) save_yaml_error(e.message)
nil nil
rescue Exception => e rescue
logger.error e.message + "\n" + e.backtrace.join("\n") save_yaml_error("Undefined error")
save_yaml_error("Undefined yaml error")
nil nil
end end
......
...@@ -89,41 +89,6 @@ module Issuable ...@@ -89,41 +89,6 @@ module Issuable
opened? || reopened? opened? || reopened?
end end
#
# Votes
#
# Return the number of -1 comments (downvotes)
def downvotes
filter_superceded_votes(notes.select(&:downvote?), notes).size
end
def downvotes_in_percent
if votes_count.zero?
0
else
100.0 - upvotes_in_percent
end
end
# Return the number of +1 comments (upvotes)
def upvotes
filter_superceded_votes(notes.select(&:upvote?), notes).size
end
def upvotes_in_percent
if votes_count.zero?
0
else
100.0 / votes_count * upvotes
end
end
# Return the total number of votes
def votes_count
upvotes + downvotes
end
def subscribed?(user) def subscribed?(user)
subscription = subscriptions.find_by_user_id(user.id) subscription = subscriptions.find_by_user_id(user.id)
...@@ -183,18 +148,4 @@ module Issuable ...@@ -183,18 +148,4 @@ module Issuable
def notes_with_associations def notes_with_associations
notes.includes(:author, :project) notes.includes(:author, :project)
end end
private
def filter_superceded_votes(votes, notes)
filteredvotes = [] + votes
votes.each do |vote|
if vote.superceded?(notes)
filteredvotes.delete(vote)
end
end
filteredvotes
end
end end
...@@ -8,8 +8,9 @@ module Sortable ...@@ -8,8 +8,9 @@ module Sortable
included do included do
# By default all models should be ordered # By default all models should be ordered
# by created_at field starting from newest # by created_at field starting from newest
default_scope { order(id: :desc) } default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) }
......
...@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base ...@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base
Event::PUSHED, ["MergeRequest", "Issue"], Event::PUSHED, ["MergeRequest", "Issue"],
[Event::CREATED, Event::CLOSED, Event::MERGED]) [Event::CREATED, Event::CLOSED, Event::MERGED])
end end
def latest_update_time
row = select(:updated_at, :project_id).reorder(id: :desc).take
row ? row.updated_at : nil
end
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
end end
def proper? def proper?
......
...@@ -53,6 +53,14 @@ class Group < Namespace ...@@ -53,6 +53,14 @@ class Group < Namespace
def reference_pattern def reference_pattern
User.reference_pattern User.reference_pattern
end end
def public_and_given_groups(ids)
where('public IS TRUE OR namespaces.id IN (?)', ids)
end
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
end end
def to_reference(_from_project = nil) def to_reference(_from_project = nil)
......
...@@ -40,16 +40,20 @@ class Note < ActiveRecord::Base ...@@ -40,16 +40,20 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' }
validates :author, presence: true
mount_uploader :attachment, AttachmentUploader mount_uploader :attachment, AttachmentUploader
# Scopes # Scopes
scope :awards, ->{ where(is_award: true) }
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") } scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) } scope :not_inline, ->{ where(line_code: [nil, '']) }
...@@ -97,6 +101,12 @@ class Note < ActiveRecord::Base ...@@ -97,6 +101,12 @@ class Note < ActiveRecord::Base
def search(query) def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%") where("LOWER(note) like :query", query: "%#{query.downcase}%")
end end
def grouped_awards
awards.select(:note).distinct.map do |note|
[ note.note, where(note: note.note) ]
end
end
end end
def cross_reference? def cross_reference?
...@@ -288,44 +298,6 @@ class Note < ActiveRecord::Base ...@@ -288,44 +298,6 @@ class Note < ActiveRecord::Base
nil nil
end end
DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)
# Check if the note is a downvote
def downvote?
votable? && note.start_with?(*DOWNVOTES)
end
UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)
# Check if the note is an upvote
def upvote?
votable? && note.start_with?(*UPVOTES)
end
def superceded?(notes)
return false unless vote?
notes.each do |note|
next if note == self
if note.vote? &&
self[:author_id] == note[:author_id] &&
self[:created_at] <= note[:created_at]
return true
end
end
false
end
def vote?
upvote? || downvote?
end
def votable?
for_issue? || (for_merge_request? && !for_diff_line?)
end
# Mentionable override. # Mentionable override.
def gfm_reference(from_project = nil) def gfm_reference(from_project = nil)
noteable.gfm_reference(from_project) noteable.gfm_reference(from_project)
......
...@@ -313,6 +313,10 @@ class Project < ActiveRecord::Base ...@@ -313,6 +313,10 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC') joins(join_body).reorder('join_note_counts.amount DESC')
end end
def visible_to_user(user)
where(id: user.authorized_projects.select(:id).reorder(nil))
end
end end
def team def team
......
...@@ -417,14 +417,12 @@ class User < ActiveRecord::Base ...@@ -417,14 +417,12 @@ class User < ActiveRecord::Base
end end
end end
# Groups user has access to # Returns the groups a user has access to
def authorized_groups def authorized_groups
@authorized_groups ||= begin union = Gitlab::SQL::Union.
group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) new([groups.select(:id), authorized_projects.select(:namespace_id)])
Group.where(id: group_ids)
end
end
<<<<<<< HEAD
def authorized_projects_id def authorized_projects_id
@authorized_projects_id ||= begin @authorized_projects_id ||= begin
project_ids = personal_projects.pluck(:id) project_ids = personal_projects.pluck(:id)
...@@ -441,19 +439,20 @@ class User < ActiveRecord::Base ...@@ -441,19 +439,20 @@ class User < ActiveRecord::Base
project_ids.push(*groups_projects.where(members: scope).pluck(:id)) project_ids.push(*groups_projects.where(members: scope).pluck(:id))
project_ids.push(*projects.where(members: scope).pluck(:id).uniq) project_ids.push(*projects.where(members: scope).pluck(:id).uniq)
end end
=======
Group.where("namespaces.id IN (#{union.to_sql})")
>>>>>>> b6f0eddce552d7423869e9072a7a0706e309dbdf
end end
# Projects user has access to # Returns the groups a user is authorized to access.
def authorized_projects def authorized_projects
@authorized_projects ||= Project.where(id: authorized_projects_id) Project.where("projects.id IN (#{projects_union.to_sql})")
end end
def owned_projects def owned_projects
@owned_projects ||= @owned_projects ||=
begin Project.where('namespace_id IN (?) OR namespace_id = ?',
namespace_ids = owned_groups.pluck(:id).push(namespace.id) owned_groups.select(:id), namespace.id).joins(:namespace)
Project.in_namespace(namespace_ids).joins(:namespace)
end
end end
# Team membership in authorized projects # Team membership in authorized projects
...@@ -772,12 +771,25 @@ class User < ActiveRecord::Base ...@@ -772,12 +771,25 @@ class User < ActiveRecord::Base
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
end end
def contributed_projects_ids # Returns the projects a user contributed to in the last year.
Event.contributions.where(author_id: self). #
# This method relies on a subquery as this performs significantly better
# compared to a JOIN when coupled with, for example,
# `Project.visible_to_user`. That is, consider the following code:
#
# some_user.contributed_projects.visible_to_user(other_user)
#
# If this method were to use a JOIN the resulting query would take roughly 200
# ms on a database with a similar size to GitLab.com's database. On the other
# hand, using a subquery means we can get the exact same data in about 40 ms.
def contributed_projects
events = Event.select(:project_id).
contributions.where(author_id: self).
where("created_at > ?", Time.now - 1.year). where("created_at > ?", Time.now - 1.year).
reorder(project_id: :desc). uniq.
select(:project_id). reorder(nil)
uniq.map(&:project_id)
Project.where(id: events)
end end
def restricted_signup_domains def restricted_signup_domains
...@@ -810,8 +822,27 @@ class User < ActiveRecord::Base ...@@ -810,8 +822,27 @@ class User < ActiveRecord::Base
def ci_authorized_runners def ci_authorized_runners
@ci_authorized_runners ||= begin @ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject.joins(:project). runner_ids = Ci::RunnerProject.joins(:project).
where(ci_projects: { gitlab_id: master_or_owner_projects_id }).select(:runner_id) where("ci_projects.gitlab_id IN (#{ci_projects_union.to_sql})").
select(:runner_id)
Ci::Runner.specific.where(id: runner_ids) Ci::Runner.specific.where(id: runner_ids)
end end
end end
private
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
projects.select(:id)])
end
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
other = projects.where(members: scope)
Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
other.select(:id)])
end
end end
...@@ -58,12 +58,6 @@ class GitPushService ...@@ -58,12 +58,6 @@ class GitPushService
@push_data = build_push_data(oldrev, newrev, ref) @push_data = build_push_data(oldrev, newrev, ref)
# If CI was disabled but .gitlab-ci.yml file was pushed
# we enable CI automatically
if !project.builds_enabled? && gitlab_ci_yaml?(newrev)
project.enable_ci
end
EventCreateService.new.push(project, user, @push_data) EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks) project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks)
...@@ -134,10 +128,4 @@ class GitPushService ...@@ -134,10 +128,4 @@ class GitPushService
def commit_user(commit) def commit_user(commit)
commit.author || user commit.author || user
end end
def gitlab_ci_yaml?(sha)
@project.repository.blob_at(sha, '.gitlab-ci.yml')
rescue Rugged::ReferenceError
nil
end
end end
...@@ -5,11 +5,16 @@ module Notes ...@@ -5,11 +5,16 @@ module Notes
note.author = current_user note.author = current_user
note.system = false note.system = false
if contains_emoji_only?(params[:note])
note.is_award = true
note.note = emoji_name(params[:note])
end
if note.save if note.save
notification_service.new_note(note) notification_service.new_note(note)
# Skip system notes, like status changes and cross-references. # Skip system notes, like status changes and cross-references and awards
unless note.system unless note.system || note.is_award
event_service.leave_note(note, note.author) event_service.leave_note(note, note.author)
note.create_cross_references! note.create_cross_references!
execute_hooks(note) execute_hooks(note)
...@@ -28,5 +33,13 @@ module Notes ...@@ -28,5 +33,13 @@ module Notes
note.project.execute_hooks(note_data, :note_hooks) note.project.execute_hooks(note_data, :note_hooks)
note.project.execute_services(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks)
end end
def contains_emoji_only?(note)
note =~ /\A:[-_+[:alnum:]]*:\s?\z/
end
def emoji_name(note)
note.match(/\A:([-_+[:alnum:]]*):\s?/)[1]
end
end end
end end
...@@ -102,6 +102,7 @@ class NotificationService ...@@ -102,6 +102,7 @@ class NotificationService
# ignore gitlab service messages # ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed') return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true return true if note.cross_reference? && note.system == true
return true if note.is_award
target = note.noteable target = note.noteable
......
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url xml.id dashboard_projects_url
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group) xml.id group_url(@group)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
%input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox") %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input markdown-area' - classes << ' js-gfm-input markdown-area'
- if defined?(f) && f
= f.text_area attr, class: classes, placeholder: '' = f.text_area attr, class: classes, placeholder: ''
- else
= text_area_tag attr, nil, class: classes, placeholder: ''
%a.zen-enter-link(tabindex="-1" href="#") %a.zen-enter-link(tabindex="-1" href="#")
%i.fa.fa-expand = icon('expand')
Edit in fullscreen Edit in fullscreen
%a.zen-leave-link(href="#") %a.zen-leave-link(href="#")
%i.fa.fa-compress = icon('compress')
...@@ -19,4 +19,4 @@ ...@@ -19,4 +19,4 @@
- if allowed_tree_edit? - if allowed_tree_edit?
.btn-group{ role: "group" } .btn-group{ role: "group" }
%button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace
%button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete
...@@ -5,21 +5,19 @@ ...@@ -5,21 +5,19 @@
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Create New Directory %h3.page-title Create New Directory
.modal-body .modal-body
= form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, id: 'dir-create-form', class: 'form-horizontal' do = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form' do
.form-group .form-group
= label_tag :dir_name, 'Directory Name', class: 'control-label' = label_tag :dir_name, 'Directory Name', class: 'control-label'
.col-sm-10 .col-sm-10
= text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control' = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control'
= render 'shared/commit_message_container', params: params, placeholder: ''
- unless @project.empty_repo? = render 'shared/new_commit_form', placeholder: "Add new directory"
.form-group
= label_tag :branch_name, 'Branch', class: 'control-label'
.col-sm-10
= text_field_tag 'new_branch', @ref, class: "form-control"
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
= submit_tag "Create directory", class: 'btn btn-primary btn-create' = submit_tag "Create directory", class: 'btn btn-primary btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
:javascript :javascript
disableButtonIfAnyEmptyField($("#dir-create-form"), ".form-control", ".btn-create"); disableButtonIfAnyEmptyField($(".js-create-dir-form"), ".form-control", ".btn-create");
new NewCommitForm($('.js-create-dir-form'))
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
.modal-content .modal-content
.modal-header .modal-header
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Remove #{@blob.name} %h3.page-title Delete #{@blob.name}
%p.light
From branch
%strong= @ref
.modal-body .modal-body
= form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-requires-input' do = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do
= render 'shared/commit_message_container', params: params, = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
placeholder: 'Removed this file because...'
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
= button_tag 'Remove file', class: 'btn btn-remove btn-remove-file' = button_tag 'Delete file', class: 'btn btn-remove btn-remove-file'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
:javascript
new NewCommitForm($('.js-replace-blob-form'))
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title #{title} %h3.page-title #{title}
.modal-body .modal-body
= form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do
.dropzone .dropzone
.dropzone-previews.blob-upload-dropzone-previews .dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light %p.dz-message.light
...@@ -13,19 +13,15 @@ ...@@ -13,19 +13,15 @@
= link_to 'click to upload', '#', class: "markdown-selector" = link_to 'click to upload', '#', class: "markdown-selector"
%br %br
.dropzone-alerts{class: "alert alert-danger data", style: "display:none"} .dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
= render 'shared/commit_message_container', params: params,
placeholder: placeholder = render 'shared/new_commit_form', placeholder: placeholder
- unless @project.empty_repo?
.form-group.branch
= label_tag 'branch', class: 'control-label' do
Branch
.col-sm-10
= text_field_tag 'new_branch', @ref, class: "form-control"
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
= button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
:javascript :javascript
disableButtonIfEmptyField($('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'); disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}'); new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
new NewCommitForm($('.js-upload-blob-form'))
...@@ -13,15 +13,9 @@ ...@@ -13,15 +13,9 @@
%i.fa.fa-eye %i.fa.fa-eye
= editing_preview_title(@blob.name) = editing_preview_title(@blob.name)
= form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input') do = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
.form-group.branch
= label_tag 'branch', class: 'control-label' do
Branch
.col-sm-10
= text_field_tag 'new_branch', @ref, class: "form-control"
= hidden_field_tag 'last_commit', @last_commit = hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'content', '', id: "file-content"
...@@ -30,3 +24,4 @@ ...@@ -30,3 +24,4 @@
:javascript :javascript
blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
new NewCommitForm($('.js-edit-blob-form'))
...@@ -2,20 +2,13 @@ ...@@ -2,20 +2,13 @@
= render "header_title" = render "header_title"
.gray-content-block.top-block .gray-content-block.top-block
Create a new file %h3.page-title
Create New File
.file-editor .file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do
= render 'projects/blob/editor', ref: @ref = render 'projects/blob/editor', ref: @ref
= render 'shared/commit_message_container', params: params, = render 'shared/new_commit_form', placeholder: "Add new file"
placeholder: 'Add new file'
- unless @project.empty_repo?
.form-group.branch
= label_tag 'branch', class: 'control-label' do
Branch
.col-sm-10
= text_field_tag 'new_branch', @ref, class: "form-control js-quick-submit"
= hidden_field_tag 'content', '', id: 'file-content' = hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref, = render 'projects/commit_button', ref: @ref,
...@@ -23,3 +16,4 @@ ...@@ -23,3 +16,4 @@
:javascript :javascript
blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null)
new NewCommitForm($('.js-new-blob-form'))
...@@ -10,6 +10,4 @@ ...@@ -10,6 +10,4 @@
= render 'projects/blob/remove' = render 'projects/blob/remove'
- title = "Replace #{@blob.name}" - title = "Replace #{@blob.name}"
= render 'projects/blob/upload', title: title, placeholder: title, = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put
button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id),
method: :put
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= render 'shared/show_aside' = render 'shared/show_aside'
.gray-content-block.second-block .gray-content-block.second-block.oneline-block
.row .row
.col-md-9 .col-md-9
.votes-holder.pull-right .votes-holder.pull-right
......
...@@ -29,8 +29,6 @@ ...@@ -29,8 +29,6 @@
.issue-info .issue-info
= "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe
- if issue.votes_count > 0
= render 'votes/votes_inline', votable: issue
- if issue.milestone - if issue.milestone
&nbsp; &nbsp;
%span %span
......
...@@ -14,8 +14,10 @@ ...@@ -14,8 +14,10 @@
#votes= render 'votes/votes_block', votable: @merge_request #votes= render 'votes/votes_block', votable: @merge_request
= render "projects/merge_requests/show/participants" = render "projects/merge_requests/show/participants"
.col-md-3 .col-md-3
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} .input-group.cross-project-reference
%span.slead.has_tooltip{title: 'Cross-project reference'}
= cross_project_reference(@project, @merge_request) = cross_project_reference(@project, @merge_request)
= clipboard_button
.row .row
%section.col-md-9 %section.col-md-9
......
...@@ -34,8 +34,6 @@ ...@@ -34,8 +34,6 @@
.merge-request-info .merge-request-info
= "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe
- if merge_request.votes_count > 0
= render 'votes/votes_inline', votable: merge_request
- if merge_request.milestone_id? - if merge_request.milestone_id?
&nbsp; &nbsp;
%span %span
......
...@@ -35,26 +35,6 @@ ...@@ -35,26 +35,6 @@
- if note.updated_by && note.updated_by != note.author - if note.updated_by && note.updated_by != note.author
by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)} by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
- if note.superceded?(@notes)
- if note.upvote?
%span.vote.upvote.label.label-gray.strikethrough
= icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-gray.strikethrough
= icon('thumbs-down')
\-1
- else
- if note.upvote?
%span.vote.upvote.label.label-success
= icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-danger
= icon('thumbs-down')
\-1
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text .note-text
= preserve do = preserve do
......
...@@ -11,10 +11,9 @@ ...@@ -11,10 +11,9 @@
.prepend-top-default .prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f| = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit' = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
.prepend-top-default .prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save' = f.submit 'Save changes', class: 'btn btn-save'
= link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project) xml.id namespace_project_url(@project.namespace, @project)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
New git tag New git tag
%hr %hr
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal tag-form" do = form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form" do
.form-group .form-group
= label_tag :tag_name, 'Name for new tag', class: 'control-label' = label_tag :tag_name, 'Name for new tag', class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -30,16 +30,7 @@ ...@@ -30,16 +30,7 @@
= label_tag :release_description, 'Release notes', class: 'control-label' = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
.zennable = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control'
%input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
.zen-backdrop
= text_area_tag :release_description, nil, class: 'js-gfm-input markdown-area description js-quick-submit form-control', placeholder: ''
%a.zen-enter-link(tabindex="-1" href="#")
= icon('expand')
Edit in fullscreen
%a.zen-leave-link(href="#")
= icon('compress')
= render 'projects/notes/hints' = render 'projects/notes/hints'
.help-block (Optional) You can add release notes to your tag. It will be stored in the GitLab database and shown on the tags page .help-block (Optional) You can add release notes to your tag. It will be stored in the GitLab database and shown on the tags page
.form-actions .form-actions
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
= render "projects/tree/readme", readme: tree.readme = render "projects/tree/readme", readme: tree.readme
- 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 New File', 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'
:javascript :javascript
......
.form-group.commit_message-group .form-group.commit_message-group
= label_tag 'commit_message', class: 'control-label' do - nonce = SecureRandom.hex
= label_tag "commit_message-#{nonce}", class: 'control-label' do
Commit message Commit message
.col-sm-10 .col-sm-10
.commit-message-container .commit-message-container
.max-width-marker .max-width-marker
= text_area_tag 'commit_message', = text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]), (params[:commit_message] || local_assigns[:text]),
class: 'form-control js-quick-submit', placeholder: local_assigns[:placeholder], class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder],
required: true, rows: (local_assigns[:rows] || 3) required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint] - if local_assigns[:hint]
%p.hint %p.hint
Try to keep the first line under 52 characters Try to keep the first line under 52 characters
......
= render 'shared/commit_message_container', placeholder: placeholder
- unless @project.empty_repo?
.form-group.branch
= label_tag 'branch', class: 'control-label' do
Branch
.col-sm-10
= text_field_tag 'new_branch', @new_branch || @ref, class: "form-control js-new-branch"
.form-group.js-create-merge-request-form-group
.col-sm-offset-2.col-sm-10
.checkbox
- nonce = SecureRandom.hex
= label_tag "create_merge_request-#{nonce}" do
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
Start a <strong>new merge request</strong> with this commit
= hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html" xml.link href: user_url(@user), rel: "alternate", type: "text/html"
xml.id user_url(@user) xml.id user_url(@user)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
.votes.votes-block .awards.votes-block
.btn-group - votable.notes.awards.grouped_awards.each do |emoji, notes|
- unless votable.upvotes.zero? .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
.btn.btn-sm.disabled.cgreen .icon{"data-emoji" => "#{emoji}"}
%i.fa.fa-thumbs-up = image_tag url_to_emoji(emoji), height: "20px", width: "20px"
= votable.upvotes .counter
- unless votable.downvotes.zero? = notes.count
.btn.btn-sm.disabled.cred
%i.fa.fa-thumbs-down - if current_user
= votable.downvotes .dropdown.awards-controls
%a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"}
= icon('smile-o')
%ul.dropdown-menu.awards-menu
- emoji_list.each do |emoji|
%li{"data-emoji" => "#{emoji}"}= image_tag url_to_emoji(emoji), height: "20px", width: "20px"
- if current_user
:coffeescript
post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"
noteable_type = "#{votable.class.name.underscore}"
noteable_id = "#{votable.id}"
window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id)
$(".awards-menu li").click (e)->
emoji = $(this).data("emoji")
awards_handler.addAward(emoji)
$(".awards").on "click", ".award", (e)->
emoji = $(this).find(".icon").data("emoji")
awards_handler.addAward(emoji)
$(".award").tooltip()
.votes.votes-inline
- unless votable.upvotes.zero?
%span.upvotes.cgreen
+ #{votable.upvotes}
- unless votable.downvotes.zero?
\/
- unless votable.downvotes.zero?
%span.downvotes.cred
\- #{votable.downvotes}
...@@ -126,7 +126,7 @@ production: &base ...@@ -126,7 +126,7 @@ production: &base
## Git LFS ## Git LFS
lfs: lfs:
enabled: false enabled: true
# The location where LFS objects are stored (default: shared/lfs-objects). # The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects # storage_path: shared/lfs-objects
......
...@@ -231,7 +231,7 @@ Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mail ...@@ -231,7 +231,7 @@ Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mail
# Git LFS # Git LFS
# #
Settings['lfs'] ||= Settingslogic.new({}) Settings['lfs'] ||= Settingslogic.new({})
Settings.lfs['enabled'] = false if Settings.lfs['enabled'].nil? Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root) Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
# #
......
...@@ -716,6 +716,10 @@ Gitlab::Application.routes.draw do ...@@ -716,6 +716,10 @@ Gitlab::Application.routes.draw do
member do member do
delete :delete_attachment delete :delete_attachment
end end
collection do
post :award_toggle
end
end end
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
......
class AddIsAwardToNotes < ActiveRecord::Migration
def change
add_column :notes, :is_award, :boolean, default: false, null: false
add_index :notes, :is_award
end
end
class AddProjectsPublicIndex < ActiveRecord::Migration
def change
add_index :namespaces, :public
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20151116144118) do ActiveRecord::Schema.define(version: 20151118162244) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -607,6 +607,7 @@ ActiveRecord::Schema.define(version: 20151116144118) do ...@@ -607,6 +607,7 @@ ActiveRecord::Schema.define(version: 20151116144118) do
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: true do |t| create_table "notes", force: true do |t|
...@@ -623,12 +624,14 @@ ActiveRecord::Schema.define(version: 20151116144118) do ...@@ -623,12 +624,14 @@ ActiveRecord::Schema.define(version: 20151116144118) do
t.boolean "system", default: false, null: false t.boolean "system", default: false, null: false
t.text "st_diff" t.text "st_diff"
t.integer "updated_by_id" t.integer "updated_by_id"
t.boolean "is_award", default: false, null: false
end end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree 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", "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", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", 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_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", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
......
...@@ -31,8 +31,6 @@ Parameters: ...@@ -31,8 +31,6 @@ Parameters:
"project_id": 3, "project_id": 3,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"upvotes": 0,
"downvotes": 0,
"author": { "author": {
"id": 1, "id": 1,
"username": "admin", "username": "admin",
...@@ -77,8 +75,6 @@ Parameters: ...@@ -77,8 +75,6 @@ Parameters:
"project_id": 3, "project_id": 3,
"title": "test1", "title": "test1",
"state": "merged", "state": "merged",
"upvotes": 0,
"downvotes": 0,
"author": { "author": {
"id": 1, "id": 1,
"username": "admin", "username": "admin",
...@@ -126,8 +122,6 @@ Parameters: ...@@ -126,8 +122,6 @@ Parameters:
"updated_at": "2015-02-02T20:08:49.959Z", "updated_at": "2015-02-02T20:08:49.959Z",
"target_branch": "secret_token", "target_branch": "secret_token",
"source_branch": "version-1-9", "source_branch": "version-1-9",
"upvotes": 0,
"downvotes": 0,
"author": { "author": {
"name": "Chad Hamill", "name": "Chad Hamill",
"username": "jarrett", "username": "jarrett",
...@@ -198,8 +192,6 @@ Parameters: ...@@ -198,8 +192,6 @@ Parameters:
"project_id": 3, "project_id": 3,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"upvotes": 0,
"downvotes": 0,
"author": { "author": {
"id": 1, "id": 1,
"username": "admin", "username": "admin",
...@@ -250,8 +242,6 @@ Parameters: ...@@ -250,8 +242,6 @@ Parameters:
"title": "test1", "title": "test1",
"description": "description1", "description": "description1",
"state": "opened", "state": "opened",
"upvotes": 0,
"downvotes": 0,
"author": { "author": {
"id": 1, "id": 1,
"username": "admin", "username": "admin",
...@@ -304,8 +294,6 @@ Parameters: ...@@ -304,8 +294,6 @@ Parameters:
"project_id": 3, "project_id": 3,
"title": "test1", "title": "test1",
"state": "merged", "state": "merged",
"upvotes": 0,
"downvotes": 0,
"author": { "author": {
"id": 1, "id": 1,
"username": "admin", "username": "admin",
......
...@@ -32,9 +32,7 @@ Parameters: ...@@ -32,9 +32,7 @@ Parameters:
"created_at": "2013-09-30T13:46:01Z" "created_at": "2013-09-30T13:46:01Z"
}, },
"created_at": "2013-10-02T09:22:45Z", "created_at": "2013-10-02T09:22:45Z",
"system": true, "system": true
"upvote": false,
"downvote": false
}, },
{ {
"id": 305, "id": 305,
...@@ -49,9 +47,7 @@ Parameters: ...@@ -49,9 +47,7 @@ Parameters:
"created_at": "2013-09-30T13:46:01Z" "created_at": "2013-09-30T13:46:01Z"
}, },
"created_at": "2013-10-02T09:56:03Z", "created_at": "2013-10-02T09:56:03Z",
"system": false, "system": false
"upvote": false,
"downvote": false
} }
] ]
``` ```
......
...@@ -29,7 +29,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ...@@ -29,7 +29,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
``` ```
Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts). uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts), lfs (LFS objects).
Use a comma to specify several options at the same time. Use a comma to specify several options at the same time.
``` ```
......
...@@ -55,7 +55,7 @@ In `config/gitlab.yml`: ...@@ -55,7 +55,7 @@ In `config/gitlab.yml`:
* When SSH is set as a remote, Git LFS objects still go through HTTPS * When SSH is set as a remote, Git LFS objects still go through HTTPS
* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended * Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported * Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the url to Git config manually (see #troubleshooting-tips) * Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting-tips)
## Using Git LFS ## Using Git LFS
...@@ -77,11 +77,10 @@ git commit -am "Added Debian iso" # commit the file meta data ...@@ -77,11 +77,10 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server git push origin master # sync the git repo and large file to the GitLab server
``` ```
Downloading a single large file is also very simple: Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication.
```bash ```bash
git clone git@gitlab.example.com:group/project.git git clone git@gitlab.example.com:group/project.git
git lfs fetch debian.iso # download the large file
``` ```
......
Feature: Award Emoji
Background:
Given I sign in as a user
And I own project "Shop"
And project "Shop" has issue "Bugfix"
And I visit "Bugfix" issue page
@javascript
Scenario: I add and remove award in the issue
Given I click to emoji-picker
And I click to emoji in the picker
Then I have award added
And I can remove it by clicking to icon
\ No newline at end of file
...@@ -42,7 +42,7 @@ Feature: Project Source Browse Files ...@@ -42,7 +42,7 @@ Feature: Project Source Browse Files
And I fill the new branch name And I fill the new branch name
And I click on "Upload file" And I click on "Upload file"
Then I can see the new text file Then I can see the new text file
And I am redirected to the uploaded file on new branch And I am redirected to the new merge request page
And I can see the new commit message And I can see the new commit message
@javascript @javascript
...@@ -64,7 +64,7 @@ Feature: Project Source Browse Files ...@@ -64,7 +64,7 @@ Feature: Project Source Browse Files
And I fill the commit message And I fill the commit message
And I fill the new branch name And I fill the new branch name
And I click on "Commit Changes" And I click on "Commit Changes"
Then I am redirected to the new file on new branch Then I am redirected to the new merge request page
And I should see its new content And I should see its new content
@javascript @javascript
...@@ -134,7 +134,7 @@ Feature: Project Source Browse Files ...@@ -134,7 +134,7 @@ Feature: Project Source Browse Files
And I fill the commit message And I fill the commit message
And I fill the new branch name And I fill the new branch name
And I click on "Commit Changes" And I click on "Commit Changes"
Then I am redirected to the ".gitignore" on new branch Then I am redirected to the new merge request page
And I should see its new content And I should see its new content
@javascript @wip @javascript @wip
...@@ -154,7 +154,7 @@ Feature: Project Source Browse Files ...@@ -154,7 +154,7 @@ Feature: Project Source Browse Files
And I fill the commit message And I fill the commit message
And I fill the new branch name And I fill the new branch name
And I click on "Create directory" And I click on "Create directory"
Then I am redirected to the new directory Then I am redirected to the new merge request page
@javascript @javascript
Scenario: I attempt to create an existing directory Scenario: I attempt to create an existing directory
...@@ -174,12 +174,12 @@ Feature: Project Source Browse Files ...@@ -174,12 +174,12 @@ Feature: Project Source Browse Files
Then I see diff Then I see diff
@javascript @javascript
Scenario: I can remove file and commit Scenario: I can delete file and commit
Given I click on ".gitignore" file in repo Given I click on ".gitignore" file in repo
And I see the ".gitignore" And I see the ".gitignore"
And I click on "Remove" And I click on "Delete"
And I fill the commit message And I fill the commit message
And I click on "Remove file" And I click on "Delete file"
Then I am redirected to the files URL Then I am redirected to the files URL
And I don't see the ".gitignore" And I don't see the ".gitignore"
......
class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
include Select2Helper
step 'I visit "Bugfix" issue page' do
visit namespace_project_issue_path(@project.namespace, @project, @issue)
end
step 'I click to emoji-picker' do
page.within ".awards-controls" do
page.find(".add-award").click
end
end
step 'I click to emoji in the picker' do
page.within ".awards-menu" do
page.first("img").click
end
end
step 'I can remove it by clicking to icon' do
page.within ".awards" do
page.first(".award").click
expect(page).to_not have_selector ".award"
end
end
step 'I have award added' do
page.within ".awards" do
expect(page).to have_selector ".award"
expect(page.find(".award .counter")).to have_content "1"
end
end
step 'project "Shop" has issue "Bugfix"' do
@project = Project.find_by(name: "Shop")
@issue = create(:issue, title: "Bugfix", project: project)
end
end
...@@ -98,12 +98,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -98,12 +98,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_button 'Create directory' click_button 'Create directory'
end end
step 'I click on "Remove"' do step 'I click on "Delete"' do
click_button 'Remove' click_button 'Delete'
end end
step 'I click on "Remove file"' do step 'I click on "Delete file"' do
click_button 'Remove file' click_button 'Delete file'
end end
step 'I click on "Replace"' do step 'I click on "Replace"' do
...@@ -142,7 +142,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -142,7 +142,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end end
step 'I can see new file page' do step 'I can see new file page' do
expect(page).to have_content "new file" expect(page).to have_content "Create New File"
expect(page).to have_content "Commit message" expect(page).to have_content "Commit message"
end end
...@@ -225,10 +225,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -225,10 +225,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore')) expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore'))
end end
step 'I am redirected to the ".gitignore" on new branch' do
expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/.gitignore'))
end
step 'I am redirected to the permalink URL' do step 'I am redirected to the permalink URL' do
expect(current_path).to( expect(current_path).to(
eq(namespace_project_blob_path(@project.namespace, @project, eq(namespace_project_blob_path(@project.namespace, @project,
...@@ -247,20 +243,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -247,20 +243,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
@project.namespace, @project, 'master/' + new_file_name_with_directory)) @project.namespace, @project, 'master/' + new_file_name_with_directory))
end end
step 'I am redirected to the new file on new branch' do step 'I am redirected to the new merge request page' do
expect(current_path).to eq(namespace_project_blob_path( expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project))
@project.namespace, @project, 'new_branch_name/' + new_file_name))
end
step 'I am redirected to the uploaded file on new branch' do
expect(current_path).to eq(namespace_project_blob_path(
@project.namespace, @project,
'new_branch_name/' + File.basename(test_text_file)))
end
step 'I am redirected to the new directory' do
expect(current_path).to eq(namespace_project_tree_path(
@project.namespace, @project, 'new_branch_name/' + new_dir_name))
end end
step 'I am redirected to the root directory' do step 'I am redirected to the root directory' do
......
...@@ -172,7 +172,7 @@ module API ...@@ -172,7 +172,7 @@ module API
end end
class MergeRequest < ProjectEntity class MergeRequest < ProjectEntity
expose :target_branch, :source_branch, :upvotes, :downvotes expose :target_branch, :source_branch
expose :author, :assignee, using: Entities::UserBasic expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id expose :source_project_id, :target_project_id
expose :label_names, as: :labels expose :label_names, as: :labels
...@@ -202,8 +202,6 @@ module API ...@@ -202,8 +202,6 @@ module API
expose :author, using: Entities::UserBasic expose :author, using: Entities::UserBasic
expose :created_at expose :created_at
expose :system?, as: :system expose :system?, as: :system
expose :upvote?, as: :upvote
expose :downvote?, as: :downvote
end end
class MRNote < Grape::Entity class MRNote < Grape::Entity
......
class AwardEmoji
EMOJI_LIST = [
"+1", "-1", "100", "blush", "heart", "smile", "rage",
"beers", "disappointed", "ok_hand",
"helicopter", "shit", "airplane", "alarm_clock",
"ambulance", "anguished", "two_hearts", "wink"
]
def self.path_to_emoji_image(name)
"emoji/#{Emoji.emoji_filename(name)}.png"
end
end
require 'backup/files'
module Backup
class Lfs < Files
def initialize
super('lfs', Settings.lfs.storage_path)
end
def create_files_dir
Dir.mkdir(app_files_dir, 0700)
end
end
end
...@@ -154,7 +154,7 @@ module Backup ...@@ -154,7 +154,7 @@ module Backup
end end
def archives_to_backup def archives_to_backup
%w{uploads builds artifacts}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
end end
def folders_to_backup def folders_to_backup
......
...@@ -10,23 +10,9 @@ module Gitlab ...@@ -10,23 +10,9 @@ module Gitlab
@request = request @request = request
end end
# Return a response for a download request
# Can be a response to:
# Request from a user to get the file
# Request from gitlab-workhorse which file to serve to the user
def render_download_hypermedia_response(oid)
render_response_to_download do
if check_download_accept_header?
render_lfs_download_hypermedia(oid)
else
render_not_found
end
end
end
def render_download_object_response(oid) def render_download_object_response(oid)
render_response_to_download do render_response_to_download do
if check_download_sendfile_header? && check_download_accept_header? if check_download_sendfile_header?
render_lfs_sendfile(oid) render_lfs_sendfile(oid)
else else
render_not_found render_not_found
...@@ -34,20 +20,15 @@ module Gitlab ...@@ -34,20 +20,15 @@ module Gitlab
end end
end end
def render_lfs_api_auth def render_batch_operation_response
render_response_to_push do
request_body = JSON.parse(@request.body.read) request_body = JSON.parse(@request.body.read)
return render_not_found if request_body.empty? || request_body['objects'].empty? case request_body["operation"]
when "download"
response = build_response(request_body['objects']) render_batch_download(request_body)
[ when "upload"
200, render_batch_upload(request_body)
{ else
"Content-Type" => "application/json; charset=utf-8", render_not_found
"Cache-Control" => "private",
},
[JSON.dump(response)]
]
end end
end end
...@@ -71,13 +52,24 @@ module Gitlab ...@@ -71,13 +52,24 @@ module Gitlab
end end
end end
def render_unsupported_deprecated_api
[
501,
{ "Content-Type" => "application/json; charset=utf-8" },
[JSON.dump({
'message' => 'Server supports batch API only, please update your Git LFS client to version 0.6.0 and up.',
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
})]
]
end
private private
def render_not_enabled def render_not_enabled
[ [
501, 501,
{ {
"Content-Type" => "application/vnd.git-lfs+json", "Content-Type" => "application/json; charset=utf-8",
}, },
[JSON.dump({ [JSON.dump({
'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.', 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
...@@ -142,18 +134,35 @@ module Gitlab ...@@ -142,18 +134,35 @@ module Gitlab
end end
end end
def render_lfs_download_hypermedia(oid) def render_batch_upload(body)
return render_not_found unless oid.present? return render_not_found if body.empty? || body['objects'].nil?
lfs_object = object_for_download(oid) render_response_to_push do
if lfs_object response = build_upload_batch_response(body['objects'])
[ [
200, 200,
{ "Content-Type" => "application/vnd.git-lfs+json" }, {
[JSON.dump(download_hypermedia(oid))] "Content-Type" => "application/json; charset=utf-8",
"Cache-Control" => "private",
},
[JSON.dump(response)]
]
end
end
def render_batch_download(body)
return render_not_found if body.empty? || body['objects'].nil?
render_response_to_download do
response = build_download_batch_response(body['objects'])
[
200,
{
"Content-Type" => "application/json; charset=utf-8",
"Cache-Control" => "private",
},
[JSON.dump(response)]
] ]
else
render_not_found
end end
end end
...@@ -199,10 +208,6 @@ module Gitlab ...@@ -199,10 +208,6 @@ module Gitlab
@env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile" @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
end end
def check_download_accept_header?
@env['HTTP_ACCEPT'].to_s == "application/vnd.git-lfs+json; charset=utf-8"
end
def user_can_fetch? def user_can_fetch?
# Check user access against the project they used to initiate the pull # Check user access against the project they used to initiate the pull
@user.can?(:download_code, @origin_project) @user.can?(:download_code, @origin_project)
...@@ -266,42 +271,56 @@ module Gitlab ...@@ -266,42 +271,56 @@ module Gitlab
@project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
end end
def build_response(objects) def build_upload_batch_response(objects)
selected_objects = select_existing_objects(objects) selected_objects = select_existing_objects(objects)
upload_hypermedia(objects, selected_objects) upload_hypermedia_links(objects, selected_objects)
end end
def download_hypermedia(oid) def build_download_batch_response(objects)
{ selected_objects = select_existing_objects(objects)
'_links' => {
'download' => download_hypermedia_links(objects, selected_objects)
{ end
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{oid}",
def download_hypermedia_links(all_objects, existing_objects)
all_objects.each do |object|
if existing_objects.include?(object['oid'])
object['actions'] = {
'download' => {
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}",
'header' => { 'header' => {
'Accept' => "application/vnd.git-lfs+json; charset=utf-8",
'Authorization' => @env['HTTP_AUTHORIZATION'] 'Authorization' => @env['HTTP_AUTHORIZATION']
}.compact }.compact
} }
} }
else
object['error'] = {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it",
} }
end end
def upload_hypermedia(all_objects, existing_objects)
all_objects.each do |object|
object['_links'] = hypermedia_links(object) unless existing_objects.include?(object['oid'])
end end
{ 'objects' => all_objects } { 'objects' => all_objects }
end end
def hypermedia_links(object) def upload_hypermedia_links(all_objects, existing_objects)
{ all_objects.each do |object|
"upload" => { # generate actions only for non-existing objects
next if existing_objects.include?(object['oid'])
object['actions'] = {
'upload' => {
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}", 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
'header' => { 'Authorization' => @env['HTTP_AUTHORIZATION'] } 'header' => {
'Authorization' => @env['HTTP_AUTHORIZATION']
}.compact }.compact
} }
}
end
{ 'objects' => all_objects }
end end
end end
end end
......
...@@ -34,7 +34,7 @@ module Gitlab ...@@ -34,7 +34,7 @@ module Gitlab
case path_match[1] case path_match[1]
when "info/lfs" when "info/lfs"
lfs.render_download_hypermedia_response(oid) lfs.render_unsupported_deprecated_api
when "gitlab-lfs" when "gitlab-lfs"
lfs.render_download_object_response(oid) lfs.render_download_object_response(oid)
else else
...@@ -48,7 +48,9 @@ module Gitlab ...@@ -48,7 +48,9 @@ module Gitlab
# Check for Batch API # Check for Batch API
if post_path[0].ends_with?("/info/lfs/objects/batch") if post_path[0].ends_with?("/info/lfs/objects/batch")
lfs.render_lfs_api_auth lfs.render_batch_operation_response
elsif post_path[0].ends_with?("/info/lfs/objects")
lfs.render_unsupported_deprecated_api
else else
nil nil
end end
......
module Gitlab
module SQL
# Class for building SQL UNION statements.
#
# ORDER BYs are dropped from the relations as the final sort order is not
# guaranteed any way.
#
# Example usage:
#
# union = Gitlab::SQL::Union.new(user.personal_projects, user.projects)
# sql = union.to_sql
#
# Project.where("id IN (#{sql})")
class Union
def initialize(relations)
@relations = relations
end
def to_sql
# Some relations may include placeholders for prepared statements, these
# aren't incremented properly when joining relations together this way.
# By using "unprepared_statements" we remove the usage of placeholders
# (thus fixing this problem), at a slight performance cost.
fragments = ActiveRecord::Base.connection.unprepared_statement do
@relations.map do |rel|
rel.reorder(nil).to_sql
end
end
fragments.join("\nUNION\n")
end
end
end
end
...@@ -13,6 +13,7 @@ namespace :gitlab do ...@@ -13,6 +13,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:uploads:create"].invoke Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke
backup = Backup::Manager.new backup = Backup::Manager.new
backup.pack backup.pack
...@@ -34,6 +35,7 @@ namespace :gitlab do ...@@ -34,6 +35,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads") Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds") Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts") Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts")
Rake::Task["gitlab:backup:lfs:restore"].invoke unless backup.skipped?("lfs")
Rake::Task["gitlab:shell:setup"].invoke Rake::Task["gitlab:shell:setup"].invoke
backup.cleanup backup.cleanup
...@@ -134,6 +136,25 @@ namespace :gitlab do ...@@ -134,6 +136,25 @@ namespace :gitlab do
end end
end end
namespace :lfs do
task create: :environment do
$progress.puts "Dumping lfs objects ... ".blue
if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
$progress.puts "[SKIPPED]".cyan
else
Backup::Lfs.new.dump
$progress.puts "done".green
end
end
task restore: :environment do
$progress.puts "Restoring lfs objects ... ".blue
Backup::Lfs.new.restore
$progress.puts "done".green
end
end
def configure_cron_mode def configure_cron_mode
if ENV['CRON'] if ENV['CRON']
# We need an object we can say 'puts' and 'print' to; let's use a # We need an object we can say 'puts' and 'print' to; let's use a
......
require 'spec_helper'
describe SnippetsController do
describe 'GET #show' do
let(:user) { create(:user) }
context 'when the personal snippet is private' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
context 'when signed in' do
before do
sign_in(user)
end
context 'when signed in user is not the author' do
let(:other_author) { create(:author) }
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
it 'responds with status 404' do
get :show, id: other_personal_snippet.to_param
expect(response.status).to eq(404)
end
end
context 'when signed in user is the author' do
it 'renders the snippet' do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
expect(response.status).to eq(200)
end
end
end
context 'when not signed in' do
it 'redirects to the sign in page' do
get :show, id: personal_snippet.to_param
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'when the personal snippet is internal' do
let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
context 'when signed in' do
before do
sign_in(user)
end
it 'renders the snippet' do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
expect(response.status).to eq(200)
end
end
context 'when not signed in' do
it 'redirects to the sign in page' do
get :show, id: personal_snippet.to_param
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'when the personal snippet is public' do
let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
context 'when signed in' do
before do
sign_in(user)
end
it 'renders the snippet' do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
expect(response.status).to eq(200)
end
end
context 'when not signed in' do
it 'renders the snippet' do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
expect(response.status).to eq(200)
end
end
end
context 'when the personal snippet does not exist' do
context 'when signed in' do
before do
sign_in(user)
end
it 'responds with status 404' do
get :show, id: 'doesntexist'
expect(response.status).to eq(404)
end
end
context 'when not signed in' do
it 'responds with status 404' do
get :show, id: 'doesntexist'
expect(response.status).to eq(404)
end
end
end
end
end
...@@ -16,15 +16,28 @@ describe UsersController do ...@@ -16,15 +16,28 @@ describe UsersController do
context 'with rendered views' do context 'with rendered views' do
render_views render_views
it 'renders the show template' do describe 'when logged in' do
before do
sign_in(user) sign_in(user)
end
it 'renders the show template' do
get :show, username: user.username get :show, username: user.username
expect(response).to be_success expect(response).to be_success
expect(response).to render_template('show') expect(response).to render_template('show')
end end
end end
describe 'when logged out' do
it 'renders the show template' do
get :show, username: user.username
expect(response).to be_success
expect(response).to render_template('show')
end
end
end
end end
describe 'GET #calendar' do describe 'GET #calendar' do
......
...@@ -169,6 +169,18 @@ FactoryGirl.define do ...@@ -169,6 +169,18 @@ FactoryGirl.define do
title title
content content
file_name file_name
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
end
trait :internal do
visibility_level Gitlab::VisibilityLevel::INTERNAL
end
trait :private do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
end end
factory :snippet do factory :snippet do
......
require 'spec_helper'
describe ContributedProjectsFinder do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:finder) { described_class.new(source_user) }
let!(:public_project) { create(:project, :public) }
let!(:private_project) { create(:project, :private) }
before do
private_project.team << [source_user, Gitlab::Access::MASTER]
private_project.team << [current_user, Gitlab::Access::DEVELOPER]
public_project.team << [source_user, Gitlab::Access::MASTER]
create(:event, action: Event::PUSHED, project: public_project,
target: public_project, author: source_user)
create(:event, action: Event::PUSHED, project: private_project,
target: private_project, author: source_user)
end
describe 'without a current user' do
subject { finder.execute }
it { is_expected.to eq([public_project]) }
end
describe 'with a current user' do
subject { finder.execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
end
end
require 'spec_helper'
describe GroupsFinder do
let(:user) { create :user }
let!(:group) { create :group }
let!(:public_group) { create :group, public: true }
describe :execute do
it 'finds public group' do
groups = GroupsFinder.new.execute(user)
expect(groups.size).to eq(1)
expect(groups.first).to eq(public_group)
end
end
end
require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
let(:user) { create(:user) }
let(:group1) { create(:group) }
let(:group2) { create(:group) }
let(:group3) { create(:group) }
let(:group4) { create(:group, public: true) }
let!(:public_project) { create(:project, :public, group: group1) }
let!(:internal_project) { create(:project, :internal, group: group2) }
let!(:private_project) { create(:project, :private, group: group3) }
let(:finder) { described_class.new }
describe 'with a user' do
subject { finder.execute(user) }
describe 'when the user is not a member of any groups' do
it { is_expected.to eq([group4, group2, group1]) }
end
describe 'when the user is a member of a group' do
before do
group3.add_user(user, Gitlab::Access::DEVELOPER)
end
it { is_expected.to eq([group4, group3, group2, group1]) }
end
describe 'when the user is a member of a private project' do
before do
private_project.team.add_user(user, Gitlab::Access::DEVELOPER)
end
it { is_expected.to eq([group4, group3, group2, group1]) }
end
end
describe 'without a user' do
subject { finder.execute }
it { is_expected.to eq([group4, group1]) }
end
end
end
require 'spec_helper'
describe JoinedGroupsFinder do
describe '#execute' do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:group1) { create(:group) }
let(:group2) { create(:group) }
let(:group3) { create(:group) }
let(:group4) { create(:group, public: true) }
let!(:public_project) { create(:project, :public, group: group1) }
let!(:internal_project) { create(:project, :internal, group: group2) }
let!(:private_project) { create(:project, :private, group: group3) }
let(:finder) { described_class.new(source_user) }
before do
[group1, group2, group3, group4].each do |group|
group.add_user(source_user, Gitlab::Access::MASTER)
end
end
describe 'with a current user' do
describe 'when the current user has access to the projects of the source user' do
before do
private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER)
end
subject { finder.execute(current_user) }
it { is_expected.to eq([group4, group3, group2, group1]) }
end
describe 'when the current user does not have access to the projects of the source user' do
subject { finder.execute(current_user) }
it { is_expected.to eq([group4, group2, group1]) }
end
end
describe 'without a current user' do
subject { finder.execute }
it { is_expected.to eq([group4, group1]) }
end
end
end
require 'spec_helper'
describe PersonalProjectsFinder do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:finder) { described_class.new(source_user) }
let!(:public_project) do
create(:project, :public, namespace: source_user.namespace, name: 'A',
path: 'A')
end
let!(:private_project) do
create(:project, :private, namespace: source_user.namespace, name: 'B',
path: 'B')
end
before do
private_project.team << [current_user, Gitlab::Access::DEVELOPER]
end
describe 'without a current user' do
subject { finder.execute }
it { is_expected.to eq([public_project]) }
end
describe 'with a current user' do
subject { finder.execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
end
end
require 'spec_helper' require 'spec_helper'
describe ProjectsFinder do describe ProjectsFinder do
<<<<<<< HEAD
let(:user) { create :user } let(:user) { create :user }
let(:group) { create :group } let(:group) { create :group }
let(:group2) { create :group } let(:group2) { create :group }
...@@ -13,45 +14,59 @@ describe ProjectsFinder do ...@@ -13,45 +14,59 @@ describe ProjectsFinder do
let(:project6) { create(:empty_project, :internal, group: group2) } let(:project6) { create(:empty_project, :internal, group: group2) }
let(:project7) { create(:empty_project, :public, group: group2) } let(:project7) { create(:empty_project, :public, group: group2) }
let(:project8) { create(:empty_project, :private, group: group2) } let(:project8) { create(:empty_project, :private, group: group2) }
=======
describe '#execute' do
let(:user) { create(:user) }
context 'non authenticated' do let!(:private_project) { create(:project, :private) }
subject { ProjectsFinder.new.execute(nil, group: group) } let!(:internal_project) { create(:project, :internal) }
let!(:public_project) { create(:project, :public) }
>>>>>>> b6f0eddce552d7423869e9072a7a0706e309dbdf
it { is_expected.to include(project1) } let(:finder) { described_class.new }
it { is_expected.not_to include(project2) }
it { is_expected.not_to include(project3) }
it { is_expected.not_to include(project4) }
end
context 'authenticated' do describe 'without a group' do
subject { ProjectsFinder.new.execute(user, group: group) } describe 'without a user' do
subject { finder.execute }
it { is_expected.to include(project1) } it { is_expected.to eq([public_project]) }
it { is_expected.to include(project2) }
it { is_expected.not_to include(project3) }
it { is_expected.not_to include(project4) }
end end
context 'authenticated, project member' do describe 'with a user' do
before { project3.team << [user, :developer] } subject { finder.execute(user) }
subject { ProjectsFinder.new.execute(user, group: group) } describe 'without private projects' do
it { is_expected.to eq([public_project, internal_project]) }
end
it { is_expected.to include(project1) } describe 'with private projects' do
it { is_expected.to include(project2) } before do
it { is_expected.to include(project3) } private_project.team.add_user(user, Gitlab::Access::MASTER)
it { is_expected.not_to include(project4) }
end end
context 'authenticated, group member' do it do
before { group.add_developer(user) } is_expected.to eq([public_project, internal_project,
private_project])
end
end
end
end
subject { ProjectsFinder.new.execute(user, group: group) } describe 'with a group' do
let(:group) { public_project.group }
it { is_expected.to include(project1) } describe 'without a user' do
it { is_expected.to include(project2) } subject { finder.execute(nil, group: group) }
it { is_expected.to include(project3) }
it { is_expected.to include(project4) } it { is_expected.to eq([public_project]) }
end
describe 'with a user' do
subject { finder.execute(user, group: group) }
it { is_expected.to eq([public_project, internal_project]) }
end
end
end end
context 'authenticated, group member with project shared with group' do context 'authenticated, group member with project shared with group' do
......
...@@ -127,4 +127,30 @@ describe IssuesHelper do ...@@ -127,4 +127,30 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") } it { is_expected.to eq("!1, !2, or !3") }
end end
describe "#url_to_emoji" do
it "returns url" do
expect(url_to_emoji("smile")).to include("emoji/1F604.png")
end
end
describe "#emoji_list" do
it "returns url" do
expect(emoji_list).to be_kind_of(Array)
end
end
describe "#note_active_class" do
before do
@note = create :note
@note1 = create :note
end
it "returns empty string for unauthenticated user" do
expect(note_active_class(Note.all, nil)).to eq("")
end
it "returns active string for author" do
expect(note_active_class(Note.all, @note.author)).to eq("active")
end
end
end end
...@@ -425,8 +425,12 @@ module Ci ...@@ -425,8 +425,12 @@ module Ci
end end
describe "Error handling" do describe "Error handling" do
it "fails to parse YAML" do
expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
end
it "indicates that object is invalid" do it "indicates that object is invalid" do
expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError) expect{GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
end end
it "returns errors if tags parameter is invalid" do it "returns errors if tags parameter is invalid" do
......
...@@ -26,34 +26,71 @@ describe Gitlab::Lfs::Router do ...@@ -26,34 +26,71 @@ describe Gitlab::Lfs::Router do
let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
let(:sample_size) { 499013 } let(:sample_size) { 499013 }
let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 0.6.0 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
describe 'when lfs is disabled' do describe 'when lfs is disabled' do
before do before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" env['REQUEST_METHOD'] = 'POST'
body = {
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078
},
{ 'oid' => sample_oid,
'size' => sample_size
}
],
'operation' => 'upload'
}.to_json
env['rack.input'] = StringIO.new(body)
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
end end
it 'responds with 501' do it 'responds with 501' do
respond_with_disabled = [ 501,
{ "Content-Type"=>"application/vnd.git-lfs+json" },
["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]
]
expect(lfs_router_auth.try_call).to match_array(respond_with_disabled) expect(lfs_router_auth.try_call).to match_array(respond_with_disabled)
end end
end end
describe 'when fetching lfs object using deprecated API' do
before do
enable_lfs
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
end
it 'responds with 501' do
expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
end
end
describe 'when fetching lfs object' do describe 'when fetching lfs object' do
before do before do
enable_lfs enable_lfs
env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8"
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}"
end end
describe 'when user is authenticated' do describe 'and request comes from gitlab-workhorse' do
context 'and user has project download access' do context 'without user being authorized' do
it "responds with status 401" do
expect(lfs_router_noauth.try_call.first).to eq(401)
end
end
context 'with required headers' do
before do
env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
end
context 'when user does not have project access' do
it "responds with status 403" do
expect(lfs_router_auth.try_call.first).to eq(403)
end
end
context 'when user has project access' do
before do before do
@auth = authorize(user)
env["HTTP_AUTHORIZATION"] = @auth
project.lfs_objects << lfs_object project.lfs_objects << lfs_object
project.team << [user, :master] project.team << [user, :master]
end end
...@@ -62,193 +99,250 @@ describe Gitlab::Lfs::Router do ...@@ -62,193 +99,250 @@ describe Gitlab::Lfs::Router do
expect(lfs_router_auth.try_call.first).to eq(200) expect(lfs_router_auth.try_call.first).to eq(200)
end end
it "responds with download hypermedia" do it "responds with the file location" do
json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first) expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") end
expect(json_response['_links']['download']['header']).to eq("Authorization" => @auth, "Accept" => "application/vnd.git-lfs+json; charset=utf-8")
end end
end end
context 'and user does not have project access' do context 'without required headers' do
it "responds with status 403" do it "responds with status 403" do
expect(lfs_router_auth.try_call.first).to eq(403) expect(lfs_router_auth.try_call.first).to eq(403)
end end
end end
end end
describe 'when user is unauthenticated' do
context 'and user does not have download access' do
it "responds with status 401" do
expect(lfs_router_noauth.try_call.first).to eq(401)
end
end end
context 'and user has download access' do describe 'when handling lfs request using deprecated API' do
before do before do
project.team << [user, :master] enable_lfs
env['REQUEST_METHOD'] = 'POST'
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects"
end end
it "responds with status 401" do it 'responds with 501' do
expect(lfs_router_noauth.try_call.first).to eq(401) expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
end
end end
end end
describe 'and project is public' do describe 'when handling lfs batch request' do
context 'and project has access to the lfs object' do
before do before do
public_project.lfs_objects << lfs_object enable_lfs
env['REQUEST_METHOD'] = 'POST'
env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
end end
context 'and user is authenticated' do describe 'download' do
it "responds with status 200 and sends download hypermedia" do describe 'when user is authenticated' do
expect(lfs_router_public_auth.try_call.first).to eq(200) before do
json_response = ActiveSupport::JSON.decode(lfs_router_public_auth.try_call.last.first) body = { 'operation' => 'download',
'objects' => [
expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") { 'oid' => sample_oid,
expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8") 'size' => sample_size
end }]
}.to_json
env['rack.input'] = StringIO.new(body)
end end
context 'and user is unauthenticated' do describe 'when user has download access' do
it "responds with status 200 and sends download hypermedia" do before do
expect(lfs_router_public_noauth.try_call.first).to eq(200) @auth = authorize(user)
json_response = ActiveSupport::JSON.decode(lfs_router_public_noauth.try_call.last.first) env["HTTP_AUTHORIZATION"] = @auth
project.team << [user, :reporter]
expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}")
expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8")
end
end
end end
context 'and project does not have access to the lfs object' do context 'when downloading an lfs object that is assigned to our project' do
it "responds with status 404" do before do
expect(lfs_router_public_auth.try_call.first).to eq(404) project.lfs_objects << lfs_object
end end
it 'responds with status 200 and href to download' do
response = lfs_router_auth.try_call
expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first)
expect(response_body).to eq('objects' => [
{ 'oid' => sample_oid,
'size' => sample_size,
'actions' => {
'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'header' => { 'Authorization' => @auth }
}
}
}])
end end
end end
describe 'and request comes from gitlab-workhorse' do context 'when downloading an lfs object that is assigned to other project' do
before do before do
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}" public_project.lfs_objects << lfs_object
end end
context 'without user being authorized' do
it "responds with status 401" do it 'responds with status 200 and error message' do
expect(lfs_router_noauth.try_call.first).to eq(401) response = lfs_router_auth.try_call
expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first)
expect(response_body).to eq('objects' => [
{ 'oid' => sample_oid,
'size' => sample_size,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it",
}
}])
end end
end end
context 'with required headers' do context 'when downloading a lfs object that does not exist' do
before do before do
env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile" body = { 'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078
}]
}.to_json
env['rack.input'] = StringIO.new(body)
end end
context 'when user does not have project access' do it "responds with status 200 and error message" do
it "responds with status 403" do response = lfs_router_auth.try_call
expect(lfs_router_auth.try_call.first).to eq(403) expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first)
expect(response_body).to eq('objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it",
}
}])
end end
end end
context 'when user has project access' do context 'when downloading one new and one existing lfs object' do
before do before do
body = { 'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078
},
{ 'oid' => sample_oid,
'size' => sample_size
}
]
}.to_json
env['rack.input'] = StringIO.new(body)
project.lfs_objects << lfs_object project.lfs_objects << lfs_object
project.team << [user, :master]
end end
it "responds with status 200" do it "responds with status 200 with upload hypermedia link for the new object" do
expect(lfs_router_auth.try_call.first).to eq(200) response = lfs_router_auth.try_call
end expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first)
it "responds with the file location" do expect(response_body).to eq('objects' => [
expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) 'size' => 1575078,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it",
}
},
{ 'oid' => sample_oid,
'size' => sample_size,
'actions' => {
'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'header' => { 'Authorization' => @auth }
}
}
}])
end end
end end
end end
context 'without required headers' do context 'when user does is not member of the project' do
it "responds with status 403" do before do
expect(lfs_router_auth.try_call.first).to eq(403) @auth = authorize(user)
env["HTTP_AUTHORIZATION"] = @auth
project.team << [user, :guest]
end end
it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403)
end end
end end
describe 'from a forked public project' do context 'when user does not have download access' do
before do before do
env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" @auth = authorize(user)
env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" env["HTTP_AUTHORIZATION"] = @auth
project.team << [user, :guest]
end end
context "when fetching a lfs object" do it 'responds with 403' do
context "and user has project download access" do expect(lfs_router_auth.try_call.first).to eq(403)
before do
public_project.lfs_objects << lfs_object
end end
it "can download the lfs object" do
expect(lfs_router_forked_auth.try_call.first).to eq(200)
json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}")
expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8")
end end
end end
context "and user is not authenticated but project is public" do context 'when user is not authenticated' do
before do before do
public_project.lfs_objects << lfs_object body = { 'operation' => 'download',
end 'objects' => [
{ 'oid' => sample_oid,
'size' => sample_size
}],
it "can download the lfs object" do }.to_json
expect(lfs_router_forked_auth.try_call.first).to eq(200) env['rack.input'] = StringIO.new(body)
end
end end
context "and user has project download access" do describe 'is accessing public project' do
before do before do
env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897" public_project.lfs_objects << lfs_object
@auth = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
env["HTTP_AUTHORIZATION"] = @auth
lfs_object_two = create(:lfs_object, :with_file, oid: "91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", size: 1575078)
public_project.lfs_objects << lfs_object_two
end end
it "can get a lfs object that is not in the forked project" do it 'responds with status 200 and href to download' do
expect(lfs_router_forked_auth.try_call.first).to eq(200) response = lfs_router_public_noauth.try_call
expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first)
json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) expect(response_body).to eq('objects' => [
expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") { 'oid' => sample_oid,
expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8", "Authorization" => @auth) 'size' => sample_size,
'actions' => {
'download' => {
'href' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'header' => {}
}
}
}])
end end
end end
context "and user has project download access" do describe 'is accessing non-public project' do
before do before do
env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b" project.lfs_objects << lfs_object
lfs_object_three = create(:lfs_object, :with_file, oid: "267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b", size: 127192524)
project.lfs_objects << lfs_object_three
end end
it "cannot get a lfs object that is not in the project" do it 'responds with authorization required' do
expect(lfs_router_forked_auth.try_call.first).to eq(404) expect(lfs_router_noauth.try_call.first).to eq(401)
end
end
end end
end end
end end
describe 'when initiating pushing of the lfs object' do
before do
enable_lfs
env['REQUEST_METHOD'] = 'POST'
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
end end
describe 'upload' do
describe 'when user is authenticated' do describe 'when user is authenticated' do
before do before do
body = { 'objects' => [{ body = { 'operation' => 'upload',
'oid' => sample_oid, 'objects' => [
{ 'oid' => sample_oid,
'size' => sample_size 'size' => sample_size
}] }]
}.to_json }.to_json
...@@ -259,7 +353,7 @@ describe Gitlab::Lfs::Router do ...@@ -259,7 +353,7 @@ describe Gitlab::Lfs::Router do
before do before do
@auth = authorize(user) @auth = authorize(user)
env["HTTP_AUTHORIZATION"] = @auth env["HTTP_AUTHORIZATION"] = @auth
project.team << [user, :master] project.team << [user, :developer]
end end
context 'when pushing an lfs object that already exists' do context 'when pushing an lfs object that already exists' do
...@@ -276,15 +370,16 @@ describe Gitlab::Lfs::Router do ...@@ -276,15 +370,16 @@ describe Gitlab::Lfs::Router do
expect(response['objects'].first['size']).to eq(sample_size) expect(response['objects'].first['size']).to eq(sample_size)
expect(lfs_object.projects.pluck(:id)).to_not include(project.id) expect(lfs_object.projects.pluck(:id)).to_not include(project.id)
expect(lfs_object.projects.pluck(:id)).to include(public_project.id) expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
expect(response['objects'].first).to have_key('_links') expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
end end
end end
context 'when pushing a lfs object that does not exist' do context 'when pushing a lfs object that does not exist' do
before do before do
body = { body = { 'operation' => 'upload',
'objects' => [{ 'objects' => [
'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078 'size' => 1575078
}] }]
}.to_json }.to_json
...@@ -300,14 +395,14 @@ describe Gitlab::Lfs::Router do ...@@ -300,14 +395,14 @@ describe Gitlab::Lfs::Router do
expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
expect(response_body['objects'].first['size']).to eq(1575078) expect(response_body['objects'].first['size']).to eq(1575078)
expect(lfs_object.projects.pluck(:id)).not_to include(project.id) expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth) expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
end end
end end
context 'when pushing one new and one existing lfs object' do context 'when pushing one new and one existing lfs object' do
before do before do
body = { body = { 'operation' => 'upload',
'objects' => [ 'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
'size' => 1575078 'size' => 1575078
...@@ -318,7 +413,7 @@ describe Gitlab::Lfs::Router do ...@@ -318,7 +413,7 @@ describe Gitlab::Lfs::Router do
] ]
}.to_json }.to_json
env['rack.input'] = StringIO.new(body) env['rack.input'] = StringIO.new(body)
public_project.lfs_objects << lfs_object project.lfs_objects << lfs_object
end end
it "responds with status 200 with upload hypermedia link for the new object" do it "responds with status 200 with upload hypermedia link for the new object" do
...@@ -328,17 +423,14 @@ describe Gitlab::Lfs::Router do ...@@ -328,17 +423,14 @@ describe Gitlab::Lfs::Router do
response_body = ActiveSupport::JSON.decode(response.last.first) response_body = ActiveSupport::JSON.decode(response.last.first)
expect(response_body['objects']).to be_kind_of(Array) expect(response_body['objects']).to be_kind_of(Array)
expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
expect(response_body['objects'].first['size']).to eq(1575078) expect(response_body['objects'].first['size']).to eq(1575078)
expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth) expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth)
expect(response_body['objects'].last['oid']).to eq(sample_oid) expect(response_body['objects'].last['oid']).to eq(sample_oid)
expect(response_body['objects'].last['size']).to eq(sample_size) expect(response_body['objects'].last['size']).to eq(sample_size)
expect(lfs_object.projects.pluck(:id)).to_not include(project.id) expect(response_body['objects'].last).to_not have_key('actions')
expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
expect(response_body['objects'].last).to have_key('_links')
end end
end end
end end
...@@ -351,6 +443,12 @@ describe Gitlab::Lfs::Router do ...@@ -351,6 +443,12 @@ describe Gitlab::Lfs::Router do
end end
context 'when user is not authenticated' do context 'when user is not authenticated' do
before do
env['rack.input'] = StringIO.new(
{ 'objects' => [], 'operation' => 'upload' }.to_json
)
end
context 'when user has push access' do context 'when user has push access' do
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -369,6 +467,23 @@ describe Gitlab::Lfs::Router do ...@@ -369,6 +467,23 @@ describe Gitlab::Lfs::Router do
end end
end end
describe 'unsupported' do
before do
body = { 'operation' => 'other',
'objects' => [
{ 'oid' => sample_oid,
'size' => sample_size
}]
}.to_json
env['rack.input'] = StringIO.new(body)
end
it 'responds with status 404' do
expect(lfs_router_public_noauth.try_call.first).to eq(404)
end
end
end
describe 'when pushing a lfs object' do describe 'when pushing a lfs object' do
before do before do
enable_lfs enable_lfs
......
require 'spec_helper'
describe Gitlab::SQL::Union do
describe '#to_sql' do
it 'returns a String joining relations together using a UNION' do
rel1 = User.where(email: 'alice@example.com')
rel2 = User.where(email: 'bob@example.com')
union = described_class.new([rel1, rel2])
sql1 = rel1.reorder(nil).to_sql
sql2 = rel2.reorder(nil).to_sql
expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}")
end
end
end
require 'spec_helper'
describe Issue, 'Votes' do
let(:issue) { create(:issue) }
describe "#upvotes" do
it "with no notes has a 0/0 score" do
expect(issue.upvotes).to eq(0)
end
it "should recognize non-+1 notes" do
add_note "No +1 here"
expect(issue.notes.size).to eq(1)
expect(issue.notes.first.upvote?).to be_falsey
expect(issue.upvotes).to eq(0)
end
it "should recognize a single +1 note" do
add_note "+1 This is awesome"
expect(issue.upvotes).to eq(1)
end
it 'should recognize multiple +1 notes' do
add_note '+1 This is awesome', create(:user)
add_note '+1 I want this', create(:user)
expect(issue.upvotes).to eq(2)
end
it 'should not count 2 +1 votes from the same user' do
add_note '+1 This is awesome'
add_note '+1 I want this'
expect(issue.upvotes).to eq(1)
end
end
describe "#downvotes" do
it "with no notes has a 0/0 score" do
expect(issue.downvotes).to eq(0)
end
it "should recognize non--1 notes" do
add_note "Almost got a -1"
expect(issue.notes.size).to eq(1)
expect(issue.notes.first.downvote?).to be_falsey
expect(issue.downvotes).to eq(0)
end
it "should recognize a single -1 note" do
add_note "-1 This is bad"
expect(issue.downvotes).to eq(1)
end
it "should recognize multiple -1 notes" do
add_note('-1 This is bad', create(:user))
add_note('-1 Away with this', create(:user))
expect(issue.downvotes).to eq(2)
end
end
describe "#votes_count" do
it "with no notes has a 0/0 score" do
expect(issue.votes_count).to eq(0)
end
it "should recognize non notes" do
add_note "No +1 here"
expect(issue.notes.size).to eq(1)
expect(issue.votes_count).to eq(0)
end
it "should recognize a single +1 note" do
add_note "+1 This is awesome"
expect(issue.votes_count).to eq(1)
end
it "should recognize a single -1 note" do
add_note "-1 This is bad"
expect(issue.votes_count).to eq(1)
end
it "should recognize multiple notes" do
add_note('+1 This is awesome', create(:user))
add_note('-1 This is bad', create(:user))
add_note('+1 I want this', create(:user))
expect(issue.votes_count).to eq(3)
end
it 'should not count 2 -1 votes from the same user' do
add_note '-1 This is suspicious'
add_note '-1 This is bad'
expect(issue.votes_count).to eq(1)
end
end
describe "#upvotes_in_percent" do
it "with no notes has a 0% score" do
expect(issue.upvotes_in_percent).to eq(0)
end
it "should count a single 1 note as 100%" do
add_note "+1 This is awesome"
expect(issue.upvotes_in_percent).to eq(100)
end
it 'should count multiple +1 notes as 100%' do
add_note('+1 This is awesome', create(:user))
add_note('+1 I want this', create(:user))
expect(issue.upvotes_in_percent).to eq(100)
end
it 'should count fractions for multiple +1 and -1 notes correctly' do
add_note('+1 This is awesome', create(:user))
add_note('+1 I want this', create(:user))
add_note('-1 This is bad', create(:user))
add_note('+1 me too', create(:user))
expect(issue.upvotes_in_percent).to eq(75)
end
end
describe "#downvotes_in_percent" do
it "with no notes has a 0% score" do
expect(issue.downvotes_in_percent).to eq(0)
end
it "should count a single -1 note as 100%" do
add_note "-1 This is bad"
expect(issue.downvotes_in_percent).to eq(100)
end
it 'should count multiple -1 notes as 100%' do
add_note('-1 This is bad', create(:user))
add_note('-1 Away with this', create(:user))
expect(issue.downvotes_in_percent).to eq(100)
end
it 'should count fractions for multiple +1 and -1 notes correctly' do
add_note('+1 This is awesome', create(:user))
add_note('+1 I want this', create(:user))
add_note('-1 This is bad', create(:user))
add_note('+1 me too', create(:user))
expect(issue.downvotes_in_percent).to eq(25)
end
end
describe '#filter_superceded_votes' do
it 'should count a users vote only once amongst multiple votes' do
add_note('-1 This needs work before I will accept it')
add_note('+1 I want this', create(:user))
add_note('+1 This is is awesome', create(:user))
add_note('+1 this looks good now')
add_note('+1 This is awesome', create(:user))
add_note('+1 me too', create(:user))
expect(issue.downvotes).to eq(0)
expect(issue.upvotes).to eq(5)
end
it 'should count each users vote only once' do
add_note '-1 This needs work before it will be accepted'
add_note '+1 I like this'
add_note '+1 I still like this'
add_note '+1 I really like this'
add_note '+1 Give me this now!!!!'
expect(issue.downvotes).to eq(0)
expect(issue.upvotes).to eq(1)
end
it 'should count a users vote only once without caring about comments' do
add_note '-1 This needs work before it will be accepted'
add_note 'Comment 1'
add_note 'Another comment'
add_note '+1 vote'
add_note 'final comment'
expect(issue.downvotes).to eq(0)
expect(issue.upvotes).to eq(1)
end
end
def add_note(text, author = issue.author)
created_at = Time.now - 1.hour + Note.count.seconds
issue.notes << create(:note,
note: text,
project: issue.project,
author_id: author.id,
created_at: created_at)
end
end
...@@ -64,4 +64,42 @@ describe Event do ...@@ -64,4 +64,42 @@ describe Event do
it { expect(@event.branch_name).to eq("master") } it { expect(@event.branch_name).to eq("master") }
it { expect(@event.author).to eq(@user) } it { expect(@event.author).to eq(@user) }
end end
describe '.latest_update_time' do
describe 'when events are present' do
let(:time) { Time.utc(2015, 1, 1) }
before do
create(:closed_issue_event, updated_at: time)
create(:closed_issue_event, updated_at: time + 5)
end
it 'returns the latest update time' do
expect(Event.latest_update_time).to eq(time + 5)
end
end
describe 'when no events exist' do
it 'returns nil' do
expect(Event.latest_update_time).to be_nil
end
end
end
describe '.limit_recent' do
let!(:event1) { create(:closed_issue_event) }
let!(:event2) { create(:closed_issue_event) }
describe 'without an explicit limit' do
subject { Event.limit_recent }
it { is_expected.to eq([event2, event1]) }
end
describe 'with an explicit limit' do
subject { Event.limit_recent(1) }
it { is_expected.to eq([event2]) }
end
end
end end
...@@ -38,6 +38,33 @@ describe Group do ...@@ -38,6 +38,33 @@ describe Group do
it { is_expected.not_to validate_presence_of :owner } it { is_expected.not_to validate_presence_of :owner }
end end
describe '.public_and_given_groups' do
let!(:public_group) { create(:group, public: true) }
subject { described_class.public_and_given_groups([group.id]) }
it { is_expected.to eq([public_group, group]) }
end
describe '.visible_to_user' do
let!(:group) { create(:group) }
let!(:user) { create(:user) }
subject { described_class.visible_to_user(user) }
describe 'when the user has access to a group' do
before do
group.add_user(user, Gitlab::Access::MASTER)
end
it { is_expected.to eq([group]) }
end
describe 'when the user does not have access to any groups' do
it { is_expected.to eq([]) }
end
end
describe '#to_reference' do describe '#to_reference' do
it 'returns a String reference to the object' do it 'returns a String reference to the object' do
expect(group.to_reference).to eq "@#{group.name}" expect(group.to_reference).to eq "@#{group.name}"
......
...@@ -32,77 +32,6 @@ describe Note do ...@@ -32,77 +32,6 @@ describe Note do
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
end end
describe '#votable?' do
it 'is true for issue notes' do
note = build(:note_on_issue)
expect(note).to be_votable
end
it 'is true for merge request notes' do
note = build(:note_on_merge_request)
expect(note).to be_votable
end
it 'is false for merge request diff notes' do
note = build(:note_on_merge_request_diff)
expect(note).not_to be_votable
end
it 'is false for commit notes' do
note = build(:note_on_commit)
expect(note).not_to be_votable
end
it 'is false for commit diff notes' do
note = build(:note_on_commit_diff)
expect(note).not_to be_votable
end
end
describe 'voting score' do
it 'recognizes a neutral note' do
note = build(:votable_note, note: 'This is not a +1 note')
expect(note).not_to be_upvote
expect(note).not_to be_downvote
end
it 'recognizes a neutral emoji note' do
note = build(:votable_note, note: "I would :+1: this, but I don't want to")
expect(note).not_to be_upvote
expect(note).not_to be_downvote
end
it 'recognizes a +1 note' do
note = build(:votable_note, note: '+1 for this')
expect(note).to be_upvote
end
it 'recognizes a +1 emoji as a vote' do
note = build(:votable_note, note: ':+1: for this')
expect(note).to be_upvote
end
it 'recognizes a thumbsup emoji as a vote' do
note = build(:votable_note, note: ':thumbsup: for this')
expect(note).to be_upvote
end
it 'recognizes a -1 note' do
note = build(:votable_note, note: '-1 for this')
expect(note).to be_downvote
end
it 'recognizes a -1 emoji as a vote' do
note = build(:votable_note, note: ':-1: for this')
expect(note).to be_downvote
end
it 'recognizes a thumbsdown emoji as a vote' do
note = build(:votable_note, note: ':thumbsdown: for this')
expect(note).to be_downvote
end
end
describe "Commit notes" do describe "Commit notes" do
let!(:note) { create(:note_on_commit, note: "+1 from me") } let!(:note) { create(:note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable } let!(:commit) { note.noteable }
...@@ -139,10 +68,6 @@ describe Note do ...@@ -139,10 +68,6 @@ describe Note do
it "should be recognized by #for_commit_diff_line?" do it "should be recognized by #for_commit_diff_line?" do
expect(note).to be_for_commit_diff_line expect(note).to be_for_commit_diff_line
end end
it "should not be votable" do
expect(note).not_to be_votable
end
end end
describe 'authorization' do describe 'authorization' do
...@@ -204,4 +129,16 @@ describe Note do ...@@ -204,4 +129,16 @@ describe Note do
it { expect(Note.search('wow')).to include(note) } it { expect(Note.search('wow')).to include(note) }
end end
describe :grouped_awards do
before do
create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true
end
it "returns grouped array of notes" do
expect(Note.grouped_awards.first.first).to eq("smile")
expect(Note.grouped_awards.first.last).to match_array(Note.all)
end
end
end end
...@@ -345,17 +345,6 @@ describe Project do ...@@ -345,17 +345,6 @@ describe Project do
expect(project1.star_count).to eq(0) expect(project1.star_count).to eq(0)
expect(project2.star_count).to eq(0) expect(project2.star_count).to eq(0)
end end
it 'is decremented when an upvoter account is deleted' do
user = create :user
project = create :project, :public
user.toggle_star(project)
project.reload
expect(project.star_count).to eq(1)
user.destroy
project.reload
expect(project.star_count).to eq(0)
end
end end
describe :avatar_type do describe :avatar_type do
...@@ -494,4 +483,23 @@ describe Project do ...@@ -494,4 +483,23 @@ describe Project do
end end
end end
end end
describe '.visible_to_user' do
let!(:project) { create(:project, :private) }
let!(:user) { create(:user) }
subject { described_class.visible_to_user(user) }
describe 'when a user has access to a project' do
before do
project.team.add_user(user, Gitlab::Access::MASTER)
end
it { is_expected.to eq([project]) }
end
describe 'when a user does not have access to any projects' do
it { is_expected.to eq([]) }
end
end
end end
...@@ -720,7 +720,7 @@ describe User do ...@@ -720,7 +720,7 @@ describe User do
end end
end end
describe "#contributed_projects_ids" do describe "#contributed_projects" do
subject { create(:user) } subject { create(:user) }
let!(:project1) { create(:project) } let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project3) } let!(:project2) { create(:project, forked_from_project: project3) }
...@@ -735,15 +735,15 @@ describe User do ...@@ -735,15 +735,15 @@ describe User do
end end
it "includes IDs for projects the user has pushed to" do it "includes IDs for projects the user has pushed to" do
expect(subject.contributed_projects_ids).to include(project1.id) expect(subject.contributed_projects).to include(project1)
end end
it "includes IDs for projects the user has had merge requests merged into" do it "includes IDs for projects the user has had merge requests merged into" do
expect(subject.contributed_projects_ids).to include(project3.id) expect(subject.contributed_projects).to include(project3)
end end
it "doesn't include IDs for unrelated projects" do it "doesn't include IDs for unrelated projects" do
expect(subject.contributed_projects_ids).not_to include(project2.id) expect(subject.contributed_projects).not_to include(project2)
end end
end end
...@@ -792,4 +792,30 @@ describe User do ...@@ -792,4 +792,30 @@ describe User do
expect(subject.recent_push).to eq(nil) expect(subject.recent_push).to eq(nil)
end end
end end
describe '#authorized_groups' do
let!(:user) { create(:user) }
let!(:private_group) { create(:group) }
before do
private_group.add_user(user, Gitlab::Access::MASTER)
end
subject { user.authorized_groups }
it { is_expected.to eq([private_group]) }
end
describe '#authorized_projects' do
let!(:user) { create(:user) }
let!(:private_project) { create(:project, :private) }
before do
private_project.team << [user, Gitlab::Access::MASTER]
end
subject { user.authorized_projects }
it { is_expected.to eq([private_project]) }
end
end end
...@@ -53,7 +53,7 @@ module Ci ...@@ -53,7 +53,7 @@ module Ci
end end
end end
it 'fails commits without .gitlab-ci.yml' do it 'skips commits without .gitlab-ci.yml' do
stub_ci_commit_yaml_file(nil) stub_ci_commit_yaml_file(nil)
result = service.execute(project, user, result = service.execute(project, user,
ref: 'refs/heads/0_1', ref: 'refs/heads/0_1',
...@@ -63,7 +63,24 @@ module Ci ...@@ -63,7 +63,24 @@ module Ci
) )
expect(result).to be_persisted expect(result).to be_persisted
expect(result.builds.any?).to be_falsey expect(result.builds.any?).to be_falsey
expect(result.status).to eq('failed') expect(result.status).to eq('skipped')
expect(result.yaml_errors).to be_nil
end
it 'skips commits if yaml is invalid' do
message = 'message'
allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
stub_ci_commit_yaml_file('invalid: file: file')
commits = [{ message: message }]
commit = service.execute(project, user,
ref: 'refs/tags/0_1',
before: '00000000',
after: '31das312',
commits: commits
)
expect(commit.builds.any?).to be false
expect(commit.status).to eq('failed')
expect(commit.yaml_errors).to_not be_nil
end end
describe :ci_skip? do describe :ci_skip? do
...@@ -100,7 +117,7 @@ module Ci ...@@ -100,7 +117,7 @@ module Ci
end end
it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
stub_ci_commit_yaml_file('invalid: file') stub_ci_commit_yaml_file('invalid: file: fiile')
commits = [{ message: message }] commits = [{ message: message }]
commit = service.execute(project, user, commit = service.execute(project, user,
ref: 'refs/tags/0_1', ref: 'refs/tags/0_1',
...@@ -110,6 +127,7 @@ module Ci ...@@ -110,6 +127,7 @@ module Ci
) )
expect(commit.builds.any?).to be false expect(commit.builds.any?).to be false
expect(commit.status).to eq("skipped") expect(commit.status).to eq("skipped")
expect(commit.yaml_errors).to be_nil
end end
end end
......
...@@ -24,4 +24,38 @@ describe Notes::CreateService do ...@@ -24,4 +24,38 @@ describe Notes::CreateService do
it { expect(@note.note).to eq('Awesome comment') } it { expect(@note.note).to eq('Awesome comment') }
end end
end end
describe "award emoji" do
before do
project.team << [user, :master]
end
it "creates emoji note" do
opts = {
note: ':smile: ',
noteable_type: 'Issue',
noteable_id: issue.id
}
@note = Notes::CreateService.new(project, user, opts).execute
expect(@note).to be_valid
expect(@note.note).to eq('smile')
expect(@note.is_award).to be_truthy
end
it "creates regular note if emoji name is invalid" do
opts = {
note: ':smile: moretext: ',
noteable_type: 'Issue',
noteable_id: issue.id
}
@note = Notes::CreateService.new(project, user, opts).execute
expect(@note).to be_valid
expect(@note.note).to eq(opts[:note])
expect(@note.is_award).to be_falsy
end
end
end end
...@@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do
end end
def reenable_backup_sub_tasks def reenable_backup_sub_tasks
%w{db repo uploads builds artifacts}.each do |subtask| %w{db repo uploads builds artifacts lfs}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end end
end end
...@@ -49,7 +49,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -49,7 +49,7 @@ describe 'gitlab:app namespace rake task' do
to raise_error(SystemExit) to raise_error(SystemExit)
end end
it 'should invoke restoration on mach' do it 'should invoke restoration on match' do
allow(YAML).to receive(:load_file). allow(YAML).to receive(:load_file).
and_return({ gitlab_version: gitlab_version }) and_return({ gitlab_version: gitlab_version })
expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
...@@ -57,6 +57,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -57,6 +57,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke) expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end end
...@@ -114,7 +115,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -114,7 +115,7 @@ describe 'gitlab:app namespace rake task' do
it 'should set correct permissions on the tar contents' do it 'should set correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen( tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz} %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
) )
expect(exit_status).to eq(0) expect(exit_status).to eq(0)
expect(tar_contents).to match('db/') expect(tar_contents).to match('db/')
...@@ -122,12 +123,13 @@ describe 'gitlab:app namespace rake task' do ...@@ -122,12 +123,13 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('repositories/') expect(tar_contents).to match('repositories/')
expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/) expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/)
end end
it 'should delete temp directories' do it 'should delete temp directories' do
temp_dirs = Dir.glob( temp_dirs = Dir.glob(
File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts}') File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}')
) )
expect(temp_dirs).to be_empty expect(temp_dirs).to be_empty
...@@ -163,13 +165,14 @@ describe 'gitlab:app namespace rake task' do ...@@ -163,13 +165,14 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen( tar_contents, _exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz} %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
) )
expect(tar_contents).to match('db/') expect(tar_contents).to match('db/')
expect(tar_contents).to match('uploads.tar.gz') expect(tar_contents).to match('uploads.tar.gz')
expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).not_to match('repositories/') expect(tar_contents).not_to match('repositories/')
end end
...@@ -183,6 +186,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -183,6 +186,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke
expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke
expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive :invoke
expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment