Commit 20575228 authored by Robert Speicher's avatar Robert Speicher

Merge remote-tracking branch 'ce/master' into ce-to-ee

parents b44c7df8 f7da99ae
......@@ -14,19 +14,24 @@ v 8.6.0 (unreleased)
- Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Rewrite logo to simplify SVG code (Sean Lang)
- Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
v 8.5.5
- Ensure removing a project removes associated Todo entries.
- Prevent a 500 error in Todos when author was removed.
- Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
v 8.5.4
- Do not cache requests for badges (including builds badge)
......@@ -95,7 +100,7 @@ v 8.5.1
v 8.5.0
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
- Cache various Repository methods to improve performance (Yorick Peterse)
- Fix duplicated branch creation/deletion Web hooks/service notifications when using Web UI (Stan Hu)
- Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu)
- Ensure rake tasks that don't need a DB connection can be run without one
- Update New Relic gem to 3.14.1.311 (Stan Hu)
- Add "visibility" flag to GET /projects api endpoint
......@@ -231,7 +236,7 @@ v 8.4.0
- The default GitLab logo now acts as a loading indicator
- LDAP group sync: Remove user from group when they are removed from LDAP
- Fix caching issue where build status was not updating in project dashboard (Stan Hu)
- Accept 2xx status codes for successful Web hook triggers (Stan Hu)
- Accept 2xx status codes for successful Webhook triggers (Stan Hu)
- Fix missing date of month in network graph when commits span a month (Stan Hu)
- Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
- Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
......@@ -334,7 +339,7 @@ v 8.3.0
- Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu)
- Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
- Handle and report SSL errors in Web hook test (Stan Hu)
- Handle and report SSL errors in Webhook test (Stan Hu)
- Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu)
- Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
- WIP identifier on merge requests no longer requires trailing space
......@@ -554,7 +559,7 @@ v 8.1.0
- Ensure code blocks are properly highlighted after a note is updated
- Fix wrong access level badge on MR comments
- Hide password in the service settings form
- Move CI web hooks page to project settings area
- Move CI webhooks page to project settings area
- Fix User Identities API. It now allows you to properly create or update user's identities.
- Add user preference to change layout width (Peter Göbel)
- Use commit status in merge request widget as preferred source of CI status
......@@ -597,7 +602,7 @@ v 8.0.3
- Fix URL shown in Slack notifications
- Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
- Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
- Add work_in_progress key to MR web hooks (Ben Boeckel)
- Add work_in_progress key to MR webhooks (Ben Boeckel)
v 8.0.2
- Fix default avatar not rendering in network graph (Stan Hu)
......@@ -888,7 +893,7 @@ v 7.12.0
- Fix milestone "Browse Issues" button.
- Set milestone on new issue when creating issue from index with milestone filter active.
- Make namespace API available to all users (Stan Hu)
- Add web hook support for note events (Stan Hu)
- Add webhook support for note events (Stan Hu)
- Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
- Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
- Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
......@@ -995,7 +1000,7 @@ v 7.11.0
- Add "Create Merge Request" buttons to commits and branches pages and push event.
- Show user roles by comments.
- Fix automatic blocking of auto-created users from Active Directory.
- Call merge request web hook for each new commits (Arthur Gautier)
- Call merge request webhook for each new commits (Arthur Gautier)
- Use SIGKILL by default in Sidekiq::MemoryKiller
- Fix mentioning of private groups.
- Add style for <kbd> element in markdown
......@@ -1169,7 +1174,7 @@ v 7.9.0
- Add brakeman (security scanner for Ruby on Rails)
- Slack username and channel options
- Add grouped milestones from all projects to dashboard.
- Web hook sends pusher email as well as commiter
- Webhook sends pusher email as well as commiter
- Add Bitbucket omniauth provider.
- Add Bitbucket importer.
- Support referencing issues to a project whose name starts with a digit
......@@ -1292,7 +1297,7 @@ v 7.8.0
- Allow notification email to be set separately from primary email.
- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
- Don't have Markdown preview fail for long comments/wiki pages.
- When test web hook - show error message instead of 500 error page if connection to hook url was reset
- When test webhook - show error message instead of 500 error page if connection to hook url was reset
- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
- Added persistent collapse button for left side nav bar (Jason Blanchard)
- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
......@@ -1309,7 +1314,7 @@ v 7.8.0
- Show projects user contributed to on user page. Show stars near project on user page.
- Improve database performance for GitLab
- Add Asana service (Jeremy Benoist)
- Improve project web hooks with extra data
- Improve project webhooks with extra data
v 7.7.2
- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
......@@ -1794,7 +1799,7 @@ v 6.4.0
- Side-by-side diff view (Steven Thonus)
- Internal projects (Jason Hollingsworth)
- Allow removal of avatar (Drew Blessing)
- Project web hooks now support issues and merge request events
- Project webhooks now support issues and merge request events
- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
- Expire event cache on avatar creation/removal (Drew Blessing)
- Archiving old projects (Steven Thonus)
......@@ -1864,7 +1869,7 @@ v 6.2.0
- Added search for projects by name to api (Izaak Alpert)
- Make default user theme configurable (Izaak Alpert)
- Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
- Rake tasks for web hooks management (Jonhnny Weslley)
- Rake tasks for webhooks management (Jonhnny Weslley)
- Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
- API: Remove group
- API: Remove project
......@@ -2067,7 +2072,7 @@ v 4.2.0
- Async gitolite calls
- added satellites logs
- can_create_group, can_create_team booleans for User
- Process web hooks async
- Process webhooks async
- GFM: Fix images escaped inside links
- Network graph improved
- Switchable branches for network graph
......@@ -2101,7 +2106,7 @@ v 4.1.0
v 4.0.0
- Remove project code and path from API. Use id instead
- Return valid cloneable url to repo for web hook
- Return valid cloneable url to repo for webhook
- Fixed backup issue
- Reorganized settings
- Fixed commits compare
......
......@@ -6,6 +6,7 @@
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
......@@ -63,6 +64,19 @@
).done (projects) ->
callback(projects)
newLabel: (project_id, data, callback) ->
url = Api.buildUrl(Api.labels_path)
url = url.replace(':id', project_id)
data.private_token = gon.api_token
$.ajax(
url: url
type: "POST"
data: data
dataType: "json"
).done (label) ->
callback(label)
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
......
class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40]
constructor: (@dropdown, @options) ->
@input = @dropdown.find(".dropdown-input .dropdown-input-field")
# Key events
timeout = ""
@input.on "keyup", (e) =>
if e.keyCode is 13 && @input.val() isnt ""
if @options.enterCallback
@options.enterCallback()
return
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur e.keyCode
search_text = @input.val()
if blur_field
@input.blur()
if @options.remote
@options.query search_text, (data) =>
@options.callback(data)
else
@filter search_text
, 250
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
filter: (search_text) ->
data = @options.data()
results = data
if search_text isnt ""
results = fuzzaldrinPlus.filter(data, search_text,
key: @options.keys
)
@options.callback results
class GitLabDropdownRemote
constructor: (@dataEndpoint, @options) ->
execute: ->
if typeof @dataEndpoint is "string"
@fetchData()
else if typeof @dataEndpoint is "function"
if @options.beforeSend
@options.beforeSend()
# Fetch the data by calling the data funcfion
@dataEndpoint "", (data) =>
if @options.success
@options.success(data)
if @options.beforeSend
@options.beforeSend()
# Fetch the data through ajax if the data is a string
fetchData: ->
$.ajax(
url: @dataEndpoint,
dataType: @options.dataType,
beforeSend: =>
if @options.beforeSend
@options.beforeSend()
success: (data) =>
if @options.success
@options.success(data)
)
class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
constructor: (@el, @options) ->
self = @
@dropdown = $(@el).parent()
search_fields = if @options.search then @options.search.fields else [];
if @options.data
# Remote data
@remote = new GitLabDropdownRemote @options.data, {
dataType: @options.dataType,
beforeSend: @toggleLoading.bind(@)
success: (data) =>
@fullData = data
@parseData @fullData
}
# Init filiterable
if @options.filterable
@filter = new GitLabDropdownFilter @dropdown,
remote: @options.filterRemote
query: @options.data
keys: @options.search.fields
data: =>
return @fullData
callback: (data) =>
@parseData data
enterCallback: =>
@selectFirstRow()
# Event listeners
@dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden
if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
e.preventDefault()
e.stopPropagation()
@togglePage()
if @options.selectable
selector = ".dropdown-content a"
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) ->
self.rowClicked $(@)
if self.options.clicked
self.options.clicked()
toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
togglePage: ->
menu = $('.dropdown-menu', @dropdown)
if menu.hasClass(PAGE_TWO_CLASS)
if @remote
@remote.execute()
menu.toggleClass PAGE_TWO_CLASS
parseData: (data) ->
@renderedData = data
# Render each row
html = $.map data, (obj) =>
return @renderItem(obj)
if @options.filterable and data.length is 0
# render no matching results
html = [@noResults()]
# Render the full menu
full_html = @renderMenu(html.join(""))
@appendMenu(full_html)
opened: =>
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
if @options.filterable
@dropdown.find(".dropdown-input-field").focus()
hidden: =>
if @options.filterable
@dropdown.find(".dropdown-input-field").blur().val("")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
# Render the full menu
renderMenu: (html) ->
menu_html = ""
if @options.renderMenu
menu_html = @options.renderMenu(html)
else
menu_html = "<ul>#{html}</ul>"
return menu_html
# Append the menu into the dropdown
appendMenu: (html) ->
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
$(selector, @dropdown).html html
# Render the row
renderItem: (data) ->
html = ""
return "<li class='divider'></li>" if data is "divider"
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
else
selected = if @options.isSelected then @options.isSelected(data) else false
url = if @options.url then @options.url(data) else "#"
text = if @options.text then @options.text(data) else ""
cssClass = "";
if selected
cssClass = "is-active"
html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>"
html += text
html += "</a>"
html += "</li>"
return html
noResults: ->
html = "<li>"
html += "<a href='#' class='is-focused'>"
html += "No matching results."
html += "</a>"
html += "</li>"
rowClicked: (el) ->
fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']")
if el.hasClass(ACTIVE_CLASS)
field.remove()
else
fieldName = @options.fieldName
selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
field.remove()
# Toggle active class for the tick mark
el.toggleClass "is-active"
if value
if !field.length
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />"
@dropdown.before input
@dropdown.parent().find("input[name='#{fieldName}']").val value
selectFirstRow: ->
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
# similute a click on the first link
$(selector).trigger "click"
$.fn.glDropdown = (opts) ->
return @.each ->
new GitLabDropdown @, opts
class @IssueStatusSelect
constructor: ->
$('.js-issue-status').each (i, el) ->
fieldName = $(el).data("field-name")
$(el).glDropdown(
selectable: true
fieldName: fieldName
id: (obj, el) ->
$(el).data("id")
)
class @LabelsSelect
constructor: ->
$('.js-label-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
labelUrl = $(dropdown).data("labels")
selectedLabel = $(dropdown).data('selected')
if selectedLabel
selectedLabel = selectedLabel.split(",")
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
if newLabelField.length
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
newColorField.val $(this).data("color")
$('.js-dropdown-label-color-preview')
.css 'background-color', $(this).data("color")
.addClass 'is-active'
$('.js-new-label-btn').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
if newLabelField.val() isnt "" && newColorField.val() isnt ""
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
$('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
$(dropdown).glDropdown(
data: (term, callback) ->
# We have to fetch the JS version of the labels list because there is no
# public facing JSON url for labels
$.ajax(
url: labelUrl
).done (data) ->
html = $(data)
data = []
html.find('.label-row a').each ->
data.push(
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No label'
)
if showAny
data.unshift(
title: 'Any label'
)
if data.length > 2
data.splice 2, 0, "divider"
callback data
renderRow: (label) ->
if $.isArray(selectedLabel)
selected = ""
$.each selectedLabel, (i, selectedLbl) ->
selectedLbl = selectedLbl.trim()
if selected is "" && label.title is selectedLbl
selected = "is-active"
else
selected = if label.title is selectedLabel then "is-active" else ""
"<li>
<a href='#' class='#{selected}'>
#{label.title}
</a>
</li>"
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
id: (label) ->
label.title
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
......@@ -6,6 +6,7 @@
class @MarkdownPreview
# Minimum number of users referenced before triggering a warning
referenceThreshold: 10
ajaxCache: {}
showPreview: (form) ->
preview = form.find('.js-md-preview')
......@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) ->
return unless window.markdown_preview_path
return success(@ajaxCache.response) if text == @ajaxCache.text
$.ajax
type: 'POST'
url: window.markdown_preview_path
data: { text: text }
dataType: 'json'
success: success
success: (response) =>
@ajaxCache = text: text, response: response
success(response)
hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users')
......@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button'
lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = ->
$form = $(this)
......@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form)
$(document).on 'click', previewButtonSelector, (e) ->
e.preventDefault()
$(document).on 'markdown-preview:show', (e, $form) ->
return unless $form
$form = $(this).closest('form')
lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active')
......@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form)
$(document).on 'click', writeButtonSelector, (e) ->
e.preventDefault()
$(document).on 'markdown-preview:hide', (e, $form) ->
return unless $form
$form = $(this).closest('form')
lastTextareaPreviewed = null
# toggle tabs
$form.find(writeButtonSelector).parent().addClass('active')
......@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content
$form.find('.md-write-holder').show()
$form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide()
$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
$target = $(keyboardEvent.target)
if $target.is('textarea.markdown-area')
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
keyboardEvent.preventDefault()
else if lastTextareaPreviewed
$target = lastTextareaPreviewed
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
keyboardEvent.preventDefault()
$(document).on 'click', previewButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:show', [$form])
$(document).on 'click', writeButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:hide', [$form])
class @MilestoneSelect
constructor: ->
$('.js-milestone-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
milestonesUrl = $(dropdown).data('milestones')
selectedMilestone = $(dropdown).data('selected')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
useId = $(dropdown).data('use-id')
$(dropdown).glDropdown(
data: (term, callback) ->
$.ajax(
url: milestonesUrl
).done (data) ->
html = $(data)
data = []
html.find('.milestone strong a').each ->
link = $(@).attr("href").split("/")
data.push(
id: link[link.length - 1]
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No Milestone'
)
if showAny
data.unshift(
title: 'Any Milestone'
)
if data.length > 2
data.splice 2, 0, "divider"
callback(data)
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
text: (milestone) ->
milestone.title
id: (milestone) ->
if !useId
if milestone.title isnt "Any milestone"
milestone.title
else
""
else
milestone.id
isSelected: (milestone) ->
milestone.title is selectedMilestone
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
......@@ -31,7 +31,7 @@ class @Notes
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
# change note in UI after update
$(document).on "ajax:success", "form.edit_note", @updateNote
$(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
......@@ -72,7 +72,7 @@ class @Notes
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
$(document).off "ajax:success", "form.edit_note"
$(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete"
......@@ -347,22 +347,26 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").hide()
note.find(".note-header").hide()
base_form = note.find(".note-edit-form")
form = base_form.clone().insertAfter(base_form)
form.addClass('current-note-edit-form gfm-form')
form.find('.div-dropzone').remove()
form = note.find(".note-edit-form")
isNewForm = form.is(':not(.gfm-form)')
if isNewForm
form.addClass('gfm-form')
form.addClass('current-note-edit-form')
form.show()
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
# Setup markdown form
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
if isNewForm
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
textarea = form.find("textarea")
textarea.focus()
autosize(textarea)
if isNewForm
autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
......@@ -371,7 +375,8 @@ class @Notes
textarea.val ""
textarea.val value
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
if isNewForm
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
Called in response to clicking the edit note link
......@@ -383,7 +388,9 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").show()
note.find(".note-header").show()
note.find(".current-note-edit-form").remove()
note.find(".current-note-edit-form")
.removeClass("current-note-edit-form")
.hide()
###
Called in response to deleting a note of any kind.
......
......@@ -2,6 +2,7 @@
init: ->
$(".projects-list-filter").off('keyup')
this.initSearch()
this.initPagination()
initSearch: ->
@timer = null
......@@ -29,3 +30,8 @@
# Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json"
initPagination: ->
$('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
$('.projects-list-holder').replaceWith(data.html)
)
......@@ -4,11 +4,15 @@ class @Shortcuts
Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
toggleMarkdownPreview: (e) =>
$(document).triggerHandler('markdown-preview:toggle', [e])
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
......@@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
Mousetrap.stopCallback = (->
defaultStopCallback = Mousetrap.stopCallback
return (e, element, combo) ->
# allowed shortcuts if textarea, input, contenteditable are focused
if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
return false
else
return defaultStopCallback.apply(@, arguments)
)()
......@@ -3,6 +3,81 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
$('.js-user-search').each (i, dropdown) =>
@projectId = $(dropdown).data('project-id')
@showCurrentUser = $(dropdown).data('current-user')
showNullUser = $(dropdown).data('null-user')
showAnyUser = $(dropdown).data('any-user')
firstUser = $(dropdown).data('first-user')
selectedId = $(dropdown).data('selected')
$(dropdown).glDropdown(
data: (term, callback) =>
@users term, (users) =>
if term.length is 0
showDivider = 0
if firstUser
# Move current user to the front of the list
for obj, index in users
if obj.username == firstUser
users.splice(index, 1)
users.unshift(obj)
break
if showNullUser
showDivider += 1
users.unshift(
name: 'Unassigned',
id: 0
)
if showAnyUser
showDivider += 1
name = showAnyUser
name = 'Any User' if name == true
anyUser = {
name: name,
id: null
}
users.unshift(anyUser)
if showDivider
users.splice(showDivider, 0, "divider")
# Send the data back
callback users
filterable: true
filterRemote: true
search:
fields: ['name', 'username']
selectable: true
fieldName: $(dropdown).data('field-name')
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
renderRow: (user) ->
username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false
selected = if user.id is selectedId then "is-active" else ""
img = ""
if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li>
<a href='#' class='dropdown-menu-user-link #{selected}'>
#{img}
<strong class='dropdown-menu-user-full-name'>
#{user.name}
</strong>
<span class='dropdown-menu-user-username'>
#{username}
</span>
</a>
</li>"
)
$('.ajax-users-select').each (i, select) =>
@skipLdap = $(select).hasClass('skip_ldap')
@projectId = $(select).data('project-id')
......
......@@ -28,6 +28,10 @@
border-bottom: 1px solid $border-color;
color: $gl-gray;
a {
color: $md-link-color;
}
&.oneline-block {
line-height: 42px;
}
......
......@@ -17,6 +17,47 @@
.dropdown-menu {
display: block;
}
.dropdown-menu-toggle {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu-toggle {
position: relative;
width: 160px;
padding: 6px 20px 6px 10px;
background-color: $dropdown-toggle-bg;
color: $dropdown-toggle-color;
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
border-radius: 2px;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.fa {
position: absolute;
top: 50%;
right: 6px;
margin-top: -4px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu {
......@@ -24,7 +65,7 @@
position: absolute;
top: 100%;
left: 0;
z-index: 9999;
z-index: 9;
width: 240px;
margin-top: 2px;
margin-bottom: 0;
......@@ -36,6 +77,21 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
.dropdown-content {
display: none;
}
.dropdown-loading {
display: block;
}
}
ul {
margin: 0;
padding: 0;
}
li {
text-align: left;
list-style: none;
......@@ -61,13 +117,70 @@
white-space: nowrap;
overflow: hidden;
&:hover {
&:hover,
&:focus,
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
outline: 0;
}
}
}
.dropdown-menu-paging {
.dropdown-page-two,
.dropdown-menu-back {
display: none;
}
&.is-page-two {
.dropdown-page-one {
display: none;
}
.dropdown-page-two,
.dropdown-menu-back {
display: block;
}
}
}
.dropdown-menu-user {
.avatar {
float: left;
width: 30px;
height: 30px;
margin: 0 10px 0 0;
}
}
.dropdown-menu-user-link {
padding-top: 7px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name {
display: block;
margin-bottom: 2px;
font-weight: 600;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-menu-user-username {
display: block;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-select {
width: 280px;
}
.dropdown-menu-align-right {
left: auto;
right: 0;
......@@ -101,3 +214,130 @@
font-size: 13px;
line-height: 22px;
}
.dropdown-title {
position: relative;
margin-bottom: 10px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 10px;
font-weight: 600;
line-height: 1;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
border-bottom: 1px solid $dropdown-divider-color;
overflow: hidden;
}
.dropdown-title-button {
position: absolute;
top: -1px;
padding: 0;
color: $dropdown-title-btn-color;
font-size: 14px;
border: 0;
background: none;
outline: 0;
&:hover {
color: darken($dropdown-title-btn-color, 15%);
}
}
.dropdown-menu-close {
right: 0;
}
.dropdown-menu-back {
left: 0;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
.fa {
position: absolute;
top: 10px;
right: 10px;
color: #C7C7C7;
font-size: 12px;
pointer-events: none;
}
}
.dropdown-input-field {
width: 100%;
padding: 0 7px;
color: $dropdown-input-color;
line-height: 30px;
border: 1px solid $dropdown-divider-color;
border-radius: 2px;
outline: 0;
&:focus {
color: $dropdown-link-color;
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+ .fa {
color: $dropdown-link-color;
}
}
&:hover {
+ .fa {
color: $dropdown-link-color;
}
}
}
.dropdown-content {
max-height: 200px;
overflow-y: scroll;
}
.dropdown-footer {
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
border-top: 1px solid $dropdown-divider-color;
}
.dropdown-footer-list {
font-size: 14px;
a {
padding-left: 10px;
}
}
.dropdown-loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
z-index: 9;
background-color: $dropdown-loading-bg;
font-size: 28px;
.fa {
position: absolute;
top: 50%;
left: 50%;
margin-top: -14px;
margin-left: -14px;
}
}
.dropdown-menu-labels {
.label {
position: relative;
width: 30px;
margin-right: 5px;
text-indent: -99999px;
}
}
......@@ -169,6 +169,7 @@
*/
&.code {
padding: 0;
-webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
......
.filter-item {
margin-right: 6px;
vertical-align: top;
}
@media (min-width: 800px) {
......
......@@ -149,13 +149,13 @@
}
&:hover > a.anchor {
$size: 16px;
$size: 14px;
position: absolute;
right: 100%;
top: 50%;
margin-top: -$size/2;
margin-right: 0px;
padding-right: 20px;
margin-top: -11px;
margin-right: 0;
padding-right: 15px;
display: inline-block;
width: $size;
height: $size;
......
......@@ -138,3 +138,15 @@ $dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
$dropdown-caret-color: #54565B;
$dropdown-title-btn-color: #BFBFBF;
$dropdown-input-color: #C7C7C7;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
$dropdown-toggle-color: #626262;
$dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
......@@ -94,6 +94,7 @@
position: relative;
font-family: $monospace_font;
$left: 12px;
overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker {
width: 72ch;
color: rgba(0, 0, 0, 0.0);
......
......@@ -7,6 +7,28 @@
display: inline-block;
margin-right: 10px;
}
&.suggest-colors-dropdown {
margin-bottom: 5px;
a {
@include border-radius(0);
width: 36.7px;
margin-right: 0;
margin-bottom: -5px;
}
}
}
.dropdown-label-color-preview {
display: none;
margin-top: 5px;
width: 100%;
height: 25px;
&.is-active {
display: block;
}
}
.label-row {
......
......@@ -28,3 +28,11 @@
border: 1px solid;
line-height: 32px;
}
.markdown-snippet-copy {
position: fixed;
top: -10px;
left: -10px;
max-height: 0;
max-width: 0;
}
......@@ -250,6 +250,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check
if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease
unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
......
......@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = @projects.page(params[:page]).per(PER_PAGE)
@last_push = current_user.recent_push
......@@ -32,7 +32,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = @projects.page(params[:page]).per(PER_PAGE)
@last_push = current_user.recent_push
@groups = []
......
......@@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
......@@ -23,7 +23,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
@projects = TrendingProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects)
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
......@@ -39,7 +39,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = ProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
......
......@@ -42,12 +42,12 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
def render_ci_status(ci_commit)
def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}",
data: { toggle: 'tooltip', placement: 'left' }
data: { toggle: 'tooltip', placement: tooltip_placement }
end
def no_runners_for_project?(project)
......
module DropdownsHelper
def dropdown_tag(toggle_text, options: {}, &block)
content_tag :div, class: "dropdown" do
data_attr = { toggle: "dropdown" }
if options.has_key?(:data)
data_attr = options[:data].merge(data_attr)
end
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
output = ""
if options.has_key?(:title)
output << dropdown_title(options[:title])
end
if options.has_key?(:filter)
output << dropdown_filter(options[:placeholder])
end
output << content_tag(:div, class: "dropdown-content") do
capture(&block) if block && !options.has_key?(:footer_content)
end
if block && options.has_key?(:footer_content)
output << content_tag(:div, class: "dropdown-footer") do
capture(&block)
end
end
output << dropdown_loading
output.html_safe
end
dropdown_output.html_safe
end
end
def dropdown_toggle(toggle_text, data_attr, options)
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
output << icon('chevron-down')
output.html_safe
end
end
def dropdown_title(title, back: false)
content_tag :div, class: "dropdown-title" do
title_output = ""
if back
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
icon('arrow-left')
end
end
title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times')
end
title_output.html_safe
end
end
def dropdown_filter(placeholder)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search')
filter_output.html_safe
end
end
def dropdown_content(&block)
content_tag(:div, class: "dropdown-content") do
if block
capture(&block)
end
end
end
def dropdown_footer(&block)
content_tag(:div, class: "dropdown-footer") do
if block
capture(&block)
end
end
end
def dropdown_loading
content_tag :div, class: "dropdown-loading" do
icon('spinner spin')
end
end
end
......@@ -40,7 +40,7 @@ module SearchHelper
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
{ label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") },
]
end
......
......@@ -620,11 +620,11 @@ class Project < ActiveRecord::Base
end
def ci_services
services.select { |service| service.category == :ci }
services.where(category: :ci)
end
def ci_service
@ci_service ||= ci_services.find(&:activated?)
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
def jira_tracker?
......
......@@ -641,6 +641,13 @@ class User < ActiveRecord::Base
end
end
def try_obtain_ldap_lease
# After obtaining this lease LDAP checks will be blocked for 600 seconds
# (10 minutes) for this user.
lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
lease.try_obtain
end
def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self]
......
......@@ -12,7 +12,7 @@ class GitPushService < BaseService
# 1. Creates the push event
# 2. Updates merge requests
# 3. Recognizes cross-references from commit messages
# 4. Executes the project's web hooks
# 4. Executes the project's webhooks
# 5. Executes the project's services
# 6. Checks if the project's main language has changed
#
......
......@@ -22,6 +22,14 @@
%td.shortcut
.key ?
%td Show this dialog
%tr
%td.shortcut
- if browser.mac?
.key &#8984; shift p
- else
.key ctrl shift p
%td Toggle Markdown preview
%tbody
%tr
%th
......
......@@ -18,6 +18,8 @@
= link_to 'Nav', '#nav'
%li
= link_to 'Buttons', '#buttons'
%li
= link_to 'Dropdowns', '#dropdowns'
%li
= link_to 'Panels', '#panels'
%li
......@@ -180,9 +182,9 @@
.nav-controls
= text_field_tag 'sample', nil, class: 'form-control'
.dropdown
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span Sort by name
%b.caret
= icon('chevron-down')
%ul.dropdown-menu
%li
%a Sort by date
......@@ -212,6 +214,227 @@
%button.btn.btn-danger{:type => "button"} Danger
%button.btn.btn-link{:type => "button"} Link
%h2#dropdowns Dropdowns
.example
.clearfix
.dropdown.inline.pull-left
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a{href: "#"}
Dropdown Option
.dropdown.inline.pull-right
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
%a{href: "#"}
Dropdown Option
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-selectable
%li
%a.is-active{href: "#"}
Dropdown Option
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.is-active{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li.divider
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
.dropdown-footer
%strong Tip:
If an author is not a member of this project, you can still filter by his name while using the search field.
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown loading
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
.dropdown-title
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.is-active{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li.divider
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
.dropdown-footer
%strong Tip:
If an author is not a member of this project, you can still filter by his name while using the search field.
.dropdown-loading
= icon('spinner spin')
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown user
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
.dropdown-title
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30)
%strong.dropdown-menu-user-full-name
= current_user.name
.dropdown-menu-user-username
= current_user.to_reference
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown page 2
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
.dropdown-page-one
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
= icon('arrow-left')
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30)
%strong.dropdown-menu-user-full-name
= current_user.name
.dropdown-menu-user-username
= current_user.to_reference
.dropdown-page-two
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
= icon('arrow-left')
%span Create label
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Name new label"}
.dropdown-content
%button.btn.btn-primary
Create
.example
%div
.dropdown.inline
%button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Projects
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Go to project
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
.dropdown-loading
= icon('spinner spin')
:javascript
$('#js-project-dropdown').glDropdown({
data: function (term, callback) {
Api.projects(term, "last_activity_at", function (data) {
callback(data);
});
},
text: function (project) {
return project.name_with_namespace || project.name;
},
selectable: true,
fieldName: "author_id",
filterable: true,
search: {
fields: ['name_with_namespace']
},
id: function (data) {
return data.id;
},
isSelected: function (data) {
return data.id === 2;
}
})
.example
%div
= dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" })
%h2#panels Panels
.row
......
......@@ -25,10 +25,10 @@
%span
Deploy Keys
= nav_link(controller: :hooks) do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
= icon('link fw')
%span
Web Hooks
Webhooks
= nav_link(controller: :git_hooks) do
= link_to namespace_project_git_hooks_path(@project.namespace, @project), title: "Git Hooks" do
= icon('git-square fw')
......
- unless @project.empty_repo?
- if can? current_user, :download_code, @project
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
= icon('download')
- page_title "Web Hooks"
- page_title "Webhooks"
%h3.page-title
Web hooks
Webhooks
%p.light
#{link_to "Web hooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
#{link_to "Webhooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
used for binding events when something is happening within the project.
%hr.clearfix
......@@ -70,12 +70,12 @@
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
.form-actions
= f.submit "Add Web Hook", class: "btn btn-create"
= f.submit "Add Webhook", class: "btn btn-create"
-if @hooks.any?
.panel.panel-default
.panel-heading
Web hooks (#{@hooks.count})
Webhooks (#{@hooks.count})
%ul.well-list
- @hooks.each do |hook|
%li
......
......@@ -11,7 +11,7 @@
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
\!#{merge_request.iid}
= merge_request.to_reference
%span.merge-request-info
%strong
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
......
.note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, class: '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 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'
......
......@@ -7,22 +7,77 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
= users_select_tag(:author_id, selected: params[:author_id],
placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
- if params[:author_id]
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } })
.filter-item.inline
= users_select_tag(:assignee_id, selected: params[:assignee_id],
placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
- if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
.filter-item.inline.milestone-filter
= select_tag('milestone_title', projects_milestones_options,
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Milestone'})
- if params[:milestone_title]
= hidden_field_tag(:milestone_title, params[:milestone_title])
= dropdown_tag("Milestone", options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do
- if @project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, @project
%li
= link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
Create new
%li
= link_to namespace_project_milestones_path(@project.namespace, @project) do
- if can? current_user, :admin_milestone, @project
Manage milestones
- else
View milestones
.filter-item.inline.labels-filter
= select_tag('label_name', projects_labels_options,
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Label'})
- if params[:label_name]
= hidden_field_tag(:label_name, params[:label_name])
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}}
%span.dropdown-toggle-text
Label
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-page-one
= dropdown_title("Filter by label")
= dropdown_filter("Search labels")
= dropdown_content
- if @project
= dropdown_footer do
%ul.dropdown-footer-list
- if can? current_user, :admin_label, @project
%li
%a.dropdown-toggle-page{href: "#"}
Create new
%li
= link_to namespace_project_labels_path(@project.namespace, @project) do
- if can? current_user, :admin_label, @project
Manage labels
- else
View labels
- if can? current_user, :admin_label, @project
.dropdown-page-two
= dropdown_title("Create new label", back: true)
= dropdown_content do
%input#new_label_color{type: "hidden"}
%input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
.dropdown-label-color-preview.js-dropdown-label-color-preview
.suggest-colors.suggest-colors-dropdown
- suggested_colors.each do |color|
= link_to '#', style: "background-color: #{color}", data: { color: color } do
&nbsp
%button.btn.btn-primary.js-new-label-btn{type: "button"}
Create
= dropdown_loading
.dropdown-loading
= icon('spinner spin')
- if controller.controller_name == 'issues'
.filter-item.inline.weight-filter
......@@ -37,11 +92,18 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
= select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
%li
%a{href: "#", data: {id: "reopen"}} Open
%li
%a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
= users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
= select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
......@@ -53,6 +115,9 @@
:javascript
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
......
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
%textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
= @snippet.data
.file-content.wiki
= render_markup(@snippet.file_name, @snippet.data)
- else
......
......@@ -4,6 +4,7 @@ require 'rails/all'
require 'devise'
I18n.config.enforce_available_locales = false
Bundler.require(:default, Rails.env)
require_relative '../lib/gitlab/redis_config'
module Gitlab
REDIS_CACHE_NAMESPACE = 'cache:gitlab'
......@@ -67,22 +68,7 @@ module Gitlab
end
end
# Use Redis caching across all environments
redis_config_file = Rails.root.join('config', 'resque.yml')
redis_url_string = if File.exists?(redis_config_file)
YAML.load_file(redis_config_file)[Rails.env]
else
"redis://localhost:6379"
end
# Redis::Store does not handle Unix sockets well, so let's do it for them
redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(redis_url_string)
redis_uri = URI.parse(redis_url_string)
if redis_uri.scheme == 'unix'
redis_config_hash[:path] = redis_uri.path
end
redis_config_hash = Gitlab::RedisConfig.redis_store_options
redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
config.cache_store = :redis_store, redis_config_hash
......
......@@ -13,9 +13,12 @@ end
if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
redis_config = Gitlab::RedisConfig.redis_store_options
redis_config[:namespace] = 'session:gitlab'
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
servers: redis_config,
key: '_gitlab_session',
secure: Gitlab.config.gitlab.https,
httponly: true,
......
# Custom Redis configuration
config_file = Rails.root.join('config', 'resque.yml')
resque_url = if File.exists?(config_file)
YAML.load_file(config_file)[Rails.env]
else
"redis://localhost:6379"
end
SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab'
Sidekiq.configure_server do |config|
config.redis = {
url: resque_url,
namespace: 'resque:gitlab'
url: Gitlab::RedisConfig.url,
namespace: SIDEKIQ_REDIS_NAMESPACE
}
config.server_middleware do |chain|
......@@ -39,7 +32,7 @@ end
Sidekiq.configure_client do |config|
config.redis = {
url: resque_url,
namespace: 'resque:gitlab'
url: Gitlab::RedisConfig.url,
namespace: SIDEKIQ_REDIS_NAMESPACE
}
end
......@@ -2,6 +2,7 @@
<%
require "yaml"
require "json"
require_relative "lib/gitlab/redis_config"
rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
......@@ -17,13 +18,7 @@ if File.exists?(config_file)
config['mailbox'] = "inbox" if config['mailbox'].nil?
if config['enabled'] && config['address'] && config['address'].include?('%{key}')
redis_config_file = "config/resque.yml"
redis_url =
if File.exists?(redis_config_file)
YAML.load_file(redis_config_file)[rails_env]
else
"redis://localhost:6379"
end
redis_url = Gitlab::RedisConfig.new(rails_env).url
%>
-
:host: <%= config['host'].to_json %>
......
......@@ -14,7 +14,7 @@
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
- [Analytics](analytics/README.md)
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
- [GitLab Pages](pages/README.md) Using GitLab Pages.
- [Custom templates for issues and merge requests](customization/issue_and_merge_request_template.md) Pre-fill the description of issues and merge requests to your liking.
......@@ -59,7 +59,7 @@ be linked with your base image. Below is a list of examples you may use:
- [Audit Events](administration/audit_events.md) Check how user access changed in projects and groups.
- [Changing the appearance of the login page](customization/branded_login_page.md) Make the login page branded for your GitLab instance.
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough.
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
- [Email](tools/email.md) Email GitLab users from GitLab
- [Git Hooks](git_hooks/git_hooks.md) Advanced push rules for your project.
- [Help message](customization/help_message.md) Set information about administrators of your GitLab instance.
......@@ -72,7 +72,7 @@ be linked with your base image. Below is a list of examples you may use:
- [Log system](logs/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
......
......@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
Please explore [web hooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Please explore [webhooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
......
......@@ -29,6 +29,8 @@
## GitLab Flavored Markdown (GFM)
_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.
You can use GFM in
......@@ -88,8 +90,8 @@ GFM will autolink almost any URL you copy and paste into your text.
## Code and Syntax Highlighting
_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the rouge website._
_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the Rouge website._
Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. Only the fenced code blocks support syntax highlighting.
......@@ -591,3 +593,4 @@ By including colons in the header row, you can align the text within that column
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
......@@ -6,6 +6,6 @@
- [Features](features.md)
- [Maintenance](maintenance.md) and self-checks
- [User management](user_management.md)
- [Web hooks](web_hooks.md)
- [Webhooks](web_hooks.md)
- [Import](import.md) of git repositories in bulk
- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators
# Web hooks
# Webhooks
## Add a web hook for **ALL** projects:
## Add a webhook for **ALL** projects:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook"
# source installations
bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" RAILS_ENV=production
## Add a web hook for projects in a given **NAMESPACE**:
## Add a webhook for projects in a given **NAMESPACE**:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme
# source installations
bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
## Remove a web hook from **ALL** projects using:
## Remove a webhook from **ALL** projects using:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook"
# source installations
bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" RAILS_ENV=production
## Remove a web hook from projects in a given **NAMESPACE**:
## Remove a webhook from projects in a given **NAMESPACE**:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme
# source installations
bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
## List **ALL** web hooks:
## List **ALL** webhooks:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:list
# source installations
bundle exec rake gitlab:web_hook:list RAILS_ENV=production
## List the web hooks from projects in a given **NAMESPACE**:
## List the webhooks from projects in a given **NAMESPACE**:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:list NAMESPACE=/
......
......@@ -2,7 +2,7 @@
- [Password length limits](password_length_limits.md)
- [Rack attack](rack_attack.md)
- [Web Hooks and insecure internal web services](webhooks.md)
- [Webhooks and insecure internal web services](webhooks.md)
- [Information exclusivity](information_exclusivity.md)
- [Reset your root password](reset_root_password.md)
- [User File Uploads](user_file_uploads.md)
......
# Web Hooks and insecure internal web services
# Webhooks and insecure internal web services
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Web Hooks.
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
With [Web Hooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Web Hook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the web hook is triggered and the POST request is sent.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
Because Web Hook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
If a web service does not require authentication, Web Hooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
\ No newline at end of file
# Web hooks
# Webhooks
_**Note:**
Starting from GitLab 8.5:_
......@@ -7,11 +7,11 @@ Starting from GitLab 8.5:_
- _the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key_
- _the `project.http_url` key is deprecated in favor of the `project.git_http_url` key_
Project web hooks allow you to trigger an URL if new code is pushed or a new issue is created.
Project webhooks allow you to trigger an URL if new code is pushed or a new issue is created.
You can configure web hooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the web hook URL.
You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the webhook URL.
Web hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.
Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.
In GitLab Enterprise Edition you can configure web hooks globally for the whole
group. You can add the group level web hooks on the group settings page
......@@ -23,7 +23,7 @@ By default, the SSL certificate of the webhook endpoint is verified based on
an internal list of Certificate Authorities,
which means the certificate cannot be self-signed.
You can turn this off in the web hook settings in your GitLab projects.
You can turn this off in the webhook settings in your GitLab projects.
![SSL Verification](ssl.png)
......
......@@ -36,13 +36,22 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
select2(current_user.id, from: "#author_id")
select2(nil, from: "#assignee_id")
execute_script('$("#assignee_id").val("")')
execute_script('$(".js-user-search").first().click()')
sleep 1
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
sleep 1
end
step 'I click "All" link' do
select2(nil, from: "#author_id")
select2(nil, from: "#assignee_id")
execute_script('$(".js-user-search").first().click()')
sleep 1
execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
sleep 1
execute_script('$(".js-user-search").eq(1).click()')
sleep 1
execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
sleep 1
end
def should_see(issue)
......
......@@ -40,13 +40,22 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
select2(current_user.id, from: "#author_id")
select2(nil, from: "#assignee_id")
execute_script('$("#assignee_id").val("")')
execute_script('$(".js-user-search").first().click()')
sleep 0.5
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
sleep 2
end
step 'I click "All" link' do
select2(nil, from: "#author_id")
select2(nil, from: "#assignee_id")
execute_script('$(".js-user-search").first().click()')
sleep 0.5
execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
sleep 2
execute_script('$(".js-user-search").eq(1).click()')
sleep 0.5
execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
sleep 2
end
def should_see(merge_request)
......
......@@ -26,7 +26,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Hooks" tab' do
click_link('Web Hooks')
click_link('Webhooks')
end
step 'I click the "Deploy Keys" tab' do
......@@ -46,7 +46,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'the active sub nav should be Hooks' do
ensure_active_sub_nav('Web Hooks')
ensure_active_sub_nav('Webhooks')
end
step 'the active sub nav should be Deploy Keys' do
......
......@@ -25,14 +25,14 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
step 'I submit new hook' do
@url = FFaker::Internet.uri("http")
fill_in "hook_url", with: @url
expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1)
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
end
step 'I submit new hook with SSL verification enabled' do
@url = FFaker::Internet.uri("http")
fill_in "hook_url", with: @url
check "hook_enable_ssl_verification"
expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1)
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
end
step 'I should see newly created hook' do
......
......@@ -29,7 +29,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
step 'I click link "bug"' do
select2('bug', from: "#label_name")
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
sleep 2
end
step 'I click link "feature"' do
......
......@@ -27,7 +27,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click link "Closed"' do
click_link "Closed"
find('.issues-state-filters a', text: "Closed").click
end
step 'I click button "Unsubscribe"' do
......@@ -63,14 +63,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click "author" dropdown' do
first('#s2id_author_id').click
page.find('.js-author-search').click
sleep 1
end
step 'I see current user as the first user' do
expect(page).to have_selector('.user-result', visible: true, count: 3)
users = page.all('.user-name')
expect(page).to have_selector('.dropdown-content', visible: true)
users = page.all('.dropdown-menu-author .dropdown-content li a')
expect(users[0].text).to eq 'Any Author'
expect(users[1].text).to eq current_user.name
expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}"
end
step 'I submit new issue "500 error on profile"' do
......
module Gitlab
# This class implements an 'exclusive lease'. We call it a 'lease'
# because it has a set expiry time. We call it 'exclusive' because only
# one caller may obtain a lease for a given key at a time. The
# implementation is intended to work across GitLab processes and across
# servers. It is a 'cheap' alternative to using SQL queries and updates:
# you do not need to change the SQL schema to start using
# ExclusiveLease.
#
# It is important to choose the timeout wisely. If the timeout is very
# high (1 hour) then the throughput of your operation gets very low (at
# most once an hour). If the timeout is lower than how long your
# operation may take then you cannot count on exclusivity. For example,
# if the timeout is 10 seconds and you do an operation which may take 20
# seconds then two overlapping operations may hold a lease for the same
# key at the same time.
#
class ExclusiveLease
def initialize(key, timeout:)
@key, @timeout = key, timeout
end
# Try to obtain the lease. Return true on success,
# false if the lease is already taken.
def try_obtain
# Performing a single SET is atomic
!!redis.set(redis_key, '1', nx: true, ex: @timeout)
end
private
def redis
# Maybe someday we want to use a connection pool...
@redis ||= Redis.new(url: Gitlab::RedisConfig.url)
end
def redis_key
"gitlab:exclusive_lease:#{@key}"
end
end
end
......@@ -63,7 +63,7 @@ module Gitlab
end
# This method provide a sample data generated with
# existing project and commits to test web hooks
# existing project and commits to test webhooks
def build_sample(project, user)
commits = project.repository.commits(project.default_branch, nil, 3)
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
......
module Gitlab
class RedisConfig
attr_reader :url
def self.url
new.url
end
def self.redis_store_options
url = new.url
redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url)
# Redis::Store does not handle Unix sockets well, so let's do it for them
redis_uri = URI.parse(url)
if redis_uri.scheme == 'unix'
redis_config_hash[:path] = redis_uri.path
end
redis_config_hash
end
def initialize(rails_env=nil)
rails_env ||= Rails.env
config_file = File.expand_path('../../../config/resque.yml', __FILE__)
@url = "redis://localhost:6379"
if File.exists?(config_file)
@url =YAML.load_file(config_file)[rails_env]
end
end
end
end
......@@ -3,7 +3,7 @@ module Gitlab
def self.allowed?(user)
return false if user.blocked?
if user.requires_ldap_check?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
end
......
......@@ -4,16 +4,16 @@ namespace :cache do
desc "GitLab | Clear redis cache"
task :clear => :environment do
redis_store = Rails.cache.instance_variable_get(:@data)
redis = Redis.new(url: Gitlab::RedisConfig.url)
cursor = REDIS_SCAN_START_STOP
loop do
cursor, keys = redis_store.scan(
cursor, keys = redis.scan(
cursor,
match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*",
count: CLEAR_BATCH_SIZE
)
redis_store.del(*keys) if keys.any?
redis.del(*keys) if keys.any?
break if cursor == REDIS_SCAN_START_STOP
end
......
namespace :gitlab do
namespace :web_hook do
desc "GitLab | Adds a web hook to the projects"
desc "GitLab | Adds a webhook to the projects"
task :add => :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
puts "Adding web hook '#{web_hook_url}' to:"
puts "Adding webhook '#{web_hook_url}' to:"
projects.find_each(batch_size: 1000) do |project|
print "- #{project.name} ... "
web_hook = project.hooks.new(url: web_hook_url)
......@@ -20,7 +20,7 @@ namespace :gitlab do
end
end
desc "GitLab | Remove a web hook from the projects"
desc "GitLab | Remove a webhook from the projects"
task :rm => :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
......@@ -28,12 +28,12 @@ namespace :gitlab do
projects = find_projects(namespace_path)
projects_ids = projects.pluck(:id)
puts "Removing web hooks with the url '#{web_hook_url}' ... "
puts "Removing webhooks with the url '#{web_hook_url}' ... "
count = WebHook.where(url: web_hook_url, project_id: projects_ids, type: 'ProjectHook').delete_all
puts "#{count} web hooks were removed."
puts "#{count} webhooks were removed."
end
desc "GitLab | List web hooks"
desc "GitLab | List webhooks"
task :list => :environment do
namespace_path = ENV['NAMESPACE']
......@@ -43,7 +43,7 @@ namespace :gitlab do
puts "#{hook.project.name.truncate(20).ljust(20)} -> #{hook.url}"
end
puts "\n#{web_hooks.size} web hooks found."
puts "\n#{web_hooks.size} webhooks found."
end
end
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)">
<g id="Page-1" sketch:type="MSShapeGroup">
<g id="Fill-1-+-Group-24">
<g id="Group-24">
<g id="Group">
<path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path>
<path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path>
<path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path>
<path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path>
<path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path>
<path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path>
<path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<svg width="210" height="210" viewBox="0 0 210 210" xmlns="http://www.w3.org/2000/svg">
<path d="M105.0614 203.655l38.64-118.921h-77.28l38.64 118.921z" fill="#e24329"/>
<path d="M105.0614 203.6548l-38.64-118.921h-54.153l92.793 118.921z" fill="#fc6d26"/>
<path d="M12.2685 84.7341l-11.742 36.139c-1.071 3.296.102 6.907 2.906 8.944l101.629 73.838-92.793-118.921z" fill="#fca326"/>
<path d="M12.2685 84.7342h54.153l-23.273-71.625c-1.197-3.686-6.411-3.685-7.608 0l-23.272 71.625z" fill="#e24329"/>
<path d="M105.0614 203.6548l38.64-118.921h54.153l-92.793 118.921z" fill="#fc6d26"/>
<path d="M197.8544 84.7341l11.742 36.139c1.071 3.296-.102 6.907-2.906 8.944l-101.629 73.838 92.793-118.921z" fill="#fca326"/>
<path d="M197.8544 84.7342h-54.153l23.273-71.625c1.197-3.686 6.411-3.685 7.608 0l23.272 71.625z" fill="#e24329"/>
</svg>
require 'rails_helper'
feature 'Issue filtering by Milestone', feature: true do
include Select2Helper
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
......@@ -31,6 +29,9 @@ feature 'Issue filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
select2(title, from: '#milestone_title')
find(".js-milestone-select").click
sleep 0.5
find(".milestone-filter a", text: title).click
sleep 1
end
end
require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
include Select2Helper
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
......@@ -31,6 +29,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
select2(title, from: '#milestone_title')
find(".js-milestone-select").click
sleep 0.5
find(".milestone-filter a", text: title).click
sleep 1
end
end
require 'spec_helper'
describe Gitlab::ExclusiveLease do
it 'cannot obtain twice before the lease has expired' do
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
expect(lease.try_obtain).to eq(true)
expect(lease.try_obtain).to eq(false)
end
it 'can obtain after the lease has expired' do
timeout = 1
lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
lease.try_obtain # start the lease
sleep(2 * timeout) # lease should have expired now
expect(lease.try_obtain).to eq(true)
end
def unique_key
SecureRandom.hex(10)
end
end
......@@ -31,7 +31,7 @@ describe ServiceHook, models: true do
WebMock.stub_request(:post, @service_hook.url)
end
it "POSTs to the web hook URL" do
it "POSTs to the webhook URL" do
@service_hook.execute(@data)
expect(WebMock).to have_requested(:post, @service_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' }
......
......@@ -52,7 +52,7 @@ describe WebHook, models: true do
WebMock.stub_request(:post, @project_hook.url)
end
it "POSTs to the web hook URL" do
it "POSTs to the webhook URL" do
@project_hook.execute(@data, 'push_hooks')
expect(WebMock).to have_requested(:post, @project_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' }
......
......@@ -105,6 +105,31 @@ describe Issue, models: true do
end
end
describe '#referenced_merge_requests' do
it 'returns the referenced merge requests' do
project = create(:project, :public)
mr1 = create(:merge_request,
source_project: project,
source_branch: 'master',
target_branch: 'feature')
mr2 = create(:merge_request,
source_project: project,
source_branch: 'feature',
target_branch: 'master')
issue = create(:issue, description: mr1.to_reference, project: project)
create(:note_on_issue,
noteable: issue,
note: mr2.to_reference,
project_id: project.id)
expect(issue.referenced_merge_requests).to eq([mr1, mr2])
end
end
it_behaves_like 'an editable mentionable' do
subject { create(:issue) }
......
......@@ -188,8 +188,8 @@ describe GitPushService, services: true do
end
describe "Web Hooks" do
context "execute web hooks" do
describe "Webhooks" do
context "execute webhooks" do
it "when pushing a branch for the first time" do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
......
......@@ -78,8 +78,8 @@ describe GitTagPushService, services: true do
end
end
describe "Web Hooks" do
context "execute web hooks" do
describe "Webhooks" do
context "execute webhooks" do
it "when pushing tags" do
expect(project).to receive(:execute_hooks)
service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0')
......
......@@ -11,7 +11,7 @@ describe PostReceive do
end
end
context "web hook" do
context "webhook" do
let(:project) { create(:project) }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment