Commit 0dff7b44 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch 'master' into issue_3400_port

# Conflicts:
#	app/assets/javascripts/gl_dropdown.js.coffee
parents 62b260cc 4a4a79aa
{
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
"color-shorthand": true,
"element-case": "lower",
"space-before-colon": "",
"space-after-colon": " ",
"space-before-combinator": " ",
"space-after-combinator": " ",
"space-between-declarations": "\n",
"space-before-opening-brace": " ",
"space-after-opening-brace": "\n",
"space-before-closing-brace": "\n",
"unitless-zero": true
"exclude": [
"app/assets/stylesheets/framework/tw_bootstrap_variables.scss",
"app/assets/stylesheets/framework/fonts.scss"
],
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
"color-shorthand": true,
"element-case": "lower",
"space-before-colon": "",
"space-after-colon": " ",
"space-before-combinator": " ",
"space-after-combinator": " ",
"space-between-declarations": "\n",
"space-before-opening-brace": " ",
"space-after-opening-brace": "\n",
"space-before-closing-brace": "\n",
"unitless-zero": true
}
......@@ -128,7 +128,6 @@ scss-lint:
- bundle exec rake scss_lint
tags:
- ruby
allow_failure: true
brakeman:
stage: test
......
......@@ -100,7 +100,7 @@ linters:
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: true
enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
v 8.7.0 (unreleased)
- Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu)
- Fix avatar stretching by providing a cropping feature
v 8.6.1
- Add option to reload the schema before restoring a database backup. !2807
- Display navigation controls on mobile. !3214
- Fixed bug where participants would not work correctly on merge requests. !3329
- Fix sorting issues by votes on the groups issues page results in SQL errors. !3333
- Restrict notifications for confidential issues. !3334
- Do not allow to move issue if it has not been persisted. !3340
- Add a confirmation step before deleting an issuable. !3341
- Fixes issue with signin button overflowing on mobile. !3342
- Auto collapses the navigation sidebar when resizing. !3343
- Fix build dependencies, when the dependency is a string. !3344
- Shows error messages when trying to create label in dropdown menu. !3345
- Fixes issue with assign milestone not loading milestone list. !3346
- Fix an issue causing the Dashboard/Milestones page to be blank. !3348
v 8.6.0
- Add ability to move issue to another project
- Prevent tokens in the import URL to be showed by the UI
- Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
- Make HTTP(s) label consistent on clone bar (Stan Hu)
- Add confidential issues
- Bump gitlab_git to 9.0.3 (Stan Hu)
- Fix diff image view modes (2-up, swipe, onion skin) not working (Stan Hu)
......@@ -18,9 +38,11 @@ v 8.6.0 (unreleased)
setup. A password can be provided during setup (see installation docs), or
GitLab will ask the user to create a new one upon first visit.
- Fix issue when pushing to projects ending in .wiki
- Properly display YAML front matter in Markdown
- Add support for wiki with UTF-8 page names (Hiroyuki Sato)
- Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room
- Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
- HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta)
......@@ -60,6 +82,7 @@ v 8.6.0 (unreleased)
- User deletion is now done in the background so the request can not time out
- Canceled builds are now ignored in compound build status if marked as `allowed to fail`
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
v 8.5.8
- Bump Git version requirement to 2.7.4
......
......@@ -16,6 +16,7 @@
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical debt](#technical-debt)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- [Merge request description format](#merge-request-description-format)
......@@ -242,6 +243,28 @@ addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
### Technical debt
In order to track things that can be improved in GitLab's codebase, we created
the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
This label should be added to issues that describe things that can be improved,
shortcuts that have been taken, code that needs refactoring, features that need
additional attention, and all other things that have been left behind due to
high velocity of development.
Everyone can create an issue, though you may need to ask for adding a specific
label, if you do not have permissions to do it by yourself. Additional labels
can be combined with the `technical debt` label, to make it easier to schedule
the improvements for a release.
Issues tagged with the `technical debt` label have the same priority like issues
that describe a new feature to be introduced in GitLab, and should be scheduled
for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is
associated with in the description of the issue.
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
......
......@@ -1064,6 +1064,3 @@ DEPENDENCIES
web-console (~> 2.0)
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
BUNDLED WITH
1.11.2
......@@ -74,6 +74,8 @@
dataType: "json"
).done (label) ->
callback(label)
.error (message) ->
callback(message.responseJSON)
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
......
......@@ -43,6 +43,7 @@
#= require jquery.nicescroll
#= require_tree .
#= require fuzzaldrin-plus
#= require cropper
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......@@ -139,7 +140,7 @@ $ ->
# Initialize tooltips
$('body').tooltip(
selector: '.has_tooltip, [data-toggle="tooltip"]'
selector: '.has-tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
$el.data('placement') || 'bottom'
......@@ -218,13 +219,20 @@ $ ->
$this = $(this)
$this.attr 'value', $this.val()
$sidebarGutterToggle = $('.js-sidebar-toggle')
$navIconToggle = $('.toggle-nav-collapse')
$(document)
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
$gutterIcon = $('.js-sidebar-toggle').find('i')
$gutterIcon = $sidebarGutterToggle.find('i')
if $gutterIcon.hasClass('fa-angle-double-right')
$gutterIcon.closest('a').trigger('click')
$sidebarGutterToggle.trigger('click')
$navIcon = $navIconToggle.find('.fa')
if $navIcon.hasClass('fa-angle-left')
$navIconToggle.trigger('click')
$(document)
.off 'click', '.js-sidebar-toggle'
......
......@@ -122,7 +122,7 @@ class @AwardsHandler
nodes = []
nodes.push(
"<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
"<button class='btn award-control js-emoji-btn has-tooltip active' title='me'>",
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
"<span class='award-control-text js-counter'>1</span>",
"</button>"
......
class GitLabCrop
# Matches everything but the file name
FILENAMEREGEX = /^.*[\\\/]/
constructor: (input, opts = {}) ->
@fileInput = $(input)
# We should rename to avoid spec to fail
# Form will submit the proper input filed with a file using FormData
@fileInput
.attr('name', "#{@fileInput.attr('name')}-trigger")
.attr('id', "#{@fileInput.attr('id')}-trigger")
# Set defaults
{
@exportWidth = 200
@exportHeight = 200
@cropBoxWidth = 200
@cropBoxHeight = 200
@form = @fileInput.parents('form')
# Required params
@filename
@previewImage
@modalCrop
@pickImageEl
@uploadImageBtn
@modalCropImg
} = opts
# Ensure needed elements are jquery objects
# If selector is provided we will convert them to a jQuery Object
@filename = @getElement(@filename)
@previewImage = @getElement(@previewImage)
@pickImageEl = @getElement(@pickImageEl)
# Modal elements usually are outside the @form element
@modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
@uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
@modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
@cropActionsBtn = @modalCrop.find('[data-method]')
@bindEvents()
getElement: (selector) ->
$(selector, @form)
bindEvents: ->
_this = @
@fileInput.on 'change', (e) ->
_this.onFileInputChange(e, @)
@pickImageEl.on 'click', @onPickImageClick
@modalCrop.on 'shown.bs.modal', @onModalShow
@modalCrop.on 'hidden.bs.modal', @onModalHide
@uploadImageBtn.on 'click', @onUploadImageBtnClick
@cropActionsBtn.on 'click', (e) ->
btn = @
_this.onActionBtnClick(btn)
@croppedImageBlob = null
onPickImageClick: =>
@fileInput.trigger('click')
onModalShow: =>
_this = @
@modalCropImg.cropper(
viewMode: 1
center: false
aspectRatio: 1
modal: true
scalable: false
rotatable: false
zoomable: true
dragMode: 'move'
guides: false
zoomOnTouch: false
zoomOnWheel: false
cropBoxMovable: false
cropBoxResizable: false
toggleDragModeOnDblclick: false
built: ->
$image = $(@)
container = $image.cropper 'getContainerData'
cropBoxWidth = _this.cropBoxWidth;
cropBoxHeight = _this.cropBoxHeight;
$image.cropper('setCropBoxData',
width: cropBoxWidth,
height: cropBoxHeight,
left: (container.width - cropBoxWidth) / 2,
top: (container.height - cropBoxHeight) / 2
)
)
onModalHide: =>
@modalCropImg
.attr('src', '') # Remove attached image
.cropper('destroy') # Destroy cropper instance
onUploadImageBtnClick: (e) =>
e.preventDefault()
@setBlob()
@setPreview()
@modalCrop.modal('hide')
@fileInput.val('')
onActionBtnClick: (btn) ->
data = $(btn).data()
if @modalCropImg.data('cropper') && data.method
result = @modalCropImg.cropper data.method, data.option
onFileInputChange: (e, input) ->
@readFile(input)
readFile: (input) ->
_this = @
reader = new FileReader
reader.onload = ->
_this.modalCropImg.attr('src', reader.result)
_this.modalCrop.modal('show')
reader.readAsDataURL(input.files[0])
dataURLtoBlob: (dataURL) ->
binary = atob(dataURL.split(',')[1])
array = []
for v, k in binary
array.push(binary.charCodeAt(k))
new Blob([new Uint8Array(array)], type: 'image/png')
setPreview: ->
@previewImage.attr('src', @dataURL)
filename = @fileInput.val().replace(FILENAMEREGEX, '')
@filename.text(filename)
setBlob: ->
@dataURL = @modalCropImg.cropper('getCroppedCanvas',
width: 200
height: 200
).toDataURL('image/png')
@croppedImageBlob = @dataURLtoBlob(@dataURL)
getBlob: ->
@croppedImageBlob
$.fn.glCrop = (opts) ->
return @.each ->
$(@).data('glcrop', new GitLabCrop(@, opts))
class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40]
HAS_VALUE_CLASS = "has-value"
constructor: (@dropdown, @options) ->
constructor: (@input, @options) ->
{
@input
@filterInputBlur = true
} = @options
$inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
# Clear click
$clearButton.on 'click', (e) =>
e.preventDefault()
e.stopPropagation()
@input
.val('')
.trigger('keyup')
.focus()
# Key events
timeout = ""
@input.on "keyup", (e) =>
if e.keyCode is 13 && @input.val() isnt ""
if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.addClass HAS_VALUE_CLASS
else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.removeClass HAS_VALUE_CLASS
if e.keyCode is 13 and @input.val() isnt ""
if @options.enterCallback
@options.enterCallback()
return
......@@ -119,7 +136,9 @@ class GitLabDropdown
# Init filterable
if @options.filterable
@filter = new GitLabDropdownFilter @dropdown,
@input = @dropdown.find('.dropdown-input .dropdown-input-field')
@filter = new GitLabDropdownFilter @input,
filterInputBlur: @filterInputBlur
input: @filterInput
remote: @options.filterRemote
......@@ -129,6 +148,7 @@ class GitLabDropdown
return @fullData
callback: (data) =>
@parseData data
@highlightRow 1
enterCallback: =>
@selectFirstRow()
......@@ -278,11 +298,19 @@ class GitLabDropdown
noResults: ->
html = "<li>"
html += "<a href='#' class='is-focused'>"
html += "<a href='#' class='dropdown-menu-empty-link is-focused'>"
html += "No matching results."
html += "</a>"
html += "</li>"
highlightRow: (index) ->
if @input.val() isnt ""
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
$(selector).addClass 'is-focused'
rowClicked: (el) ->
fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']")
......@@ -326,7 +354,7 @@ class GitLabDropdown
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
# similute a click on the first link
# simulate a click on the first link
$(selector).trigger "click"
$.fn.glDropdown = (opts) ->
......
#= require jquery.waitforimages
class @IssuableContext
constructor: ->
@initParticipants()
new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
......@@ -17,3 +17,27 @@ class @IssuableContext
block.find('.js-select2').select2("open")
$(".right-sidebar").niceScroll()
initParticipants: ->
_this = @
$(document).on "click", ".js-participants-more", @toggleHiddenParticipants
$(".js-participants-author").each (i) ->
if i >= _this.PARTICIPANTS_ROW_COUNT
$(@)
.addClass "js-participants-hidden"
.hide()
toggleHiddenParticipants: (e) ->
e.preventDefault()
currentText = $(this).text().trim()
lessText = $(this).data("less-text")
originalText = $(this).data("original-text")
if currentText is originalText
$(this).text(lessText)
else
$(this).text(originalText)
$(".js-participants-hidden").toggle()
......@@ -7,7 +7,6 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
@fixAffixScroll()
@initParticipants()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
......@@ -85,27 +84,3 @@ class @Issue
type: 'PATCH'
url: $('form.js-issuable-update').attr('action')
data: patchData
initParticipants: ->
_this = @
$(document).on "click", ".js-participants-more", @toggleHiddenParticipants
$(".js-participants-author").each (i) ->
if i >= _this.PARTICIPANTS_ROW_COUNT
$(@)
.addClass "js-participants-hidden"
.hide()
toggleHiddenParticipants: (e) ->
e.preventDefault()
currentText = $(this).text().trim()
lessText = $(this).data("less-text")
originalText = $(this).data("original-text")
if currentText is originalText
$(this).text(lessText)
else
$(this).text(originalText)
$(".js-participants-hidden").toggle()
......@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl = $dropdown.data('labels')
selectedLabel = $dropdown.data('selected')
if selectedLabel
selectedLabel = selectedLabel.split(',')
selectedLabel = selectedLabel.toString().split(',')
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
showNo = $dropdown.data('show-no')
......@@ -14,28 +14,81 @@ class @LabelsSelect
defaultLabel = $dropdown.data('default-label')
if newLabelField.length
$newLabelCreateButton = $('.js-new-label-btn')
$colorPreview = $('.js-dropdown-label-color-preview')
$newLabelError = $dropdown.parent().find('.js-label-error')
$newLabelError.hide()
# Suggested colors in the dropdown to chose from pre-chosen colors
$('.suggest-colors-dropdown a').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
newColorField.val $(this).data('color')
$('.js-dropdown-label-color-preview')
newColorField
.val($(this).data('color'))
.trigger('change')
$colorPreview
.css 'background-color', $(this).data('color')
.parent()
.addClass 'is-active'
$('.js-new-label-btn').on 'click', (e) ->
# Cancel button takes back to first page
resetForm = ->
newLabelField
.val ''
.trigger 'change'
newColorField
.val ''
.trigger 'change'
$colorPreview
.css 'background-color', ''
.parent()
.removeClass 'is-active'
$('.dropdown-menu-back').on 'click', ->
resetForm()
$('.js-cancel-label-btn').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
resetForm()
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
# Listen for change and keyup events on label and color field
# This allows us to enable the button when ready
enableLabelCreateButton = ->
if newLabelField.val() isnt '' and newColorField.val() isnt ''
$('.js-new-label-btn').disable()
$newLabelCreateButton.enable()
else
$newLabelCreateButton.disable()
newLabelField.on 'keyup change', enableLabelCreateButton
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
newColorField.on 'keyup change', enableLabelCreateButton
# Send the API call to create the label
$newLabelCreateButton
.disable()
.on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
if newLabelField.val() isnt '' and newColorField.val() isnt ''
$newLabelError.hide()
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
if label.message?
$newLabelError
.text label.message
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
$dropdown.glDropdown(
data: (term, callback) ->
......@@ -68,8 +121,11 @@ class @LabelsSelect
else
selected = if label.title is selectedLabel then 'is-active' else ''
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li>
<a href='#' class='#{selected}'>
#{color}
#{label.title}
</a>
</li>"
......
class @Profile
constructor: ->
constructor: (opts = {}) ->
{
@form = $('.edit-user')
} = opts
# Automatically submit the Preferences form when any of its radio buttons change
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
......@@ -17,14 +21,46 @@ class @Profile
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
$('.js-choose-user-avatar-button').bind "click", ->
form = $(this).closest("form")
form.find(".js-user-avatar-input").click()
@bindEvents()
cropOpts =
filename: '.js-avatar-filename'
previewImage: '.avatar-image .avatar'
modalCrop: '.modal-profile-crop'
pickImageEl: '.js-choose-user-avatar-button'
uploadImageBtn: '.js-upload-user-avatar'
modalCropImg: '.modal-profile-crop-image'
@avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
bindEvents: ->
@form.on 'submit', @onSubmitForm
onSubmitForm: (e) =>
e.preventDefault()
@saveForm()
saveForm: ->
self = @
formData = new FormData(@form[0])
formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png')
$('.js-user-avatar-input').bind "change", ->
form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '')
form.find(".js-avatar-filename").text(filename)
$.ajax
url: @form.attr('action')
type: @form.attr('method')
data: formData
dataType: "json"
processData: false
contentType: false
success: (response) ->
new Flash(response.message, 'notice')
error: (jqXHR) ->
new Flash(jqXHR.responseJSON.message, 'alert')
complete: ->
window.scrollTo 0, 0
# Enable submit button after requests ends
self.form.find(':input[disabled]').enable()
$ ->
# Extract the SSH Key title from its comment
......
......@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
......
......@@ -95,4 +95,4 @@ window.ContributorsStatGraphUtil =
if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
true
else
false
\ No newline at end of file
false
......@@ -30,6 +30,7 @@ class @UsersSelect
if showNullUser
showDivider += 1
users.unshift(
beforeDivider: true
name: 'Unassigned',
id: 0
)
......@@ -39,6 +40,7 @@ class @UsersSelect
name = showAnyUser
name = 'Any User' if name == true
anyUser = {
beforeDivider: true
name: name,
id: null
}
......@@ -75,20 +77,27 @@ class @UsersSelect
selected = if user.id is selectedId then "is-active" else ""
img = ""
if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li>
<a href='#' class='dropdown-menu-user-link #{selected}'>
#{img}
<strong class='dropdown-menu-user-full-name'>
if user.beforeDivider?
"<li>
<a href='#' class='#{selected}'>
#{user.name}
</strong>
<span class='dropdown-menu-user-username'>
#{username}
</span>
</a>
</li>"
</a>
</li>"
else
if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li>
<a href='#' class='dropdown-menu-user-link #{selected}'>
#{img}
<strong class='dropdown-menu-user-full-name'>
#{user.name}
</strong>
<span class='dropdown-menu-user-username'>
#{username}
</span>
</a>
</li>"
)
$('.ajax-users-select').each (i, select) =>
......
......@@ -9,6 +9,7 @@
*= require_self
*= require dropzone/basic
*= require cal-heatmap
*= require cropper.css
*/
/*
......
......@@ -107,10 +107,28 @@
margin: 0;
font-size: 23px;
font-weight: normal;
margin: 16px 0 5px 0;
margin: 16px 0 5px;
color: #4c4e54;
font-size: 23px;
line-height: 1.1;
h1 {
color: #313236;
margin-bottom: 6px;
font-size: 23px;
}
.visibility-icon {
display: inline-block;
margin-left: 5px;
font-size: 18px;
color: $gray;
}
p {
padding: 0 $gl-padding;
color: #5c5d5e;
}
}
.cover-desc {
......
......@@ -292,8 +292,11 @@ table {
}
.btn-sign-in {
margin-top: 10px;
text-shadow: none;
@media (min-width: $screen-sm-min) {
margin-top: 11px;
}
}
.side-filters {
......@@ -375,7 +378,6 @@ table {
position: absolute;
top: 0;
right: 0;
width: 250px !important;
visibility: hidden;
}
}
......
......@@ -75,7 +75,7 @@
width: 240px;
margin-top: 2px;
margin-bottom: 0;
padding: 10px 10px;
padding: 10px;
font-size: 14px;
font-weight: normal;
background-color: $dropdown-bg;
......@@ -130,6 +130,12 @@
text-decoration: none;
outline: 0;
}
&.dropdown-menu-empty-link {
&.is-focused {
background-color: $dropdown-empty-row-bg;
}
}
}
}
......@@ -183,7 +189,7 @@
}
.dropdown-select {
width: 280px;
width: 300px;
}
.dropdown-menu-align-right {
......@@ -237,7 +243,7 @@
.dropdown-title-button {
position: absolute;
top: -1px;
top: 0;
padding: 0;
color: $dropdown-title-btn-color;
font-size: 14px;
......@@ -270,6 +276,22 @@
font-size: 12px;
pointer-events: none;
}
.dropdown-input-clear {
display: none;
cursor: pointer;
pointer-events: all;
}
&.has-value {
.dropdown-input-clear {
display: block;
}
.dropdown-input-search {
display: none;
}
}
}
.dropdown-input-field {
......@@ -286,13 +308,13 @@
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+ .fa {
~ .fa {
color: $dropdown-link-color;
}
}
&:hover {
+ .fa {
~ .fa {
color: $dropdown-link-color;
}
}
......@@ -338,11 +360,12 @@
}
}
.dropdown-menu-labels {
.label {
position: relative;
width: 30px;
margin-right: 5px;
text-indent: -99999px;
}
.dropdown-label-box {
position: relative;
top: 3px;
margin-right: 5px;
display: inline-block;
width: 15px;
height: 15px;
border-radius: $border-radius-base;
}
......@@ -11,3 +11,11 @@
}
}
}
@media (max-width: $screen-xs-max) {
.filter-item {
display: block;
margin: 0 0 10px;
}
}
// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
// the way the `src` property is formatted in this file.
// scss-lint:disable SpaceAfterPropertyColon
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
......
......@@ -117,4 +117,4 @@ body {
&.ui_violet {
@include gitlab-theme(#98c, $theme-violet, #436, #325);
}
}
\ No newline at end of file
}
......@@ -70,6 +70,11 @@ header {
.header-content {
height: $header-height;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
.title {
margin: 0;
......
......@@ -16,7 +16,7 @@ body {
}
.container .content {
margin: 0 0;
margin: 0;
}
.navless-container {
......
/**
* Generic mixins
*/
@mixin box-shadow($shadow) {
@mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
......
......@@ -102,6 +102,10 @@
display: inline-block;
}
.icon-label {
display: none;
}
input {
height: 34px;
display: inline-block;
......@@ -124,9 +128,38 @@
}
}
/* Hide on extra small devices (phones) */
@media (max-width: $screen-xs-max) {
display: none;
padding-bottom: 0;
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
margin: 0 0 10px;
display: block;
width: 100%;
}
form {
display: block;
height: auto;
input {
width: 100%;
margin: 0 0 10px;
}
}
.input-short {
width: 100%;
}
.icon-label {
display: inline-block;
}
// Applies on /dashboard/issues
.project-item-select-holder {
display: block;
margin: 0;
}
}
/* Small devices (tablets, 768px and lower) */
......
#logo {
z-index: 2;
position: absolute;
width: 58px;
cursor: pointer;
}
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
......@@ -18,28 +25,10 @@
position: absolute;
left: 0;
}
#logo {
z-index: 2;
position: absolute;
width: 58px;
cursor: pointer;
}
&.right-sidebar-expanded {
/* Extra small devices (phones, less than 768px) */
/* No media query since this is the default in Bootstrap */
padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
padding-right: $gutter_width;
}
}
}
.sidebar-wrapper {
z-index: 999;
z-index: 1000;
background: $background-color;
}
......@@ -202,53 +191,27 @@
}
}
@mixin expanded-sidebar {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width;
}
}
.sidebar-wrapper {
width: $sidebar_width;
.nav-sidebar {
width: $sidebar_width;
}
.nav-sidebar li a{
width: 230px;
.collapse-nav a {
width: $sidebar_width;
position: fixed;
bottom: 0;
left: 0;
font-size: 13px;
background: transparent;
height: 40px;
text-align: center;
line-height: 40px;
transition-duration: .3s;
outline: none;
&.back-link {
i {
opacity: 0;
}
}
}
&:hover {
text-decoration: none;
}
}
@mixin collapsed-sidebar {
.page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width;
}
}
.sidebar-wrapper {
width: $sidebar_collapsed_width;
......@@ -293,35 +256,48 @@
}
}
.collapse-nav a {
width: $sidebar_width;
position: fixed;
bottom: 0;
left: 0;
font-size: 13px;
background: transparent;
height: 40px;
text-align: center;
line-height: 40px;
transition-duration: .3s;
outline: none;
}
.page-sidebar-expanded {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
.sidebar-wrapper {
width: $sidebar_width;
.nav-sidebar {
width: $sidebar_width;
}
.nav-sidebar li a {
width: 230px;
.collapse-nav a:hover {
text-decoration: none;
background: #f2f6f7;
&.back-link {
i {
opacity: 0;
}
}
}
}
}
.page-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
@include collapsed-sidebar;
.right-sidebar-collapsed {
padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
@include collapsed-sidebar;
padding-right: $sidebar_collapsed_width;
}
}
.page-sidebar-expanded {
@include expanded-sidebar;
.right-sidebar-expanded {
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
padding-right: $sidebar_collapsed_width;
}
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
}
}
......@@ -39,8 +39,8 @@
h1 {
font-size: 1.3em;
font-weight: 600;
margin: 24px 0 12px 0;
padding: 0 0 10px 0;
margin: 24px 0 12px;
padding: 0 0 10px;
border-bottom: 1px solid #e7e9ed;
color: #313236;
}
......@@ -48,27 +48,27 @@
h2 {
font-size: 1.2em;
font-weight: 600;
margin: 24px 0 12px 0;
margin: 24px 0 12px;
color: #313236;
}
h3 {
margin: 24px 0 12px 0;
margin: 24px 0 12px;
font-size: 1.1em;
}
h4 {
margin: 24px 0 12px 0;
margin: 24px 0 12px;
font-size: 0.98em;
}
h5 {
margin: 24px 0 12px 0;
margin: 24px 0 12px;
font-size: 0.95em;
}
h6 {
margin: 24px 0 12px 0;
margin: 24px 0 12px;
font-size: 0.90em;
}
......@@ -76,7 +76,7 @@
color: #7f8fa4;
font-size: inherit;
padding: 8px 21px;
margin: 12px 0 12px;
margin: 12px 0;
border-left: 3px solid #e7e9ed;
}
......@@ -88,13 +88,13 @@
p {
color: #5c5d5e;
margin: 6px 0 0 0;
margin: 6px 0 0;
}
table {
@extend .table;
@extend .table-bordered;
margin: 12px 0 12px 0;
margin: 12px 0;
color: #5c5d5e;
th {
background: #f8fafc;
......@@ -102,7 +102,7 @@
}
pre {
margin: 12px 0 12px 0;
margin: 12px 0;
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
......@@ -191,7 +191,7 @@ body {
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
margin: 12px 7px 12px 7px;
margin: 12px 7px;
}
h1, h2, h3, h4, h5, h6 {
......
......@@ -168,13 +168,14 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
*/
$dropdown-bg: #fff;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: rgba(#000, .04);
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
$dropdown-border-color: rgba(#000, .1);
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #c7c7c7;
$dropdown-input-color: #555;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6);
......
......@@ -3,12 +3,12 @@ img {
height: auto;
}
p.details {
font-style:italic;
color:#777
font-style: italic;
color: #777
}
.footer p {
font-size:small;
color:#777
font-size: small;
color: #777
}
pre.commit-message {
white-space: pre-wrap;
......@@ -20,5 +20,5 @@ pre.commit-message {
color: #090;
}
.file-stats .deleted-file {
color: #B00;
color: #b00;
}
......@@ -34,9 +34,9 @@
background: #fff
}
.visibility-levels {
.controls {
margin-bottom: 9px;
.visibility-levels {
.controls {
margin-bottom: 9px;
}
i {
......
......@@ -132,7 +132,7 @@
}
.image-info {
font-size: 12px;
margin: 5px 0 0 0;
margin: 5px 0 0;
color: grey;
}
......
......@@ -183,7 +183,7 @@
.block {
width: $sidebar_collapsed_width - 1px;
margin-left: -19px;
padding: 15px 0 0 0;
padding: 15px 0 0;
border-bottom: none;
overflow: hidden;
}
......@@ -273,12 +273,12 @@
}
.participants-list {
margin: -5px -5px;
margin: -5px;
}
.participants-author {
display: inline-block;
padding: 5px 5px;
padding: 5px;
.author_link {
display: block;
......
......@@ -9,28 +9,45 @@
}
&.suggest-colors-dropdown {
margin-bottom: 5px;
margin-top: 10px;
margin-bottom: 10px;
border-radius: $border-radius-base;
overflow: hidden;
a {
@include border-radius(0);
width: 36.7px;
width: (100% / 7);
margin-right: 0;
margin-bottom: -5px;
}
}
}
.dropdown-label-color-preview {
display: none;
margin-top: 5px;
width: 100%;
height: 25px;
.dropdown-new-label {
.dropdown-content {
max-height: 260px;
}
}
.dropdown-label-color-input {
position: relative;
margin-bottom: 10px;
&.is-active {
display: block;
padding-left: 32px;
}
}
.dropdown-label-color-preview {
position: absolute;
left: 0;
top: 0;
width: 32px;
height: 32px;
border-top-left-radius: $border-radius-base;
border-bottom-left-radius: $border-radius-base;
}
.label-row {
.label {
padding: 9px;
......@@ -45,3 +62,10 @@
.label-subscription {
display: inline-block;
}
.dropdown-labels-error {
padding: 5px 10px;
margin-bottom: 10px;
background-color: $gl-danger;
color: $white-light;
}
......@@ -45,7 +45,7 @@
.login-heading h3 {
font-weight: 300;
line-height: 1.5;
margin: 0 0 10px 0;
margin: 0 0 10px;
}
.login-footer {
......
......@@ -54,7 +54,7 @@
}
.account-well {
padding: 10px 10px;
padding: 10px;
background-color: $help-well-bg;
border: 1px solid $help-well-border;
border-radius: $border-radius-base;
......@@ -197,3 +197,24 @@
width: 105px;
}
}
.modal-profile-crop {
.modal-dialog {
width: 380px;
@media (max-width: $screen-sm-min) {
width: auto;
}
}
.profile-crop-image-container {
height: 300px;
margin: 0 auto;
}
.crop-controls {
padding: 10px 0 0 0;
text-align: center;
}
}
......@@ -68,28 +68,6 @@
}
}
.project-home-desc {
h1 {
color: #313236;
margin: 0;
margin-bottom: 6px;
font-size: 23px;
font-weight: normal;
}
.visibility-icon {
display: inline-block;
margin-left: 5px;
font-size: 18px;
color: $gray;
}
p {
padding: 0 $gl-padding;
color: #5c5d5e;
}
}
.project-repo-buttons {
margin-top: 20px;
margin-bottom: 0;
......@@ -333,7 +311,7 @@ pre.light-well {
}
.git-empty {
margin: 0 7px 0 7px;
margin: 0 7px;
h5 {
color: #5c5d5e;
......
......@@ -16,7 +16,7 @@
#contributors {
.contributors-list {
margin: 0 0 10px 0;
margin: 0 0 10px;
list-style: none;
padding: 0;
}
......
......@@ -21,19 +21,19 @@
$l-white: #fff;
.term-bold {
font-weight: bold;
font-weight: bold;
}
.term-italic {
font-style: italic;
font-style: italic;
}
.term-conceal {
visibility: hidden;
visibility: hidden;
}
.term-underline {
text-decoration: underline;
text-decoration: underline;
}
.term-cross {
text-decoration: line-through;
text-decoration: line-through;
}
.term-fg-black {
......
......@@ -61,6 +61,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:session_expire_delay,
:default_project_visibility,
:default_snippet_visibility,
:default_group_visibility,
:restricted_signup_domains_raw,
:version_check_enabled,
:admin_notification_email,
......
......@@ -59,6 +59,6 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
params.require(:group).permit(:name, :description, :path, :avatar)
params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level)
end
end
class Admin::ProjectsController < Admin::ApplicationController
before_action :project, only: [:show, :transfer]
before_action :group, only: [:show, :transfer]
before_action :repository, only: [:show, :transfer]
def index
@projects = Project.all
......
......@@ -23,7 +23,6 @@ class ApplicationController < ActionController::Base
helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
helper_method :repository, :can_collaborate_with_project?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
......@@ -116,47 +115,6 @@ class ApplicationController < ActionController::Base
abilities.allowed?(object, action, subject)
end
def project
unless @project
namespace = params[:namespace_id]
id = params[:project_id] || params[:id]
# Redirect from
# localhost/group/project.git
# to
# localhost/group/project
#
if id =~ /\.git\Z/
redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return
end
project_path = "#{namespace}/#{id}"
@project = Project.find_with_namespace(project_path)
if @project and can?(current_user, :read_project, @project)
if @project.path_with_namespace != project_path
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
end
@project
elsif current_user.nil?
@project = nil
authenticate_user!
else
@project = nil
render_404 and return
end
end
@project
end
def repository
@repository ||= project.repository
end
def authorize_project!(action)
return access_denied! unless can?(current_user, action, project)
end
def access_denied!
render "errors/access_denied", layout: "errors", status: 404
end
......@@ -165,14 +123,6 @@ class ApplicationController < ActionController::Base
render "errors/git_not_found.html", layout: "errors", status: 404
end
def method_missing(method_sym, *arguments, &block)
if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
authorize_project!($1.to_sym)
else
super
end
end
def render_403
head :forbidden
end
......@@ -181,10 +131,6 @@ class ApplicationController < ActionController::Base
render file: Rails.root.join("public", "404"), layout: false, status: "404"
end
def require_non_empty_project
redirect_to @project if @project.empty_repo?
end
def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
......@@ -410,13 +356,6 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
def can_collaborate_with_project?(project = nil)
project ||= @project
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
end
private
def set_default_sort
......
......@@ -6,7 +6,6 @@ module GlobalMilestones
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
@milestones = Kaminari.paginate_array(@milestones).page(params[:page])
end
def milestone
......
module IssuableActions
extend ActiveSupport::Concern
included do
before_action :authorize_destroy_issuable!, only: :destroy
end
def destroy
issuable.destroy
name = issuable.class.name.titleize.downcase
flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
end
private
def authorize_destroy_issuable!
unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
end
end
end
class Dashboard::ApplicationController < ApplicationController
layout 'dashboard'
private
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
labels = Label.where(project_id: projects).select(:title, :color).uniq(:title)
respond_to do |format|
format.json { render json: labels }
end
end
end
......@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include GlobalMilestones
before_action :projects
before_action :milestones, only: [:index]
before_action :milestone, only: [:show]
def index
respond_to do |format|
format.html do
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
render json: milestones
end
end
end
def show
end
private
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end
......@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController
include MergeRequestsAction
before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests, :labels, :milestones]
before_action :projects, only: [:issues, :merge_requests]
respond_to :html
......@@ -20,29 +20,6 @@ class DashboardController < Dashboard::ApplicationController
end
end
def labels
labels = Label.where(project_id: @projects).select(:title, :color).uniq(:title)
respond_to do |format|
format.json do
render json: labels
end
end
end
def milestones
milestones = Milestone.where(project_id: @projects).active
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
respond_to do |format|
format.json do
render json: grouped_milestones
end
end
end
protected
def load_events
......@@ -57,8 +34,4 @@ 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
class Explore::GroupsController < Explore::ApplicationController
def index
@groups = Group.order_id_desc
@groups = GroupsFinder.new.execute(current_user)
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page])
......
class Groups::ApplicationController < ApplicationController
layout 'group'
skip_before_action :authenticate_user!
before_action :group
private
def group
@group ||= Group.find_by(path: params[:group_id])
end
unless @group
id = params[:group_id] || params[:id]
@group = Group.find_by(path: id)
unless @group && can?(current_user, :read_group, @group)
@group = nil
def authorize_read_group!
unless @group and can?(current_user, :read_group, @group)
if current_user.nil?
return authenticate_user!
else
return render_404
if current_user.nil?
authenticate_user!
else
render_404
end
end
end
@group
end
def group_projects
@projects ||= GroupProjectsFinder.new(group).execute(current_user)
end
def authorize_admin_group!
......
class Groups::AvatarsController < Groups::ApplicationController
before_action :authorize_admin_group!
def destroy
@group.remove_avatar!
@group.save
......
class Groups::GroupMembersController < Groups::ApplicationController
skip_before_action :authenticate_user!, only: [:index]
# Authorize
before_action :authorize_read_group!
before_action :authorize_admin_group_member!, except: [:index, :leave]
def index
......
class Groups::MilestonesController < Groups::ApplicationController
include GlobalMilestones
before_action :projects
before_action :milestones, only: [:index]
before_action :group_projects
before_action :milestone, only: [:show, :update]
before_action :authorize_group_milestone!, only: [:create, :update]
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index
respond_to do |format|
format.html do
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
end
end
def new
......@@ -17,7 +21,7 @@ class Groups::MilestonesController < Groups::ApplicationController
project_ids = params[:milestone][:project_ids]
title = milestone_params[:title]
@group.projects.where(id: project_ids).each do |project|
@projects.where(id: project_ids).each do |project|
Milestones::CreateService.new(project, current_user, milestone_params).execute
end
......@@ -37,7 +41,7 @@ class Groups::MilestonesController < Groups::ApplicationController
private
def authorize_group_milestone!
def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group)
end
......@@ -48,8 +52,4 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
end
def projects
@projects ||= @group.projects
end
end
......@@ -5,16 +5,15 @@ class GroupsController < Groups::ApplicationController
respond_to :html
skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests]
before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create]
# Authorize
before_action :authorize_read_group!, except: [:index, :show, :new, :create, :autocomplete]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests]
before_action :event_filter, only: [:activity]
layout :determine_layout
......@@ -28,11 +27,9 @@ class GroupsController < Groups::ApplicationController
end
def create
@group = Group.new(group_params)
@group.name = @group.path.dup unless @group.name
@group = Groups::CreateService.new(current_user, group_params).execute
if @group.save
@group.add_owner(current_user)
if @group.persisted?
redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
else
render action: "new"
......@@ -41,12 +38,13 @@ class GroupsController < Groups::ApplicationController
def show
@last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace)
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:filter_projects].blank?
@shared_projects = @group.shared_projects
@shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
respond_to do |format|
format.html
......@@ -83,7 +81,7 @@ class GroupsController < Groups::ApplicationController
end
def update
if @group.update_attributes(group_params)
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
render action: "edit"
......@@ -98,26 +96,6 @@ class GroupsController < Groups::ApplicationController
protected
def group
@group ||= Group.find_by(path: params[:id])
@group || render_404
end
def load_projects
@projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
end
# Dont allow unauthorized access to group
def authorize_read_group!
unless @group and (@projects.present? or can?(current_user, :read_group, @group))
if current_user.nil?
return authenticate_user!
else
return render_404
end
end
end
def authorize_create_group!
unless can?(current_user, :create_group, nil)
return render_404
......@@ -135,7 +113,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock)
end
def load_events
......
......@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
if user
redirect_to user_path(user)
elsif group
elsif group && can?(current_user, :read_group, namespace)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
......
......@@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController
def update
user_params.except!(:email) if @user.ldap_user?
if @user.update_attributes(user_params)
flash[:notice] = "Profile was successfully updated"
else
messages = @user.errors.full_messages.uniq.join('. ')
flash[:alert] = "Failed to update profile. #{messages}"
end
respond_to do |format|
format.html { redirect_back_or_default(default: { action: 'show' }) }
if @user.update_attributes(user_params)
message = "Profile was successfully updated"
format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
format.json { render json: { message: message } }
else
message = @user.errors.full_messages.uniq.join('. ')
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
format.json { render json: { message: message }, status: :unprocessable_entity }
end
end
end
......
class Projects::ApplicationController < ApplicationController
skip_before_action :authenticate_user!
before_action :project
before_action :repository
layout 'project'
def authenticate_user!
# Restrict access to Projects area only
# for non-signed users
if !current_user
helper_method :repository, :can_collaborate_with_project?
private
def project
unless @project
namespace = params[:namespace_id]
id = params[:project_id] || params[:id]
project_with_namespace = "#{params[:namespace_id]}/#{id}"
@project = Project.find_with_namespace(project_with_namespace)
return if @project && @project.public?
# Redirect from
# localhost/group/project.git
# to
# localhost/group/project
#
if id =~ /\.git\Z/
redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
return
end
project_path = "#{namespace}/#{id}"
@project = Project.find_with_namespace(project_path)
if @project && can?(current_user, :read_project, @project)
if @project.path_with_namespace != project_path
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace)
end
else
@project = nil
if current_user.nil?
authenticate_user!
else
render_404
end
end
end
@project
end
def repository
@repository ||= project.repository
end
def can_collaborate_with_project?(project = nil)
project ||= @project
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
end
def authorize_project!(action)
return access_denied! unless can?(current_user, action, project)
end
def method_missing(method_sym, *arguments, &block)
if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
authorize_project!($1.to_sym)
else
super
end
end
super
def require_non_empty_project
redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo?
end
def require_branch_head
......@@ -26,8 +80,6 @@ class Projects::ApplicationController < ApplicationController
end
end
private
def apply_diff_view_cookie!
view = params[:view] || cookies[:diff_view]
cookies.permanent[:diff_view] = params[:view] = view if view
......
class Projects::AvatarsController < Projects::ApplicationController
include BlobHelper
before_action :project
before_action :authorize_admin_project!, only: [:destroy]
def show
@blob = @repository.blob_at_branch('master', @project.avatar_in_git)
......
class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
include IssuableActions
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show]
......@@ -133,6 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
......
class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper
include IssuableActions
before_action :module_enabled
before_action :merge_request, only: [
......@@ -255,6 +256,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
......
class Projects::UploadsController < Projects::ApplicationController
skip_before_action :authenticate_user!, :reject_blocked!, :project,
skip_before_action :reject_blocked!, :project,
:repository, if: -> { action_name == 'show' && image? }
before_action :authorize_upload_file!, only: [:create]
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
......@@ -26,6 +28,8 @@ class Projects::UploadsController < Projects::ApplicationController
send_file uploader.file.path, disposition: disposition
end
private
def uploader
return @uploader if defined?(@uploader)
......
class ProjectsController < ApplicationController
class ProjectsController < Projects::ApplicationController
include ExtractsPath
skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :authenticate_user!, except: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
......
......@@ -108,7 +108,7 @@ class UsersController < ApplicationController
end
def load_groups
@groups = @user.groups.order_id_desc
@groups = JoinedGroupsFinder.new(@user).execute(current_user)
end
def projects_for_current_user
......
class ContributedProjectsFinder
class ContributedProjectsFinder < UnionFinder
def initialize(user)
@user = user
end
......@@ -11,27 +11,19 @@ class ContributedProjectsFinder
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
segments = all_projects(current_user)
relation.includes(:namespace).order_id_desc
find_union(segments, Project).includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.contributed_projects.visible_to_user(current_user)
def all_projects(current_user)
projects = []
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_projects.select(:id)])
projects << @user.contributed_projects.visible_to_user(current_user) if current_user
projects << @user.contributed_projects.public_to_user(current_user)
Project.where("projects.id IN (#{union.to_sql})")
end
def public_projects
@user.contributed_projects.public_only
projects
end
end
class GroupProjectsFinder < UnionFinder
def initialize(group, options = {})
@group = group
@options = options
end
def execute(current_user = nil)
segments = group_projects(current_user)
find_union(segments, Project)
end
private
def group_projects(current_user)
only_owned = @options.fetch(:only_owned, false)
only_shared = @options.fetch(:only_shared, false)
projects = []
if current_user
if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
else
unless only_shared
projects << @group.projects.visible_to_user(current_user)
projects << @group.projects.public_to_user(current_user)
end
unless only_owned
projects << @group.shared_projects.visible_to_user(current_user)
projects << @group.shared_projects.public_to_user(current_user)
end
end
else
projects << @group.projects.public_only unless only_shared
projects << @group.shared_projects.public_only unless only_owned
end
projects
end
end
class GroupsFinder < UnionFinder
def execute(current_user = nil)
segments = all_groups(current_user)
find_union(segments, Group).order_id_desc
end
private
def all_groups(current_user)
groups = []
groups << current_user.authorized_groups if current_user
groups << Group.unscoped.public_to_user(current_user)
groups
end
end
......@@ -80,9 +80,10 @@ class IssuableFinder
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
@projects = current_user.authorized_projects.reorder(nil)
elsif group
@projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
else
@projects = ProjectsFinder.new.execute(current_user, group: group).
reorder(nil)
@projects = ProjectsFinder.new.execute(current_user).reorder(nil)
end
end
......@@ -171,14 +172,12 @@ class IssuableFinder
def by_scope(items)
case params[:scope]
when 'created-by-me', 'authored' then
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
when 'all' then
items
when 'assigned-to-me' then
when 'assigned-to-me'
items.where(assignee_id: current_user.id)
else
raise 'You must specify default scope'
items
end
end
......@@ -198,8 +197,7 @@ class IssuableFinder
end
def by_group(items)
items = items.of_group(group) if group
# Selection by group is already covered by `by_project` and `projects`
items
end
......
class JoinedGroupsFinder < UnionFinder
def initialize(user)
@user = user
end
# Finds the groups of the source user, optionally limited to those visible to
# the current user.
def execute(current_user = nil)
segments = all_groups(current_user)
find_union(segments, Group).order_id_desc
end
private
def all_groups(current_user)
groups = []
groups << @user.authorized_groups.visible_to_user(current_user) if current_user
groups << @user.authorized_groups.public_to_user(current_user)
groups
end
end
class PersonalProjectsFinder
class PersonalProjectsFinder < UnionFinder
def initialize(user)
@user = user
end
......@@ -11,31 +11,19 @@ class PersonalProjectsFinder
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
segments = all_projects(current_user)
relation.includes(:namespace).order_id_desc
find_union(segments, Project).includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.personal_projects.visible_to_user(current_user)
def all_projects(current_user)
projects = []
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_and_internal_projects.select(:id)])
projects << @user.personal_projects.visible_to_user(current_user) if current_user
projects << @user.personal_projects.public_to_user(current_user)
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
projects
end
end
class ProjectsFinder
# 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.
class ProjectsFinder < UnionFinder
def execute(current_user = nil, options = {})
group = options[:group]
segments = all_projects(current_user)
if group
segments = group_projects(current_user, group)
else
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
find_union(segments, Project)
end
private
def group_projects(current_user, group)
return [group.projects.public_only] unless current_user
user_group_projects = [
group_projects_for_user(current_user, group),
group.shared_projects.visible_to_user(current_user)
]
if current_user.external?
user_group_projects << group.projects.public_only
else
user_group_projects << group.projects.public_and_internal_only
end
end
def all_projects(current_user)
return [public_projects] unless current_user
projects = []
if current_user.external?
[current_user.authorized_projects, public_projects]
else
[current_user.authorized_projects, public_and_internal_projects]
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
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user)
def public_and_internal_projects
Project.unscoped.public_and_internal_only
projects
end
end
class UnionFinder
def find_union(segments, klass)
if segments.length > 1
union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
klass.where("#{klass.table_name}.id IN (#{union.to_sql})")
else
segments.first
end
end
end
......@@ -27,7 +27,7 @@ module BlobHelper
link_opts)
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
button_tag "Edit", class: "btn btn-default disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn'
elsif can?(current_user, :fork_project, project)
......@@ -50,9 +50,9 @@ module BlobHelper
return unless blob
if !on_top_of_branch?(project, ref)
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
......
......@@ -24,7 +24,7 @@ module ButtonHelper
def http_clone_button(project)
klass = 'http-selector'
klass << ' has_tooltip' if current_user.try(:require_password?)
klass << ' has-tooltip' if current_user.try(:require_password?)
protocol = gitlab_config.protocol.upcase
......@@ -41,7 +41,7 @@ module ButtonHelper
def ssh_clone_button(project)
klass = 'ssh-selector'
klass << ' has_tooltip' if current_user.try(:require_ssh_key?)
klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
content_tag :a, 'SSH',
class: klass,
......
......@@ -182,7 +182,7 @@ module CommitsHelper
end
options = {
class: "commit-#{options[:source]}-link has_tooltip",
class: "commit-#{options[:source]}-link has-tooltip",
data: { 'original-title'.to_sym => sanitize(source_email) }
}
......
......@@ -70,7 +70,8 @@ module DropdownsHelper
def dropdown_filter(placeholder)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search')
filter_output << icon('search', class: "dropdown-input-search")
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
filter_output.html_safe
end
......
......@@ -19,6 +19,10 @@ module GroupsHelper
end
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
def group_icon(group)
if group.is_a?(String)
group = Group.find_by(path: group)
......
......@@ -56,7 +56,7 @@ module LabelsHelper
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="label color-label #{"has_tooltip" if tooltip}" ) +
span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
%(style="background-color: #{label_color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
......@@ -114,7 +114,7 @@ module LabelsHelper
if @project
namespace_project_labels_path(@project.namespace, @project, :json)
else
labels_dashboard_path(:json)
dashboard_labels_path(:json)
end
end
......
......@@ -50,7 +50,7 @@ module MilestonesHelper
if @project
namespace_project_milestones_path(@project.namespace, @project, :json)
else
milestones_dashboard_path(:json)
dashboard_milestones_path(:json)
end
end
......
......@@ -52,7 +52,7 @@ module ProjectsHelper
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
end
end
......
......@@ -19,6 +19,8 @@ module VisibilityLevelHelper
case form_model
when Project
project_visibility_level_description(level)
when Group
group_visibility_level_description(level)
when Snippet
snippet_visibility_level_description(level, form_model)
end
......@@ -35,6 +37,17 @@ module VisibilityLevelHelper
end
end
def group_visibility_level_description(level)
case level
when Gitlab::VisibilityLevel::PRIVATE
"The group and its projects can only be viewed by members."
when Gitlab::VisibilityLevel::INTERNAL
"The group and any internal projects can be viewed by any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
"The group and any public projects can be viewed without any authentication."
end
end
def snippet_visibility_level_description(level, snippet = nil)
case level
when Gitlab::VisibilityLevel::PRIVATE
......@@ -50,6 +63,23 @@ module VisibilityLevelHelper
end
end
def visibility_icon_description(form_model)
case form_model
when Project
project_visibility_icon_description(form_model.visibility_level)
when Group
group_visibility_icon_description(form_model.visibility_level)
end
end
def group_visibility_icon_description(level)
"#{visibility_level_label(level)} - #{group_visibility_level_description(level)}"
end
def project_visibility_icon_description(level)
"#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
end
def visibility_level_label(level)
Project.visibility_levels.key(level)
end
......@@ -67,8 +97,11 @@ module VisibilityLevelHelper
current_application_settings.default_snippet_visibility
end
def default_group_visibility
current_application_settings.default_group_visibility
end
def skip_level?(form_model, level)
form_model.is_a?(Project) &&
!form_model.visibility_level_allowed?(level)
form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
end
end
......@@ -85,7 +85,7 @@ class Ability
subject.group
end
if group && group.projects.public_only.any?
if group && group.public?
[:read_group]
else
[]
......@@ -114,6 +114,13 @@ class Ability
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
if project.owner == user ||
(project.group && project.group.has_owner?(user)) ||
user.admin?
rules.push(*project_owner_rules)
end
if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
......@@ -121,14 +128,6 @@ class Ability
rules << :read_build if project.public_builds?
end
if project.owner == user || user.admin?
rules.push(*project_admin_rules)
end
if project.group && project.group.has_owner?(user)
rules.push(*project_admin_rules)
end
if project.archived?
rules -= project_archived_rules
end
......@@ -171,7 +170,8 @@ class Ability
:read_note,
:create_project,
:create_issue,
:create_note
:create_note,
:upload_file
]
end
......@@ -228,14 +228,16 @@ class Ability
]
end
def project_admin_rules
@project_admin_rules ||= project_master_rules + [
def project_owner_rules
@project_owner_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
:remove_project,
:archive_project,
:remove_fork_project
:remove_fork_project,
:destroy_merge_request,
:destroy_issue
]
end
......@@ -273,11 +275,9 @@ class Ability
def group_abilities(user, group)
rules = []
if user.admin? || group.users.include?(user) || ProjectsFinder.new.execute(user, group: group).any?
rules << :read_group
end
rules << :read_group if can_read_group?(user, group)
# Only group masters and group owners can create new projects in group
# Only group masters and group owners can create new projects
if group.has_master?(user) || group.has_owner?(user) || user.admin?
rules += [
:create_projects,
......@@ -290,13 +290,23 @@ class Ability
rules += [
:admin_group,
:admin_namespace,
:admin_group_member
:admin_group_member,
:change_visibility_level
]
end
rules.flatten
end
def can_read_group?(user, group)
return true if user.admin?
return true if group.public?
return true if group.internal? && !user.external?
return true if group.users.include?(user)
GroupProjectsFinder.new(group).execute(user).any?
end
def namespace_abilities(user, namespace)
rules = []
......
......@@ -18,6 +18,7 @@
# max_attachment_size :integer default(10), not null
# default_project_visibility :integer
# default_snippet_visibility :integer
# default_group_visibility :integer
# restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
......
......@@ -230,7 +230,7 @@ class Commit
end
def revert_message
%Q{Revert "#{title}"\n\n#{revert_description}}
%Q{Revert "#{title.strip}"\n\n#{revert_description}}
end
def reverts_commit?(commit)
......
......@@ -7,7 +7,10 @@ module InternalId
end
def set_iid
max_iid = project.send(self.class.name.tableize).maximum(:iid)
records = project.send(self.class.name.tableize)
records = records.with_deleted if self.paranoid?
max_iid = records.maximum(:iid)
self.iid = max_iid.to_i + 1
end
......
......@@ -41,7 +41,7 @@ module Issuable
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.merge(Project.non_archived) }
scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
delegate :name,
:email,
......@@ -58,6 +58,8 @@ module Issuable
attr_mentionable :description, cache: true
participant :author, :assignee, :notes_with_associations
strip_attributes :title
acts_as_paranoid
end
module ClassMethods
......
......@@ -2,15 +2,16 @@
#
# Table name: namespaces
#
# id :integer not null, primary key
# name :string(255) not null
# path :string(255) not null
# owner_id :integer
# created_at :datetime
# updated_at :datetime
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
# id :integer not null, primary key
# name :string(255) not null
# path :string(255) not null
# owner_id :integer
# visibility_level :integer default(20), not null
# created_at :datetime
# updated_at :datetime
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
#
require 'carrierwave/orm/activerecord'
......@@ -18,6 +19,7 @@ require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
......@@ -27,6 +29,8 @@ class Group < Namespace
has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
......@@ -74,6 +78,21 @@ class Group < Namespace
name
end
def visibility_level_field
visibility_level
end
def visibility_level_allowed_by_projects
allowed_by_projects = self.projects.where('visibility_level > ?', self.visibility_level).none?
unless allowed_by_projects
level_name = Gitlab::VisibilityLevel.level_name(visibility_level).downcase
self.errors.add(:visibility_level, "#{level_name} is not allowed since there are projects with higher visibility.")
end
allowed_by_projects
end
def avatar_url(size = nil)
if avatar.present?
[gitlab_config.url, avatar.url].join
......
......@@ -36,9 +36,6 @@ class Issue < ActiveRecord::Base
validates :project, presence: true
scope :of_group,
->(group) { where(project_id: group.projects.select(:id).reorder(nil)) }
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
......@@ -149,7 +146,8 @@ class Issue < ActiveRecord::Base
return false unless user.can?(:admin_issue, to_project)
end
!moved? && user.can?(:admin_issue, self.project)
!moved? && persisted? &&
user.can?(:admin_issue, self.project)
end
def to_branch_name
......
......@@ -97,12 +97,12 @@ class Label < ActiveRecord::Base
end
end
def open_issues_count
issues.opened.count
def open_issues_count(user = nil)
issues.visible_to_user(user).opened.count
end
def closed_issues_count
issues.closed.count
def closed_issues_count(user = nil)
issues.visible_to_user(user).closed.count
end
def open_merge_requests_count
......
......@@ -131,7 +131,6 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches
validate :validate_fork
scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) }
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
......
......@@ -83,7 +83,7 @@ class Milestone < ActiveRecord::Base
end
def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first
self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
end
def to_reference(from_project = nil)
......
......@@ -73,7 +73,7 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
# update visibility_levet of forks
# update visibility_level of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
......@@ -197,6 +197,8 @@ class Project < ActiveRecord::Base
validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validate :visibility_level_allowed_by_group
validate :visibility_level_allowed_as_fork
add_authentication_token_field :runners_token
before_save :ensure_runners_token
......@@ -215,8 +217,6 @@ class Project < ActiveRecord::Base
scope :in_group_namespace, -> { joins(:group) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
......@@ -246,10 +246,6 @@ class Project < ActiveRecord::Base
end
class << self
def public_and_internal_levels
[Project::PUBLIC, Project::INTERNAL]
end
def abandoned
where('projects.last_activity_at < ?', 6.months.ago)
end
......@@ -308,7 +304,7 @@ class Project < ActiveRecord::Base
end
def find_with_namespace(id)
namespace_path, project_path = id.split('/')
namespace_path, project_path = id.split('/', 2)
return nil if !namespace_path || !project_path
......@@ -435,6 +431,7 @@ class Project < ActiveRecord::Base
def safe_import_url
result = URI.parse(self.import_url)
result.password = '*****' unless result.password.nil?
result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user
result.to_s
rescue
self.import_url
......@@ -442,10 +439,25 @@ class Project < ActiveRecord::Base
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
self.errors.add(:limit_reached, "Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
end
rescue
errors[:base] << ("Can't check your ability to create project")
self.errors.add(:base, "Can't check your ability to create project")
end
def visibility_level_allowed_by_group
return if visibility_level_allowed_by_group?
level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
group_level_name = Gitlab::VisibilityLevel.level_name(self.group.visibility_level).downcase
self.errors.add(:visibility_level, "#{level_name} is not allowed in a #{group_level_name} group.")
end
def visibility_level_allowed_as_fork
return if visibility_level_allowed_as_fork?
level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.")
end
def to_param
......@@ -961,9 +973,25 @@ class Project < ActiveRecord::Base
issues.opened.count
end
def visibility_level_allowed?(level)
def visibility_level_allowed_as_fork?(level = self.visibility_level)
return true unless forked?
Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
# self.forked_from_project will be nil before the project is saved, so
# we need to go through the relation
original_project = forked_project_link.forked_from_project
return true unless original_project
level <= original_project.visibility_level
end
def visibility_level_allowed_by_group?(level = self.visibility_level)
return true unless group
level <= group.visibility_level
end
def visibility_level_allowed?(level = self.visibility_level)
visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
end
def runners_token
......
......@@ -43,12 +43,9 @@ class BaseService
def deny_visibility_level(model, denied_visibility_level = nil)
denied_visibility_level ||= model.visibility_level
level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level)
level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level).downcase
model.errors.add(
:visibility_level,
"#{level_name} visibility has been restricted by your GitLab administrator"
)
model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
end
private
......
module Ci
class CreateBuildsService
def execute(commit, stage, ref, tag, user, trigger_request, status)
builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
......
......@@ -6,8 +6,7 @@ class CreateSnippetService < BaseService
snippet = project.snippets.build(params)
end
unless Gitlab::VisibilityLevel.allowed_for?(current_user,
params[:visibility_level])
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(snippet)
return snippet
end
......
module Groups
class BaseService < ::BaseService
attr_accessor :group, :current_user, :params
def initialize(group, user, params = {})
@group, @current_user, @params = group, user, params.dup
end
end
end
module Groups
class CreateService < Groups::BaseService
def initialize(user, params = {})
@current_user, @params = user, params.dup
end
def execute
@group = Group.new(params)
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(@group)
return @group
end
@group.name ||= @group.path.dup
@group.save
@group.add_owner(current_user)
@group
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment