"
highlightRow: (index) ->
if @filterInput.val() isnt ""
@@ -351,6 +361,8 @@ class GitLabDropdown
# Toggle the dropdown label
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
+ else
+ selectedObject
else
if !value?
field.remove()
@@ -366,7 +378,7 @@ class GitLabDropdown
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value?
- if !field.length
+ if !field.length and fieldName
# Create hidden input for form
input = ""
if @options.inputId?
@@ -378,16 +390,81 @@ class GitLabDropdown
return selectedObject
- selectFirstRow: ->
- selector = '.dropdown-content li:first-child a'
+ selectRowAtIndex: (index) ->
+ selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
+
if @dropdown.find(".dropdown-toggle-page").length
- selector = ".dropdown-page-one .dropdown-content li:first-child a"
+ selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link
- $(selector).trigger "click"
+ $(selector, @dropdown).trigger "click"
+
+ addArrowKeyEvent: ->
+ ARROW_KEY_CODES = [38, 40]
+ $input = @dropdown.find(".dropdown-input-field")
+
+ selector = '.dropdown-content li:not(.divider)'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one #{selector}"
+
+ $('body').on 'keydown', (e) =>
+ currentKeyCode = e.which
+
+ if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ PREV_INDEX = currentIndex
+ $listItems = $(selector, @dropdown)
+
+ # if @options.filterable
+ # $input.blur()
+
+ if currentKeyCode is 40
+ # Move down
+ currentIndex += 1 if currentIndex < ($listItems.length - 1)
+ else if currentKeyCode is 38
+ # Move up
+ currentIndex -= 1 if currentIndex > 0
+
+ @highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX
+
+ return false
+
+ if currentKeyCode is 13
+ @selectRowAtIndex currentIndex
+
+ removeArrayKeyEvent: ->
+ $('body').off 'keydown'
+
+ highlightRowAtIndex: ($listItems, index) ->
+ # Remove the class for the previously focused row
+ $('.is-focused', @dropdown).removeClass 'is-focused'
+
+ # Update the class for the row at the specific index
+ $listItem = $listItems.eq(index)
+ $listItem.find('a:first-child').addClass "is-focused"
+
+ # Dropdown content scroll area
+ $dropdownContent = $listItem.closest('.dropdown-content')
+ dropdownScrollTop = $dropdownContent.scrollTop()
+ dropdownContentHeight = $dropdownContent.outerHeight()
+ dropdownContentTop = $dropdownContent.prop('offsetTop')
+ dropdownContentBottom = dropdownContentTop + dropdownContentHeight
+
+ # Get the offset bottom of the list item
+ listItemHeight = $listItem.outerHeight()
+ listItemTop = $listItem.prop('offsetTop')
+ listItemBottom = listItemTop + listItemHeight
+
+ if listItemBottom > dropdownContentBottom + dropdownScrollTop
+ # Scroll the dropdown content down
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom)
+ else if listItemTop < dropdownContentTop + dropdownScrollTop
+ # Scroll the dropdown content up
+ $dropdownContent.scrollTop(listItemTop - dropdownContentTop)
$.fn.glDropdown = (opts) ->
return @.each ->
if (!$.data @, 'glDropdown')
$.data(@, 'glDropdown', new GitLabDropdown @, opts)
-
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index d663e34871..946d83b7bd 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -6,25 +6,10 @@ class @Issue
constructor: ->
# Prevent duplicate event bindings
@disableTaskList()
- @fixAffixScroll()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
- fixAffixScroll: ->
- fixAffix = ->
- $discussion = $('.issuable-discussion')
- $sidebar = $('.issuable-sidebar')
- if $sidebar.hasClass('no-affix')
- $sidebar.removeClass(['affix-top','affix'])
- discussionHeight = $discussion.height()
- sidebarHeight = $sidebar.height()
- if sidebarHeight > discussionHeight
- $discussion.height(sidebarHeight + 50)
- $sidebar.addClass('no-affix')
- $(window).on('resize', fixAffix)
- fixAffix()
-
initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
@@ -49,7 +34,7 @@ class @Issue
issueStatus = if isClose then 'close' else 'open'
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
- if data.saved
+ if 'id' of data
$(document).trigger('issuable:change');
if isClose
$('a.btn-close').addClass('hidden')
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index b1479bfb44..0d9f2094c2 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -26,6 +26,20 @@
$(".selected_issue").bind "change", Issues.checkChanged
+ # Update state filters if present in page
+ updateStateFilters: ->
+ stateFilters = $('.issues-state-filters')
+ newParams = {}
+ paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
+
+ for paramKey in paramKeys
+ newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
+
+ if stateFilters.length
+ stateFilters.find('a').each ->
+ initialUrl = $(this).attr 'href'
+ $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
+
# Make sure we trigger ajax request only after user stop typing
initSearch: ->
@timer = null
@@ -54,6 +68,7 @@
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
+ Issues.updateStateFilters()
dataType: "json"
checkChanged: ->
diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee
new file mode 100644
index 0000000000..abd556e0b4
--- /dev/null
+++ b/app/assets/javascripts/lib/url_utility.js.coffee
@@ -0,0 +1,31 @@
+((w) ->
+
+ w.gl ?= {}
+ w.gl.utils ?= {}
+
+ w.gl.utils.getUrlParameter = (sParam) ->
+ sPageURL = decodeURIComponent(window.location.search.substring(1))
+ sURLVariables = sPageURL.split('&')
+ sParameterName = undefined
+ i = 0
+ while i < sURLVariables.length
+ sParameterName = sURLVariables[i].split('=')
+ if sParameterName[0] is sParam
+ return if sParameterName[1] is undefined then true else sParameterName[1]
+ i++
+
+ # #
+ # @param {Object} params - url keys and value to merge
+ # @param {String} url
+ # #
+ w.gl.utils.mergeUrlParams = (params, url) ->
+ newUrl = decodeURIComponent(url)
+ for paramName, paramValue of params
+ pattern = new RegExp "\\b(#{paramName}=).*?(&|$)"
+ if url.search(pattern) >= 0
+ newUrl = newUrl.replace pattern, "$1#{paramValue}$2"
+ else
+ newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
+ newUrl
+
+) window
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 6af5a48a0b..1f46e33142 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -15,8 +15,6 @@ class @MergeRequest
this.$('.show-all-commits').on 'click', =>
this.showAllCommits()
- @fixAffixScroll();
-
@initTabs()
# Prevent duplicate event bindings
@@ -30,20 +28,6 @@ class @MergeRequest
$: (selector) ->
this.$el.find(selector)
- fixAffixScroll: ->
- fixAffix = ->
- $discussion = $('.issuable-discussion')
- $sidebar = $('.issuable-sidebar')
- if $sidebar.hasClass('no-affix')
- $sidebar.removeClass(['affix-top','affix'])
- discussionHeight = $discussion.height()
- sidebarHeight = $sidebar.height()
- if sidebarHeight > discussionHeight
- $discussion.height(sidebarHeight + 50)
- $sidebar.addClass('no-affix')
- $(window).on('resize', fixAffix)
- fixAffix()
-
initTabs: ->
if @opts.action != 'new'
# `MergeRequests#new` has no tab-persisting or lazy-loading behavior
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
index 7102a0673e..84a8887fbc 100644
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ b/app/assets/javascripts/merge_request_widget.js.coffee
@@ -15,6 +15,8 @@ class @MergeRequestWidget
@pollCIStatus()
notifyPermissions()
+ setOpts: (@opts) ->
+
mergeInProgress: (deleteSourceBranch = false)->
$.ajax
type: 'GET'
@@ -48,7 +50,7 @@ class @MergeRequestWidget
@getCIStatus(true)
@readyForCICheck = false
- ), 5000
+ ), 10000
getCIStatus: (showNotification) ->
_this = @
@@ -61,6 +63,10 @@ class @MergeRequestWidget
@firstCICheck = false
@opts.ci_status = data.status
+ if @opts.ci_status is ''
+ @opts.ci_status = data.status
+ return
+
if data.status isnt @opts.ci_status
@showCIStatus data.status
if data.coverage
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index ff06c57f2b..86e3b860fc 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -251,13 +251,11 @@ class @Notes
Sets some hidden fields in the form.
###
setupMainTargetNoteForm: ->
-
# find the form
form = $(".js-new-note-form")
- # insert the form after the button
- form.clone().replaceAll $(".js-main-target-form")
- form = form.prev("form")
+ # Set a global clone of the form for later cloning
+ @formClone = form.clone()
# show the form
@setupNoteForm(form)
@@ -266,9 +264,7 @@ class @Notes
form.removeClass "js-new-note-form"
form.addClass "js-main-target-form"
- # remove unnecessary fields and buttons
form.find("#note_line_code").remove()
- form.find(".js-close-discussion-note-form").remove()
###
General note form setup.
@@ -297,7 +293,14 @@ class @Notes
else
previewButton.removeClass("turn-on").addClass "turn-off"
+ textarea.on 'focus', ->
+ $(this).closest('.md-area').addClass 'is-focused'
+
+ textarea.on 'blur', ->
+ $(this).closest('.md-area').removeClass 'is-focused'
+
autosize(textarea)
+
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
@@ -307,7 +310,6 @@ class @Notes
]
# remove notify commit author checkbox for non-commit notes
- form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit"
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
@@ -455,15 +457,15 @@ class @Notes
Shows the note form below the notes.
###
replyToDiscussionNote: (e) =>
- form = $(".js-new-note-form")
+ form = @formClone.clone()
replyLink = $(e.target).closest(".js-discussion-reply-button")
replyLink.hide()
# insert the form after the button
- form.clone().insertAfter replyLink
+ replyLink.after form
# show the form
- @setupDiscussionNoteForm(replyLink, replyLink.next("form"))
+ @setupDiscussionNoteForm(replyLink, form)
###
Shows the diff or discussion form and does some setup on it.
@@ -488,7 +490,9 @@ class @Notes
.text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form
form.find(".js-note-text").focus()
- form.addClass "js-discussion-note-form"
+ form
+ .removeClass('js-main-target-form')
+ .addClass("discussion-form js-discussion-note-form")
###
Called when clicking on the "add a comment" button on the side of a diff line.
@@ -498,9 +502,8 @@ class @Notes
###
addDiffNote: (e) =>
e.preventDefault()
- link = e.currentTarget
- form = $(".js-new-note-form")
- row = $(link).closest("tr")
+ $link = $(e.currentTarget)
+ row = $link.closest("tr")
nextRow = row.next()
hasNotes = nextRow.is(".notes_holder")
addForm = false
@@ -509,7 +512,7 @@ class @Notes
# In parallel view, look inside the correct left/right pane
if @isParallelView()
- lineType = $(link).data("lineType")
+ lineType = $link.data("lineType")
targetContent += "." + lineType
rowCssToAdd = "
"
@@ -531,11 +534,11 @@ class @Notes
addForm = true
if addForm
- newForm = form.clone()
+ newForm = @formClone.clone()
newForm.appendTo row.next().find(targetContent)
# show the form
- @setupDiscussionNoteForm $(link), newForm
+ @setupDiscussionNoteForm $link, newForm
###
Called in response to "cancel" on a diff note form.
@@ -560,7 +563,6 @@ class @Notes
cancelDiscussionForm: (e) =>
e.preventDefault()
- form = $(".js-new-note-form")
form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form)
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index 030655491b..6a7b4ad1db 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -62,6 +62,8 @@ class @SearchAutocomplete
search:
fields: ['text']
data: @getData.bind(@)
+ selectable: true
+ clicked: @onClick.bind(@)
getData: (term, callback) ->
_this = @
@@ -102,6 +104,8 @@ class @SearchAutocomplete
lastCategory = suggestion.category
data.push
+ id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
+ category: suggestion.category
text: suggestion.label
url: suggestion.url
@@ -133,12 +137,19 @@ class @SearchAutocomplete
}
bindEvents: ->
+ $(document).on 'click', @onDocumentClick
@searchInput.on 'keydown', @onSearchInputKeyDown
@searchInput.on 'keyup', @onSearchInputKeyUp
@searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus
- @searchInput.on 'blur', @onSearchInputBlur
- @clearInput.on 'click', @onRemoveLocationClick
+ @clearInput.on 'click', @onClearInputClick
+
+ onDocumentClick: (e) =>
+ # If clicking outside the search box
+ # And search input is not focused
+ # And we are not clicking inside a suggestion
+ if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
+ @onSearchInputBlur()
enableAutocomplete: ->
# No need to enable anything if user is not logged in
@@ -181,6 +192,8 @@ class @SearchAutocomplete
# We should display the menu only when input is not empty
@enableAutocomplete()
+ @wrap.toggleClass 'has-value', !!e.target.value
+
# Avoid falsy value to be returned
return
@@ -189,27 +202,20 @@ class @SearchAutocomplete
e.stopImmediatePropagation()
onSearchInputFocus: =>
+ @isFocused = true
@wrap.addClass('search-active')
- onRemoveLocationClick: (e) =>
+ onClearInputClick: (e) =>
e.preventDefault()
- @removeLocationBadge()
@searchInput.val('').focus()
- @skipBlurEvent = true
onSearchInputBlur: (e) =>
- @skipBlurEvent = false
+ @isFocused = false
+ @wrap.removeClass('search-active')
- # We should wait to make sure we are not clearing the input instead
- setTimeout( =>
- return if @skipBlurEvent
-
- @wrap.removeClass('search-active')
-
- # If input is blank then restore state
- if @searchInput.val() is ''
- @restoreOriginalState()
- , 150)
+ # If input is blank then restore state
+ if @searchInput.val() is ''
+ @restoreOriginalState()
addLocationBadge: (item) ->
category = if item.category? then "#{item.category}: " else ''
@@ -268,3 +274,23 @@ class @SearchAutocomplete
"
@dropdownContent.html(html)
+
+ onClick: (item, $el, e) ->
+ if location.pathname.indexOf(item.url) isnt -1
+ e.preventDefault()
+ if not @badgePresent
+ if item.category is 'Projects'
+ @projectInputEl.val(item.id)
+ @addLocationBadge(
+ value: 'This project'
+ )
+
+ if item.category is 'Groups'
+ @groupInputEl.val(item.id)
+ @addLocationBadge(
+ value: 'This group'
+ )
+
+ $el.removeClass('is-active')
+ @disableAutocomplete()
+ @searchInput.val('').focus()
diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee
index e1c5446eaa..99f35ecfb0 100644
--- a/app/assets/javascripts/zen_mode.js.coffee
+++ b/app/assets/javascripts/zen_mode.js.coffee
@@ -42,7 +42,7 @@ class @ZenMode
$(e.currentTarget).trigger('zen_mode:leave')
$(document).on 'zen_mode:enter', (e) =>
- @enter(e.target.parentNode)
+ @enter($(e.target).closest('.md-area').find('.zen-backdrop'))
$(document).on 'zen_mode:leave', (e) =>
@exit()
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index db1a8b1bf7..2ade341c9d 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -125,13 +125,6 @@ p.time {
height: 150px;
}
-// Fixes alignment on notes.
-.new_note {
- label {
- text-align: left;
- }
-}
-
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
img { max-width: 100% }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index a26ace5cc1..b15f4e7bd5 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -20,6 +20,7 @@
margin: 0;
text-align: left;
padding: 10px $gl-padding;
+ word-wrap: break-word;
.file-actions {
float: right;
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 8328aac4e7..c8f86d60e3 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -1,9 +1,7 @@
.div-dropzone-wrapper {
.div-dropzone {
position: relative;
- padding: 0;
- border: 0;
- margin-bottom: 5px;
+ margin-bottom: -5px;
.div-dropzone-focus {
border-color: #66afe9 !important;
@@ -25,12 +23,10 @@
.div-dropzone-spinner {
position: absolute;
- top: 100%;
- left: 100%;
- margin-top: -1.1em;
- margin-left: -1.1em;
+ bottom: 10px;
+ right: 5px;
opacity: 0;
- font-size: 30px;
+ font-size: 20px;
transition: opacity 200ms ease-in-out;
}
@@ -65,17 +61,30 @@
position: relative;
}
+.md-header {
+ .nav-links {
+ .active {
+ a {
+ border-bottom-color: #000;
+ }
+ }
+
+ a {
+ padding-top: 0;
+ line-height: 1;
+ }
+ }
+}
+
.referenced-users {
color: #4c4e54;
padding-top: 10px;
}
.md-preview-holder {
- background: #fff;
- border: 1px solid #ddd;
- min-height: 169px;
- padding: 5px;
- box-shadow: none;
+ min-height: 167px;
+ padding: 10px 0;
+ overflow-x: auto;
}
.markdown-area {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index c3c7bc9fdb..7b2aada5a0 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -250,7 +250,7 @@ a > code {
* Textareas intended for GFM
*
*/
-textarea.js-gfm-input {
+.js-gfm-input {
font-family: $monospace_font;
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 98fe794d36..8d3ad934a5 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #e75e40;
$orange-dark: #ce5237;
-$red-light: #f06559;
-$red-normal: #e52c5a;
-$red-dark: #d22852;
+$red-light: #e52c5a;
+$red-normal: #d22852;
+$red-dark: darken($red-normal, 5%);
$border-white-light: #f1f2f4;
$border-white-normal: #d6dae2;
@@ -128,9 +128,9 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #ce5237;
$border-orange-dark: #c14e35;
-$border-red-light: #f24f41;
-$border-red-normal: #d22852;
-$border-red-dark: #ca264f;
+$border-red-light: #d22852;
+$border-red-normal: #ca264f;
+$border-red-dark: darken($border-red-normal, 5%);
$help-well-bg: #fafafa;
$help-well-border: #e5e5e5;
@@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
/*
* Search Box
*/
-$search-input-border-color: $dropdown-input-focus-border;
+$search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
-$search-input-width: $dropdown-width;
+$search-input-width: 244px;
$location-badge-color: #aaa;
$location-badge-bg: $gray-normal;
+$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed;
-$location-active-color: $gl-text-color;
-$location-active-bg: $search-input-border-color;
+$location-icon-active-color: #807e7e;
/*
* Notes
@@ -217,3 +217,9 @@ $notes-light-color: #8e8e8e;
$notes-action-color: #c3c3c3;
$notes-role-color: #8e8e8e;
$notes-role-border-color: #e4e4e4;
+
+$note-disabled-comment-color: #b2b2b2;
+$note-form-border-color: #e5e5e5;
+$note-toolbar-color: #959494;
+
+$zen-control-hover-color: #111;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 02e24ec7c4..f870ea0d87 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -1,61 +1,62 @@
-.zennable {
- a.js-zen-enter {
- color: $gl-gray;
- position: absolute;
+.zen-backdrop {
+ &.fullscreen {
+ background-color: white;
+ position: fixed;
top: 0;
- right: 4px;
- line-height: 56px;
- }
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1031;
- a.js-zen-leave {
- display: none;
- color: $gl-text-color;
- position: absolute;
- top: 10px;
- right: 10px;
- padding: 5px;
- font-size: 36px;
-
- &:hover {
- color: #111;
+ textarea {
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+ color: #000;
+ font-size: 20px;
+ line-height: 26px;
+ padding: 30px;
+ display: block;
+ outline: none;
+ resize: none;
+ height: 100vh;
+ max-width: 900px;
+ margin: 0 auto;
}
- }
- .zen-backdrop {
- &.fullscreen {
- background-color: white;
- position: fixed;
+ .zen-control-leave {
+ display: block;
+ position: absolute;
top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 1031;
-
- textarea {
- border: none;
- box-shadow: none;
- border-radius: 0;
- color: #000;
- font-size: 20px;
- line-height: 26px;
- padding: 30px;
- display: block;
- outline: none;
- resize: none;
- height: 100vh;
- max-width: 900px;
- margin: 0 auto;
- }
-
- a.js-zen-enter {
- display: none;
- }
-
- a.js-zen-leave {
- display: block;
- position: absolute;
- top: 0;
- }
}
}
}
+
+.zen-cotrol {
+ padding: 0;
+ color: #555;
+ background: none;
+ border: 0;
+}
+
+.zen-control-full {
+ color: $note-toolbar-color;
+
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+}
+
+.zen-control-leave {
+ display: none;
+ color: $gl-text-color;
+ position: absolute;
+ right: 10px;
+ padding: 5px;
+ font-size: 36px;
+
+ &:hover {
+ color: $zen-control-hover-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index d3eda1a57e..5917f08972 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -33,8 +33,12 @@
.description {
margin-top: 6px;
- p:last-child {
- margin-bottom: 0;
+ p {
+ overflow-x: auto;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index f1368d74b3..7a12aa9647 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -59,10 +59,15 @@
border-collapse: separate;
margin: 0;
padding: 0;
+
.line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
}
+
+ td {
+ white-space: nowrap;
+ }
}
tr.line_holder.parallel {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7ff63ca20b..1c6a420897 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -195,42 +195,6 @@
line-height: 31px;
}
-.disabled-comment-area {
- padding: 16px 0;
-
- .disabled-profile {
- width: 40px;
- height: 40px;
- background: $border-gray-dark;
- border-radius: 20px;
- display: inline-block;
- margin-right: 10px;
- }
-
- .disabled-comment {
- background: $gray-light;
- display: inline-block;
- vertical-align: top;
- height: 200px;
- border-radius: 4px;
- border: 1px solid $border-gray-normal;
- padding-top: 90px;
- text-align: center;
- right: 20px;
- position: absolute;
- left: 70px;
- margin-bottom: 20px;
-
- span {
- color: #b2b2b2;
-
- a {
- color: $md-link-color;
- }
- }
- }
-}
-
.builds {
.table-holder {
overflow-x: scroll;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 655f88b0c2..a909776b43 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -1,10 +1,6 @@
/**
* Note Form
*/
-
-.comment-btn {
- @extend .btn-create;
-}
.reply-btn {
@extend .btn-primary;
margin: 10px $gl-padding;
@@ -17,16 +13,17 @@
}
.diff-file,
.discussion {
- .new_note {
+ .new-note {
margin: 0;
border: none;
}
}
-.new_note {
+
+.new-note {
display: none;
}
-.new_note, .note-edit-form {
+.new-note, .note-edit-form {
.note-form-actions {
margin-top: $gl-padding;
}
@@ -40,19 +37,16 @@
img {
max-width: 100%;
}
-
- .note_text {
- width: 100%;
- }
-
- .comment-hints {
- margin-top: -12px;
- }
}
-/* loading indicator */
-.notes-busy {
- margin: 18px;
+.note-textarea {
+ padding: 10px 0;
+ font-family: $regular_font;
+ border: 0;
+
+ &:focus {
+ outline: 0;
+ }
}
.note-image-attach {
@@ -62,38 +56,29 @@
}
.common-note-form {
- margin: 0;
- background: #fff;
- padding: $gl-padding;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- margin-bottom: -$gl-padding;
-}
+ .md-area {
+ padding: $gl-padding-top $gl-padding;
+ border: 1px solid $note-form-border-color;
+ border-radius: $border-radius-base;
-.note-form-actions {
- .note-form-option {
- margin-top: 8px;
- margin-left: 30px;
- @extend .pull-left;
- }
+ &.is-focused {
+ border-color: $focus-border-color;
+ box-shadow: 0 0 2px rgba(#000, .2),
+ 0 0 4px rgba($focus-border-color, .4);
- .js-notify-commit-author {
- float: left;
- }
-
- .write-preview-btn {
- // makes the "absolute" position for links relative to this
- position: relative;
-
- // preview/edit buttons
- > a {
- position: absolute;
- right: 5px;
- top: 8px;
+ .comment-toolbar,
+ .nav-links {
+ border-color: $focus-border-color;
+ }
}
}
}
+.discussion-form {
+ padding: $gl-padding-top $gl-padding;
+ background-color: #fff;
+}
+
.note-edit-form {
display: none;
font-size: 15px;
@@ -152,11 +137,49 @@
}
}
-.comment-hints {
- color: #999;
- background: #fff;
- padding: 7px;
- margin-top: -7px;
- border: 1px solid $border-color;
- font-size: 13px;
+.comment-toolbar {
+ padding-top: $gl-padding-top;
+ color: $note-toolbar-color;
+ border-top: 1px solid $border-color;
+}
+
+.toolbar-button {
+ padding: 0;
+ background: none;
+ border: 0;
+ font-size: 14px;
+ line-height: 16px;
+
+ &:hover,
+ &:focus {
+ color: $gl-link-color;
+ outline: 0;
+ }
+
+ @media (min-width: $screen-md-min) {
+ float: left;
+ margin-right: $gl-padding;
+
+ &:last-child {
+ float: right;
+ margin-right: 0;
+ }
+ }
+}
+
+.toolbar-button-icon {
+ position: relative;
+ top: 1px;
+ margin-right: 3px;
+ color: inherit;
+ font-size: 16px;
+}
+
+.toolbar-text {
+ font-size: 14px;
+ line-height: 16px;
+
+ @media (min-width: $screen-md-min) {
+ float: left;
+ }
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 92fcaaeeac..aca86457c7 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -20,6 +20,12 @@ ul.notes {
.timeline-content {
margin-left: 55px;
+
+ &.timeline-content-form {
+ @media (max-width: $screen-sm-max) {
+ margin-left: 0;
+ }
+ }
}
.note-created-ago, .note-updated-at {
@@ -76,7 +82,7 @@ ul.notes {
// On diffs code should wrap nicely and not overflow
pre {
code {
- white-space: pre-wrap;
+ white-space: pre;
}
}
@@ -149,7 +155,7 @@ ul.notes {
&.notes_content {
background-color: #fff;
border-width: 1px 0;
- padding-top: 0;
+ padding: 0;
vertical-align: top;
&.parallel {
border-width: 1px;
@@ -281,3 +287,21 @@ ul.notes {
}
}
}
+
+.disabled-comment {
+ margin-left: -$gl-padding-top;
+ margin-right: -$gl-padding-top;
+ background-color: $gray-light;
+ border-radius: $border-radius-base;
+ border: 1px solid $border-gray-normal;
+ color: $note-disabled-comment-color;
+ line-height: 200px;
+
+ .disabled-comment-text {
+ line-height: normal;
+ }
+
+ a {
+ color: $gl-link-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4e6aa8cd1a..fcca9d4faf 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -315,7 +315,7 @@ pre.light-well {
}
.git-empty {
- margin: 0 7px;
+ margin: 0 7px 7px;
h5 {
color: #5c5d5e;
@@ -401,7 +401,7 @@ pre.light-well {
}
.commit_short_id {
- margin-right: 5px;
+ margin: 0 5px;
color: $gl-link-color;
font-weight: 600;
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 3c74d25beb..f0f3744c6f 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -135,25 +135,25 @@
.location-badge {
@include transition(all .15s);
- background-color: $location-active-bg;
+ background-color: $location-badge-active-bg;
color: $white-light;
}
.search-input-wrap {
i {
- color: $location-active-color;
+ color: $location-icon-active-color;
}
}
+ }
- &.has-location-badge {
- .search-icon {
- display: none;
- }
+ &.has-value {
+ .search-icon {
+ display: none;
+ }
- .clear-icon {
- cursor: pointer;
- display: block;
- }
+ .clear-icon {
+ cursor: pointer;
+ display: block;
}
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 5e5e38a0ba..dbb6daf0d7 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,4 +1,4 @@
-.container-fluid .content {
+.container-fluid {
.ci-status {
padding: 2px 7px;
margin-right: 5px;
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 4089091d56..c6b3105544 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -5,7 +5,7 @@ class Admin::ProjectsController < Admin::ApplicationController
def index
@projects = Project.all
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
- @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
+ @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
@projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.non_archived unless params[:with_archived].present?
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 21135f7d60..d28e96c3f1 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -55,7 +55,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
else
saml_user = Gitlab::Saml::User.new(oauth)
- saml_user.save
+ saml_user.save if saml_user.changed?
@user = saml_user.gl_user
continue_login_process
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 657ee94cfd..74150ad606 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -68,7 +68,9 @@ class Projects::ApplicationController < ApplicationController
end
def require_non_empty_project
- redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo?
+ # Be sure to return status code 303 to avoid a double DELETE:
+ # http://api.rubyonrails.org/classes/ActionController/Redirecting.html
+ redirect_to namespace_project_path(@project.namespace, @project), status: 303 if @project.empty_repo?
end
def require_branch_head
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 6d4d436098..824aa41db5 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -1,5 +1,12 @@
class Projects::BadgesController < Projects::ApplicationController
- before_action :no_cache_headers
+ layout 'project_settings'
+ before_action :authorize_admin_project!, only: [:index]
+ before_action :no_cache_headers, except: [:index]
+
+ def index
+ @ref = params[:ref] || @project.default_branch || 'master'
+ @build_badge = Gitlab::Badge::Build.new(@project, @ref)
+ end
def build
badge = Gitlab::Badge::Build.new(project, params[:ref])
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index c0a5373492..d09e7375b6 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -48,7 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to namespace_project_branches_path(@project.namespace,
- @project)
+ @project), status: 303
end
format.js { render status: status[:return_code] }
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index e7bddc4a6f..e457db2f0b 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -94,9 +94,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def apply_import
- giver = Project.find(params[:source_project_id])
- status = @project.team.import(giver, current_user)
- notice = status ? "Successfully imported" : "Import failed"
+ source_project = Project.find(params[:source_project_id])
+
+ if can?(current_user, :read_project_member, source_project)
+ status = @project.team.import(source_project, current_user)
+ notice = status ? "Successfully imported" : "Import failed"
+ else
+ return render_404
+ end
redirect_to(namespace_project_project_members_path(project.namespace, project),
notice: notice)
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 00df1c9c96..d79f16e6a5 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -24,6 +24,8 @@ class Projects::RefsController < Projects::ApplicationController
namespace_project_find_file_path(@project.namespace, @project, @id)
when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id)
+ when "badges"
+ namespace_project_badges_path(@project.namespace, @project, ref: @id)
else
namespace_project_commits_path(@project.namespace, @project, @id)
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 02ceb8f433..9f3a4a6972 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -88,6 +88,20 @@ class Projects::WikisController < Projects::ApplicationController
)
end
+ def markdown_preview
+ text = params[:text]
+
+ ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
+ ext.analyze(text)
+
+ render json: {
+ body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
+ references: {
+ users: ext.users.map(&:username)
+ }
+ }
+ end
+
def git_access
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8c3a74c823..3cc37e5985 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController
def update
status = ::Projects::UpdateService.new(@project, current_user, project_params).execute
+ # Refresh the repo in case anything changed
+ @repository = project.repository
+
respond_to do |format|
if status
flash[:notice] = "Project '#{@project.name}' was successfully updated."
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index f6313255cb..f9f0483876 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -50,12 +50,15 @@ class BuildsEmailService < Service
def execute(push_data)
return unless supported_events.include?(push_data[:object_kind])
+ return unless should_build_be_notified?(push_data)
- if should_build_be_notified?(push_data)
+ recipients = all_recipients(push_data)
+
+ if recipients.any?
BuildEmailWorker.perform_async(
push_data[:build_id],
- all_recipients(push_data),
- push_data,
+ recipients,
+ push_data
)
end
end
@@ -84,7 +87,7 @@ class BuildsEmailService < Service
end
def all_recipients(data)
- all_recipients = recipients.split(',')
+ all_recipients = recipients.split(',').compact.reject(&:blank?)
if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}"
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 36c9ee92da..dc74c02760 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -55,15 +55,15 @@ class GitPushService < BaseService
end
def update_main_language
+ # Performance can be bad so for now only check main_language once
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937
+ return if @project.main_language.present?
+
return unless is_default_branch?
return unless push_to_new_branch? || push_to_existing_branch?
current_language = @project.repository.main_language
-
- unless current_language == @project.main_language
- return @project.update_attributes(main_language: current_language)
- end
-
+ @project.update_attributes(main_language: current_language)
true
end
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 9d4ab9847a..6b208c3d0b 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,6 +1,6 @@
-- if controller.controller_path =~ /^groups/
+- if controller.controller_path =~ /^groups/ && @group.persisted?
- label = 'This group'
-- if controller.controller_path =~ /^projects/
+- if controller.controller_path =~ /^projects/ && @project.persisted?
- label = 'This project'
.search.search-form{class: "#{'has-location-badge' if label.present?}"}
@@ -21,8 +21,8 @@
%a.is-focused.dropdown-menu-empty-link
Loading...
= dropdown_loading
- %i.search-icon
- %i.clear-icon.js-clear-input
+ %i.search-icon
+ %i.clear-icon.js-clear-input
= hidden_field_tag :group_id, @group.try(:id)
= hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index dc3050f02e..d429a92846 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -51,8 +51,13 @@
= icon('code fw')
%span
Variables
- = nav_link path: 'triggers#index' do
+ = nav_link(controller: :triggers) do
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
= icon('retweet fw')
%span
Triggers
+ = nav_link(controller: :badges) do
+ = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
+ = icon('star-half-empty fw')
+ %span
+ Badges
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index ab527e8e43..a7ef31acd3 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -5,10 +5,14 @@
- content_for :scripts_body_top do
- project = @target_project || @project
+ - if @project_wiki
+ - markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project)
+ - else
+ - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
- if current_user
:javascript
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
- window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}";
+ window.markdown_preview_path = "#{markdown_preview_path}";
- content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 1fb37ef662..4920910fee 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,18 +1,19 @@
.md-area
- .md-header.clearfix
+ .md-header
%ul.nav-links
%li.active
- %a.js-md-write-button(href="#md-write-holder" tabindex="-1")
+ %a.js-md-write-button{ href: "#md-write-holder" }
Write
%li
- %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1")
+ %a.js-md-preview-button{ href: "#md-preview-holder" }
Preview
+ %li.pull-right
+ %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' }
+ Go full screen
- %div
- .md-write-holder
- = yield
- .md.md-preview-holder.hide
- .js-md-preview{class: (preview_class if defined?(preview_class))}
+ .md-write-holder
+ = yield
+ .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))}
- if defined?(referenced_users) && referenced_users
%div.referenced-users.hide
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index e701253d7d..bddff5cdcb 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,12 +1,8 @@
-.zennable
- .zen-backdrop
- - classes << ' js-gfm-input js-autosize markdown-area'
- - if defined?(f) && f
- = f.text_area attr, class: classes
- - else
- = text_area_tag attr, nil, class: classes
- %a.js-zen-enter(tabindex="-1" href="#")
- = icon('expand')
- Edit in fullscreen
- %a.js-zen-leave(tabindex="-1" href="#")
- = icon('compress')
+.zen-backdrop
+ - classes << ' js-gfm-input js-autosize markdown-area'
+ - if defined?(f) && f
+ = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..."
+ - else
+ = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..."
+ %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
+ = icon('compress')
diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml
new file mode 100644
index 0000000000..c22384ddf4
--- /dev/null
+++ b/app/views/projects/badges/index.html.haml
@@ -0,0 +1,24 @@
+- page_title 'Badges'
+- badges_path = namespace_project_badges_path(@project.namespace, @project)
+- header_title project_title(@project, 'Badges', badges_path)
+
+.prepend-top-10
+ .panel.panel-default
+ .panel-heading
+ %b Builds badge ·
+ = @build_badge.to_html
+ .pull-right
+ = render 'shared/ref_switcher', destination: 'badges'
+ .panel-body
+ .row
+ .col-md-2.text-center
+ Markdown
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.md', @build_badge.to_markdown)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ HTML
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.html', @build_badge.to_html)
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 2be06aebe6..92d9535893 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -22,4 +22,6 @@
if(typeof merge_request_widget === 'undefined') {
merge_request_widget = new MergeRequestWidget(opts);
+ } else {
+ merge_request_widget.setOpts(opts);
}
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 2999befffc..23e4f93eab 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,10 +1,11 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f|
+ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field'
= render 'projects/notes/hints'
.note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
- = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel'
+ %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
+ Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index f675f092da..c446ecec2c 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= 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 js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
+= 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 js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text'
= render 'projects/notes/hints'
.error-alert
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 6e7929bdab..0b00204340 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -1,9 +1,8 @@
-.comment-hints.clearfix
- .pull-left
+.comment-toolbar.clearfix
+ .toolbar-text
+ Styling with
= link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1
- tip:
- = random_markdown_tip
- .pull-right
- = link_to '#', class: 'markdown-selector', tabindex: -1 do
- = icon('paperclip')
- Attach a file
+ is supported
+ %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ Attach a file
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 34fe1743f4..a681d6dece 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -18,7 +18,7 @@
= access
= link_to '#', title: 'Edit comment', class: 'note-action-button 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: 'note-action-button js-note-delete danger' do
+ = 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: 'note-action-button js-note-delete' do
= icon('trash-o')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 910eb6cf66..cc42aab5c5 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -1,20 +1,21 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
-.js-notes-busy
-
-.js-main-target-form
-- if can? current_user, :create_note, @project
- = render "projects/notes/form", view: diff_view
-- else
- .disabled-comment-area
- .disabled-profile
- .disabled-comment
- %span
- Please
- = link_to "register",new_user_session_path
- or
- = link_to "login",new_user_session_path
- to post a comment
+%ul.notes.timeline
+ %li.timeline-entry
+ - if can? current_user, :create_note, @project
+ .timeline-icon.hidden-xs.hidden-sm
+ %a.author_link{ href: user_path(current_user) }
+ = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
+ .timeline-content.timeline-content-form
+ = render "projects/notes/form", view: diff_view
+ - else
+ .disabled-comment.text-center
+ .disabled-comment-text.inline
+ Please
+ = link_to "register",new_user_session_path
+ or
+ = link_to "login",new_user_session_path
+ to post a comment
:javascript
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index fb1c3476f6..ddee467b14 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -345,6 +345,8 @@ production: &base
#
# - { name: 'saml',
# label: 'Our SAML Provider',
+ # groups_attribute: 'Groups',
+ # external_groups: ['Contractors', 'Freelancers'],
# args: {
# assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
# idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
@@ -352,6 +354,7 @@ production: &base
# issuer: 'https://gitlab.example.com',
# name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
# } }
+ #
# - { name: 'crowd',
# args: {
# crowd_server_url: 'CROWD SERVER URL',
diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb
index a44316bc3a..b9176688bc 100644
--- a/config/initializers/premailer.rb
+++ b/config/initializers/premailer.rb
@@ -3,5 +3,6 @@ Premailer::Rails.config.merge!(
generate_text_part: false,
preserve_styles: true,
remove_comments: true,
- remove_ids: true
+ remove_ids: true,
+ remove_scripts: false
)
diff --git a/config/routes.rb b/config/routes.rb
index 6bf22fb445..842fbb9984 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -575,6 +575,7 @@ Rails.application.routes.draw do
# Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
+ post '/wikis/markdown_preview', to:'wikis#markdown_preview'
post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
@@ -749,10 +750,11 @@ Rails.application.routes.draw do
end
resources :runner_projects, only: [:create, :destroy]
- resources :badges, only: [], path: 'badges/*ref',
- constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ resources :badges, only: [:index] do
collection do
- get :build, constraints: { format: /svg/ }
+ scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ get :build, constraints: { format: /svg/ }
+ end
end
end
end
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index a682872826..e4202025f8 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -7,8 +7,24 @@ Returns a list of project milestones.
```
GET /projects/:id/milestones
GET /projects/:id/milestones?iid=42
+GET /projects/:id/milestones?state=active
+GET /projects/:id/milestones?state=closed
```
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `iid` | integer | optional | Return only the milestone having the given `iid` |
+| `state` | string | optional | Return only `active` or `closed` milestones` |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
+```
+
+Example Response:
+
```json
[
{
@@ -25,10 +41,6 @@ GET /projects/:id/milestones?iid=42
]
```
-Parameters:
-
-- `id` (required) - The ID of a project
-- `iid` (optional) - Return the milestone having the given `iid`
## Get single milestone
diff --git a/doc/api/users.md b/doc/api/users.md
index 383e7c76ab..7d2b4897cf 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -69,6 +69,7 @@ GET /users
"state": "blocked",
"created_at": "2012-05-23T08:01:01Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -126,6 +127,7 @@ Parameters:
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -154,6 +156,7 @@ Parameters:
"confirmed_at": "2012-05-23T08:00:58Z",
"last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -191,6 +194,7 @@ Parameters:
- `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name
- `bio` (optional) - User's biography
+- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false
@@ -218,6 +222,7 @@ Parameters:
- `extern_uid` - External UID
- `provider` - External provider name
- `bio` - User's biography
+- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default)
@@ -260,6 +265,7 @@ GET /user
"state": "active",
"created_at": "2012-05-23T08:00:58Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4316f3c1f6..7da9b31e30 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -38,7 +38,7 @@ services:
- postgres
before_script:
- - bundle_install
+ - bundle install
stages:
- build
diff --git a/doc/development/README.md b/doc/development/README.md
index 1b281809af..8940b558fb 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -4,6 +4,7 @@
- [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md)
+- [Instrumentation](instrumentation.md)
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
new file mode 100644
index 0000000000..c0192bd670
--- /dev/null
+++ b/doc/development/instrumentation.md
@@ -0,0 +1,37 @@
+# Instrumenting Ruby Code
+
+GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby
+code. This can be used to measure the time spent in a specific part of a larger
+chunk of code. The resulting data is written to a separate series.
+
+To start measuring a block of Ruby code you should use
+`Gitlab::Metrics.measure` and give it a name for the series to store the data
+in:
+
+```ruby
+Gitlab::Metrics.measure(:user_logins) do
+ ...
+end
+```
+
+The first argument of this method is the series name and should be plural. This
+name will be prefixed with `rails_` or `sidekiq_` depending on whether the code
+was run in the Rails application or one of the Sidekiq workers. In the
+above example the final series names would be as follows:
+
+- rails_user_logins
+- sidekiq_user_logins
+
+Series names should be plural as this keeps the naming style in line with the
+other series names.
+
+By default metrics measured using a block contain a single value, "duration",
+which contains the number of milliseconds it took to execute the block. Custom
+values can be added by passing a Hash as the 2nd argument. Custom tags can be
+added by passing a Hash as the 3rd argument. A simple example is as follows:
+
+```ruby
+Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do
+ ...
+end
+```
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 1c3dc707f6..8a7205caaa 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -131,8 +131,75 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in.
+## External Groups
+
+>**Note:**
+This setting is only available on GitLab 8.7 and above.
+
+SAML login includes support for external groups. You can define in the SAML
+settings which groups, to which your users belong in your IdP, you wish to be
+marked as [external](../permissions/permissions.md).
+
+### Requirements
+
+First you need to tell GitLab where to look for group information. For this you
+need to make sure that your IdP server sends a specific `AttributeStament` along
+with the regular SAML response. Here is an example:
+
+```xml
+
+
+ SecurityGroup
+ Developers
+ Designers
+
+
+```
+
+The name of the attribute can be anything you like, but it must contain the groups
+to which a user belongs. In order to tell GitLab where to find these groups, you need
+to add a `groups_attribute:` element to your SAML settings. You will also need to
+tell GitLab which groups are external via the `external_groups:` element:
+
+```yaml
+{ name: 'saml',
+ label: 'Our SAML Provider',
+ groups_attribute: 'Groups',
+ external_groups: ['Freelancers', 'Interns'],
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ } }
+```
+
## Customization
+### `auto_sign_in_with_provider`
+
+You can add this setting to your GitLab configuration to automatically redirect you
+to your SAML server for authentication, thus removing the need to click a button
+before actually signing in.
+
+For omnibus package:
+
+```ruby
+gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml'
+```
+
+For installations from source:
+
+```yaml
+omniauth:
+ auto_sign_in_with_provider: saml
+```
+
+Please keep in mind that every sign in attempt will be redirected to the SAML server,
+so you will not be able to sign in using local credentials. Make sure that at least one
+of the SAML users has admin permissions.
+
### `attribute_statements`
>**Note:**
@@ -205,6 +272,10 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th
where it can then be seen in the usual logs, or as a flash message in the login
screen.
+That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
+for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for
+installations from source.
+
### Invalid audience
This error means that the IdP doesn't recognize GitLab as a valid sender and
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index e6eb1cf381..4f199b6af6 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -31,7 +31,7 @@
_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
-For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality.
+GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
You can use GFM in
@@ -47,10 +47,10 @@ You can also use other rich text files in GitLab. You might have to install a de
GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
-A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
+A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces
- Roses are red [followed by two or more spaces]
+ Roses are red [followed by two or more spaces]
Violets are blue
Sugar is sweet
@@ -67,7 +67,7 @@ It is not reasonable to italicize just _part_ of a word, especially when you're
perform_complicated_task
do_this_and_do_that_and_another_thing
-perform_complicated_task
+perform_complicated_task
do_this_and_do_that_and_another_thing
## URL auto-linking
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 3d375e47c8..6219693b8a 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -52,10 +52,11 @@ documentation](../workflow/add-user/add-user.md).
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
-| Force push to protected branches | | | | | |
-| Remove protected branches | | | | | |
+| Force push to protected branches [^2] | | | | | |
+| Remove protected branches [^2] | | | | | |
[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
## Group
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 906b66a4a6..32c3e99f45 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -125,7 +125,7 @@ module SharedDiffNote
step 'I should only see one diff form' do
page.within(diff_file_selector) do
- expect(page).to have_css("form.new_note", count: 1)
+ expect(page).to have_css("form.new-note", count: 1)
end
end
@@ -161,7 +161,7 @@ module SharedDiffNote
step 'I should see a temporary diff comment form' do
page.within(diff_file_selector) do
- expect(page).to have_css(".js-temp-notes-holder form.new_note")
+ expect(page).to have_css(".js-temp-notes-holder form.new-note")
end
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index fb0462d6e0..a3c3887ab4 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -2,7 +2,7 @@ module SharedNote
include Spinach::DSL
step 'I delete a comment' do
- page.within('.notes') do
+ page.within('.main-notes-list') do
find('.note').hover
find(".js-note-delete").click
end
@@ -128,7 +128,7 @@ module SharedNote
end
step 'I edit the last comment with a +1' do
- page.within(".notes") do
+ page.within(".main-notes-list") do
find(".note").hover
find('.js-note-edit').click
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 592100a704..231840148d 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -64,7 +64,7 @@ module API
authorize_admin_project
@branch = user_project.repository.find_branch(params[:branch])
- not_found!("Branch does not exist") unless @branch
+ not_found!("Branch") unless @branch
protected_branch = user_project.protected_branches.find_by(name: @branch.name)
protected_branch.destroy if protected_branch
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 340fc5452a..4c49442bf8 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -15,7 +15,7 @@ module API
class User < UserBasic
expose :created_at
expose :is_admin?, as: :is_admin
- expose :bio, :skype, :linkedin, :twitter, :website_url
+ expose :bio, :location, :skype, :linkedin, :twitter, :website_url
end
class Identity < Grape::Entity
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index c5cd73943f..afb6ffa360 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -3,17 +3,33 @@ module API
class Milestones < Grape::API
before { authenticate! }
+ helpers do
+ def filter_milestones_state(milestones, state)
+ case state
+ when 'active' then milestones.active
+ when 'closed' then milestones.closed
+ else milestones
+ end
+ end
+ end
+
resource :projects do
# Get a list of project milestones
#
# Parameters:
- # id (required) - The ID of a project
+ # id (required) - The ID of a project
+ # state (optional) - Return "active" or "closed" milestones
# Example Request:
# GET /projects/:id/milestones
+ # GET /projects/:id/milestones?state=active
+ # GET /projects/:id/milestones?state=closed
get ":id/milestones" do
authorize! :read_milestone, user_project
- present paginate(user_project.milestones), with: Entities::Milestone
+ milestones = user_project.milestones
+ milestones = filter_milestones_state(milestones, params[:state])
+
+ present paginate(milestones), with: Entities::Milestone
end
# Get a single project milestone
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 13ab17c690..0a14bac07c 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -58,6 +58,7 @@ module API
# extern_uid - External authentication provider UID
# provider - External provider
# bio - Bio
+ # location - Location of the user
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# confirm - Require user confirmation - true (default) or false
@@ -67,7 +68,7 @@ module API
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
user = User.build_user(attrs)
@@ -106,6 +107,7 @@ module API
# website_url - Website url
# projects_limit - Limit projects each user can create
# bio - Bio
+ # location - Location of the user
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# external - Flags the user as external - true or false(default)
@@ -114,7 +116,7 @@ module API
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
user = User.find(params[:id])
not_found!('User') unless user
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index f21dbef216..b8962379cb 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -119,7 +119,7 @@ module Banzai
elsif element_node?(node)
yield_valid_link(node) do |link, text|
- if ref_pattern && link =~ /\A#{ref_pattern}/
+ if ref_pattern && link =~ /\A#{ref_pattern}\z/
replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_text: text)
end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 7ce26db1b9..d08267a9d6 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -118,7 +118,7 @@ module Banzai
end
if path
- content_tag(:img, nil, src: path)
+ content_tag(:img, nil, src: path, class: 'gfm')
end
end
@@ -144,12 +144,18 @@ module Banzai
# if it is not.
def process_page_link_tag(parts)
if parts.size == 1
- url = parts[0].strip
+ reference = parts[0].strip
else
- name, url = *parts.compact.map(&:strip)
+ name, reference = *parts.compact.map(&:strip)
end
- content_tag(:a, name || url, href: url)
+ if url?(reference)
+ href = reference
+ else
+ href = ::File.join(project_wiki_base_path, reference)
+ end
+
+ content_tag(:a, name || reference, href: href, class: 'gfm')
end
def project_wiki
diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb
new file mode 100644
index 0000000000..06d10c9850
--- /dev/null
+++ b/lib/banzai/filter/wiki_link_filter.rb
@@ -0,0 +1,56 @@
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that "fixes" relative links to files in a repository.
+ #
+ # Context options:
+ # :project_wiki
+ class WikiLinkFilter < HTML::Pipeline::Filter
+
+ def call
+ return doc unless project_wiki?
+
+ doc.search('a:not(.gfm)').each do |el|
+ process_link_attr el.attribute('href')
+ end
+
+ doc
+ end
+
+ protected
+
+ def project_wiki?
+ !context[:project_wiki].nil?
+ end
+
+ def process_link_attr(html_attr)
+ return if html_attr.blank? || file_reference?(html_attr)
+
+ uri = URI(html_attr.value)
+ if uri.relative? && uri.path.present?
+ html_attr.value = rebuild_wiki_uri(uri).to_s
+ end
+ rescue URI::Error
+ # noop
+ end
+
+ def rebuild_wiki_uri(uri)
+ uri.path = ::File.join(project_wiki_base_path, uri.path)
+ uri
+ end
+
+ def file_reference?(html_attr)
+ !File.extname(html_attr.value).blank?
+ end
+
+ def project_wiki
+ context[:project_wiki]
+ end
+
+ def project_wiki_base_path
+ project_wiki && project_wiki.wiki_base_path
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
index 0b5a9e0b2b..c37b8e71cb 100644
--- a/lib/banzai/pipeline/wiki_pipeline.rb
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -2,8 +2,10 @@ module Banzai
module Pipeline
class WikiPipeline < FullPipeline
def self.filters
- @filters ||= super.insert_after(Filter::TableOfContentsFilter,
- Filter::GollumTagsFilter)
+ @filters ||= begin
+ super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
+ .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
+ end
end
end
end
diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb
index 28a2391dbf..e5e9fab3f5 100644
--- a/lib/gitlab/badge/build.rb
+++ b/lib/gitlab/badge/build.rb
@@ -4,12 +4,13 @@ module Gitlab
# Build badge
#
class Build
- def initialize(project, ref)
- @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
- end
+ include Gitlab::Application.routes.url_helpers
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::UrlHelper
- def to_s
- @image[:name].sub(/\.svg$/, '')
+ def initialize(project, ref)
+ @project, @ref = project, ref
+ @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
end
def type
@@ -19,6 +20,27 @@ module Gitlab
def data
File.read(@image[:path])
end
+
+ def to_s
+ @image[:name].sub(/\.svg$/, '')
+ end
+
+ def to_html
+ link_to(image_tag(image_url, alt: 'build status'), link_url)
+ end
+
+ def to_markdown
+ "[](#{link_url})"
+ end
+
+ def image_url
+ build_namespace_project_badges_url(@project.namespace,
+ @project, @ref, format: :svg)
+ end
+
+ def link_url
+ namespace_project_commits_url(@project.namespace, @project, id: @ref)
+ end
end
end
end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index da4435c730..f2b649e50a 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -33,7 +33,10 @@ module Gitlab
def allowed?
if ldap_user
- return true unless ldap_config.active_directory
+ unless ldap_config.active_directory
+ user.activate if user.ldap_blocked?
+ return true
+ end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 88a265c6af..4a3f47b5a9 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -70,6 +70,32 @@ module Gitlab
value.to_s.gsub('=', '\\=')
end
+ # Measures the execution time of a block.
+ #
+ # Example:
+ #
+ # Gitlab::Metrics.measure(:find_by_username_timings) do
+ # User.find_by_username(some_username)
+ # end
+ #
+ # series - The name of the series to store the data in.
+ # values - A Hash containing extra values to add to the metric.
+ # tags - A Hash containing extra tags to add to the metric.
+ #
+ # Returns the value yielded by the supplied block.
+ def self.measure(series, values = {}, tags = {})
+ return yield unless Transaction.current
+
+ start = Time.now.to_f
+ retval = yield
+ duration = (Time.now.to_f - start) * 1000.0
+ values = values.merge(duration: duration)
+
+ Transaction.current.add_metric(series, values, tags)
+
+ retval
+ end
+
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb
new file mode 100644
index 0000000000..32c1c9ec5b
--- /dev/null
+++ b/lib/gitlab/saml/auth_hash.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Saml
+ class AuthHash < Gitlab::OAuth::AuthHash
+
+ def groups
+ get_raw(Gitlab::Saml::Config.groups)
+ end
+
+ private
+
+ def get_raw(key)
+ # Needs to call `all` because of https://git.io/vVo4u
+ # otherwise just the first value is returned
+ auth_hash.extra[:raw_info].all[key]
+ end
+
+ end
+ end
+end
diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb
new file mode 100644
index 0000000000..0f40c00f54
--- /dev/null
+++ b/lib/gitlab/saml/config.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Saml
+ class Config
+
+ class << self
+ def options
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
+ end
+
+ def groups
+ options[:groups_attribute]
+ end
+
+ def external_groups
+ options[:external_groups]
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index b1e30110ef..c1072452ab 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -18,7 +18,7 @@ module Gitlab
@user ||= find_or_create_ldap_user
end
- if auto_link_saml_enabled?
+ if auto_link_saml_user?
@user ||= find_by_email
end
@@ -26,6 +26,16 @@ module Gitlab
@user ||= build_new_user
end
+ if external_users_enabled?
+ # Check if there is overlap between the user's groups and the external groups
+ # setting then set user as external or internal.
+ if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
+ @user.external = false
+ else
+ @user.external = true
+ end
+ end
+
@user
end
@@ -37,11 +47,23 @@ module Gitlab
end
end
+ def changed?
+ gl_user.changed? || gl_user.identities.any?(&:changed?)
+ end
+
protected
- def auto_link_saml_enabled?
+ def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user
end
+
+ def external_users_enabled?
+ !Gitlab::Saml::Config.external_groups.nil?
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash)
+ end
end
end
end
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
new file mode 100644
index 0000000000..2ba0d48919
--- /dev/null
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Admin::ProjectsController do
+ let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+
+ before do
+ sign_in(create(:admin))
+ end
+
+ describe 'GET /projects' do
+ render_views
+
+ it 'retrieves the project for the given visibility level' do
+ get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]
+ expect(response.body).to match(project.name)
+ end
+
+ it 'does not retrieve the project' do
+ get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]
+ expect(response.body).to_not match(project.name)
+ end
+ end
+end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 98ae424ed7..8ad7347211 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -93,6 +93,20 @@ describe Projects::BranchesController do
end
end
+ describe "POST destroy with HTML format" do
+ render_views
+
+ it 'returns 303' do
+ post :destroy,
+ format: :html,
+ id: 'foo/bar/baz',
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+
+ expect(response.status).to eq(303)
+ end
+ end
+
describe "POST destroy" do
render_views
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
new file mode 100644
index 0000000000..d47e4ab9a4
--- /dev/null
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -0,0 +1,49 @@
+require('spec_helper')
+
+describe Projects::ProjectMembersController do
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project, :private) }
+ let(:user) { create(:user) }
+ let(:member) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ another_project.team << [member, :guest]
+ sign_in(user)
+ end
+
+ describe '#apply_import' do
+ shared_context 'import applied' do
+ before do
+ post(:apply_import, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ source_project_id: another_project.id)
+ end
+ end
+
+ context 'when user can access source project members' do
+ before { another_project.team << [user, :guest] }
+ include_context 'import applied'
+
+ it 'imports source project members' do
+ expect(project.team_members).to include member
+ expect(response).to set_flash.to 'Successfully imported'
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ end
+ end
+
+ context 'when user is not member of a source project' do
+ include_context 'import applied'
+
+ it 'does not import team members' do
+ expect(project.team_members).to_not include member
+ end
+
+ it 'responds with not found' do
+ expect(response.status).to eq 404
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 1893e946f5..069cd917e5 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -83,6 +83,28 @@ describe ProjectsController do
end
end
+ describe "#update" do
+ render_views
+
+ let(:admin) { create(:admin) }
+
+ it "sets the repository to the right path after a rename" do
+ new_path = 'renamed_path'
+ project_params = { path: new_path }
+ controller.instance_variable_set(:@project, project)
+ sign_in(admin)
+
+ put :update,
+ namespace_id: project.namespace.to_param,
+ id: project.id,
+ project: project_params
+
+ expect(project.repository.path).to include(new_path)
+ expect(assigns(:repository).path).to eq(project.repository.path)
+ expect(response.status).to eq(200)
+ end
+ end
+
describe "#destroy" do
let(:admin) { create(:admin) }
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
new file mode 100644
index 0000000000..90822a8c12
--- /dev/null
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -0,0 +1,119 @@
+require 'rails_helper'
+
+describe 'Filter issues', feature: true do
+
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ describe 'Filter issues for assignee from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ sleep 2
+ end
+
+ context 'assignee', js: true do
+ it 'should update to current user' do
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+ end
+ end
+
+ describe 'Filter issues for milestone from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-milestone-select').click
+
+ find('.milestone-filter .dropdown-content a', text: milestone.title).click
+
+ sleep 2
+ end
+
+ context 'milestone', js: true do
+ it 'should update to current milestone' do
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+ end
+ end
+
+ describe 'Filter issues for assignee and label from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ sleep 2
+
+ find('.js-label-select').click
+
+ find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
+
+ sleep 2
+ end
+
+ context 'assignee and label', js: true do
+ it 'should update to current assignee and label' do
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index db46657c36..79000666cc 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -22,7 +22,7 @@ describe 'Issues', feature: true do
before do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
- click_link "Edit"
+ click_button "Go full screen"
end
it 'should open new issue popup' do
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 12fd8d3721..3d0d0e59fd 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -39,7 +39,7 @@ describe 'GitLab Markdown', feature: true do
end
def doc(html = @html)
- Nokogiri::HTML::DocumentFragment.parse(html)
+ @doc ||= Nokogiri::HTML::DocumentFragment.parse(html)
end
# Shared behavior that all pipelines should exhibit
@@ -230,6 +230,7 @@ describe 'GitLab Markdown', feature: true do
file = Gollum::File.new(@project_wiki.wiki)
expect(file).to receive(:path).and_return('images/example.jpg')
expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
+ allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
@html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index d9a8058efd..70d0864783 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -152,7 +152,7 @@ describe 'Comments', feature: true do
it 'has .new_note css class' do
page.within('.js-temp-notes-holder') do
- expect(subject).to have_css('.new_note')
+ expect(subject).to have_css('.new-note')
end
end
end
@@ -225,6 +225,6 @@ describe 'Comments', feature: true do
end
def click_diff_line(data = line_code)
- page.find(%Q{button[data-line-code="#{data}"]}, visible: false).click
+ execute_script("$('button[data-line-code=\"#{data}\"]').click()")
end
end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
new file mode 100644
index 0000000000..13c9b95b31
--- /dev/null
+++ b/spec/features/projects/badges/list_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'list of badges' do
+ include Select2Helper
+
+ background do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as(user)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user displays list of badges' do
+ click_link 'Badges'
+
+ expect(page).to have_content 'build status'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='build status']")
+
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/build.svg'
+ end
+ end
+
+ scenario 'user changes current ref on badges list page', js: true do
+ click_link 'Badges'
+ select2('improve/awesome', from: '#ref')
+
+ expect(page).to have_content 'badges/improve/awesome/build.svg'
+ end
+end
diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml
index 1701652c61..cb906a7fea 100644
--- a/spec/javascripts/fixtures/zen_mode.html.haml
+++ b/spec/javascripts/fixtures/zen_mode.html.haml
@@ -1,4 +1,4 @@
-.zennable
+.md-area
.zen-backdrop
%textarea#note_note.js-gfm-input.markdown-area
%a.js-zen-enter(tabindex="-1" href="#")
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index 86ba9dd8e9..ea27f36e9b 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -29,8 +29,8 @@ describe 'reopen/close issue', ->
spyOn(jQuery, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://gitlab.com/issues/6/close')
- req.success saved: true
-
+ req.success id: 34
+
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
expect($btnReopen).toBeHidden()
@@ -94,7 +94,7 @@ describe 'reopen/close issue', ->
spyOn(jQuery, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
- req.success saved: true
+ req.success id: 34
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index 5e23c5c319..fe2ce092e6 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -70,20 +70,22 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
end
context 'linking internal resources' do
- it "the created link's text will be equal to the resource's text" do
+ it "the created link's text includes the resource's text and wiki base path" do
tag = '[[wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
+ expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug')
expect(doc.at_css('a').text).to eq 'wiki-slug'
- expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ expect(doc.at_css('a')['href']).to eq expected_path
end
it "the created link's text will be link-text" do
tag = '[[link-text|wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
+ expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug')
expect(doc.at_css('a').text).to eq 'link-text'
- expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ expect(doc.at_css('a')['href']).to eq expected_path
end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 5a0d3d577a..266ebef33d 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
+
+ it 'does not process links containing issue numbers followed by text' do
+ href = "#{reference}st"
+ doc = reference_filter("")
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq(href)
+ end
end
context 'cross-project reference' do
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 3e25406e49..7aa1b4a3bf 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -11,7 +11,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo
MD
- result = described_class.call(markdown, project: spy, project_wiki: double)
+ result = described_class.call(markdown, project: spy, project_wiki: spy)
aggregate_failures do
expect(result[:output].text).not_to include '[['
@@ -29,7 +29,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo
MD
- output = described_class.to_html(markdown, project: spy, project_wiki: double)
+ output = described_class.to_html(markdown, project: spy, project_wiki: spy)
expect(output).to include('[[toc]]')
end
@@ -42,7 +42,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo
MD
- output = described_class.to_html(markdown, project: spy, project_wiki: double)
+ output = described_class.to_html(markdown, project: spy, project_wiki: spy)
aggregate_failures do
expect(output).not_to include('
')
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
index b78c2b6224..329792bb68 100644
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ b/spec/lib/gitlab/badge/build_spec.rb
@@ -3,13 +3,44 @@ require 'spec_helper'
describe Gitlab::Badge::Build do
let(:project) { create(:project) }
let(:sha) { project.commit.sha }
- let(:badge) { described_class.new(project, 'master') }
+ let(:branch) { 'master' }
+ let(:badge) { described_class.new(project, branch) }
describe '#type' do
subject { badge.type }
it { is_expected.to eq 'image/svg+xml' }
end
+ describe '#to_html' do
+ let(:html) { Nokogiri::HTML.parse(badge.to_html) }
+ let(:a_href) { html.at('a') }
+
+ it 'points to link' do
+ expect(a_href[:href]).to eq badge.link_url
+ end
+
+ it 'contains clickable image' do
+ expect(a_href.children.first.name).to eq 'img'
+ end
+ end
+
+ describe '#to_markdown' do
+ subject { badge.to_markdown }
+
+ it { is_expected.to include badge.image_url }
+ it { is_expected.to include badge.link_url }
+ end
+
+ describe '#image_url' do
+ subject { badge.image_url }
+ it { is_expected.to include "badges/#{branch}/build.svg" }
+ end
+
+ describe '#link_url' do
+ subject { badge.link_url }
+ it { is_expected.to include "commits/#{branch}" }
+ end
+
context 'build exists' do
let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
let!(:build) { create(:ci_build, commit: ci_commit) }
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index 32a19bf344..f5b66b8156 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
- it 'should block user in GitLab' do
+ it 'blocks user in GitLab' do
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
@@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do
end
it { is_expected.to be_truthy }
+
+ context 'when user cannot be found' do
+ before do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'blocks user in GitLab' do
+ access.allowed?
+ expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
+ end
+ end
+
+ context 'when user was previously ldap_blocked' do
+ before do
+ user.ldap_block
+ end
+
+ it 'unblocks the user if it exists' do
+ access.allowed?
+ expect(user).not_to be_blocked
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 0ec8a6dc5c..8f63a5f204 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Metrics do
end
end
- describe '#submit_metrics' do
+ describe '.submit_metrics' do
it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection)
pool = double(:pool)
@@ -26,7 +26,7 @@ describe Gitlab::Metrics do
end
end
- describe '#prepare_metrics' do
+ describe '.prepare_metrics' do
it 'returns a Hash with the keys as Symbols' do
metrics = described_class.
prepare_metrics([{ 'values' => {}, 'tags' => {} }])
@@ -51,7 +51,7 @@ describe Gitlab::Metrics do
end
end
- describe '#escape_value' do
+ describe '.escape_value' do
it 'escapes an equals sign' do
expect(described_class.escape_value('foo=')).to eq('foo\\=')
end
@@ -60,4 +60,45 @@ describe Gitlab::Metrics do
expect(described_class.escape_value(10)).to eq('10')
end
end
+
+ describe '.measure' do
+ context 'without a transaction' do
+ it 'returns the return value of the block' do
+ val = Gitlab::Metrics.measure(:foo) { 10 }
+
+ expect(val).to eq(10)
+ end
+ end
+
+ context 'with a transaction' do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+
+ before do
+ allow(Gitlab::Metrics::Transaction).to receive(:current).
+ and_return(transaction)
+ end
+
+ it 'adds a metric to the current transaction' do
+ expect(transaction).to receive(:add_metric).
+ with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' })
+
+ Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 }
+ end
+
+ it 'supports adding of custom values' do
+ values = { duration: a_kind_of(Numeric), number: 10 }
+
+ expect(transaction).to receive(:add_metric).
+ with(:foo, values, { tag: 'value' })
+
+ Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 }
+ end
+
+ it 'returns the return value of the block' do
+ val = Gitlab::Metrics.measure(:foo) { 10 }
+
+ expect(val).to eq(10)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index de7cd99d49..c2a51d9249 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Saml::User, lib: true do
let(:gl_user) { saml_user.gl_user }
let(:uid) { 'my-uid' }
let(:provider) { 'saml' }
- let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) }
let(:info_hash) do
{
name: 'John',
@@ -23,10 +23,20 @@ describe Gitlab::Saml::User, lib: true do
allow(Gitlab::LDAP::Config).to receive_messages(messages)
end
+ def stub_basic_saml_config
+ allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
+ end
+
+ def stub_saml_group_config(groups)
+ allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
+ end
+
+ before { stub_basic_saml_config }
+
describe 'account exists on server' do
before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
+ let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
context 'and should bind with SAML' do
- let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
it 'adds the SAML identity to the existing user' do
saml_user.save
expect(gl_user).to be_valid
@@ -36,6 +46,35 @@ describe Gitlab::Saml::User, lib: true do
expect(identity.provider).to eql 'saml'
end
end
+
+ context 'external groups' do
+ context 'are defined' do
+ it 'marks the user as external' do
+ stub_saml_group_config(%w(Freelancers))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ before { stub_saml_group_config(%w(Interns)) }
+ context 'are defined but the user does not belong there' do
+ it 'does not mark the user as external' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+
+ context 'user was external, now should not be' do
+ it 'should make user internal' do
+ existing_user.update_attribute('external', true)
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+ end
end
describe 'no account exists on server' do
@@ -68,6 +107,26 @@ describe Gitlab::Saml::User, lib: true do
end
end
+ context 'external groups' do
+ context 'are defined' do
+ it 'marks the user as external' do
+ stub_saml_group_config(%w(Freelancers))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ context 'are defined but the user does not belong there' do
+ it 'does not mark the user as external' do
+ stub_saml_group_config(%w(Interns))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+ end
+
context 'with auto_link_ldap_user disabled (default)' do
before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) }
include_examples 'to verify compliance with allow_single_sign_on'
@@ -76,12 +135,6 @@ describe Gitlab::Saml::User, lib: true do
context 'with auto_link_ldap_user enabled' do
before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) }
- context 'and no LDAP provider defined' do
- before { stub_ldap_config(providers: []) }
-
- include_examples 'to verify compliance with allow_single_sign_on'
- end
-
context 'and at least one LDAP provider is defined' do
before { stub_ldap_config(providers: %w(ldapmain)) }
@@ -89,19 +142,18 @@ describe Gitlab::Saml::User, lib: true do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
- allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] }
+ allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end
context 'and no account for the LDAP user' do
-
it 'creates a user with dual LDAP and SAML identities' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
- expect(gl_user.email).to eql 'johndoe@example.com'
+ expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to eql 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
@@ -111,13 +163,13 @@ describe Gitlab::Saml::User, lib: true do
end
context 'and LDAP user has an account already' do
- let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
- it "adds the omniauth identity to the LDAP account" do
+ let!(:existing_user) { create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
+ it 'adds the omniauth identity to the LDAP account' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
- expect(gl_user.email).to eql 'john@example.com'
+ expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to eql 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
@@ -126,19 +178,13 @@ describe Gitlab::Saml::User, lib: true do
end
end
end
-
- context 'and no corresponding LDAP person' do
- before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) }
-
- include_examples 'to verify compliance with allow_single_sign_on'
- end
end
end
end
describe 'blocking' do
- before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) }
+ before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
context 'signup with SAML only' do
context 'dont block on create' do
@@ -162,64 +208,6 @@ describe Gitlab::Saml::User, lib: true do
end
end
- context 'signup with linked omniauth and LDAP account' do
- before do
- stub_omniauth_config(auto_link_ldap_user: true)
- allow(ldap_user).to receive(:uid) { uid }
- allow(ldap_user).to receive(:username) { uid }
- allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] }
- allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
- allow(saml_user).to receive(:ldap_person).and_return(ldap_user)
- end
-
- context "and no account for the LDAP user" do
- context 'dont block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
-
- context 'block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).to be_blocked
- end
- end
- end
-
- context 'and LDAP user has an account already' do
- let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
-
- context 'dont block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
-
- context 'block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
- end
- end
-
-
context 'sign-in' do
before do
saml_user.save
@@ -245,26 +233,6 @@ describe Gitlab::Saml::User, lib: true do
expect(gl_user).not_to be_blocked
end
end
-
- context 'dont block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
-
- context 'block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
end
end
end
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 56a6dbf96f..5a85cb501d 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -141,10 +141,12 @@ shared_examples 'a new user email' do
end
shared_examples 'it should have Gmail Actions links' do
+ it { is_expected.to have_body_text '