mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-09 12:46:07 +10:00
Merge remote-tracking branch 'ee_com/8-2-stable-ee' into ce_upstream
This commit is contained in:
@@ -3,6 +3,15 @@ Please view this file on the master branch, on stable branches it's out of date.
|
||||
v 8.3.0 (unreleased)
|
||||
|
||||
v 8.2.0
|
||||
v 8.0.1
|
||||
v 8.1.0 (unreleased)
|
||||
v 8.2.0 (unreleased)
|
||||
v 8.3.0 (unreleased)
|
||||
|
||||
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.
|
||||
- Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
|
||||
- Fix Drone CI service template not saving properly (Stan Hu)
|
||||
- Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu)
|
||||
@@ -10,6 +19,10 @@ v 8.2.0
|
||||
- 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
|
||||
- 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
|
||||
- Show last project commit to default branch on project home page
|
||||
- Highlight comment based on anchor in URL
|
||||
@@ -27,6 +40,7 @@ v 8.2.0
|
||||
- Allow to define cache in `.gitlab-ci.yml`
|
||||
- Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
|
||||
- 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
|
||||
- Fix omniauth documentation setting for omnibus configuration (Jon Cairns)
|
||||
- Add "New file" link to dropdown on project page
|
||||
@@ -34,6 +48,7 @@ v 8.2.0
|
||||
- Add "added", "modified" and "removed" properties to commit object in webhook
|
||||
- Rename "Back to" links to "Go to" because its not always a case it point to place user come from
|
||||
- Allow groups to appear in the search results if the group owner allows it
|
||||
- Add email notification to former assignee upon unassignment (Adam Lieskovský)
|
||||
- New design for project graphs page
|
||||
- Remove deprecated dumped yaml file generated from previous job definitions
|
||||
- Fix incoming email config defaults
|
||||
@@ -47,6 +62,9 @@ v 8.2.0
|
||||
- Fix trailing whitespace issue in merge request/issue title
|
||||
- Fix bug when milestone/label filter was empty for dashboard issues page
|
||||
- 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)
|
||||
- Add Award Emoji to issue and merge request pages
|
||||
|
||||
v 8.1.4
|
||||
- Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic
|
||||
|
||||
## Security vulnerability disclosure
|
||||
|
||||
Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
|
||||
Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
|
||||
|
||||
## Closing policy for issues and merge requests
|
||||
|
||||
@@ -35,7 +35,7 @@ The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab
|
||||
|
||||
Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple.
|
||||
|
||||
Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there.
|
||||
Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there.
|
||||
|
||||
### Issue tracker guidelines
|
||||
|
||||
@@ -72,7 +72,7 @@ If you can, please submit a merge request with the fix or improvements including
|
||||
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
|
||||
1. Add your changes to the [CHANGELOG](CHANGELOG)
|
||||
1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message
|
||||
1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
|
||||
1. If you have multiple commits please combine them into one commit by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
|
||||
1. Push the commit to your fork
|
||||
1. Submit a merge request (MR) to the master branch
|
||||
1. The MR title should describe the change you want to make
|
||||
@@ -181,4 +181,4 @@ This code of conduct applies both within project spaces and in public spaces whe
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.6
|
||||
2.6.7
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
0.4.1
|
||||
@@ -1 +1 @@
|
||||
0.3.1
|
||||
0.4.2
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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
|
||||
$("<img>").attr({src: custom_path, width: 20, height: 20}).wrap("<div>").parent().html()
|
||||
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 + "']")
|
||||
@@ -23,18 +23,6 @@ class @BlobFileDropzone
|
||||
init: ->
|
||||
this.on 'addedfile', (file) ->
|
||||
$('.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
|
||||
|
||||
@@ -47,8 +35,9 @@ class @BlobFileDropzone
|
||||
return
|
||||
|
||||
this.on 'sending', (file, xhr, formData) ->
|
||||
formData.append('new_branch', form.find('#new_branch').val())
|
||||
formData.append('commit_message', form.find('#commit_message').val())
|
||||
formData.append('new_branch', form.find('.js-new-branch').val())
|
||||
formData.append('create_merge_request', form.find('.js-create-merge-request').val())
|
||||
formData.append('commit_message', form.find('.js-commit-message').val())
|
||||
return
|
||||
|
||||
# Override behavior of adding error underneath preview
|
||||
|
||||
@@ -9,13 +9,24 @@ $ ->
|
||||
clipboard.on 'success', (e) ->
|
||||
$(e.trigger).
|
||||
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
|
||||
e.clearSelection()
|
||||
$(e.trigger).blur()
|
||||
|
||||
# Manually hide the tooltip after 1 second
|
||||
setTimeout(->
|
||||
$(e.trigger).tooltip('hide')
|
||||
, 1000)
|
||||
# Safari doesn't support `execCommand`, so instead we inform the user to
|
||||
# copy manually.
|
||||
#
|
||||
# See http://clipboardjs.com/#browser-support
|
||||
clipboard.on 'error', (e) ->
|
||||
if /Mac/i.test(navigator.userAgent)
|
||||
title = "Press ⌘-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'))
|
||||
|
||||
@@ -28,6 +28,8 @@ class Dispatcher
|
||||
when 'projects:milestones:new', 'projects:milestones:edit'
|
||||
new ZenMode()
|
||||
new DropzoneInput($('.milestone-form'))
|
||||
when 'groups:milestones:new'
|
||||
new ZenMode()
|
||||
when 'projects:compare:show'
|
||||
new Diff()
|
||||
when 'projects:issues:new','projects:issues:edit'
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
@@ -29,6 +29,7 @@ class @Notes
|
||||
$(document).on "ajax:success", "form.edit_note", @updateNote
|
||||
|
||||
# Edit note link
|
||||
$(document).on "click", ".js-note-edit", @showEditForm
|
||||
$(document).on "click", ".note-edit-cancel", @cancelEdit
|
||||
|
||||
# Reopen and close actions for Issue/MR combined with note form submit
|
||||
@@ -66,6 +67,7 @@ class @Notes
|
||||
$(document).off "ajax:success", ".js-main-target-form"
|
||||
$(document).off "ajax:success", ".js-discussion-note-form"
|
||||
$(document).off "ajax:success", "form.edit_note"
|
||||
$(document).off "click", ".js-note-edit"
|
||||
$(document).off "click", ".note-edit-cancel"
|
||||
$(document).off "click", ".js-note-delete"
|
||||
$(document).off "click", ".js-note-attachment-delete"
|
||||
@@ -111,13 +113,16 @@ class @Notes
|
||||
renderNote: (note) ->
|
||||
# render note if it not present in loaded list
|
||||
# or skip if rendered
|
||||
if @isNewNote(note)
|
||||
if @isNewNote(note) && !note.award
|
||||
@note_ids.push(note.id)
|
||||
$('ul.main-notes-list').
|
||||
append(note.html).
|
||||
syntaxHighlight()
|
||||
@initTaskList()
|
||||
|
||||
if note.award
|
||||
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
|
||||
|
||||
###
|
||||
Check if note does not exists on page
|
||||
###
|
||||
@@ -253,7 +258,6 @@ class @Notes
|
||||
###
|
||||
addNote: (xhr, note, status) =>
|
||||
@renderNote(note)
|
||||
@updateVotes()
|
||||
|
||||
###
|
||||
Called in response to the new note form being submitted
|
||||
@@ -285,14 +289,13 @@ class @Notes
|
||||
Adds a hidden div with the original content of the note to fill the edit note form with
|
||||
if the user cancels
|
||||
###
|
||||
showEditForm: (note, formHTML) ->
|
||||
nodeText = note.find(".note-text");
|
||||
nodeText.hide()
|
||||
note.find('.note-edit-form').remove()
|
||||
nodeText.after(formHTML)
|
||||
showEditForm: (e) ->
|
||||
e.preventDefault()
|
||||
note = $(this).closest(".note")
|
||||
note.find(".note-body > .note-text").hide()
|
||||
note.find(".note-header").hide()
|
||||
form = note.find(".note-edit-form")
|
||||
base_form = note.find(".note-edit-form")
|
||||
form = base_form.clone().insertAfter(base_form)
|
||||
form.addClass('current-note-edit-form gfm-form')
|
||||
form.find('.div-dropzone').remove()
|
||||
|
||||
@@ -472,9 +475,6 @@ class @Notes
|
||||
form = $(e.target).closest(".js-discussion-note-form")
|
||||
@removeDiscussionNoteForm(form)
|
||||
|
||||
updateVotes: ->
|
||||
true
|
||||
|
||||
###
|
||||
Called after an attachment file has been selected.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil =
|
||||
for entry in log
|
||||
@add_date(entry.date, total) unless total[entry.date]?
|
||||
|
||||
data = by_author[entry.author_name] #|| by_email[entry.author_email]
|
||||
data = by_author[entry.author_name] || by_email[entry.author_email]
|
||||
data ?= @add_author(entry, by_author, by_email)
|
||||
|
||||
@add_date(entry.date, data) unless data[entry.date]
|
||||
@@ -95,5 +95,4 @@ window.ContributorsStatGraphUtil =
|
||||
if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
false
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
li {
|
||||
padding: 3px 0px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
.new-file {
|
||||
|
||||
@@ -101,3 +101,71 @@
|
||||
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
|
||||
@builds = @config_processor.builds
|
||||
@status = true
|
||||
end
|
||||
rescue Ci::GitlabCiYamlProcessor::ValidationError => e
|
||||
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
|
||||
@error = e.message
|
||||
@status = false
|
||||
rescue Exception
|
||||
rescue
|
||||
@error = "Undefined error"
|
||||
@status = false
|
||||
end
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
@@ -0,0 +1,19 @@
|
||||
module GlobalMilestones
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def milestones
|
||||
@milestones = MilestonesFinder.new.execute(@projects, params)
|
||||
@milestones = GlobalMilestone.build_collection(@milestones)
|
||||
@milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
|
||||
end
|
||||
|
||||
def milestone
|
||||
milestones = Milestone.of_projects(@projects).where(title: params[:title])
|
||||
|
||||
if milestones.present?
|
||||
@milestone = GlobalMilestone.new(params[:title], milestones)
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,34 +1,19 @@
|
||||
class Dashboard::MilestonesController < Dashboard::ApplicationController
|
||||
before_action :load_projects
|
||||
include GlobalMilestones
|
||||
|
||||
before_action :projects
|
||||
before_action :milestones, only: [:index]
|
||||
before_action :milestone, only: [:show]
|
||||
|
||||
def index
|
||||
project_milestones = case params[:state]
|
||||
when 'all'; state
|
||||
when 'closed'; state('closed')
|
||||
else state('active')
|
||||
end
|
||||
@dashboard_milestones = Milestones::GroupService.new(project_milestones).execute
|
||||
@dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
def show
|
||||
project_milestones = Milestone.where(project_id: @projects).order("due_date ASC")
|
||||
@dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_projects
|
||||
@projects = current_user.authorized_projects.sorted_by_activity.non_archived
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title]
|
||||
end
|
||||
|
||||
def state(state = nil)
|
||||
conditions = { project_id: @projects }
|
||||
conditions.reverse_merge!(state: state) if state
|
||||
Milestone.where(conditions).order("title ASC")
|
||||
def projects
|
||||
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class DashboardController < Dashboard::ApplicationController
|
||||
before_action :event_filter, only: :activity
|
||||
before_action :projects, only: [:issues, :merge_requests]
|
||||
|
||||
respond_to :html
|
||||
|
||||
@@ -47,4 +48,8 @@ class DashboardController < Dashboard::ApplicationController
|
||||
@events = @event_filter.apply_filter(@events).with_associations
|
||||
@events = @events.limit(20).offset(params[:offset] || 0)
|
||||
end
|
||||
|
||||
def projects
|
||||
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Groups::ApplicationController < ApplicationController
|
||||
layout 'group'
|
||||
before_action :group
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
class Groups::AvatarsController < ApplicationController
|
||||
class Groups::AvatarsController < Groups::ApplicationController
|
||||
def destroy
|
||||
@group = Group.find_by(path: params[:group_id])
|
||||
@group.remove_avatar!
|
||||
|
||||
@group.save
|
||||
|
||||
redirect_to edit_group_path(@group)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
class Groups::GroupMembersController < Groups::ApplicationController
|
||||
skip_before_action :authenticate_user!, only: [:index]
|
||||
before_action :group
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_read_group!
|
||||
before_action :authorize_admin_group!, except: [:index, :leave]
|
||||
before_action :authorize_admin_group_member!, only: [:create, :resend_invite]
|
||||
before_action :authorize_admin_group_member!, except: [:index, :leave]
|
||||
|
||||
def index
|
||||
@project = @group.projects.find(params[:project_id]) if params[:project_id]
|
||||
@@ -18,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
||||
end
|
||||
|
||||
@members = @members.order('access_level DESC').page(params[:page]).per(50)
|
||||
@group_member = GroupMember.new
|
||||
|
||||
@group_member = @group.group_members.new
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -36,30 +35,28 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
@member = @group.group_members.find(params[:id])
|
||||
@group_member = @group.group_members.find(params[:id])
|
||||
|
||||
return render_403 unless can?(current_user, :update_group_member, @member)
|
||||
return render_403 unless can?(current_user, :update_group_member, @group_member)
|
||||
|
||||
old_access_level = @member.human_access
|
||||
old_access_level = @group_member.human_access
|
||||
|
||||
if @member.update_attributes(member_params)
|
||||
log_audit_event(@member, action: :update, old_access_level: old_access_level)
|
||||
if @group_member.update_attributes(member_params)
|
||||
log_audit_event(@group_member, action: :update, old_access_level: old_access_level)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@group_member = @group.group_members.find(params[:id])
|
||||
|
||||
if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner.
|
||||
@group_member.destroy
|
||||
log_audit_event(@group_member, action: :destroy)
|
||||
return render_403 unless can?(current_user, :destroy_group_member, @group_member)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
|
||||
format.js { render nothing: true }
|
||||
end
|
||||
else
|
||||
return render_403
|
||||
@group_member.destroy
|
||||
log_audit_event(@group_member, action: :destroy)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
|
||||
format.js { render nothing: true }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -78,7 +75,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
||||
end
|
||||
|
||||
def leave
|
||||
@group_member = @group.group_members.where(user_id: current_user.id).first
|
||||
@group_member = @group.group_members.find_by(user_id: current_user)
|
||||
|
||||
if can?(current_user, :destroy_group_member, @group_member)
|
||||
@group_member.destroy
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
class Groups::MilestonesController < Groups::ApplicationController
|
||||
before_action :authorize_group_milestone!, only: :update
|
||||
include GlobalMilestones
|
||||
|
||||
before_action :projects
|
||||
before_action :milestones, only: [:index]
|
||||
before_action :milestone, only: [:show, :update]
|
||||
before_action :authorize_group_milestone!, only: [:create, :update]
|
||||
|
||||
def index
|
||||
project_milestones = case params[:state]
|
||||
when 'all'; state
|
||||
when 'closed'; state('closed')
|
||||
else state('active')
|
||||
end
|
||||
@group_milestones = Milestones::GroupService.new(project_milestones).execute
|
||||
@group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
def new
|
||||
@milestone = Milestone.new
|
||||
end
|
||||
|
||||
def create
|
||||
project_ids = params[:milestone][:project_ids]
|
||||
title = milestone_params[:title]
|
||||
|
||||
@group.projects.where(id: project_ids).each do |project|
|
||||
Milestones::CreateService.new(project, current_user, milestone_params).execute
|
||||
end
|
||||
|
||||
redirect_to milestone_path(title)
|
||||
end
|
||||
|
||||
def show
|
||||
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
|
||||
@group_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
|
||||
end
|
||||
|
||||
def update
|
||||
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
|
||||
@group_milestones = Milestones::GroupService.new(project_milestones).milestone(title)
|
||||
|
||||
@group_milestones.milestones.each do |milestone|
|
||||
Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone)
|
||||
@milestone.milestones.each do |milestone|
|
||||
Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.html do
|
||||
redirect_to group_milestones_path(group)
|
||||
end
|
||||
end
|
||||
redirect_back_or_default(default: milestone_path(@milestone.title))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def group
|
||||
@group ||= Group.find_by(path: params[:group_id])
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title]
|
||||
end
|
||||
|
||||
def state(state = nil)
|
||||
conditions = { project_id: group.projects }
|
||||
conditions.reverse_merge!(state: state) if state
|
||||
Milestone.where(conditions).order("title ASC")
|
||||
end
|
||||
|
||||
def authorize_group_milestone!
|
||||
return render_404 unless can?(current_user, :admin_group, group)
|
||||
return render_404 unless can?(current_user, :admin_milestones, group)
|
||||
end
|
||||
|
||||
def milestone_params
|
||||
params.require(:milestone).permit(:title, :description, :due_date, :state_event)
|
||||
end
|
||||
|
||||
def milestone_path(title)
|
||||
group_milestone_path(@group, title.parameterize, title: title)
|
||||
end
|
||||
|
||||
def projects
|
||||
@projects ||= @group.projects
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Controller for viewing a file's blame
|
||||
class Projects::BlobController < Projects::ApplicationController
|
||||
include ExtractsPath
|
||||
include CreatesMergeRequestForCommit
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
# Raised when given an invalid file path
|
||||
@@ -22,21 +23,9 @@ class Projects::BlobController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
result = Files::CreateService.new(@project, current_user, @commit_params).execute
|
||||
|
||||
if result[:status] == :success
|
||||
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
|
||||
create_commit(Files::CreateService, success_path: after_create_path,
|
||||
failure_view: :new,
|
||||
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -47,21 +36,9 @@ class Projects::BlobController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
result = Files::UpdateService.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 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
|
||||
create_commit(Files::UpdateService, success_path: after_edit_path,
|
||||
failure_view: :edit,
|
||||
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
|
||||
end
|
||||
|
||||
def preview
|
||||
@@ -77,7 +54,7 @@ class Projects::BlobController < Projects::ApplicationController
|
||||
|
||||
if result[:status] == :success
|
||||
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
|
||||
flash[:alert] = result[:message]
|
||||
render :show
|
||||
@@ -131,15 +108,51 @@ class Projects::BlobController < Projects::ApplicationController
|
||||
render_404
|
||||
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
|
||||
@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) +
|
||||
"#file-path-#{hexdigest(@path)}"
|
||||
elsif @target_branch.present?
|
||||
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
|
||||
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
|
||||
|
||||
@@ -154,7 +167,7 @@ class Projects::BlobController < Projects::ApplicationController
|
||||
|
||||
def editor_variables
|
||||
@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 =
|
||||
if action_name.to_s == 'create'
|
||||
@@ -174,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController
|
||||
@commit_params = {
|
||||
file_path: @file_path,
|
||||
current_branch: @current_branch,
|
||||
target_branch: @target_branch,
|
||||
target_branch: @new_branch,
|
||||
commit_message: params[:commit_message],
|
||||
file_content: params[:content],
|
||||
file_content_encoding: params[:encoding]
|
||||
|
||||
@@ -20,8 +20,8 @@ class Projects::CompareController < Projects::ApplicationController
|
||||
if compare_result
|
||||
@commits = Commit.decorate(compare_result.commits, @project)
|
||||
@diffs = compare_result.diffs
|
||||
@commit = @commits.last
|
||||
@first_commit = @commits.first
|
||||
@commit = @project.commit(head_ref)
|
||||
@first_commit = @project.commit(base_ref)
|
||||
@line_notes = []
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,7 +66,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||
def show
|
||||
@participants = @issue.participants(current_user)
|
||||
@note = @project.notes.new(noteable: @issue)
|
||||
@notes = @issue.notes.with_associations.fresh
|
||||
@notes = @issue.notes.nonawards.with_associations.fresh
|
||||
@noteable = @issue
|
||||
|
||||
respond_with(@issue)
|
||||
|
||||
@@ -294,7 +294,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
|
||||
# Build a note object for comment form
|
||||
@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)
|
||||
@noteable = @merge_request
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
|
||||
before_action :authorize_read_note!
|
||||
before_action :authorize_create_note!, only: [:create]
|
||||
before_action :authorize_admin_note!, only: [:update, :destroy]
|
||||
before_action :find_current_user_notes, except: [:destroy, :edit, :delete_attachment]
|
||||
before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
|
||||
|
||||
def index
|
||||
current_fetched_at = Time.now.to_i
|
||||
@@ -29,11 +29,6 @@ class Projects::NotesController < Projects::ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@note = note
|
||||
render layout: false
|
||||
end
|
||||
|
||||
def update
|
||||
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
|
||||
|
||||
@@ -63,6 +58,30 @@ class Projects::NotesController < Projects::ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def award_toggle
|
||||
noteable = if note_params[:noteable_type] == "issue"
|
||||
project.issues.find(note_params[:noteable_id])
|
||||
else
|
||||
project.merge_requests.find(note_params[:noteable_id])
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def note
|
||||
@@ -116,6 +135,9 @@ class Projects::NotesController < Projects::ApplicationController
|
||||
id: note.id,
|
||||
discussion_id: note.discussion_id,
|
||||
html: note_to_html(note),
|
||||
award: note.is_award,
|
||||
emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "",
|
||||
note: note.note,
|
||||
discussion_html: note_to_discussion_html(note),
|
||||
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
# Authorize
|
||||
before_action :authorize_admin_project!, except: :leave
|
||||
before_action :authorize_admin_project_member!, except: :leave
|
||||
|
||||
def index
|
||||
@project_members = @project.project_members
|
||||
@@ -30,10 +30,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
@project_group_links = @project.project_group_links
|
||||
end
|
||||
|
||||
def new
|
||||
@project_member = @project.project_members.new
|
||||
end
|
||||
|
||||
def create
|
||||
@project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
|
||||
members = @project.project_members.where(user_id: params[:user_ids].split(','))
|
||||
@@ -47,6 +43,9 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
|
||||
def update
|
||||
@project_member = @project.project_members.find(params[:id])
|
||||
|
||||
return render_403 unless can?(current_user, :update_project_member, @project_member)
|
||||
|
||||
old_access_level = @project_member.human_access
|
||||
|
||||
if @project_member.update_attributes(member_params)
|
||||
@@ -56,7 +55,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
|
||||
def destroy
|
||||
@project_member = @project.project_members.find(params[:id])
|
||||
|
||||
return render_403 unless can?(current_user, :destroy_project_member, @project_member)
|
||||
|
||||
@project_member.destroy
|
||||
|
||||
log_audit_event(@project_member, action: :destroy)
|
||||
|
||||
respond_to do |format|
|
||||
@@ -82,18 +85,24 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def leave
|
||||
if @project.namespace == current_user.namespace
|
||||
message = 'You can not leave your own project. Transfer or delete the project.'
|
||||
return redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
|
||||
end
|
||||
|
||||
@project_member = @project.project_members.find_by(user_id: current_user)
|
||||
@project_member.destroy
|
||||
log_audit_event(@project_member, action: :destroy)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_projects_path }
|
||||
format.js { render nothing: true }
|
||||
if can?(current_user, :destroy_project_member, @project_member)
|
||||
@project_member.destroy
|
||||
|
||||
log_audit_event(@project_member, action: :destroy)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
|
||||
format.js { render nothing: true }
|
||||
end
|
||||
else
|
||||
if current_user == @project.owner
|
||||
message = 'You can not leave your own project. Transfer or delete the project.'
|
||||
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
|
||||
else
|
||||
render_403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Controller for viewing a repository's file structure
|
||||
class Projects::TreeController < Projects::ApplicationController
|
||||
include ExtractsPath
|
||||
include CreatesMergeRequestForCommit
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
before_action :require_non_empty_project, except: [:new, :create]
|
||||
@@ -43,7 +44,7 @@ class Projects::TreeController < Projects::ApplicationController
|
||||
if result && result[:status] == :success
|
||||
flash[:notice] = "The directory has been successfully created"
|
||||
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
|
||||
else
|
||||
flash[:alert] = message
|
||||
@@ -53,6 +54,8 @@ class Projects::TreeController < Projects::ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_dir_vars
|
||||
@new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref
|
||||
@dir_name = File.join(@path, params[:dir_name])
|
||||
@@ -63,4 +66,12 @@ class Projects::TreeController < Projects::ApplicationController
|
||||
commit_message: params[:commit_message],
|
||||
}
|
||||
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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
class SnippetsController < ApplicationController
|
||||
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
|
||||
|
||||
# Allow read snippet
|
||||
before_action :authorize_read_snippet!, only: [:show]
|
||||
|
||||
# Allow modify snippet
|
||||
before_action :authorize_update_snippet!, only: [:edit, :update]
|
||||
|
||||
@@ -79,10 +82,14 @@ class SnippetsController < ApplicationController
|
||||
[Snippet::PUBLIC, Snippet::INTERNAL]).
|
||||
find(params[:id])
|
||||
else
|
||||
PersonalSnippet.are_public.find(params[:id])
|
||||
PersonalSnippet.find(params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_read_snippet!
|
||||
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
|
||||
end
|
||||
|
||||
def authorize_update_snippet!
|
||||
return render_404 unless can?(current_user, :update_personal_snippet, @snippet)
|
||||
end
|
||||
|
||||
@@ -3,14 +3,11 @@ class UsersController < ApplicationController
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
@contributed_projects = contributed_projects.joined(@user).
|
||||
reject(&:forked?)
|
||||
@contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
|
||||
|
||||
@projects = @user.personal_projects.
|
||||
where(id: authorized_projects_ids).includes(:namespace)
|
||||
@projects = PersonalProjectsFinder.new(@user).execute(current_user)
|
||||
|
||||
# Collect only groups common for both users
|
||||
@groups = @user.groups & GroupsFinder.new.execute(current_user)
|
||||
@groups = JoinedGroupsFinder.new(@user).execute(current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
@@ -53,16 +50,8 @@ class UsersController < ApplicationController
|
||||
@user = User.find_by_username!(params[:username])
|
||||
end
|
||||
|
||||
def authorized_projects_ids
|
||||
# Projects user can view
|
||||
@authorized_projects_ids ||=
|
||||
ProjectsFinder.new.execute(current_user).pluck(:id)
|
||||
end
|
||||
|
||||
def contributed_projects
|
||||
@contributed_projects = Project.
|
||||
where(id: authorized_projects_ids & @user.contributed_projects_ids).
|
||||
includes(:namespace)
|
||||
ContributedProjectsFinder.new(@user).execute(current_user)
|
||||
end
|
||||
|
||||
def contributions_calendar
|
||||
@@ -73,9 +62,13 @@ class UsersController < ApplicationController
|
||||
def load_events
|
||||
# Get user activity feed for projects common for both users
|
||||
@events = @user.recent_events.
|
||||
where(project_id: authorized_projects_ids).
|
||||
with_associations
|
||||
merge(projects_for_current_user).
|
||||
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
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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
|
||||
@@ -1,39 +1,44 @@
|
||||
class GroupsFinder
|
||||
def execute(current_user, options = {})
|
||||
all_groups(current_user)
|
||||
# Finds the groups available to the given 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
|
||||
|
||||
private
|
||||
|
||||
def all_groups(current_user)
|
||||
group_ids = if current_user
|
||||
if current_user.authorized_groups.any?
|
||||
# User has access to groups
|
||||
#
|
||||
# Return only:
|
||||
# groups with public projects
|
||||
# groups with internal projects
|
||||
# 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
|
||||
else
|
||||
# Not authenticated
|
||||
#
|
||||
# Return only:
|
||||
# groups with public projects
|
||||
Project.public_only.pluck(:namespace_id)
|
||||
end
|
||||
# This method returns the groups "current_user" can see.
|
||||
def groups_visible_to_user(current_user)
|
||||
base = groups_for_projects(public_and_internal_projects)
|
||||
|
||||
Group.where("public IS TRUE OR id IN(?)", group_ids)
|
||||
union = Gitlab::SQL::Union.
|
||||
new([base.select(:id), current_user.authorized_groups.select(:id)])
|
||||
|
||||
Group.where("namespaces.id IN (#{union.to_sql})")
|
||||
end
|
||||
|
||||
def public_groups
|
||||
groups_for_projects(public_projects)
|
||||
end
|
||||
|
||||
def groups_for_projects(projects)
|
||||
Group.public_and_given_groups(projects.select(:namespace_id))
|
||||
end
|
||||
|
||||
def public_projects
|
||||
Project.unscoped.public_only
|
||||
end
|
||||
|
||||
def public_and_internal_projects
|
||||
Project.unscoped.public_and_internal_only
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# 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
|
||||
@@ -0,0 +1,12 @@
|
||||
class MilestonesFinder
|
||||
def execute(projects, params)
|
||||
milestones = Milestone.of_projects(projects)
|
||||
milestones = milestones.order("due_date ASC")
|
||||
|
||||
case params[:state]
|
||||
when 'closed' then milestones.closed
|
||||
when 'all' then milestones
|
||||
else milestones.active
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,9 +12,9 @@ class NotesFinder
|
||||
when "commit"
|
||||
project.notes.for_commit_id(target_id).not_inline
|
||||
when "issue"
|
||||
project.issues.find(target_id).notes.inc_author
|
||||
project.issues.find(target_id).notes.nonawards.inc_author
|
||||
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"
|
||||
project.snippets.find(target_id).notes
|
||||
else
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
@@ -1,11 +1,39 @@
|
||||
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]
|
||||
|
||||
if group
|
||||
group_projects(current_user, group)
|
||||
segments = group_projects(current_user, group)
|
||||
else
|
||||
all_projects(current_user)
|
||||
segments = all_projects(current_user)
|
||||
end
|
||||
|
||||
if segments.length > 1
|
||||
union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
|
||||
|
||||
Project.where("projects.id IN (#{union.to_sql})")
|
||||
else
|
||||
segments.first
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,89 +41,37 @@ class ProjectsFinder
|
||||
|
||||
def group_projects(current_user, group)
|
||||
if current_user
|
||||
if group.users.include?(current_user)
|
||||
# User is group member
|
||||
#
|
||||
# Return ALL group projects
|
||||
group.projects
|
||||
else
|
||||
projects_members = ProjectMember.in_projects(group.projects).
|
||||
with_user(current_user)
|
||||
|
||||
if projects_members.any?
|
||||
# User is a project member
|
||||
#
|
||||
# Return only:
|
||||
# public projects
|
||||
# internal projects
|
||||
# joined projects
|
||||
#
|
||||
group.projects.where(
|
||||
"projects.id IN (?) OR projects.visibility_level IN (?)",
|
||||
projects_members.pluck(:source_id),
|
||||
Project.public_and_internal_levels
|
||||
)
|
||||
else
|
||||
# User has no access to group or group projects
|
||||
# or has access through shared project
|
||||
#
|
||||
# Return only:
|
||||
# public projects
|
||||
# internal projects
|
||||
# shared projects
|
||||
projects_ids = []
|
||||
ProjectGroupLink.where(project_id: group.projects).each do |shared_project|
|
||||
if shared_project.group.users.include?(current_user) || shared_project.project.users.include?(current_user)
|
||||
projects_ids << shared_project.project.id
|
||||
end
|
||||
end
|
||||
|
||||
group.projects.where(
|
||||
"projects.id IN (?) OR projects.visibility_level IN (?)",
|
||||
projects_ids,
|
||||
Project.public_and_internal_levels
|
||||
)
|
||||
end
|
||||
end
|
||||
[
|
||||
group_projects_for_user(current_user, group),
|
||||
group.projects.public_and_internal_only,
|
||||
group.shared_projects.visible_to_user(current_user)
|
||||
]
|
||||
else
|
||||
# Not authenticated
|
||||
#
|
||||
# Return only:
|
||||
# public projects
|
||||
group.projects.public_only
|
||||
[group.projects.public_only]
|
||||
end
|
||||
end
|
||||
|
||||
def all_projects(current_user)
|
||||
if current_user
|
||||
if current_user.authorized_projects.any?
|
||||
# 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
|
||||
# User has no access to private projects
|
||||
#
|
||||
# Return only:
|
||||
# public projects
|
||||
# internal projects
|
||||
#
|
||||
Project.public_and_internal_only
|
||||
end
|
||||
[current_user.authorized_projects, public_and_internal_projects]
|
||||
else
|
||||
# Not authenticated
|
||||
#
|
||||
# Return only:
|
||||
# public projects
|
||||
Project.public_only
|
||||
[Project.public_only]
|
||||
end
|
||||
end
|
||||
|
||||
def group_projects_for_user(current_user, group)
|
||||
if group.users.include?(current_user)
|
||||
group.projects
|
||||
else
|
||||
group.projects.visible_to_user(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def public_projects
|
||||
Project.unscoped.public_only
|
||||
end
|
||||
|
||||
def public_and_internal_projects
|
||||
Project.unscoped.public_and_internal_only
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
module DiffHelper
|
||||
def diff_view
|
||||
params[:view] == 'parallel' ? 'parallel' : 'inline'
|
||||
end
|
||||
|
||||
def allowed_diff_size
|
||||
if diff_hard_limit_enabled?
|
||||
Commit::DIFF_HARD_LIMIT_FILES
|
||||
@@ -137,7 +141,7 @@ module DiffHelper
|
||||
# Always use HTML to handle case where JSON diff rendered this button
|
||||
params_copy.delete(:format)
|
||||
|
||||
link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do
|
||||
link_to url_for(params_copy), id: "inline-diff-btn", class: (diff_view == 'inline' ? 'btn active' : 'btn') do
|
||||
'Inline'
|
||||
end
|
||||
end
|
||||
@@ -148,7 +152,7 @@ module DiffHelper
|
||||
# Always use HTML to handle case where JSON diff rendered this button
|
||||
params_copy.delete(:format)
|
||||
|
||||
link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do
|
||||
link_to url_for(params_copy), id: "parallel-diff-btn", class: (diff_view == 'parallel' ? 'btn active' : 'btn') do
|
||||
'Side-by-side'
|
||||
end
|
||||
end
|
||||
@@ -171,7 +175,7 @@ module DiffHelper
|
||||
def commit_for_diff(diff)
|
||||
if diff.deleted_file
|
||||
first_commit = @first_commit || @commit
|
||||
first_commit.parent
|
||||
first_commit.parent || @first_commit
|
||||
else
|
||||
@commit
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ module GroupsHelper
|
||||
end
|
||||
|
||||
def should_user_see_group_roles?(user, group)
|
||||
if user
|
||||
if user && group
|
||||
user.is_admin? || group.members.exists?(user_id: user.id)
|
||||
else
|
||||
false
|
||||
|
||||
@@ -87,6 +87,33 @@ module IssuesHelper
|
||||
merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ')
|
||||
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
|
||||
module_function :url_for_issue
|
||||
end
|
||||
|
||||
@@ -100,7 +100,7 @@ module LabelsHelper
|
||||
Label.where(project_id: @projects)
|
||||
end
|
||||
|
||||
grouped_labels = Labels::GroupService.new(labels).execute
|
||||
grouped_labels = GlobalLabel.build_collection(labels)
|
||||
grouped_labels.unshift(Label::None)
|
||||
grouped_labels.unshift(Label::Any)
|
||||
|
||||
|
||||
@@ -8,14 +8,6 @@ module MergeRequestsHelper
|
||||
)
|
||||
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)
|
||||
{
|
||||
merge_request: {
|
||||
|
||||
@@ -28,7 +28,7 @@ module MilestonesHelper
|
||||
Milestone.where(project_id: @projects)
|
||||
end.active
|
||||
|
||||
grouped_milestones = Milestones::GroupService.new(milestones).execute
|
||||
grouped_milestones = GlobalMilestone.build_collection(milestones)
|
||||
grouped_milestones.unshift(Milestone::None)
|
||||
grouped_milestones.unshift(Milestone::Any)
|
||||
|
||||
|
||||
+114
-51
@@ -1,21 +1,22 @@
|
||||
class Ability
|
||||
class << self
|
||||
def allowed(user, subject)
|
||||
return not_auth_abilities(user, subject) if user.nil?
|
||||
return [] unless user.kind_of?(User)
|
||||
return anonymous_abilities(user, subject) if user.nil?
|
||||
return [] unless user.is_a?(User)
|
||||
return [] if user.blocked?
|
||||
|
||||
abilities =
|
||||
abilities =
|
||||
case subject.class.name
|
||||
when "Project" then project_abilities(user, subject)
|
||||
when "Issue" then issue_abilities(user, subject)
|
||||
when "Note" then note_abilities(user, subject)
|
||||
when "ProjectSnippet" then project_snippet_abilities(user, subject)
|
||||
when "PersonalSnippet" then personal_snippet_abilities(user, subject)
|
||||
when "MergeRequest" then merge_request_abilities(user, subject)
|
||||
when "Group" then group_abilities(user, subject)
|
||||
when "Namespace" then namespace_abilities(user, subject)
|
||||
when "GroupMember" then group_member_abilities(user, subject)
|
||||
when "Project" then project_abilities(user, subject)
|
||||
when "Issue" then issue_abilities(user, subject)
|
||||
when "Note" then note_abilities(user, subject)
|
||||
when "ProjectSnippet" then project_snippet_abilities(user, subject)
|
||||
when "PersonalSnippet" then personal_snippet_abilities(user, subject)
|
||||
when "MergeRequest" then merge_request_abilities(user, subject)
|
||||
when "Group" then group_abilities(user, subject)
|
||||
when "Namespace" then namespace_abilities(user, subject)
|
||||
when "GroupMember" then group_member_abilities(user, subject)
|
||||
when "ProjectMember" then project_member_abilities(user, subject)
|
||||
else []
|
||||
end
|
||||
|
||||
@@ -35,15 +36,25 @@ class Ability
|
||||
]
|
||||
end
|
||||
|
||||
# List of possible abilities
|
||||
# for non-authenticated user
|
||||
def not_auth_abilities(user, subject)
|
||||
project = if subject.kind_of?(Project)
|
||||
# List of possible abilities for anonymous user
|
||||
def anonymous_abilities(user, subject)
|
||||
case true
|
||||
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
|
||||
elsif subject.respond_to?(:project)
|
||||
subject.project
|
||||
else
|
||||
nil
|
||||
subject.project
|
||||
end
|
||||
|
||||
if project && project.public?
|
||||
@@ -63,19 +74,29 @@ class Ability
|
||||
|
||||
rules - project_disabled_features_rules(project)
|
||||
else
|
||||
group = if subject.kind_of?(Group)
|
||||
subject
|
||||
elsif subject.respond_to?(:group)
|
||||
subject.group
|
||||
else
|
||||
nil
|
||||
end
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
if group && group.public_profile?
|
||||
[:read_group]
|
||||
else
|
||||
[]
|
||||
end
|
||||
def anonymous_group_abilities(subject)
|
||||
group = if subject.is_a?(Group)
|
||||
subject
|
||||
else
|
||||
subject.group
|
||||
end
|
||||
|
||||
if group && group.public_profile?
|
||||
[:read_group]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def anonymous_personal_snippet_abilities(snippet)
|
||||
if snippet.public?
|
||||
[:read_personal_snippet]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -247,21 +268,22 @@ class Ability
|
||||
|
||||
# Only group masters and group owners can create new projects in group
|
||||
if group.has_master?(user) || group.has_owner?(user) || user.admin?
|
||||
rules.push(*[
|
||||
rules += [
|
||||
:create_projects,
|
||||
])
|
||||
:admin_milestones
|
||||
]
|
||||
end
|
||||
|
||||
# Only group owner and administrators can admin group
|
||||
if group.has_owner?(user) || user.admin?
|
||||
rules.push(*[
|
||||
rules += [
|
||||
:admin_group,
|
||||
:admin_namespace,
|
||||
:admin_group_member
|
||||
])
|
||||
]
|
||||
|
||||
unless group.ldap_synced?
|
||||
rules << :admin_group_member
|
||||
if group.ldap_synced?
|
||||
rules.delete(:admin_group_member)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -273,16 +295,15 @@ class Ability
|
||||
|
||||
# Only namespace owner and administrators can admin it
|
||||
if namespace.owner == user || user.admin?
|
||||
rules.push(*[
|
||||
rules += [
|
||||
:create_projects,
|
||||
:admin_namespace
|
||||
])
|
||||
]
|
||||
end
|
||||
|
||||
rules.flatten
|
||||
end
|
||||
|
||||
|
||||
[:issue, :merge_request].each do |name|
|
||||
define_method "#{name}_abilities" do |user, subject|
|
||||
rules = []
|
||||
@@ -299,7 +320,7 @@ class Ability
|
||||
end
|
||||
end
|
||||
|
||||
[:note, :project_snippet, :personal_snippet].each do |name|
|
||||
[:note, :project_snippet].each do |name|
|
||||
define_method "#{name}_abilities" do |user, subject|
|
||||
rules = []
|
||||
|
||||
@@ -319,19 +340,61 @@ class Ability
|
||||
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)
|
||||
rules = []
|
||||
target_user = subject.user
|
||||
group = subject.group
|
||||
can_manage = group_abilities(user, group).include?(:admin_group_member)
|
||||
|
||||
if can_manage && (user != target_user)
|
||||
rules << :update_group_member
|
||||
rules << :destroy_group_member
|
||||
unless group.last_owner?(target_user)
|
||||
can_manage = group_abilities(user, group).include?(:admin_group_member)
|
||||
|
||||
if can_manage && user != target_user
|
||||
rules << :update_group_member
|
||||
rules << :destroy_group_member
|
||||
end
|
||||
|
||||
if user == target_user
|
||||
rules << :destroy_group_member
|
||||
end
|
||||
end
|
||||
|
||||
if !group.last_owner?(user) && (can_manage || (user == target_user))
|
||||
rules << :destroy_group_member
|
||||
rules
|
||||
end
|
||||
|
||||
def project_member_abilities(user, subject)
|
||||
rules = []
|
||||
target_user = subject.user
|
||||
project = subject.project
|
||||
|
||||
unless target_user == project.owner
|
||||
can_manage = project_abilities(user, project).include?(:admin_project_member)
|
||||
|
||||
if can_manage && user != target_user
|
||||
rules << :update_project_member
|
||||
rules << :destroy_project_member
|
||||
end
|
||||
|
||||
if user == target_user
|
||||
rules << :destroy_project_member
|
||||
end
|
||||
end
|
||||
|
||||
rules
|
||||
@@ -339,10 +402,10 @@ class Ability
|
||||
|
||||
def abilities
|
||||
@abilities ||= begin
|
||||
abilities = Six.new
|
||||
abilities << self
|
||||
abilities
|
||||
end
|
||||
abilities = Six.new
|
||||
abilities << self
|
||||
abilities
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -188,13 +188,13 @@ module Ci
|
||||
end
|
||||
|
||||
def config_processor
|
||||
return nil unless ci_yaml_file
|
||||
@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)
|
||||
nil
|
||||
rescue Exception => e
|
||||
logger.error e.message + "\n" + e.backtrace.join("\n")
|
||||
save_yaml_error("Undefined yaml error")
|
||||
rescue
|
||||
save_yaml_error("Undefined error")
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
@@ -89,39 +89,14 @@ module Issuable
|
||||
opened? || reopened?
|
||||
end
|
||||
|
||||
#
|
||||
# Votes
|
||||
#
|
||||
|
||||
# Return the number of -1 comments (downvotes)
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
def downvotes
|
||||
filter_superceded_votes(notes.select(&:downvote?), notes).size
|
||||
0
|
||||
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)
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
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
|
||||
0
|
||||
end
|
||||
|
||||
def subscribed?(user)
|
||||
@@ -183,18 +158,4 @@ module Issuable
|
||||
def notes_with_associations
|
||||
notes.includes(:author, :project)
|
||||
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
|
||||
|
||||
@@ -8,8 +8,9 @@ module Sortable
|
||||
included do
|
||||
# By default all models should be ordered
|
||||
# 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_asc, -> { reorder(created_at: :asc) }
|
||||
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
|
||||
|
||||
@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base
|
||||
Event::PUSHED, ["MergeRequest", "Issue"],
|
||||
[Event::CREATED, Event::CLOSED, Event::MERGED])
|
||||
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
|
||||
|
||||
def proper?
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
class GlobalLabel
|
||||
attr_accessor :title, :labels
|
||||
alias_attribute :name, :title
|
||||
|
||||
def self.build_collection(labels)
|
||||
labels = labels.group_by(&:title)
|
||||
|
||||
labels.map do |title, label|
|
||||
new(title, label)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(title, labels)
|
||||
@title = title
|
||||
@labels = labels
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,15 @@
|
||||
class GroupMilestone
|
||||
class GlobalMilestone
|
||||
attr_accessor :title, :milestones
|
||||
alias_attribute :name, :title
|
||||
|
||||
def self.build_collection(milestones)
|
||||
milestones = milestones.group_by(&:title)
|
||||
|
||||
milestones.map do |title, milestones|
|
||||
new(title, milestones)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(title, milestones)
|
||||
@title = title
|
||||
@milestones = milestones
|
||||
@@ -10,7 +18,7 @@ class GroupMilestone
|
||||
def safe_title
|
||||
@title.parameterize
|
||||
end
|
||||
|
||||
|
||||
def projects
|
||||
milestones.map { |milestone| milestone.project }
|
||||
end
|
||||
@@ -60,15 +68,15 @@ class GroupMilestone
|
||||
end
|
||||
|
||||
def issues
|
||||
@group_issues ||= milestones.map(&:issues).flatten.group_by(&:state)
|
||||
@issues ||= milestones.map(&:issues).flatten.group_by(&:state)
|
||||
end
|
||||
|
||||
def merge_requests
|
||||
@group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
|
||||
@merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
|
||||
end
|
||||
|
||||
def participants
|
||||
@group_participants ||= milestones.map(&:participants).flatten.compact.uniq
|
||||
@participants ||= milestones.map(&:participants).flatten.compact.uniq
|
||||
end
|
||||
|
||||
def opened_issues
|
||||
@@ -86,4 +94,8 @@ class GroupMilestone
|
||||
def closed_merge_requests
|
||||
merge_requests.values_at("closed", "merged", "locked").compact.flatten
|
||||
end
|
||||
|
||||
def complete?
|
||||
total_items_count == closed_items_count
|
||||
end
|
||||
end
|
||||
+10
-5
@@ -20,8 +20,9 @@ require 'file_size_validator'
|
||||
class Group < Namespace
|
||||
include Gitlab::ConfigHelper
|
||||
include Referable
|
||||
|
||||
|
||||
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
|
||||
alias_method :members, :group_members
|
||||
has_many :users, through: :group_members
|
||||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :shared_projects, through: :project_group_links, source: :project
|
||||
@@ -52,6 +53,14 @@ class Group < Namespace
|
||||
def reference_pattern
|
||||
User.reference_pattern
|
||||
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
|
||||
|
||||
def to_reference(_from_project = nil)
|
||||
@@ -114,10 +123,6 @@ class Group < Namespace
|
||||
has_owner?(user) && owners.size == 1
|
||||
end
|
||||
|
||||
def members
|
||||
group_members
|
||||
end
|
||||
|
||||
def avatar_type
|
||||
unless self.avatar.image?
|
||||
self.errors.add :avatar, "only images allowed"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
class GroupLabel
|
||||
attr_accessor :title, :labels
|
||||
alias_attribute :name, :title
|
||||
|
||||
def initialize(title, labels)
|
||||
@title = title
|
||||
@labels = labels
|
||||
end
|
||||
end
|
||||
+30
-10
@@ -31,13 +31,22 @@ class Member < ActiveRecord::Base
|
||||
|
||||
validates :user, presence: true, unless: :invite?
|
||||
validates :source, presence: true
|
||||
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
||||
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
||||
message: "already exists in source",
|
||||
allow_nil: true }
|
||||
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
|
||||
validates :invite_email, presence: { if: :invite? },
|
||||
email: { strict_mode: true, allow_nil: true },
|
||||
uniqueness: { scope: [:source_type, :source_id], allow_nil: true }
|
||||
validates :invite_email,
|
||||
presence: {
|
||||
if: :invite?
|
||||
},
|
||||
email: {
|
||||
strict_mode: true,
|
||||
allow_nil: true
|
||||
},
|
||||
uniqueness: {
|
||||
scope: [:source_type, :source_id],
|
||||
allow_nil: true
|
||||
}
|
||||
|
||||
scope :invite, -> { where(user_id: nil) }
|
||||
scope :non_invite, -> { where("user_id IS NOT NULL") }
|
||||
@@ -74,7 +83,7 @@ class Member < ActiveRecord::Base
|
||||
|
||||
def add_user(members, user_id, access_level, current_user = nil, skip_notification: false)
|
||||
user = user_for_id(user_id)
|
||||
|
||||
|
||||
# `user` can be either a User object or an email to be invited
|
||||
if user.is_a?(User)
|
||||
member = members.find_or_initialize_by(user_id: user.id)
|
||||
@@ -83,12 +92,23 @@ class Member < ActiveRecord::Base
|
||||
member.invite_email = user
|
||||
end
|
||||
|
||||
member.created_by ||= current_user
|
||||
member.access_level = access_level
|
||||
if can_update_member?(current_user, member)
|
||||
member.created_by ||= current_user
|
||||
member.access_level = access_level
|
||||
|
||||
member.skip_notification = skip_notification
|
||||
member.skip_notification = skip_notification
|
||||
|
||||
member.save
|
||||
member.save
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_update_member?(current_user, member)
|
||||
# There is no current user for bulk actions, in which case anything is allowed
|
||||
!current_user ||
|
||||
current_user.can?(:update_group_member, member) ||
|
||||
current_user.can?(:update_project_member, member)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -98,7 +118,7 @@ class Member < ActiveRecord::Base
|
||||
|
||||
def accept_invite!(new_user)
|
||||
return false unless invite?
|
||||
|
||||
|
||||
self.invite_token = nil
|
||||
self.invite_accepted_at = Time.now.utc
|
||||
|
||||
|
||||
@@ -545,7 +545,7 @@ class MergeRequest < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def ci_commit
|
||||
if last_commit
|
||||
if last_commit and source_project
|
||||
source_project.ci_commit(last_commit.id)
|
||||
end
|
||||
end
|
||||
|
||||
+20
-38
@@ -40,16 +40,20 @@ class Note < ActiveRecord::Base
|
||||
delegate :name, :email, to: :author, prefix: 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
|
||||
# Attachments are deprecated and are handled by Markdown uploader
|
||||
validates :attachment, file_size: { maximum: :max_attachment_size }
|
||||
|
||||
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 :author, presence: true
|
||||
|
||||
mount_uploader :attachment, AttachmentUploader
|
||||
|
||||
# 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 :inline, ->{ where("line_code IS NOT NULL") }
|
||||
scope :not_inline, ->{ where(line_code: [nil, '']) }
|
||||
@@ -97,6 +101,12 @@ class Note < ActiveRecord::Base
|
||||
def search(query)
|
||||
where("LOWER(note) like :query", query: "%#{query.downcase}%")
|
||||
end
|
||||
|
||||
def grouped_awards
|
||||
awards.select(:note).distinct.map do |note|
|
||||
[ note.note, where(note: note.note) ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cross_reference?
|
||||
@@ -288,44 +298,6 @@ class Note < ActiveRecord::Base
|
||||
nil
|
||||
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.
|
||||
def gfm_reference(from_project = nil)
|
||||
noteable.gfm_reference(from_project)
|
||||
@@ -363,6 +335,16 @@ class Note < ActiveRecord::Base
|
||||
read_attribute(:system)
|
||||
end
|
||||
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
def downvote?
|
||||
false
|
||||
end
|
||||
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
def upvote?
|
||||
false
|
||||
end
|
||||
|
||||
def editable?
|
||||
!system?
|
||||
end
|
||||
|
||||
@@ -313,6 +313,10 @@ class Project < ActiveRecord::Base
|
||||
|
||||
joins(join_body).reorder('join_note_counts.amount DESC')
|
||||
end
|
||||
|
||||
def visible_to_user(user)
|
||||
where(id: user.authorized_projects.select(:id).reorder(nil))
|
||||
end
|
||||
end
|
||||
|
||||
def team
|
||||
|
||||
@@ -32,6 +32,8 @@ class DroneCiService < CiService
|
||||
|
||||
def compose_service_hook
|
||||
hook = service_hook || build_service_hook
|
||||
# If using a service template, project may not be available
|
||||
hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
|
||||
hook.enable_ssl_verification = enable_ssl_verification
|
||||
hook.save
|
||||
end
|
||||
|
||||
@@ -86,6 +86,8 @@ class ProjectWiki
|
||||
commit = commit_details(:created, message, title)
|
||||
|
||||
wiki.write_page(title, format, content, commit)
|
||||
|
||||
update_project_activity
|
||||
rescue Gollum::DuplicatePageError => e
|
||||
@error_message = "Duplicate page: #{e.message}"
|
||||
return false
|
||||
@@ -95,10 +97,14 @@ class ProjectWiki
|
||||
commit = commit_details(:updated, message, page.title)
|
||||
|
||||
wiki.update_page(page, page.name, format, content, commit)
|
||||
|
||||
update_project_activity
|
||||
end
|
||||
|
||||
def delete_page(page, message = nil)
|
||||
wiki.delete_page(page, commit_details(:deleted, message, page.title))
|
||||
|
||||
update_project_activity
|
||||
end
|
||||
|
||||
def page_title_and_dir(title)
|
||||
@@ -146,4 +152,8 @@ class ProjectWiki
|
||||
def path_to_repo
|
||||
@path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git")
|
||||
end
|
||||
|
||||
def update_project_activity
|
||||
@project.touch(:last_activity_at)
|
||||
end
|
||||
end
|
||||
|
||||
+48
-35
@@ -417,43 +417,23 @@ class User < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Groups user has access to
|
||||
# Returns the groups a user has access to
|
||||
def authorized_groups
|
||||
@authorized_groups ||= begin
|
||||
group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
|
||||
Group.where(id: group_ids)
|
||||
end
|
||||
union = Gitlab::SQL::Union.
|
||||
new([groups.select(:id), authorized_projects.select(:namespace_id)])
|
||||
|
||||
Group.where("namespaces.id IN (#{union.to_sql})")
|
||||
end
|
||||
|
||||
def authorized_projects_id
|
||||
@authorized_projects_id ||= begin
|
||||
project_ids = personal_projects.pluck(:id)
|
||||
project_ids.push(*groups_projects.pluck(:id))
|
||||
project_ids.push(*projects.pluck(:id).uniq)
|
||||
project_ids.push(*groups.joins(:shared_projects).pluck(:project_id))
|
||||
end
|
||||
end
|
||||
|
||||
def master_or_owner_projects_id
|
||||
@master_or_owner_projects_id ||= begin
|
||||
scope = { access_level: [ Gitlab::Access::MASTER, Gitlab::Access::OWNER ] }
|
||||
project_ids = personal_projects.pluck(:id)
|
||||
project_ids.push(*groups_projects.where(members: scope).pluck(:id))
|
||||
project_ids.push(*projects.where(members: scope).pluck(:id).uniq)
|
||||
end
|
||||
end
|
||||
|
||||
# Projects user has access to
|
||||
# Returns the groups a user is authorized to access.
|
||||
def authorized_projects
|
||||
@authorized_projects ||= Project.where(id: authorized_projects_id)
|
||||
Project.where("projects.id IN (#{projects_union.to_sql})")
|
||||
end
|
||||
|
||||
def owned_projects
|
||||
@owned_projects ||=
|
||||
begin
|
||||
namespace_ids = owned_groups.pluck(:id).push(namespace.id)
|
||||
Project.in_namespace(namespace_ids).joins(:namespace)
|
||||
end
|
||||
Project.where('namespace_id IN (?) OR namespace_id = ?',
|
||||
owned_groups.select(:id), namespace.id).joins(:namespace)
|
||||
end
|
||||
|
||||
# Team membership in authorized projects
|
||||
@@ -772,12 +752,25 @@ class User < ActiveRecord::Base
|
||||
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
|
||||
end
|
||||
|
||||
def contributed_projects_ids
|
||||
Event.contributions.where(author_id: self).
|
||||
# Returns the projects a user contributed to in the last year.
|
||||
#
|
||||
# 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).
|
||||
reorder(project_id: :desc).
|
||||
select(:project_id).
|
||||
uniq.map(&:project_id)
|
||||
uniq.
|
||||
reorder(nil)
|
||||
|
||||
Project.where(id: events)
|
||||
end
|
||||
|
||||
def restricted_signup_domains
|
||||
@@ -810,8 +803,28 @@ class User < ActiveRecord::Base
|
||||
def ci_authorized_runners
|
||||
@ci_authorized_runners ||= begin
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_union
|
||||
Gitlab::SQL::Union.new([personal_projects.select(:id),
|
||||
groups_projects.select(:id),
|
||||
projects.select(:id),
|
||||
groups.joins(:shared_projects).select(:project_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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
require_relative 'base_service'
|
||||
|
||||
class CreateReleaseService < BaseService
|
||||
def execute(tag_name, release_description)
|
||||
|
||||
repository = project.repository
|
||||
existing_tag = repository.find_tag(tag_name)
|
||||
|
||||
# Only create a release if the tag exists
|
||||
if existing_tag
|
||||
release = project.releases.find_by(tag: tag_name)
|
||||
|
||||
if release
|
||||
error('Release already exists', 409)
|
||||
else
|
||||
release = project.releases.new({ tag: tag_name, description: release_description })
|
||||
release.save
|
||||
|
||||
success(release)
|
||||
end
|
||||
else
|
||||
error('Tag does not exist', 404)
|
||||
end
|
||||
end
|
||||
|
||||
def success(release)
|
||||
out = super()
|
||||
out[:release] = release
|
||||
out
|
||||
end
|
||||
end
|
||||
@@ -19,16 +19,16 @@ class CreateTagService < BaseService
|
||||
new_tag = repository.find_tag(tag_name)
|
||||
|
||||
if new_tag
|
||||
if release_description
|
||||
release = project.releases.find_or_initialize_by(tag: tag_name)
|
||||
release.update_attributes(description: release_description)
|
||||
end
|
||||
|
||||
push_data = create_push_data(project, current_user, new_tag)
|
||||
EventCreateService.new.push(project, current_user, push_data)
|
||||
project.execute_hooks(push_data.dup, :tag_push_hooks)
|
||||
project.execute_services(push_data.dup, :tag_push_hooks)
|
||||
|
||||
if release_description
|
||||
CreateReleaseService.new(@project, @current_user).
|
||||
execute(tag_name, release_description)
|
||||
end
|
||||
|
||||
success(new_tag)
|
||||
else
|
||||
error('Invalid reference name')
|
||||
|
||||
@@ -58,12 +58,6 @@ class GitPushService
|
||||
|
||||
@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)
|
||||
project.execute_hooks(@push_data.dup, :push_hooks)
|
||||
project.execute_services(@push_data.dup, :push_hooks)
|
||||
@@ -134,10 +128,4 @@ class GitPushService
|
||||
def commit_user(commit)
|
||||
commit.author || user
|
||||
end
|
||||
|
||||
def gitlab_ci_yaml?(sha)
|
||||
@project.repository.blob_at(sha, '.gitlab-ci.yml')
|
||||
rescue Rugged::ReferenceError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
module Labels
|
||||
class GroupService < ::BaseService
|
||||
def initialize(project_labels)
|
||||
@project_labels = project_labels.group_by(&:title)
|
||||
end
|
||||
|
||||
def execute
|
||||
build(@project_labels)
|
||||
end
|
||||
|
||||
def label(title)
|
||||
if title
|
||||
group_label = @project_labels[title].group_by(&:title)
|
||||
build(group_label).first
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build(label)
|
||||
label.map { |title, labels| GroupLabel.new(title, labels) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
module Milestones
|
||||
class GroupService < Milestones::BaseService
|
||||
def initialize(project_milestones)
|
||||
@project_milestones = project_milestones.group_by(&:title)
|
||||
end
|
||||
|
||||
def execute
|
||||
build(@project_milestones)
|
||||
end
|
||||
|
||||
def milestone(title)
|
||||
if title
|
||||
group_milestone = @project_milestones[title].group_by(&:title)
|
||||
build(group_milestone).first
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build(milestone)
|
||||
milestone.map{ |title, milestones| GroupMilestone.new(title, milestones) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,11 +5,16 @@ module Notes
|
||||
note.author = current_user
|
||||
note.system = false
|
||||
|
||||
if contains_emoji_only?(params[:note])
|
||||
note.is_award = true
|
||||
note.note = emoji_name(params[:note])
|
||||
end
|
||||
|
||||
if note.save
|
||||
notification_service.new_note(note)
|
||||
|
||||
# Skip system notes, like status changes and cross-references.
|
||||
unless note.system
|
||||
# Skip system notes, like status changes and cross-references and awards
|
||||
unless note.system || note.is_award
|
||||
event_service.leave_note(note, note.author)
|
||||
note.create_cross_references!
|
||||
execute_hooks(note)
|
||||
@@ -28,5 +33,13 @@ module Notes
|
||||
note.project.execute_hooks(note_data, :note_hooks)
|
||||
note.project.execute_services(note_data, :note_hooks)
|
||||
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
|
||||
|
||||
@@ -102,6 +102,7 @@ class NotificationService
|
||||
# ignore gitlab service messages
|
||||
return true if note.note.start_with?('Status changed to closed')
|
||||
return true if note.cross_reference? && note.system == true
|
||||
return true if note.is_award
|
||||
|
||||
target = note.noteable
|
||||
|
||||
@@ -361,11 +362,13 @@ class NotificationService
|
||||
end
|
||||
|
||||
def reassign_resource_email(target, project, current_user, method)
|
||||
assignee_id_was = previous_record(target, "assignee_id")
|
||||
recipients = build_recipients(target, project, current_user)
|
||||
previous_assignee_id = previous_record(target, "assignee_id")
|
||||
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
|
||||
|
||||
recipients = build_recipients(target, project, current_user, [previous_assignee])
|
||||
|
||||
recipients.each do |recipient|
|
||||
mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id)
|
||||
mailer.send(method, recipient.id, target.id, previous_assignee_id, current_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -377,9 +380,11 @@ class NotificationService
|
||||
end
|
||||
end
|
||||
|
||||
def build_recipients(target, project, current_user)
|
||||
def build_recipients(target, project, current_user, extra_recipients = nil)
|
||||
recipients = target.participants(current_user)
|
||||
|
||||
recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients
|
||||
|
||||
recipients = add_project_watchers(recipients, project)
|
||||
recipients = reject_mention_users(recipients, project)
|
||||
recipients = reject_muted_users(recipients, project)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
require_relative 'base_service'
|
||||
|
||||
class UpdateReleaseService < BaseService
|
||||
def execute(tag_name, release_description)
|
||||
|
||||
repository = project.repository
|
||||
existing_tag = repository.find_tag(tag_name)
|
||||
|
||||
if existing_tag
|
||||
release = project.releases.find_by(tag: tag_name)
|
||||
|
||||
if release
|
||||
release.update_attributes(description: release_description)
|
||||
|
||||
success(release)
|
||||
else
|
||||
error('Release does not exist', 404)
|
||||
end
|
||||
else
|
||||
error('Tag does not exist', 404)
|
||||
end
|
||||
end
|
||||
|
||||
def success(release)
|
||||
out = super()
|
||||
out[:release] = release
|
||||
out
|
||||
end
|
||||
end
|
||||
@@ -16,11 +16,11 @@
|
||||
- unless user.linkedin.blank?
|
||||
%li
|
||||
%span.light LinkedIn:
|
||||
%strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}"
|
||||
%strong= link_to user.linkedin, "https://www.linkedin.com/in/#{user.linkedin}"
|
||||
- unless user.twitter.blank?
|
||||
%li
|
||||
%span.light Twitter:
|
||||
%strong= link_to user.twitter, "http://www.twitter.com/#{user.twitter}"
|
||||
%strong= link_to user.twitter, "https://twitter.com/#{user.twitter}"
|
||||
- unless user.website_url.blank?
|
||||
%li
|
||||
%span.light Website:
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
.milestones
|
||||
%ul.content-list
|
||||
- if @dashboard_milestones.blank?
|
||||
- if @milestones.blank?
|
||||
%li
|
||||
.nothing-here-block No milestones to show
|
||||
- else
|
||||
- @dashboard_milestones.each do |milestone|
|
||||
- @milestones.each do |milestone|
|
||||
= render 'milestone', milestone: milestone
|
||||
= paginate @dashboard_milestones, theme: "gitlab"
|
||||
= paginate @milestones, theme: "gitlab"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
- page_title @dashboard_milestone.title, "Milestones"
|
||||
- page_title @milestone.title, "Milestones"
|
||||
%h4.page-title
|
||||
.issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" }
|
||||
- if @dashboard_milestone.closed?
|
||||
.issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" }
|
||||
- if @milestone.closed?
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
Milestone #{@dashboard_milestone.title}
|
||||
Milestone #{@milestone.title}
|
||||
|
||||
%hr
|
||||
- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active?
|
||||
- if @milestone.complete? && @milestone.active?
|
||||
.alert.alert-success
|
||||
%span All issues for this milestone are closed. You may close the milestone now.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
%th Open issues
|
||||
%th State
|
||||
%th Due date
|
||||
- @dashboard_milestone.milestones.each do |milestone|
|
||||
- @milestone.milestones.each do |milestone|
|
||||
%tr
|
||||
%td
|
||||
= link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
|
||||
@@ -39,46 +39,46 @@
|
||||
.context
|
||||
%p.lead
|
||||
Progress:
|
||||
#{@dashboard_milestone.closed_items_count} closed
|
||||
#{@milestone.closed_items_count} closed
|
||||
–
|
||||
#{@dashboard_milestone.open_items_count} open
|
||||
= milestone_progress_bar(@dashboard_milestone)
|
||||
#{@milestone.open_items_count} open
|
||||
= milestone_progress_bar(@milestone)
|
||||
|
||||
%ul.nav.nav-tabs
|
||||
%li.active
|
||||
= link_to '#tab-issues', 'data-toggle' => 'tab' do
|
||||
Issues
|
||||
%span.badge= @dashboard_milestone.issue_count
|
||||
%span.badge= @milestone.issue_count
|
||||
%li
|
||||
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
|
||||
Merge Requests
|
||||
%span.badge= @dashboard_milestone.merge_requests_count
|
||||
%span.badge= @milestone.merge_requests_count
|
||||
%li
|
||||
= link_to '#tab-participants', 'data-toggle' => 'tab' do
|
||||
Participants
|
||||
%span.badge= @dashboard_milestone.participants.count
|
||||
%span.badge= @milestone.participants.count
|
||||
|
||||
.pull-right
|
||||
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped"
|
||||
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped"
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active#tab-issues
|
||||
.row
|
||||
.col-md-6
|
||||
= render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues
|
||||
= render 'issues', title: "Open", issues: @milestone.opened_issues
|
||||
.col-md-6
|
||||
= render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues
|
||||
= render 'issues', title: "Closed", issues: @milestone.closed_issues
|
||||
|
||||
.tab-pane#tab-merge-requests
|
||||
.row
|
||||
.col-md-6
|
||||
= render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests
|
||||
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
|
||||
.col-md-6
|
||||
= render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests
|
||||
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
|
||||
|
||||
.tab-pane#tab-participants
|
||||
%ul.bordered-list
|
||||
- @dashboard_milestone.participants.each do |user|
|
||||
- @milestone.participants.each do |user|
|
||||
%li
|
||||
= link_to user, title: user.name, class: "darken" do
|
||||
= image_tag avatar_icon(user, 32), class: "avatar s32"
|
||||
|
||||
@@ -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, rel: "alternate", type: "text/html"
|
||||
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|
|
||||
event_to_atom(xml, event)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- user = member.user
|
||||
- return unless user || member.invite?
|
||||
- show_roles = true if show_roles.nil?
|
||||
|
||||
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
|
||||
%span{class: ("list-item-name" if show_controls)}
|
||||
@@ -25,11 +24,11 @@
|
||||
= link_to member.created_by.name, user_path(member.created_by)
|
||||
= time_ago_with_tooltip(member.created_at)
|
||||
|
||||
- if show_controls && can?(current_user, :admin_group_member, member)
|
||||
- if show_controls && can?(current_user, :admin_group_member, @group)
|
||||
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
|
||||
Resend invite
|
||||
|
||||
- if show_roles
|
||||
- if should_user_see_group_roles?(current_user, @group)
|
||||
%span.pull-right
|
||||
%strong= member.human_access
|
||||
- if show_controls
|
||||
@@ -37,6 +36,7 @@
|
||||
= button_tag class: "btn-xs btn js-toggle-button",
|
||||
title: 'Edit access level', type: 'button' do
|
||||
%i.fa.fa-pencil-square-o
|
||||
|
||||
- if can?(current_user, :destroy_group_member, member)
|
||||
|
||||
- if current_user == user
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
- page_title "Members"
|
||||
- header_title group_title(@group, "Members", group_group_members_path(@group))
|
||||
- show_roles = should_user_see_group_roles?(current_user, @group)
|
||||
|
||||
- if show_roles
|
||||
- if should_user_see_group_roles?(current_user, @group)
|
||||
%p.light
|
||||
Members of group have access to all group projects.
|
||||
Read more about permissions
|
||||
@@ -49,7 +47,7 @@
|
||||
(#{@members.total_count})
|
||||
%ul.well-list
|
||||
- @members.each do |member|
|
||||
= render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true
|
||||
= render 'groups/group_members/group_member', member: member, show_controls: true
|
||||
|
||||
= paginate @members, theme: 'gitlab'
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
:plain
|
||||
$("##{dom_id(@member)}").replaceWith('#{escape_javascript(render(@member, member: @member, show_controls: true))}');
|
||||
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}');
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
%span.label.label-gray
|
||||
= milestone.project.name
|
||||
.col-sm-6
|
||||
- if can?(current_user, :admin_group, @group)
|
||||
- if can?(current_user, :admin_milestones, @group)
|
||||
- if milestone.closed?
|
||||
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
|
||||
- else
|
||||
|
||||
@@ -3,15 +3,22 @@
|
||||
|
||||
= render 'shared/milestones_filter'
|
||||
.gray-content-block
|
||||
Only milestones from
|
||||
%strong #{@group.name}
|
||||
group are listed here.
|
||||
- if can?(current_user, :admin_milestones, @group)
|
||||
.pull-right
|
||||
%span.pull-right.hidden-xs
|
||||
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
|
||||
New Milestone
|
||||
|
||||
.oneline
|
||||
Only milestones from
|
||||
%strong #{@group.name}
|
||||
group are listed here.
|
||||
.milestones
|
||||
%ul.content-list
|
||||
- if @group_milestones.blank?
|
||||
- if @milestones.blank?
|
||||
%li
|
||||
.nothing-here-block No milestones to show
|
||||
- else
|
||||
- @group_milestones.each do |milestone|
|
||||
- @milestones.each do |milestone|
|
||||
= render 'milestone', milestone: milestone
|
||||
= paginate @group_milestones, theme: "gitlab"
|
||||
= paginate @milestones, theme: "gitlab"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
- page_title "Milestones"
|
||||
- header_title group_title(@group, "Milestones", group_milestones_path(@group))
|
||||
|
||||
%h3.page-title
|
||||
New Milestone
|
||||
|
||||
%p.light
|
||||
This will create milestone in every selected project
|
||||
%hr
|
||||
|
||||
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f|
|
||||
.row
|
||||
.col-md-6
|
||||
.form-group
|
||||
= f.label :title, "Title", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true
|
||||
%p.hint Required
|
||||
.form-group.milestone-description
|
||||
= f.label :description, "Description", class: "control-label"
|
||||
.col-sm-10
|
||||
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
|
||||
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
|
||||
.clearfix
|
||||
.error-alert
|
||||
.form-group
|
||||
= f.label :projects, "Projects", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.collection_select :project_ids, @group.projects, :id, :name,
|
||||
{ selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
|
||||
|
||||
.col-md-6
|
||||
.form-group
|
||||
= f.label :due_date, "Due Date", class: "control-label"
|
||||
.col-sm-10= f.hidden_field :due_date
|
||||
.col-sm-10
|
||||
.datepicker
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Create Milestone', class: "btn-create btn"
|
||||
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
|
||||
|
||||
|
||||
:javascript
|
||||
$(".datepicker").datepicker({
|
||||
dateFormat: "yy-mm-dd",
|
||||
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
|
||||
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
|
||||
@@ -1,22 +1,22 @@
|
||||
- page_title @group_milestone.title, "Milestones"
|
||||
- page_title @milestone.title, "Milestones"
|
||||
= render "header_title"
|
||||
|
||||
%h4.page-title
|
||||
.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
|
||||
- if @group_milestone.closed?
|
||||
.issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" }
|
||||
- if @milestone.closed?
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
Milestone #{@group_milestone.title}
|
||||
Milestone #{@milestone.title}
|
||||
.pull-right
|
||||
- if can?(current_user, :admin_group, @group)
|
||||
- if @group_milestone.active?
|
||||
= link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
|
||||
- if can?(current_user, :admin_milestones, @group)
|
||||
- if @milestone.active?
|
||||
= link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
|
||||
- else
|
||||
= link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
|
||||
= link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
|
||||
|
||||
%hr
|
||||
- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active?
|
||||
- if @milestone.complete? && @milestone.active?
|
||||
.alert.alert-success
|
||||
%span All issues for this milestone are closed. You may close the milestone now.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
%th Open issues
|
||||
%th State
|
||||
%th Due date
|
||||
- @group_milestone.milestones.each do |milestone|
|
||||
- @milestone.milestones.each do |milestone|
|
||||
%tr
|
||||
%td
|
||||
= link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
|
||||
@@ -47,46 +47,46 @@
|
||||
.context
|
||||
%p.lead
|
||||
Progress:
|
||||
#{@group_milestone.closed_items_count} closed
|
||||
#{@milestone.closed_items_count} closed
|
||||
–
|
||||
#{@group_milestone.open_items_count} open
|
||||
= milestone_progress_bar(@group_milestone)
|
||||
#{@milestone.open_items_count} open
|
||||
= milestone_progress_bar(@milestone)
|
||||
|
||||
%ul.nav.nav-tabs
|
||||
%li.active
|
||||
= link_to '#tab-issues', 'data-toggle' => 'tab' do
|
||||
Issues
|
||||
%span.badge= @group_milestone.issue_count
|
||||
%span.badge= @milestone.issue_count
|
||||
%li
|
||||
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
|
||||
Merge Requests
|
||||
%span.badge= @group_milestone.merge_requests_count
|
||||
%span.badge= @milestone.merge_requests_count
|
||||
%li
|
||||
= link_to '#tab-participants', 'data-toggle' => 'tab' do
|
||||
Participants
|
||||
%span.badge= @group_milestone.participants.count
|
||||
%span.badge= @milestone.participants.count
|
||||
|
||||
.pull-right
|
||||
= link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped"
|
||||
= link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped"
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active#tab-issues
|
||||
.row
|
||||
.col-md-6
|
||||
= render 'issues', title: "Open", issues: @group_milestone.opened_issues
|
||||
= render 'issues', title: "Open", issues: @milestone.opened_issues
|
||||
.col-md-6
|
||||
= render 'issues', title: "Closed", issues: @group_milestone.closed_issues
|
||||
= render 'issues', title: "Closed", issues: @milestone.closed_issues
|
||||
|
||||
.tab-pane#tab-merge-requests
|
||||
.row
|
||||
.col-md-6
|
||||
= render 'merge_requests', title: "Open", merge_requests: @group_milestone.opened_merge_requests
|
||||
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
|
||||
.col-md-6
|
||||
= render 'merge_requests', title: "Closed", merge_requests: @group_milestone.closed_merge_requests
|
||||
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
|
||||
|
||||
.tab-pane#tab-participants
|
||||
%ul.bordered-list
|
||||
- @group_milestone.participants.each do |user|
|
||||
- @milestone.participants.each do |user|
|
||||
%li
|
||||
= link_to user, title: user.name, class: "darken" do
|
||||
= image_tag avatar_icon(user, 32), class: "avatar s32"
|
||||
|
||||
@@ -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), rel: "alternate", type: "text/html"
|
||||
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|
|
||||
event_to_atom(xml, event)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Project #{@old_path_with_namespace} was moved to another location
|
||||
Project <%= @old_path_with_namespace %> was moved to another location
|
||||
|
||||
The project is now located under
|
||||
<%= namespace_project_url(@project.namespace, @project) %>
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
%input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
|
||||
.zen-backdrop
|
||||
- classes << ' js-gfm-input markdown-area'
|
||||
= f.text_area attr, class: classes, placeholder: ''
|
||||
- if defined?(f) && f
|
||||
= f.text_area attr, class: classes, placeholder: ''
|
||||
- else
|
||||
= text_area_tag attr, nil, class: classes, placeholder: ''
|
||||
%a.zen-enter-link(tabindex="-1" href="#")
|
||||
%i.fa.fa-expand
|
||||
= icon('expand')
|
||||
Edit in fullscreen
|
||||
%a.zen-leave-link(href="#")
|
||||
%i.fa.fa-compress
|
||||
= icon('compress')
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
- if allowed_tree_edit?
|
||||
.btn-group{ role: "group" }
|
||||
%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 @@
|
||||
%a.close{href: "#", "data-dismiss" => "modal"} ×
|
||||
%h3.page-title Create New Directory
|
||||
.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
|
||||
= label_tag :dir_name, 'Directory Name', class: 'control-label'
|
||||
.col-sm-10
|
||||
= 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?
|
||||
.form-group
|
||||
= label_tag :branch_name, 'Branch', class: 'control-label'
|
||||
.col-sm-10
|
||||
= text_field_tag 'new_branch', @ref, class: "form-control"
|
||||
|
||||
= render 'shared/new_commit_form', placeholder: "Add new directory"
|
||||
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
= submit_tag "Create directory", class: 'btn btn-primary btn-create'
|
||||
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
|
||||
|
||||
: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 @@
|
||||
.modal-content
|
||||
.modal-header
|
||||
%a.close{href: "#", "data-dismiss" => "modal"} ×
|
||||
%h3.page-title Remove #{@blob.name}
|
||||
%p.light
|
||||
From branch
|
||||
%strong= @ref
|
||||
%h3.page-title Delete #{@blob.name}
|
||||
|
||||
.modal-body
|
||||
= form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-requires-input' do
|
||||
= render 'shared/commit_message_container', params: params,
|
||||
placeholder: 'Removed this file because...'
|
||||
= 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/new_commit_form', placeholder: "Delete #{@blob.name}"
|
||||
|
||||
.form-group
|
||||
.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"
|
||||
|
||||
:javascript
|
||||
new NewCommitForm($('.js-replace-blob-form'))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
%a.close{href: "#", "data-dismiss" => "modal"} ×
|
||||
%h3.page-title #{title}
|
||||
.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-previews.blob-upload-dropzone-previews
|
||||
%p.dz-message.light
|
||||
@@ -13,19 +13,15 @@
|
||||
= link_to 'click to upload', '#', class: "markdown-selector"
|
||||
%br
|
||||
.dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
|
||||
= render 'shared/commit_message_container', params: params,
|
||||
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"
|
||||
|
||||
= render 'shared/new_commit_form', placeholder: placeholder
|
||||
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
= 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"
|
||||
|
||||
:javascript
|
||||
disableButtonIfEmptyField($('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file');
|
||||
new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}');
|
||||
disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
|
||||
new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
|
||||
new NewCommitForm($('.js-upload-blob-form'))
|
||||
|
||||
@@ -13,15 +13,9 @@
|
||||
%i.fa.fa-eye
|
||||
= 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 'shared/commit_message_container', params: params, 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"
|
||||
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
|
||||
|
||||
= hidden_field_tag 'last_commit', @last_commit
|
||||
= hidden_field_tag 'content', '', id: "file-content"
|
||||
@@ -30,3 +24,4 @@
|
||||
|
||||
:javascript
|
||||
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 @@
|
||||
= render "header_title"
|
||||
|
||||
.gray-content-block.top-block
|
||||
Create a new file
|
||||
%h3.page-title
|
||||
Create New File
|
||||
|
||||
.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 'shared/commit_message_container', params: params,
|
||||
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"
|
||||
= render 'shared/new_commit_form', placeholder: "Add new file"
|
||||
|
||||
= hidden_field_tag 'content', '', id: 'file-content'
|
||||
= render 'projects/commit_button', ref: @ref,
|
||||
@@ -23,3 +16,4 @@
|
||||
|
||||
:javascript
|
||||
blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null)
|
||||
new NewCommitForm($('.js-new-blob-form'))
|
||||
|
||||
@@ -10,6 +10,4 @@
|
||||
= render 'projects/blob/remove'
|
||||
|
||||
- title = "Replace #{@blob.name}"
|
||||
= 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
|
||||
= 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
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
= render "commit_box"
|
||||
= render "ci_menu" if @ci_commit
|
||||
= render "projects/diffs/diffs", diffs: @diffs, project: @project
|
||||
= render "projects/notes/notes_with_form", view: params[:view]
|
||||
= render "projects/notes/notes_with_form"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
- if params[:view] == 'parallel'
|
||||
- if diff_view == 'parallel'
|
||||
- fluid_layout true
|
||||
|
||||
- diff_files = safe_diff_files(diffs)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
-# Skipp all non non-supported blobs
|
||||
- return unless blob.respond_to?('text?')
|
||||
- if blob.text?
|
||||
- if params[:view] == 'parallel'
|
||||
- if diff_view == 'parallel'
|
||||
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
|
||||
- else
|
||||
= render "projects/diffs/text_file", diff_file: diff_file, index: i
|
||||
@@ -42,4 +42,3 @@
|
||||
= render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
|
||||
- else
|
||||
.nothing-here-block No preview for this file type
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
= render 'shared/show_aside'
|
||||
|
||||
.gray-content-block.second-block
|
||||
.gray-content-block.second-block.oneline-block
|
||||
.row
|
||||
.col-md-9
|
||||
.votes-holder.pull-right
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
|
||||
.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
|
||||
- if issue.votes_count > 0
|
||||
= render 'votes/votes_inline', votable: issue
|
||||
- if issue.milestone
|
||||
|
||||
%span
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
#votes= render 'votes/votes_block', votable: @merge_request
|
||||
= render "projects/merge_requests/show/participants"
|
||||
.col-md-3
|
||||
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
|
||||
= cross_project_reference(@project, @merge_request)
|
||||
.input-group.cross-project-reference
|
||||
%span.slead.has_tooltip{title: 'Cross-project reference'}
|
||||
= cross_project_reference(@project, @merge_request)
|
||||
= clipboard_button
|
||||
|
||||
.row
|
||||
%section.col-md-9
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
|
||||
.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
|
||||
- if merge_request.votes_count > 0
|
||||
= render 'votes/votes_inline', votable: merge_request
|
||||
- if merge_request.milestone_id?
|
||||
|
||||
%span
|
||||
|
||||
@@ -23,9 +23,7 @@
|
||||
.col-sm-10
|
||||
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
|
||||
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
|
||||
.hint
|
||||
.pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
|
||||
.pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
|
||||
= render 'projects/notes/hints'
|
||||
.clearfix
|
||||
.error-alert
|
||||
.col-md-6
|
||||
@@ -45,7 +43,7 @@
|
||||
|
||||
|
||||
:javascript
|
||||
$( ".datepicker" ).datepicker({
|
||||
$(".datepicker").datepicker({
|
||||
dateFormat: "yy-mm-dd",
|
||||
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
|
||||
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f|
|
||||
= hidden_field_tag :view, params[:view]
|
||||
= hidden_field_tag :view, diff_view
|
||||
= hidden_field_tag :line_type
|
||||
= note_target_fields(@note)
|
||||
= f.hidden_field :commit_id
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
.note-header
|
||||
- if note_editable?(note)
|
||||
.note-actions
|
||||
= link_to edit_namespace_project_note_path(note.project.namespace, note.project, note), title: 'Edit comment', remote: true, class: 'js-note-edit' do
|
||||
= link_to '#', title: 'Edit comment', class: 'js-note-edit' do
|
||||
= icon('pencil-square-o')
|
||||
|
||||
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do
|
||||
@@ -35,30 +35,12 @@
|
||||
- if note.updated_by && note.updated_by != note.author
|
||||
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-text
|
||||
= preserve do
|
||||
= markdown(note.note, {no_header_anchors: true})
|
||||
- if note_editable?(note)
|
||||
= render 'projects/notes/edit_form', note: note
|
||||
|
||||
- if note.attachment.url
|
||||
.note-attachment
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user