Commit 9548f061 authored by Robert Speicher's avatar Robert Speicher

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

Conflicts:

- app/services/search/global_service.rb
- app/services/search/project_service.rb
- app/services/search/snippet_service.rb
- app/views/profiles/accounts/show.html.haml
parents 197b1954 c07ef544
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
.sass-cache/ .sass-cache/
.secret .secret
.vagrant .vagrant
.byebug_history
Vagrantfile Vagrantfile
backups/* backups/*
config/aws.yml config/aws.yml
......
...@@ -74,15 +74,6 @@ spec:services: ...@@ -74,15 +74,6 @@ spec:services:
- ruby - ruby
- mysql - mysql
spec:benchmark:
stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
- ruby
- mysql
allow_failure: true
spec:other: spec:other:
stage: test stage: test
script: script:
...@@ -246,22 +237,6 @@ spec:services:ruby22: ...@@ -246,22 +237,6 @@ spec:services:ruby22:
- ruby - ruby
- mysql - mysql
spec:benchmark:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
cache:
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
allow_failure: true
spec:other:ruby22: spec:other:ruby22:
stage: test stage: test
image: ruby:2.2 image: ruby:2.2
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased) v 8.6.0 (unreleased)
- Support Golang subpackage fetching (Stan Hu)
- Contributions to forked projects are included in calendar - Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea) - Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during - Removed the default password from the initial admin account created during
...@@ -9,6 +10,7 @@ v 8.6.0 (unreleased) ...@@ -9,6 +10,7 @@ v 8.6.0 (unreleased)
- Fix issue when pushing to projects ending in .wiki - Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room - Don't load all of GitLab in mail_room
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Strip leading and trailing spaces in URL validator (evuez) - Strip leading and trailing spaces in URL validator (evuez)
...@@ -16,6 +18,7 @@ v 8.6.0 (unreleased) ...@@ -16,6 +18,7 @@ v 8.6.0 (unreleased)
- Return empty array instead of 404 when commit has no statuses in commit status API - 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) - 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) - Rewrite logo to simplify SVG code (Sean Lang)
- Refactor and greatly improve search performance
- Add support for cross-project label references - Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users - Allow search for logged out users
...@@ -27,12 +30,14 @@ v 8.6.0 (unreleased) ...@@ -27,12 +30,14 @@ v 8.6.0 (unreleased)
- Add main language of a project in the list of projects (Tiago Botelho) - 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 - Add ability to show archived projects on dashboard, explore and group pages
- Allow SSL verification to be configurable when importing GitHub projects - Allow SSL verification to be configurable when importing GitHub projects
- Move group activity to separate page
v 8.5.5 v 8.5.5
- Ensure removing a project removes associated Todo entries - Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed - Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages - Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior - Fix "Show all" link behavior
- Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4 v 8.5.4
- Do not cache requests for badges (including builds badge) - Do not cache requests for badges (including builds badge)
...@@ -42,6 +47,7 @@ v 8.5.3 ...@@ -42,6 +47,7 @@ v 8.5.3
- Sort starred projects on dashboard based on last activity by default - Sort starred projects on dashboard based on last activity by default
- Show commit message in JIRA mention comment - Show commit message in JIRA mention comment
- Makes issue page and merge request page usable on mobile browsers. - Makes issue page and merge request page usable on mobile browsers.
- Improved UI for profile settings
v 8.5.2 v 8.5.2
- Fix sidebar overlapping content when screen width was below 1200px - Fix sidebar overlapping content when screen width was below 1200px
......
...@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1' ...@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0' gem 'omniauth-google-oauth2', '~> 0.2.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.4.2' gem 'omniauth-saml', '~> 1.5.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
......
...@@ -380,7 +380,7 @@ GEM ...@@ -380,7 +380,7 @@ GEM
gitlab-license (0.0.4) gitlab-license (0.0.4)
gitlab_emoji (0.3.1) gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1) gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.0) gitlab_git (9.0.1)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -556,8 +556,8 @@ GEM ...@@ -556,8 +556,8 @@ GEM
omniauth-oauth2 (1.3.1) omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0) oauth2 (~> 1.0)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-saml (1.4.2) omniauth-saml (1.5.0)
omniauth (~> 1.1) omniauth (~> 1.3)
ruby-saml (~> 1.1, >= 1.1.1) ruby-saml (~> 1.1, >= 1.1.1)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
...@@ -716,7 +716,7 @@ GEM ...@@ -716,7 +716,7 @@ GEM
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-progressbar (1.7.5) ruby-progressbar (1.7.5)
ruby-saml (1.1.1) ruby-saml (1.1.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
uuid (~> 2.3) uuid (~> 2.3)
ruby2ruby (2.2.0) ruby2ruby (2.2.0)
...@@ -1008,7 +1008,7 @@ DEPENDENCIES ...@@ -1008,7 +1008,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0) omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0) omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.4.2) omniauth-saml (~> 1.5.0)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0) omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
......
...@@ -224,17 +224,17 @@ $ -> ...@@ -224,17 +224,17 @@ $ ->
.off 'breakpoint:change' .off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) -> .on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs' if breakpoint is 'sm' or breakpoint is 'xs'
$gutterIcon = $('aside .gutter-toggle').find('i') $gutterIcon = $('.js-sidebar-toggle').find('i')
if $gutterIcon.hasClass('fa-angle-double-right') if $gutterIcon.hasClass('fa-angle-double-right')
$gutterIcon.closest('a').trigger('click') $gutterIcon.closest('a').trigger('click')
$(document) $(document)
.off 'click', 'aside .gutter-toggle' .off 'click', '.js-sidebar-toggle'
.on 'click', 'aside .gutter-toggle', (e, triggered) -> .on 'click', '.js-sidebar-toggle', (e, triggered) ->
e.preventDefault() e.preventDefault()
$this = $(this) $this = $(this)
$thisIcon = $this.find 'i' $thisIcon = $this.find 'i'
$allGutterToggleIcons = $('.gutter-toggle i') $allGutterToggleIcons = $('.js-sidebar-toggle i')
if $thisIcon.hasClass('fa-angle-double-right') if $thisIcon.hasClass('fa-angle-double-right')
$allGutterToggleIcons $allGutterToggleIcons
.removeClass('fa-angle-double-right') .removeClass('fa-angle-double-right')
......
class @AwardsHandler class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event) => $(".js-add-award").on "click", (event) =>
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
...@@ -9,27 +9,46 @@ class @AwardsHandler ...@@ -9,27 +9,46 @@ class @AwardsHandler
$("html").on 'click', (event) -> $("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible") if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide() $(".emoji-menu").removeClass "is-visible"
$(".awards")
.off "click"
.on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock() @renderFrequentlyUsedBlock()
@setupSearch()
handleClick: (e) ->
e.preventDefault()
emoji = $(this)
.find(".icon")
.data "emoji"
awards_handler.addAward emoji
showEmojiMenu: -> showEmojiMenu: ->
if $(".emoji-menu").length if $(".emoji-menu").length
$(".emoji-menu").show() if $(".emoji-menu").is ".is-visible"
$(".emoji-menu").removeClass "is-visible"
$("#emoji_search").blur()
else
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus() $("#emoji_search").focus()
else else
$.get "/emojis", (response) -> $('.js-add-award').addClass "is-loading"
$(".add-award").after response $.get "/emojis", (response) =>
$(".emoji-menu").show() $('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus() $("#emoji_search").focus()
@setupSearch()
, 200
addAward: (emoji) -> addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji) emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, => @postEmoji emoji, =>
@addAwardToEmojiBar(emoji) @addAwardToEmojiBar(emoji)
$(".emoji-menu").hide() $(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) -> addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji) @addEmojiToFrequentlyUsedList(emoji)
...@@ -39,7 +58,7 @@ class @AwardsHandler ...@@ -39,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji) if @isActive(emoji)
@decrementCounter(emoji) @decrementCounter(emoji)
else else
counter = @findEmojiIcon(emoji).siblings(".counter") counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1) counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active") counter.parent().addClass("active")
@addMeToAuthorList(emoji) @addMeToAuthorList(emoji)
...@@ -53,7 +72,7 @@ class @AwardsHandler ...@@ -53,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active") @findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) -> decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter") counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent() emojiIcon = counter.parent()
if parseInt(counter.text()) > 1 if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1) counter.text(parseInt(counter.text()) - 1)
...@@ -70,9 +89,13 @@ class @AwardsHandler ...@@ -70,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) -> removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent() award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ") authors = award_block
.attr("data-original-title")
.split(", ")
authors.splice(authors.indexOf("me"),1) authors.splice(authors.indexOf("me"),1)
award_block.closest(".award").attr("data-original-title", authors.join(", ")) award_block
.closest(".js-emoji-btn")
.attr("data-original-title", authors.join(", "))
@resetTooltip(award_block) @resetTooltip(award_block)
addMeToAuthorList: (emoji) -> addMeToAuthorList: (emoji) ->
...@@ -98,14 +121,18 @@ class @AwardsHandler ...@@ -98,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji) emojiCssClass = @resolveNameToCssClass(emoji)
nodes = [] nodes = []
nodes.push("<div class='award active' title='me'>") nodes.push(
nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>") "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
nodes.push("<div class='counter'>1</div>") "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
nodes.push("</div>") "<span class='award-control-text js-counter'>1</span>",
"</button>"
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) )
$(".award").tooltip() emoji_node = $(nodes.join("\n"))
.insertBefore(".js-award-holder")
.find(".emoji-icon")
.data("emoji", emoji)
$('.award-control').tooltip()
resolveNameToCssClass: (emoji) -> resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
...@@ -128,7 +155,7 @@ class @AwardsHandler ...@@ -128,7 +155,7 @@ class @AwardsHandler
callback.call() callback.call()
findEmojiIcon: (emoji) -> findEmojiIcon: (emoji) ->
$(".award [data-emoji='#{emoji}']") $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: -> scrollToAwards: ->
$('body, html').animate({ $('body, html').animate({
...@@ -164,13 +191,13 @@ class @AwardsHandler ...@@ -164,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val() term = $(ev.target).val()
# Clean previous search results # Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove() $("ul.emoji-menu-search, h5.emoji-search").remove()
if term if term
# Generate a search result block # Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search") h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show() found_emojis = @searchEmojis(term).show()
ul = $("<ul>").addClass("emoji-search").append(found_emojis) ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide() $(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul) $(".emoji-menu-content").append(h5).append(ul)
else else
......
...@@ -4,6 +4,8 @@ class CiBuild ...@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) -> constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval) clearInterval(CiBuild.interval)
@initScrollButtonAffix()
if build_status == "running" || build_status == "pending" if build_status == "running" || build_status == "pending"
# #
# Bind autoscroll button to follow build output # Bind autoscroll button to follow build output
...@@ -38,4 +40,15 @@ class CiBuild ...@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: -> checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
initScrollButtonAffix: ->
$buildScroll = $('#js-build-scroll')
$body = $('body')
$buildTrace = $('#build-trace')
$buildScroll.affix(
offset:
bottom: ->
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
)
@CiBuild = CiBuild @CiBuild = CiBuild
...@@ -74,8 +74,9 @@ class Dispatcher ...@@ -74,8 +74,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length new TreeView() if $('#tree-slider').length
when 'groups:show' when 'groups:activity'
new Activities() new Activities()
when 'groups:show'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index' when 'groups:group_members:index'
new GroupMembers() new GroupMembers()
......
...@@ -189,7 +189,7 @@ class @MergeRequestTabs ...@@ -189,7 +189,7 @@ class @MergeRequestTabs
$('.container-fluid').removeClass('container-limited') $('.container-fluid').removeClass('container-limited')
shrinkView: -> shrinkView: ->
$gutterIcon = $('.gutter-toggle i') $gutterIcon = $('.js-sidebar-toggle i')
# Wait until listeners are set # Wait until listeners are set
setTimeout( -> setTimeout( ->
...@@ -197,4 +197,3 @@ class @MergeRequestTabs ...@@ -197,4 +197,3 @@ class @MergeRequestTabs
if $gutterIcon.is('.fa-angle-double-right') if $gutterIcon.is('.fa-angle-double-right')
$gutterIcon.closest('a').trigger('click',[true]) $gutterIcon.closest('a').trigger('click',[true])
, 0) , 0)
...@@ -30,6 +30,9 @@ class @Notes ...@@ -30,6 +30,9 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote $(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
# catch note ajax errors
$(document).on "ajax:error", ".js-main-target-form", @addNoteError
# change note in UI after update # change note in UI after update
$(document).on "ajax:success", "form.edit-note", @updateNote $(document).on "ajax:success", "form.edit-note", @updateNote
...@@ -51,6 +54,9 @@ class @Notes ...@@ -51,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
# reset main target form when clicking discard
$(document).on "click", ".js-note-discard", @resetMainTargetForm
# update the file name when an attachment is selected # update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment $(document).on "change", ".js-note-attachment-input", @updateFormAttachment
...@@ -85,6 +91,7 @@ class @Notes ...@@ -85,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text" $(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close" $(document).off "click", ".js-note-target-close"
$(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable') $('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container' $(document).off 'tasklist:changed', '.note .js-task-list-container'
...@@ -219,7 +226,7 @@ class @Notes ...@@ -219,7 +226,7 @@ class @Notes
Resets text and preview. Resets text and preview.
Resets buttons. Resets buttons.
### ###
resetMainTargetForm: -> resetMainTargetForm: (e) =>
form = $(".js-main-target-form") form = $(".js-main-target-form")
# remove validation errors # remove validation errors
...@@ -231,6 +238,8 @@ class @Notes ...@@ -231,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset() form.find(".js-note-text").data("autosave").reset()
@updateTargetButtons(e)
reenableTargetFormSubmitButton: -> reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form") form = $(".js-main-target-form")
...@@ -274,8 +283,10 @@ class @Notes ...@@ -274,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form" form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove() form.find('.div-dropzone').remove()
# hide discard button
form.find('.js-note-discard').hide()
# setup preview buttons # setup preview buttons
form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button") previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text") textarea = form.find(".js-note-text")
...@@ -309,6 +320,10 @@ class @Notes ...@@ -309,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) => addNote: (xhr, note, status) =>
@renderNote(note) @renderNote(note)
addNoteError: (xhr, note, status) =>
flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
flash.pinTo('.md-area')
### ###
Called in response to the new note form being submitted Called in response to the new note form being submitted
...@@ -469,6 +484,11 @@ class @Notes ...@@ -469,6 +484,11 @@ class @Notes
form.find("#note_line_code").val dataHolder.data("lineCode") form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType") form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId") form.find("#note_noteable_id").val dataHolder.data("noteableId")
form.find('.js-note-discard')
.show()
.removeClass('js-note-discard')
.addClass('js-close-discussion-note-form')
.text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form @setupNoteForm form
form.find(".js-note-text").focus() form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form" form.addClass "js-discussion-note-form"
...@@ -568,21 +588,52 @@ class @Notes ...@@ -568,21 +588,52 @@ class @Notes
updateCloseButton: (e) => updateCloseButton: (e) =>
textarea = $(e.target) textarea = $(e.target)
form = textarea.parents('form') form = textarea.parents('form')
form.find('.js-note-target-close').text('Close') closebtn = form.find('.js-note-target-close')
closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) => updateTargetButtons: (e) =>
textarea = $(e.target) textarea = $(e.target)
form = textarea.parents('form') form = textarea.parents('form')
reopenbtn = form.find('.js-note-target-reopen')
closebtn = form.find('.js-note-target-close')
discardbtn = form.find('.js-note-discard')
if textarea.val().trim().length > 0 if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen') reopentext = reopenbtn.data('alternative-text')
form.find('.js-note-target-close').text('Comment & close') closetext = closebtn.data('alternative-text')
form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
form.find('.js-note-target-close').addClass('btn-comment-and-close') if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.addClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.addClass('btn-comment-and-close')
if discardbtn.is(':hidden')
discardbtn.show()
else else
form.find('.js-note-target-reopen').text('Reopen') reopentext = reopenbtn.data('original-text')
form.find('.js-note-target-close').text('Close') closetext = closebtn.data('original-text')
form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
form.find('.js-note-target-close').removeClass('btn-comment-and-close') if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.removeClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible')
discardbtn.hide()
initTaskList: -> initTaskList: ->
@enableTaskList() @enableTaskList()
......
...@@ -4,12 +4,13 @@ class @Profile ...@@ -4,12 +4,13 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit() $(this).parents('form').submit()
$('.update-username form').on 'ajax:before', -> $('.update-username').on 'ajax:before', ->
$('.loading-gif').show() $('.loading-username').show()
$(this).find('.update-success').hide() $(this).find('.update-success').hide()
$(this).find('.update-failed').hide() $(this).find('.update-failed').hide()
$('.update-username form').on 'ajax:complete', -> $('.update-username').on 'ajax:complete', ->
$('.loading-username').hide()
$(this).find('.btn-save').enable() $(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide() $(this).find('.loading-gif').hide()
......
...@@ -157,3 +157,7 @@ ...@@ -157,3 +157,7 @@
float: right; float: right;
} }
} }
.content-block-small {
padding: 10px 0;
}
...@@ -12,11 +12,13 @@ ...@@ -12,11 +12,13 @@
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px } .prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px } .prepend-left-10 { margin-left:10px }
.prepend-left-default { margin-left:$gl-padding } .prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left:20px } .prepend-left-20 { margin-left:20px }
.append-right-5 { margin-right: 5px } .append-right-5 { margin-right: 5px }
.append-right-10 { margin-right:10px } .append-right-10 { margin-right:10px }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right:20px } .append-right-20 { margin-right:20px }
.append-bottom-0 { margin-bottom:0 }
.append-bottom-10 { margin-bottom:10px } .append-bottom-10 { margin-bottom:10px }
.append-bottom-15 { margin-bottom:15px } .append-bottom-15 { margin-bottom:15px }
.append-bottom-20 { margin-bottom:20px } .append-bottom-20 { margin-bottom:20px }
......
...@@ -294,7 +294,7 @@ ...@@ -294,7 +294,7 @@
} }
.dropdown-content { .dropdown-content {
max-height: 200px; max-height: 215px;
overflow-y: scroll; overflow-y: scroll;
} }
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
border-bottom: none; border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-max) {
width: 100%; width: 100%;
} }
} }
......
...@@ -34,13 +34,15 @@ $error-exclamation-point: #E62958; ...@@ -34,13 +34,15 @@ $error-exclamation-point: #E62958;
$border-radius-default: 3px; $border-radius-default: 3px;
$list-title-color: #333333; $list-title-color: #333333;
$list-text-color: #555555; $list-text-color: #555555;
$profile-settings-link-color: $md-link-color;
$btn-transparent-color: #8F8F8F; $btn-transparent-color: #8F8F8F;
$ssh-key-icon-color: #8F8F8F; $ssh-key-icon-color: #8F8F8F;
$ssh-key-icon-size: 18px; $ssh-key-icon-size: 18px;
$provider-btn-group-border: #E5E5E5;
$provider-btn-not-active-color: #4688F1;
/* /*
* Color schema * Color schema
*/ */
...@@ -70,7 +72,7 @@ $orange-light: rgba(252, 109, 38, 0.80); ...@@ -70,7 +72,7 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #E75E40; $orange-normal: #E75E40;
$orange-dark: #CE5237; $orange-dark: #CE5237;
$red-light: #F43263; $red-light: #F06559;
$red-normal: #E52C5A; $red-normal: #E52C5A;
$red-dark: #D22852; $red-dark: #D22852;
...@@ -94,7 +96,7 @@ $border-orange-light: #fc6d26; ...@@ -94,7 +96,7 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #CE5237; $border-orange-normal: #CE5237;
$border-orange-dark: #C14E35; $border-orange-dark: #C14E35;
$border-red-light: #E52C5A; $border-red-light: #F24F41;
$border-red-normal: #D22852; $border-red-normal: #D22852;
$border-red-dark: #CA264F; $border-red-dark: #CA264F;
...@@ -150,3 +152,10 @@ $dropdown-toggle-border-color: #EAEAEA; ...@@ -150,3 +152,10 @@ $dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4; $dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Award emoji
*/
$award-emoji-menu-bg: #FFF;
$award-emoji-menu-border: #F1F2F4;
$award-emoji-new-btn-icon-color: #DCDCDC;
.awards { .awards {
@include clearfix;
line-height: 34px; line-height: 34px;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 7px 0 0 5px;
}
.award {
@include border-radius(5px);
border: 1px solid;
padding: 0px 10px;
float: left;
margin-right: 5px;
border-color: $border-color;
cursor: pointer;
&:hover {
background-color: #dce0e5;
}
&.active {
border-color: $border-gray-light;
background-color: $gray-light;
&:hover {
background-color: #dce0e5;
}
.counter {
font-weight: bold;
}
}
.icon {
float: left;
margin-right: 10px;
}
.counter {
float: left;
}
}
.awards-controls {
position: relative;
margin-left: 10px;
float: left;
.add-award {
font-size: 24px;
color: $gl-gray;
position: relative;
top: 2px;
&:hover,
&:link {
text-decoration: none;
}
} }
}
.emoji-menu{ .emoji-menu {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
margin-top: 3px;
z-index: 1000; z-index: 1000;
display: none;
float: left;
min-width: 160px; min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px; font-size: 14px;
text-align: left; background-color: $award-emoji-menu-bg;
list-style: none; border: 1px solid $award-emoji-menu-border;
background-color: #fff; border-radius: $border-radius-base;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
box-shadow: 0 6px 12px rgba(0,0,0,.175); box-shadow: 0 6px 12px rgba(0,0,0,.175);
pointer-events: none;
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
&.is-visible {
pointer-events: all;
opacity: 1;
transform: scale(1);
}
.emoji-menu-content { .emoji-menu-content {
padding: $gl-padding; padding: $gl-padding;
...@@ -90,36 +37,97 @@ ...@@ -90,36 +37,97 @@
height: 300px; height: 300px;
overflow-y: scroll; overflow-y: scroll;
h5 { input.emoji-search{
clear: left; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
} }
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
} }
}
input.emoji-search{ .emoji-menu-list {
background: image-url("icon-search.png") 240px no-repeat; list-style: none;
} padding-left: 0;
margin-bottom: 0;
}
.emoji-menu-list-item {
padding: 3px;
margin-left: 1px;
margin-right: 1px;
}
li { .emoji-menu-btn {
display: block;
cursor: pointer; cursor: pointer;
width: 30px; width: 30px;
height: 30px; height: 30px;
text-align: center; padding: 0;
float: left; background: none;
margin: 3px; border: 0;
list-decorate: none; border-radius: $border-radius-base;
@include border-radius(5px); transition: transform .15s cubic-bezier(.3, 0, .2, 2);
&:hover { &:hover {
background-color: #ccc; background-color: transparent;
outline: 0;
transform: scale(1.3);
} }
&:focus,
&:active {
outline: 0;
} }
.emoji-icon {
display: inline-block;
position: relative;
top: 3px;
}
}
.award-menu-holder {
display: inline-block;
position: relative;
}
.award-control {
margin-right: 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
outline: 0;
&.active,
&:active {
background-color: $white-dark;
box-shadow: none;
outline: 0;
}
&.is-loading {
.award-control-icon {
display: none;
}
.award-control-icon-loading {
display: block;
} }
} }
.icon,
.award-control-icon {
float: left;
margin-right: 5px;
font-size: 20px;
}
.award-control-icon-loading {
display: none;
}
.award-control-icon {
color: $award-emoji-new-btn-icon-color;
} }
} }
...@@ -27,10 +27,25 @@ ...@@ -27,10 +27,25 @@
} }
.scroll-controls { .scroll-controls {
position: fixed; &.affix-top {
bottom: 10px; position: absolute;
left: 250px; top: 10px;
z-index: 100; right: 25px;
}
&.affix-bottom {
position: absolute;
right: 25px;
}
&.affix {
right: 30px;
bottom: 15px;
@media (min-width: $screen-md-min) {
right: 26%;
}
}
a { a {
display: block; display: block;
......
.account-page { .profile-avatar-form-option {
fieldset { hr {
margin-bottom: 15px; margin: 10px 0;
padding-bottom: 15px;
} }
} }
...@@ -20,7 +19,7 @@ ...@@ -20,7 +19,7 @@
.account-btn-link, .account-btn-link,
.profile-settings-sidebar a { .profile-settings-sidebar a {
color: $profile-settings-link-color; color: $md-link-color;
} }
.oauth-buttons { .oauth-buttons {
...@@ -172,6 +171,47 @@ ...@@ -172,6 +171,47 @@
.profile-settings-content { .profile-settings-content {
a { a {
color: $profile-settings-link-color; color: $md-link-color;
}
}
.change-username-title {
color: $gl-warning;
}
.remove-account-title {
color: $gl-danger;
}
.provider-btn-group {
display: inline-block;
margin-right: 10px;
border: 1px solid $provider-btn-group-border;
border-radius: 3px;
&:last-child {
margin-right: 0;
}
}
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
border-right: 1px solid $provider-btn-group-border;
> img {
width: 20px;
}
}
.provider-btn {
display: inline-block;
padding: 5px 10px;
margin-left: -3px;
line-height: 22px;
background-color: $gray-light;
&.not-active {
color: $provider-btn-not-active-color;
} }
} }
...@@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController ...@@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController
# Load group projects # Load group projects
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete] before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
before_action :event_filter, only: [:show, :events] before_action :event_filter, only: [:activity]
layout :determine_layout layout :determine_layout
...@@ -64,8 +64,10 @@ class GroupsController < Groups::ApplicationController ...@@ -64,8 +64,10 @@ class GroupsController < Groups::ApplicationController
end end
end end
def events def activity
respond_to do |format| respond_to do |format|
format.html
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json("events/_events", @events.count)
......
class ProjectsController < ApplicationController class ProjectsController < ApplicationController
include ExtractsPath include ExtractsPath
prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity] skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create] before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create] before_action :repository, except: [:new, :create]
...@@ -254,16 +253,6 @@ class ProjectsController < ApplicationController ...@@ -254,16 +253,6 @@ class ProjectsController < ApplicationController
end end
end end
def render_go_import
return unless params["go-get"] == "1"
@namespace = params[:namespace_id]
@id = params[:project_id] || params[:id]
@id = @id.gsub(/\.git\Z/, "")
render "go_import", layout: false
end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? project.repository_exists? && !project.empty_repo?
end end
......
...@@ -245,10 +245,17 @@ class IssuableFinder ...@@ -245,10 +245,17 @@ class IssuableFinder
items items
end end
def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming'
end
def by_milestone(items) def by_milestone(items)
if milestones? if milestones?
if filter_by_no_milestone? if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil]) items = items.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone?
upcoming = Milestone.where(project_id: projects).upcoming
items = items.joins(:milestone).where(milestones: { title: upcoming.title })
else else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
......
...@@ -53,7 +53,10 @@ class ProjectsFinder ...@@ -53,7 +53,10 @@ class ProjectsFinder
def all_projects(current_user) def all_projects(current_user)
if current_user if current_user
[current_user.authorized_projects, public_and_internal_projects] [
current_user.authorized_projects,
public_and_internal_projects
]
else else
[Project.public_only] [Project.public_only]
end end
......
...@@ -72,7 +72,7 @@ module ApplicationHelper ...@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User) if user_or_email.is_a?(User)
user = user_or_email user = user_or_email
else else
user = User.find_by(email: user_or_email.try(:downcase)) user = User.find_by_any_email(user_or_email.try(:downcase))
end end
if user if user
......
...@@ -59,6 +59,7 @@ module MilestonesHelper ...@@ -59,6 +59,7 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any) grouped_milestones.unshift(Milestone::Any)
grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end end
......
...@@ -46,9 +46,23 @@ module Ci ...@@ -46,9 +46,23 @@ module Ci
acts_as_taggable acts_as_taggable
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# This method performs a *partial* match on tokens, thus a query for "a"
# will match any runner where the token contains the letter "a". As a result
# you should *not* use this method for non-admin purposes as otherwise users
# might be able to query a list of all runners.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search(query) def self.search(query)
where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query', t = arel_table
query: "%#{query.try(:downcase)}%") pattern = "%#{query}%"
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end end
def set_default_values def set_default_values
......
...@@ -61,12 +61,29 @@ module Issuable ...@@ -61,12 +61,29 @@ module Issuable
end end
module ClassMethods module ClassMethods
# Searches for records with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(title) like :query", query: "%#{query.downcase}%") where(arel_table[:title].matches("%#{query}%"))
end end
# Searches for records with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def full_search(query) def full_search(query)
where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%") t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
def sort(method) def sort(method)
......
...@@ -37,8 +37,18 @@ class Group < Namespace ...@@ -37,8 +37,18 @@ class Group < Namespace
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
class << self class << self
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%") table = Namespace.arel_table
pattern = "%#{query}%"
where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end end
def sort(method) def sort(method)
......
...@@ -138,7 +138,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -138,7 +138,6 @@ class MergeRequest < ActiveRecord::Base
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :merged, -> { with_state(:merged) } scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) }
...@@ -165,6 +164,24 @@ class MergeRequest < ActiveRecord::Base ...@@ -165,6 +164,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/) super("merge_requests", /(?<merge_request>\d+)/)
end end
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
# ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
# using multiple sub-queries especially when combined with an OR statement.
# UNIONs on the other hand perform much better in these cases.
#
# relation - An ActiveRecord::Relation that returns a list of Projects.
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
source = where(source_project_id: relation).select(:id)
target = where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
......
...@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base ...@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id) MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId include InternalId
include Sortable include Sortable
...@@ -59,9 +60,18 @@ class Milestone < ActiveRecord::Base ...@@ -59,9 +60,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title alias_attribute :name, :title
class << self class << self
# Searches for milestones matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
query = "%#{query}%" t = arel_table
where("title like ? or description like ?", query, query) pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
end end
...@@ -73,6 +83,10 @@ class Milestone < ActiveRecord::Base ...@@ -73,6 +83,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/) super("milestones", /(?<milestone>\d+)/)
end end
def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]") escaped_title = self.title.gsub("]", "\\]")
......
...@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base ...@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end end
# Searches for namespaces matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation
def search(query) def search(query)
where("name LIKE :query OR path LIKE :query", query: "%#{query}%") t = arel_table
pattern = "%#{query}%"
where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end end
def clean_path(path) def clean_path(path)
......
...@@ -45,6 +45,7 @@ class Note < ActiveRecord::Base ...@@ -45,6 +45,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
before_validation :set_award! before_validation :set_award!
before_validation :clear_blank_line_code!
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
...@@ -65,7 +66,7 @@ class Note < ActiveRecord::Base ...@@ -65,7 +66,7 @@ class Note < ActiveRecord::Base
scope :searchable, ->{ where("is_award IS FALSE AND system IS FALSE") } scope :searchable, ->{ where("is_award IS FALSE AND system IS FALSE") }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") } scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) } scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) } scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) } scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) } scope :common, ->{ where(noteable_type: ["", nil]) }
...@@ -107,8 +108,18 @@ class Note < ActiveRecord::Base ...@@ -107,8 +108,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym [:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end end
# Searches for notes matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%") table = arel_table
pattern = "%#{query}%"
where(table[:note].matches(pattern))
end end
def grouped_awards def grouped_awards
...@@ -371,6 +382,10 @@ class Note < ActiveRecord::Base ...@@ -371,6 +382,10 @@ class Note < ActiveRecord::Base
private private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
def awards_supported? def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line? (for_issue? || for_merge_request?) && !for_diff_line?
end end
......
...@@ -302,13 +302,31 @@ class Project < ActiveRecord::Base ...@@ -302,13 +302,31 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end end
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
# search. On MySQL a regular "LIKE" is used as it's already
# case-insensitive.
#
# query - The search query as a String.
def search(query) def search(query)
ptable = arel_table
ntable = Namespace.arel_table
pattern = "%#{query}%"
projects = select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
namespaces = select(:id).
joins(:namespace). joins(:namespace).
where('LOWER(projects.name) LIKE :query OR where(ntable[:name].matches(pattern))
LOWER(projects.path) LIKE :query OR
LOWER(namespaces.name) LIKE :query OR union = Gitlab::SQL::Union.new([projects, namespaces])
LOWER(projects.description) LIKE :query',
query: "%#{query.try(:downcase)}%") where("projects.id IN (#{union.to_sql})")
end end
def search_by_visibility(level) def search_by_visibility(level)
...@@ -316,7 +334,10 @@ class Project < ActiveRecord::Base ...@@ -316,7 +334,10 @@ class Project < ActiveRecord::Base
end end
def search_by_title(query) def search_by_title(query)
non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") pattern = "%#{query}%"
table = Project.arel_table
non_archived.where(table[:name].matches(pattern))
end end
def find_with_namespace(id) def find_with_namespace(id)
...@@ -1042,13 +1063,13 @@ class Project < ActiveRecord::Base ...@@ -1042,13 +1063,13 @@ class Project < ActiveRecord::Base
end end
def valid_runners_token? token def valid_runners_token? token
self.runners_token && self.runners_token == token self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
# TODO (ayufan): For now we use runners_token (backward compatibility) # TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build # In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token def valid_build_token? token
self.builds_enabled? && self.runners_token && self.runners_token == token self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
def build_coverage_enabled? def build_coverage_enabled?
......
...@@ -26,7 +26,7 @@ class CiService < Service ...@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci' default_value_for :category, 'ci'
def valid_token?(token) def valid_token?(token)
self.respond_to?(:token) && self.token.present? && self.token == token self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def supported_events def supported_events
......
...@@ -114,12 +114,32 @@ class Snippet < ActiveRecord::Base ...@@ -114,12 +114,32 @@ class Snippet < ActiveRecord::Base
end end
class << self class << self
# Searches for snippets with a matching title or file name.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end end
# Searches for snippets with matching content.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search_code(query) def search_code(query)
where('(content LIKE :query)', query: "%#{query}%") table = Snippet.arel_table
pattern = "%#{query}%"
where(table[:content].matches(pattern))
end end
def accessible_to(user) def accessible_to(user)
......
...@@ -294,8 +294,22 @@ class User < ActiveRecord::Base ...@@ -294,8 +294,22 @@ class User < ActiveRecord::Base
end end
end end
# Searches users matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%") table = arel_table
pattern = "%#{query}%"
where(
table[:name].matches(pattern).
or(table[:email].matches(pattern)).
or(table[:username].matches(pattern))
)
end end
def by_login(login) def by_login(login)
......
...@@ -10,13 +10,16 @@ module Search ...@@ -10,13 +10,16 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present? group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user) projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group projects = projects.in_namespace(group.id) if group
project_ids = projects.pluck(:id)
<<<<<<< HEAD
if Gitlab.config.elasticsearch.enabled if Gitlab.config.elasticsearch.enabled
Gitlab::Elastic::SearchResults.new(project_ids, params[:search]) Gitlab::Elastic::SearchResults.new(project_ids, params[:search])
else else
Gitlab::SearchResults.new(project_ids, params[:search]) Gitlab::SearchResults.new(project_ids, params[:search])
end end
=======
Gitlab::SearchResults.new(projects, params[:search])
>>>>>>> ce/master
end end
end end
end end
...@@ -7,6 +7,7 @@ module Search ...@@ -7,6 +7,7 @@ module Search
end end
def execute def execute
<<<<<<< HEAD
if Gitlab.config.elasticsearch.enabled if Gitlab.config.elasticsearch.enabled
Gitlab::Elastic::ProjectSearchResults.new(project.id, Gitlab::Elastic::ProjectSearchResults.new(project.id,
params[:search], params[:search],
...@@ -16,6 +17,11 @@ module Search ...@@ -16,6 +17,11 @@ module Search
params[:search], params[:search],
params[:repository_ref]) params[:repository_ref])
end end
=======
Gitlab::ProjectSearchResults.new(project,
params[:search],
params[:repository_ref])
>>>>>>> ce/master
end end
end end
end end
...@@ -7,6 +7,7 @@ module Search ...@@ -7,6 +7,7 @@ module Search
end end
def execute def execute
<<<<<<< HEAD
snippet_ids = Snippet.accessible_to(current_user).pluck(:id) snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
if Gitlab.config.elasticsearch.enabled if Gitlab.config.elasticsearch.enabled
...@@ -14,6 +15,11 @@ module Search ...@@ -14,6 +15,11 @@ module Search
else else
Gitlab::SnippetSearchResults.new(snippet_ids, params[:search]) Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
end end
=======
snippets = Snippet.accessible_to(current_user)
Gitlab::SnippetSearchResults.new(snippets, params[:search])
>>>>>>> ce/master
end end
end end
end end
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
.emoji-menu-content .emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis| - AwardEmoji.emoji_by_category.each do |category, emojis|
%h5= AwardEmoji::CATEGORIES[category] %h5.emoji-menu-title
%ul = AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji| - emojis.each do |emoji|
%li %li.pull-left.text-center.emoji-menu-list-item
%button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
.hidden-xs
= render "events/event_last_push", event: @last_push
.nav-block
- if current_user
.controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
.content_list
= spinner
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
- page_title "Activity"
- header_title group_title(@group, "Activity", activity_group_path(@group))
%section.activities
= render 'activities'
...@@ -30,9 +30,6 @@ ...@@ -30,9 +30,6 @@
%ul.nav-links %ul.nav-links
%li.active %li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
%li
= link_to "#projects", 'data-toggle' => 'tab' do = link_to "#projects", 'data-toggle' => 'tab' do
Projects Projects
- if @shared_projects.present? - if @shared_projects.present?
...@@ -43,17 +40,7 @@ ...@@ -43,17 +40,7 @@
- if can?(current_user, :read_group, @group) - if can?(current_user, :read_group, @group)
%div{ class: container_class } %div{ class: container_class }
.tab-content .tab-content
.tab-pane.active#activity .tab-pane.active#projects
.activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
= render 'shared/event_filter'
.content_list{data: {href: events_group_path}}
= spinner
.tab-pane#projects
= render "projects", projects: @projects = render "projects", projects: @projects
.tab-pane#shared .tab-pane#shared
= render "shared_projects", projects: @shared_projects = render "shared_projects", projects: @shared_projects
......
...@@ -9,10 +9,15 @@ ...@@ -9,10 +9,15 @@
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do = link_to group_path(@group), title: 'Home' do
= icon('dashboard fw') = icon('group fw')
%span %span
Group Group
- if can?(current_user, :read_group, @group) - if can?(current_user, :read_group, @group)
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: 'Activity' do
= icon('dashboard fw')
%span
Activity
- if current_user - if current_user
= nav_link(controller: [:group, :milestones]) do = nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do = link_to group_milestones_path(@group), title: 'Milestones' do
......
...@@ -5,64 +5,58 @@ ...@@ -5,64 +5,58 @@
.alert.alert-info .alert.alert-info
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.account-page.prepend-top-default .row.prepend-top-default
.panel.panel-default.update-token .col-lg-3.profile-settings-sidebar
.panel-heading %h4.prepend-top-0
Reset Private token Private Token
.panel-body
= form_for @user, url: reset_private_token_profile_path, method: :put do |f|
.data
%p %p
Your private token is used to access application resources without authentication. Your private token is used to access application resources without authentication.
%br .col-lg-9
It can be used for atom feeds or the API. = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f|
%span.cred
Keep it secret!
%p.cgray %p.cgray
- if current_user.private_token - if current_user.private_token
= label_tag "token", "Private token", class: "label-light"
= text_field_tag "token", current_user.private_token, class: "form-control" = text_field_tag "token", current_user.private_token, class: "form-control"
- else - else
%span You don`t have one yet. Click generate to fix it. %span You don`t have one yet. Click generate to fix it.
%p.help-block
.form-actions It can be used for atom feeds or the API. Keep it secret!
.prepend-top-default
- if current_user.private_token - if current_user.private_token
= f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default" = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
- else - else
= f.submit 'Generate', class: "btn btn-default" = f.submit 'Generate', class: "btn btn-default"
%hr
.panel.panel-default .row.prepend-top-default
.panel-heading .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Two-factor Authentication Two-factor Authentication
.panel-body
- if current_user.two_factor_enabled?
.pull-right
= link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm',
data: { confirm: 'Are you sure?' }
%p.text-success
%strong
Two-factor Authentication is enabled
%p
If you lose your recovery codes you can
%strong
= succeed ',' do
= link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' }
invalidating all previous codes.
- else
%p %p
Increase your account's security by enabling two-factor authentication (2FA). Increase your account's security by enabling two-factor authentication (2FA).
.col-lg-9
%p %p
Each time you log in you’ll be required to provide your username and Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
password as usual, plus a randomly-generated code from your phone. - if !current_user.two_factor_enabled?
%p
.form-actions Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
= link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.append-bottom-10
- if button_based_providers.any? = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
.panel.panel-default - else
.panel-heading = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger',
data: { confirm: 'Are you sure?' }
%hr
- if button_based_providers.any?
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Social sign-in
%p
Activate signin with one of the following services
.col-lg-9
%label.label-light
Connected Accounts Connected Accounts
<<<<<<< HEAD
.panel-body .panel-body
.oauth-buttons.append-bottom-10 .oauth-buttons.append-bottom-10
%p Click on icon to activate signin with one of the following services %p Click on icon to activate signin with one of the following services
...@@ -96,20 +90,58 @@ ...@@ -96,20 +90,58 @@
= user_url(@user) = user_url(@user)
.form-actions .form-actions
= f.submit 'Save username', class: "btn btn-warning" = f.submit 'Save username', class: "btn btn-warning"
=======
%p Click on icon to activate signin with one of the following services
- button_based_providers.each do |provider|
.provider-btn-group
.provider-btn-image
= provider_image_tag(provider)
- if auth_active?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
- else
= link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do
Connect
%hr
- if current_user.can_change_username?
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0.change-username-title
Change username
%p
Changing your username will change path to all personal projects!
.col-lg-9
= form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f|
.form-group
= f.label :username, "Path", class: "label-light"
.input-group
.input-group-addon
= "#{root_url}u/"
= f.text_field :username, required: true, class: 'form-control'
.help-block
Current path:
= "#{root_url}u/#{current_user.username}"
.prepend-top-default
= f.button class: "btn btn-warning", type: "submit" do
= icon "spinner spin", class: "hidden loading-username"
Update username
%hr
>>>>>>> ce/master
- if signup_enabled? - if signup_enabled?
.panel.panel-danger.remove-account .row.prepend-top-default
.panel-heading .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0.remove-account-title
Remove account Remove account
.panel-body .col-lg-9
- if @user.can_be_removed? - if @user.can_be_removed?
%p Deleting an account has the following effects: %p
Deleting an account has the following effects:
%ul %ul
%li All user content like authored issues, snippets, comments will be removed %li All user content like authored issues, snippets, comments will be removed
- rp = current_user.personal_projects.count - rp = current_user.personal_projects.count
- unless rp.zero? - unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored %li #{pluralize rp, 'personal project'} will be removed and cannot be restored
.form-actions
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
...@@ -118,3 +150,4 @@ ...@@ -118,3 +150,4 @@
%strong #{@user.solo_owned_groups.map(&:name).join(', ')} %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
%p %p
You must transfer ownership or delete these groups before you can delete your account. You must transfer ownership or delete these groups before you can delete your account.
.append-bottom-default
- page_title 'Two-factor Authentication', 'Account' - page_title 'Two-factor Authentication', 'Account'
%h2.page-title Two-factor Authentication (2FA) .row.prepend-top-default
%p .col-lg-3
Download the Google Authenticator application from App Store for iOS or Google %h4.prepend-top-0
Play for Android and scan this code. Two-factor Authentication (2FA)
%p
Increase your account's security by enabling two-factor authentication (2FA).
.col-lg-9
%p
Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
%p
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.row.append-bottom-10
%hr .col-md-3
= raw @qr_code
= form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f| .col-md-9
.account-well
%p.prepend-top-0.append-bottom-0
Can't scan the code?
%p.prepend-top-0.append-bottom-0
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
= current_user.email
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
%p.two-factor-new-manual-content
Time based: Yes
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error - if @error
.alert.alert-danger .alert.alert-danger
= @error = @error
.form-group .form-group
.col-lg-2.col-lg-offset-2 = label_tag :pin_code, nil, class: "label-light"
= raw @qr_code = text_field_tag :pin_code, nil, class: "form-control", required: true
.col-lg-7.col-lg-offset-1.manual-instructions .prepend-top-default
%h3 Can't scan the code? = submit_tag 'Enable two-factor authentication', class: 'btn btn-success'
%p
To add the entry manually, provide the following details to the
application on your phone.
%dl
%dt Account
%dd= current_user.email
%dl
%dt Key
%dd= current_user.otp_secret.scan(/.{4}/).join(' ')
%dl
%dt Time based
%dd Yes
.form-group
= label_tag :pin_code, nil, class: "control-label"
.col-lg-10
= text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
.form-actions
= submit_tag 'Submit', class: 'btn btn-success'
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
.autoscroll-container .autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
.clearfix .clearfix
.scroll-controls #js-build-scroll.scroll-controls
= link_to '#up-build-trace', class: 'btn' do = link_to '#up-build-trace', class: 'btn' do
%i.fa.fa-angle-up %i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do = link_to '#down-build-trace', class: 'btn' do
......
!!! 5
%html
%head
- web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/')
%meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"}
- content_for :note_actions do - content_for :note_actions do
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes #notes
= render 'projects/notes/notes_with_form' = render 'projects/notes/notes_with_form'
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
%span.hidden-sm.hidden-md.hidden-lg %span.hidden-sm.hidden-md.hidden-lg
= icon('circle-o') = icon('circle-o')
%a.btn.btn-default.pull-right.hidden-sm.hidden-md.hidden-lg.gutter-toggle{ href: "#" } %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left') = icon('angle-double-left')
.issue-meta .issue-meta
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
.merge-requests .merge-requests
= render 'merge_requests' = render 'merge_requests'
.content-block .content-block.content-block-small
= render 'votes/votes_block', votable: @issue = render 'votes/votes_block', votable: @issue
.row .row
......
- content_for :note_actions do - content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request) - if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open? - if @merge_request.open?
= link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.closed? - if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
#notes= render "projects/notes/notes_with_form" #notes= render "projects/notes/notes_with_form"
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
.tab-content .tab-content
#notes.notes.tab-pane.voting_notes #notes.notes.tab-pane.voting_notes
.content-block.oneline-block .content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request = render 'votes/votes_block', votable: @merge_request
.row .row
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= @merge_request.state_human_name = @merge_request.state_human_name
%span.hidden-sm.hidden-md.hidden-lg %span.hidden-sm.hidden-md.hidden-lg
= icon(@merge_request.state_icon_name) = icon(@merge_request.state_icon_name)
%a.btn.btn-default.pull-right.hidden-sm.hidden-md.hidden-lg.gutter-toggle{ href: "#" } %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left') = icon('angle-double-left')
.issue-meta .issue-meta
%strong.identifier %strong.identifier
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
.error-alert .error-alert
.note-form-actions.clearfix .note-form-actions.clearfix
= f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
= yield(:note_actions) = yield(:note_actions)
%a.btn.btn-nr.btn-cancel.js-close-discussion-note-form Cancel %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
Discard draft
...@@ -6,13 +6,20 @@ ...@@ -6,13 +6,20 @@
- else - else
Any Any
%b.caret %b.caret
%ul.dropdown-menu .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Filter results by group
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-content
%ul
%li %li
= link_to search_filter_path(group_id: nil) do = link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do
Any Any
%li.divider
- current_user.authorized_groups.sort_by(&:name).each do |group| - current_user.authorized_groups.sort_by(&:name).each do |group|
%li %li
= link_to search_filter_path(group_id: group.id, project_id: nil) do = link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do
= group.name = group.name
.dropdown.inline.prepend-left-10.project-filter .dropdown.inline.prepend-left-10.project-filter
...@@ -23,11 +30,18 @@ ...@@ -23,11 +30,18 @@
- else - else
Any Any
%b.caret %b.caret
%ul.dropdown-menu .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Filter results by project
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-content
%ul
%li %li
= link_to search_filter_path(project_id: nil) do = link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do
Any Any
%li.divider
- current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
%li %li
= link_to search_filter_path(project_id: project.id, group_id: nil) do = link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do
= project.name_with_namespace = project.name_with_namespace
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
of of
= issuables_count(issuable) = issuables_count(issuable)
%span.pull-right %span.pull-right
%a.gutter-toggle{href: '#'} %a.gutter-toggle.js-sidebar-toggle{href: '#'}
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable) - if prev_issuable = prev_issuable_for(issuable)
......
.awards.votes-block .awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
.award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)} %button.btn.award-control.js-emoji-btn.has_tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}}
= emoji_icon(emoji) = emoji_icon(emoji)
.counter %span.award-control-text.js-counter
= notes.count = notes.count
- if current_user - if current_user
.awards-controls %div.award-menu-holder.js-award-holder
%a.add-award{"href" => "#"} %a.btn.award-control.js-add-award{"href" => "#"}
= icon('smile-o') = icon('smile-o', {class: "award-control-icon"})
= icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
%span.award-control-text
Add
- if current_user - if current_user
:javascript :javascript
...@@ -23,17 +26,3 @@ ...@@ -23,17 +26,3 @@
noteable_id, noteable_id,
aliases aliases
); );
$(".awards").on("click", ".emoji-menu-content li", function(e) {
var emoji = $(this).find(".emoji-icon").data("emoji");
awards_handler.addAward(emoji);
});
$(".awards").on("click", ".award", function(e) {
var emoji = $(this).find(".icon").data("emoji");
awards_handler.addAward(emoji);
});
$(".award").tooltip();
$(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false});
...@@ -34,7 +34,7 @@ module Gitlab ...@@ -34,7 +34,7 @@ module Gitlab
config.encoding = "utf-8" config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file. # Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables) config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables, :import_url)
# Enable escaping HTML in JSON. # Enable escaping HTML in JSON.
config.active_support.escape_html_entities_in_json = true config.active_support.escape_html_entities_in_json = true
......
...@@ -203,11 +203,11 @@ Devise.setup do |config| ...@@ -203,11 +203,11 @@ Devise.setup do |config|
# If you want to use other strategies, that are not supported by Devise, or # If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block. # change the failure app, you can configure them inside the config.warden block.
# #
# config.warden do |manager| config.warden do |manager|
# manager.failure_app = AnotherApp manager.failure_app = Gitlab::DeviseFailure
# manager.intercept_401 = false # manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy # manager.default_strategies(scope: :user).unshift :some_external_strategy
# end end
if Gitlab::LDAP::Config.enabled? if Gitlab::LDAP::Config.enabled?
Gitlab::LDAP::Config.servers.each do |server| Gitlab::LDAP::Config.servers.each do |server|
......
Rails.application.config.middleware.use(Gitlab::Middleware::Go)
# This patches ActiveRecord so indexes created using the MySQL adapter ignore
# any PostgreSQL specific options (e.g. `using: :gin`).
#
# These patches do the following for MySQL:
#
# 1. Indexes created using the :opclasses option are ignored (as they serve no
# purpose on MySQL).
# 2. When creating an index with `using: :gin` the `using` option is discarded
# as :gin is not a valid value for MySQL.
# 3. The `:opclasses` option is stripped from add_index_options in case it's
# used anywhere other than in the add_index methods.
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
alias_method :__gitlab_add_index, :add_index
alias_method :__gitlab_add_index_sql, :add_index_sql
alias_method :__gitlab_add_index_options, :add_index_options
def add_index(table_name, column_name, options = {})
unless options[:opclasses]
__gitlab_add_index(table_name, column_name, options)
end
end
def add_index_sql(table_name, column_name, options = {})
unless options[:opclasses]
__gitlab_add_index_sql(table_name, column_name, options)
end
end
def add_index_options(table_name, column_name, options = {})
if options[:using] and options[:using] == :gin
options = options.dup
options.delete(:using)
end
if options[:opclasses]
options = options.dup
options.delete(:opclasses)
end
__gitlab_add_index_options(table_name, column_name, options)
end
end
end
end
end
# rubocop:disable all
# These changes add support for PostgreSQL operator classes when creating
# indexes and dumping/loading schemas. Taken from Rails pull request
# https://github.com/rails/rails/pull/19090.
#
# License:
#
# Copyright (c) 2004-2016 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'date'
require 'set'
require 'bigdecimal'
require 'bigdecimal/util'
# As the Struct definition is changed in this PR/patch we have to first remove
# the existing one.
ActiveRecord::ConnectionAdapters.send(:remove_const, :IndexDefinition)
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses) #:nodoc:
end
end
end
module ActiveRecord
module ConnectionAdapters # :nodoc:
module SchemaStatements
def add_index_options(table_name, column_name, options = {}) #:nodoc:
column_names = Array(column_name)
index_name = index_name(table_name, column: column_names)
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclasses)
index_type = options[:unique] ? "UNIQUE" : ""
index_type = options[:type].to_s if options.key?(:type)
index_name = options[:name].to_s if options.key?(:name)
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
if options.key?(:algorithm)
algorithm = index_algorithms.fetch(options[:algorithm]) {
raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
}
end
using = "USING #{options[:using]}" if options[:using].present?
if supports_partial_index?
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end
if index_name.length > max_index_length
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
[index_name, index_type, index_columns, index_options, algorithm, using]
end
end
end
end
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module SchemaStatements
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
result = query(<<-SQL, 'SCHEMA')
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
AND t.relname = '#{table_name}'
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
ORDER BY i.relname
SQL
result.map do |row|
index_name = row[0]
unique = row[1] == 't'
indkey = row[2].split(" ")
inddef = row[3]
oid = row[4]
columns = Hash[query(<<-SQL, "SCHEMA")]
SELECT a.attnum, a.attname
FROM pg_attribute a
WHERE a.attrelid = #{oid}
AND a.attnum IN (#{indkey.join(",")})
SQL
column_names = columns.values_at(*indkey).compact
unless column_names.empty?
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
where = inddef.scan(/WHERE (.+)$/).flatten[0]
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass|
column, opclass = column_and_opclass.split(' ').map(&:strip)
[column, opclass] if opclass
end.compact]
IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, opclasses)
end
end.compact
end
def add_index(table_name, column_name, options = {}) #:nodoc:
index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}"
end
protected
def quoted_columns_for_index(column_names, options = {})
column_opclasses = options[:opclasses] || {}
column_names.map {|name| "#{quote_column_name(name)} #{column_opclasses[name]}"}
end
end
end
end
end
module ActiveRecord
class SchemaDumper
private
def indexes(table, stream)
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
statement_parts = [
"add_index #{remove_prefix_and_suffix(index.table).inspect}",
index.columns.inspect,
"name: #{index.name.inspect}",
]
statement_parts << 'unique: true' if index.unique
index_lengths = (index.lengths || []).compact
statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
index_orders = index.orders || {}
statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
statement_parts << "where: #{index.where.inspect}" if index.where
statement_parts << "using: #{index.using.inspect}" if index.using
statement_parts << "type: #{index.type.inspect}" if index.type
statement_parts << "opclasses: #{index.opclasses}" if index.opclasses.present?
" #{statement_parts.join(', ')}"
end
stream.puts add_index_statements.sort.join("\n")
stream.puts
end
end
end
end
...@@ -399,7 +399,7 @@ Rails.application.routes.draw do ...@@ -399,7 +399,7 @@ Rails.application.routes.draw do
get :issues get :issues
get :merge_requests get :merge_requests
get :projects get :projects
get :events get :activity
end end
collection do collection do
......
class AddTrigramIndexesForSearching < ActiveRecord::Migration
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
unless trigrams_enabled?
raise 'You must enable the pg_trgm extension. You can do so by running ' \
'"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \
'done for every GitLab database. For more information see ' \
'http://www.postgresql.org/docs/current/static/sql-createextension.html'
end
# trigram indexes are case-insensitive so we can just index the column
# instead of indexing lower(column)
to_index.each do |table, columns|
columns.each do |column|
execute "CREATE INDEX CONCURRENTLY index_#{table}_on_#{column}_trigram ON #{table} USING gin(#{column} gin_trgm_ops);"
end
end
end
def down
return unless Gitlab::Database.postgresql?
to_index.each do |table, columns|
columns.each do |column|
remove_index table, name: "index_#{table}_on_#{column}_trigram"
end
end
end
def trigrams_enabled?
res = execute("SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' AND installed_version IS NOT NULL;")
row = res.first
row && row['enabled'] == 't' ? true : false
end
def to_index
{
ci_runners: [:token, :description],
issues: [:title, :description],
merge_requests: [:title, :description],
milestones: [:title, :description],
namespaces: [:name, :path],
notes: [:note],
projects: [:name, :path, :description],
snippets: [:title, :file_name],
users: [:username, :name, :email]
}
end
end
class DisallowBlankLineCodeOnNote < ActiveRecord::Migration
def up
execute("UPDATE notes SET line_code = NULL WHERE line_code = ''")
end
def down
# noop
end
end
...@@ -15,6 +15,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -15,6 +15,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
enable_extension "pg_trgm"
create_table "abuse_reports", force: :cascade do |t| create_table "abuse_reports", force: :cascade do |t|
t.integer "reporter_id" t.integer "reporter_id"
...@@ -279,6 +280,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -279,6 +280,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "architecture" t.string "architecture"
end end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
create_table "ci_services", force: :cascade do |t| create_table "ci_services", force: :cascade do |t|
t.string "type" t.string "type"
t.string "title" t.string "title"
...@@ -486,11 +490,13 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -486,11 +490,13 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t| create_table "keys", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
...@@ -627,12 +633,14 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -627,12 +633,14 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree
add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "milestones", force: :cascade do |t| create_table "milestones", force: :cascade do |t|
t.string "title", null: false t.string "title", null: false
...@@ -646,10 +654,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -646,10 +654,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
end end
add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t| create_table "namespaces", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
...@@ -666,8 +676,10 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -666,8 +676,10 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t| create_table "notes", force: :cascade do |t|
...@@ -693,6 +705,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -693,6 +705,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
...@@ -821,9 +834,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -821,9 +834,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
...@@ -901,7 +917,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -901,7 +917,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
...@@ -1037,9 +1055,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -1037,9 +1055,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree
add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
create_table "users_star_projects", force: :cascade do |t| create_table "users_star_projects", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
### CI User documentation ### CI User documentation
- [Get started with GitLab CI](quick_start/README.md) - [Get started with GitLab CI](quick_start/README.md)
- [CI examples for various languages](examples/README.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
- [Learn how `.gitlab-ci.yml` works](yaml/README.md) - [Learn how `.gitlab-ci.yml` works](yaml/README.md)
- [Configure a Runner, the application that runs your builds](runners/README.md) - [Configure a Runner, the application that runs your builds](runners/README.md)
...@@ -14,24 +15,4 @@ ...@@ -14,24 +15,4 @@
- [Build artifacts](build_artifacts/README.md) - [Build artifacts](build_artifacts/README.md)
- [User permissions](permissions/README.md) - [User permissions](permissions/README.md)
- [API](api/README.md) - [API](api/README.md)
- [CI services (linked docker containers)](services/README.md)
### CI Examples
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
- [Test your PHP applications](examples/php.md)
- [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
- [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
- [Test Clojure applications](examples/test-clojure-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
- Help your favorite programming language and GitLab by sending a merge request
with a guide for that language.
### CI Services
GitLab CI uses the `services` keyword to define what docker containers should
be linked with your base image. Below is a list of examples you may use:
- [Using MySQL](services/mysql.md)
- [Using PostgreSQL](services/postgres.md)
- [Using Redis](services/redis.md)
- [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
...@@ -64,7 +64,7 @@ Save the file and restart GitLab: `sudo service gitlab restart`. ...@@ -64,7 +64,7 @@ Save the file and restart GitLab: `sudo service gitlab restart`.
For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line: For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line:
``` ```
gitlab-rails['gitlab_default_projects_features_builds'] = false gitlab_rails['gitlab_default_projects_features_builds'] = false
``` ```
Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`. Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`.
## Build script examples # CI Examples
- [Testing a PHP application](php.md)
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) - [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md) - [Test a Clojure application](test-clojure-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
- Help your favorite programming language and GitLab by sending a merge request
with a guide for that language.
## Languages ## Outside the documentation
This is a list of languages you can test with GitLab CI. Each section has - [Blost post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
comprehensive documentation and comes with a test repository hosted on - [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
GitLab.com. - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
- [Testing PHP](php.md)
...@@ -223,20 +223,13 @@ You can access a builds badge image using following link: ...@@ -223,20 +223,13 @@ You can access a builds badge image using following link:
http://example.gitlab.com/namespace/project/badges/branch/build.svg http://example.gitlab.com/namespace/project/badges/branch/build.svg
``` ```
Awesome! You started using CI in GitLab!
## Examples ## Examples
Visit the [examples README][examples] to see a list of examples using GitLab Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages. CI with various languages.
## Next steps
Awesome! You started using CI in GitLab!
Next you can look into doing more with the CI. Many people are using GitLab
to package, containerize, test and deploy software.
Visit our various languages examples at <https://gitlab.com/groups/gitlab-examples>.
[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation [runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md [examples]: ../examples/README.md
......
# Development # Development
- [Architecture](architecture.md) of GitLab - [Architecture](architecture.md) of GitLab
- [Benchmarking](benchmarking.md)
- [CI setup](ci_setup.md) for testing GitLab - [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid - [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md) - [How to dump production data to staging](db_dump.md)
......
# Benchmarking
GitLab CE comes with a set of benchmarks that are executed for every build. This
makes it easier to measure performance of certain components over time.
Benchmarks are written as RSpec tests using a few extra helpers. To write a
benchmark, first tag the top-level `describe`:
```ruby
describe MaruTheCat, benchmark: true do
end
```
This ensures the benchmark is executed separately from other test collections.
It also exposes the various RSpec matchers used for writing benchmarks to the
test group.
Next, lets write the actual benchmark:
```ruby
describe MaruTheCat, benchmark: true do
let(:maru) { MaruTheChat.new }
describe '#jump_in_box' do
benchmark_subject { maru.jump_in_box }
it { is_expected.to iterate_per_second(9000) }
end
end
```
Here `benchmark_subject` is a small wrapper around RSpec's `subject` method that
makes it easier to specify the subject of a benchmark. Using RSpec's regular
`subject` would require us to write the following instead:
```ruby
subject { -> { maru.jump_in_box } }
```
The `iterate_per_second` matcher defines the amount of times per second a
subject should be executed. The higher the amount of iterations the better.
By default the allowed standard deviation is a maximum of 30%. This can be
adjusted by chaining the `with_maximum_stddev` on the `iterate_per_second`
matcher:
```ruby
it { is_expected.to iterate_per_second(9000).with_maximum_stddev(50) }
```
This can be useful if the code in question depends on external resources of
which the performance can vary a lot (e.g. physical HDDs, network calls, etc).
However, in most cases 30% should be enough so only change this when really
needed.
## Benchmarks Location
Benchmarks should be stored in `spec/benchmarks` and should follow the regular
Rails specs structure. That is, model benchmarks go in `spec/benchmark/models`,
benchmarks for code in the `lib` directory go in `spec/benchmarks/lib`, etc.
## Underlying Technology
The benchmark setup uses [benchmark-ips][benchmark-ips] which takes care of the
heavy lifting such as warming up code, calculating iterations, standard
deviation, etc.
[benchmark-ips]: https://github.com/evanphx/benchmark-ips
...@@ -97,6 +97,17 @@ To change the Unicorn workers when you have the Omnibus package please see [the ...@@ -97,6 +97,17 @@ To change the Unicorn workers when you have the Omnibus package please see [the
If you want to run the database separately expect a size of about 1 MB per user. If you want to run the database separately expect a size of about 1 MB per user.
### PostgreSQL Requirements
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
GitLab database. This extension can be enabled (using a PostgreSQL super user)
by running the following query for every database:
CREATE EXTENSION pg_trgm;
On some systems you may need to install an additional package (e.g.
`postgresql-contrib`) for this extension to become available.
## Redis and Sidekiq ## Redis and Sidekiq
Redis stores all user sessions and the background task queue. Redis stores all user sessions and the background task queue.
......
...@@ -131,6 +131,58 @@ On the sign in page there should now be a SAML button below the regular sign in ...@@ -131,6 +131,58 @@ 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 Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in. will be returned to GitLab and will be signed in.
## Customization
### `attribute_statements`
>**Note:**
This setting is only available on GitLab 8.6 and above.
This setting should only be used to map attributes that are part of the
OmniAuth info hash schema.
`attribute_statements` is used to map Attribute Names in a SAMLResponse to entries
in the OmniAuth [info hash](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema#schema-10-and-later).
For example, if your SAMLResponse contains an Attribute called 'EmailAddress',
specify `{ email: ['EmailAddress'] }` to map the Attribute to the
corresponding key in the info hash. URI-named Attributes are also supported, e.g.
`{ email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] }`.
This setting allows you tell GitLab where to look for certain attributes required
to create an account. Like mentioned above, if your IdP sends the user's email
address as `EmailAddress` instead of `email`, let GitLab know by setting it on
your configuration:
```yaml
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',
attribute_statements: { email: ['EmailAddress'] }
}
```
### `allowed_clock_drift`
The clock of the Identity Provider may drift slightly ahead of your system clocks.
To allow for a small amount of clock drift you can use `allowed_clock_drift` within
your settings. Its value must be given in a number (and/or fraction) of seconds.
The value given is added to the current time at which the response is validated.
```yaml
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',
attribute_statements: { email: ['EmailAddress'] },
allowed_clock_drift: 1 # for one second clock drift
}
```
## Troubleshooting ## Troubleshooting
### 500 error after login ### 500 error after login
......
...@@ -15,6 +15,10 @@ Feature: Groups ...@@ -15,6 +15,10 @@ Feature: Groups
Scenario: I should see group "Owned" dashboard list Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page When I visit group "Owned" page
Then I should see group "Owned" projects list Then I should see group "Owned" projects list
@javascript
Scenario: I should see group "Owned" activity feed
When I visit group "Owned" activity page
And I should see projects activity feed And I should see projects activity feed
Scenario: I should see group "Owned" issues list Scenario: I should see group "Owned" issues list
......
...@@ -99,9 +99,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -99,9 +99,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end end
step 'I reset my token' do step 'I reset my token' do
page.within '.update-token' do page.within '.private-token' do
@old_token = @user.private_token @old_token = @user.private_token
click_button "Reset" click_button "Reset private token"
end end
end end
......
...@@ -10,7 +10,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -10,7 +10,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I click the thumbsup award Emoji' do step 'I click the thumbsup award Emoji' do
page.within '.awards' do page.within '.awards' do
thumbsup = page.find('.award .emoji-1F44D') thumbsup = page.first('.award-control')
thumbsup.click thumbsup.click
thumbsup.hover thumbsup.hover
sleep 0.3 sleep 0.3
...@@ -18,23 +18,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -18,23 +18,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end end
step 'I click to emoji-picker' do step 'I click to emoji-picker' do
page.within '.awards-controls' do page.within '.awards' do
page.find('.add-award').click page.find('.js-add-award').click
end end
end end
step 'I click to emoji in the picker' do step 'I click to emoji in the picker' do
page.within '.emoji-menu-content' do page.within '.emoji-menu-content' do
page.first('.emoji-icon').click page.first('.js-emoji-btn').click
end end
end end
step 'I can remove it by clicking to icon' do step 'I can remove it by clicking to icon' do
page.within '.awards' do page.within '.awards' do
expect do expect do
page.find('.award.active').click page.find('.js-emoji-btn.active').click
sleep 0.3 sleep 0.3
end.to change{ page.all(".award").size }.from(3).to(2) end.to change{ page.all(".award-control.js-emoji-btn").size }.from(3).to(2)
end end
end end
...@@ -49,23 +49,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -49,23 +49,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
sleep 0.2 sleep 0.2
page.within '.awards' do page.within '.awards' do
expect(page).to have_selector '.award' expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.award.active .counter')).to have_content '1' expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
expect(page.find('.award.active')['data-original-title']).to eq('me') expect(page.find('.js-emoji-btn.active')['data-original-title']).to eq('me')
end end
end end
step 'I have no awards added' do step 'I have no awards added' do
page.within '.awards' do page.within '.awards' do
expect(page).to have_selector '.award' expect(page).to have_selector '.award-control.js-emoji-btn'
expect(page.all('.award').size).to eq(2) expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
# Check tooltip data # Check tooltip data
page.all('.award').each do |element| page.all('.award-control.js-emoji-btn').each do |element|
expect(element['title']).to eq("") expect(element['title']).to eq("")
end end
page.all('.award .counter').each do |element| page.all('.award-control .js-counter').each do |element|
expect(element).to have_content '0' expect(element).to have_content '0'
end end
end end
...@@ -79,7 +79,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -79,7 +79,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I leave comment with a single emoji' do step 'I leave comment with a single emoji' do
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
fill_in 'note[note]', with: ':smile:' fill_in 'note[note]', with: ':smile:'
click_button 'Add Comment' click_button 'Comment'
end end
end end
......
...@@ -268,7 +268,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -268,7 +268,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I leave a comment with code block' do step 'I leave a comment with code block' do
page.within(".js-main-target-form") do page.within(".js-main-target-form") do
fill_in "note[note]", with: "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```" fill_in "note[note]", with: "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```"
click_button "Add Comment" click_button "Comment"
sleep 0.05 sleep 0.05
end end
end end
......
...@@ -419,7 +419,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -419,7 +419,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.within(".js-discussion-note-form") do page.within(".js-discussion-note-form") do
fill_in "note_note", with: "Line is correct" fill_in "note_note", with: "Line is correct"
click_button "Add Comment" click_button "Comment"
end end
page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do
...@@ -432,7 +432,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -432,7 +432,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.within(".js-discussion-note-form") do page.within(".js-discussion-note-form") do
fill_in "note_note", with: "Line is wrong on here" fill_in "note_note", with: "Line is wrong on here"
click_button "Add Comment" click_button "Comment"
end end
end end
...@@ -653,7 +653,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -653,7 +653,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
def leave_comment(message) def leave_comment(message)
page.within(".js-discussion-note-form", visible: true) do page.within(".js-discussion-note-form", visible: true) do
fill_in "note_note", with: message fill_in "note_note", with: message
click_button "Add Comment" click_button "Comment"
end end
page.within(".notes_holder", visible: true) do page.within(".notes_holder", visible: true) do
expect(page).to have_content message expect(page).to have_content message
......
...@@ -77,7 +77,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps ...@@ -77,7 +77,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
step 'I leave a comment like "Good snippet!"' do step 'I leave a comment like "Good snippet!"' do
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
fill_in "note_note", with: "Good snippet!" fill_in "note_note", with: "Good snippet!"
click_button "Add Comment" click_button "Comment"
end end
end end
......
...@@ -361,7 +361,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -361,7 +361,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end end
step 'I can see the new rendered SVG image' do step 'I can see the new rendered SVG image' do
expect(find('.file-content')).to have_css('img') expect(page).to have_css('.file-content img')
end end
private private
......
...@@ -93,14 +93,14 @@ module SharedDiffNote ...@@ -93,14 +93,14 @@ module SharedDiffNote
page.within("form[id$='#{sample_commit.line_code}']") do page.within("form[id$='#{sample_commit.line_code}']") do
fill_in 'note[note]', with: ':smile:' fill_in 'note[note]', with: ':smile:'
click_button('Add Comment') click_button('Comment')
end end
end end
end end
step 'I submit the diff comment' do step 'I submit the diff comment' do
page.within(diff_file_selector) do page.within(diff_file_selector) do
click_button("Add Comment") click_button("Comment")
end end
end end
......
...@@ -182,7 +182,7 @@ module SharedIssuable ...@@ -182,7 +182,7 @@ module SharedIssuable
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
fill_in 'note[note]', with: "##{issuable.to_reference(project)}" fill_in 'note[note]', with: "##{issuable.to_reference(project)}"
click_button 'Add Comment' click_button 'Comment'
end end
end end
......
...@@ -17,7 +17,7 @@ module SharedNote ...@@ -17,7 +17,7 @@ module SharedNote
step 'I leave a comment like "XML attached"' do step 'I leave a comment like "XML attached"' do
page.within(".js-main-target-form") do page.within(".js-main-target-form") do
fill_in "note[note]", with: "XML attached" fill_in "note[note]", with: "XML attached"
click_button "Add Comment" click_button "Comment"
end end
end end
...@@ -30,7 +30,7 @@ module SharedNote ...@@ -30,7 +30,7 @@ module SharedNote
step 'I submit the comment' do step 'I submit the comment' do
page.within(".js-main-target-form") do page.within(".js-main-target-form") do
click_button "Add Comment" click_button "Comment"
end end
end end
...@@ -115,7 +115,7 @@ module SharedNote ...@@ -115,7 +115,7 @@ module SharedNote
step 'I leave a comment with a header containing "Comment with a header"' do step 'I leave a comment with a header containing "Comment with a header"' do
page.within(".js-main-target-form") do page.within(".js-main-target-form") do
fill_in "note[note]", with: "# Comment with a header" fill_in "note[note]", with: "# Comment with a header"
click_button "Add Comment" click_button "Comment"
sleep 0.05 sleep 0.05
end end
end end
......
...@@ -27,6 +27,10 @@ module SharedPaths ...@@ -27,6 +27,10 @@ module SharedPaths
visit group_path(Group.find_by(name: "Owned")) visit group_path(Group.find_by(name: "Owned"))
end end
step 'I visit group "Owned" activity page' do
visit activity_group_path(Group.find_by(name: "Owned"))
end
step 'I visit group "Owned" issues page' do step 'I visit group "Owned" issues page' do
visit issues_group_path(Group.find_by(name: "Owned")) visit issues_group_path(Group.find_by(name: "Owned"))
end end
......
...@@ -10,7 +10,7 @@ module Ci ...@@ -10,7 +10,7 @@ module Ci
attr_reader :before_script, :image, :services, :variables, :path, :cache attr_reader :before_script, :image, :services, :variables, :path, :cache
def initialize(config, path = nil) def initialize(config, path = nil)
@config = YAML.safe_load(config, [Symbol]) @config = YAML.safe_load(config, [Symbol], [], true)
@path = path @path = path
unless @config.is_a? Hash unless @config.is_a? Hash
......
module Gitlab
class DeviseFailure < Devise::FailureApp
protected
# Override `Devise::FailureApp#request_format` to handle a special case
#
# This tells Devise to handle an unauthenticated `.zip` request as an HTML
# request (i.e., redirect to sign in).
#
# Otherwise, Devise would respond with a 401 Unauthorized with
# `Content-Type: application/zip` and a response body in plaintext, and the
# browser would freak out.
#
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/12944
def request_format
if request.format == :zip
Mime::Type.lookup_by_extension(:html).ref
else
super
end
end
end
end
...@@ -45,12 +45,15 @@ module Gitlab ...@@ -45,12 +45,15 @@ module Gitlab
direction: :asc).each do |raw_data| direction: :asc).each do |raw_data|
pull_request = PullRequestFormatter.new(project, raw_data) pull_request = PullRequestFormatter.new(project, raw_data)
if !pull_request.cross_project? && pull_request.valid? if pull_request.valid?
merge_request = MergeRequest.create!(pull_request.attributes) merge_request = MergeRequest.new(pull_request.attributes)
if merge_request.save
import_comments(pull_request.number, merge_request) import_comments(pull_request.number, merge_request)
import_comments_on_diff(pull_request.number, merge_request) import_comments_on_diff(pull_request.number, merge_request)
end end
end end
end
true true
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
......
...@@ -17,16 +17,12 @@ module Gitlab ...@@ -17,16 +17,12 @@ module Gitlab
} }
end end
def cross_project?
source_repo.id != target_repo.id
end
def number def number
raw_data.number raw_data.number
end end
def valid? def valid?
source_branch.present? && target_branch.present? !cross_project? && source_branch.present? && target_branch.present?
end end
private private
...@@ -53,6 +49,10 @@ module Gitlab ...@@ -53,6 +49,10 @@ module Gitlab
raw_data.body || "" raw_data.body || ""
end end
def cross_project?
source_repo.present? && target_repo.present? && source_repo.id != target_repo.id
end
def description def description
formatter.author_line(author) + body formatter.author_line(author) + body
end end
......
# A dumb middleware that returns a Go HTML document if the go-get=1 query string
# is used irrespective if the namespace/project exists
module Gitlab
module Middleware
class Go
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if go_request?(request)
render_go_doc(request)
else
@app.call(env)
end
end
private
def render_go_doc(request)
body = go_body(request)
response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
response.finish
end
def go_request?(request)
request["go-get"].to_i == 1 && request.env["PATH_INFO"].present?
end
def go_body(request)
base_url = Gitlab.config.gitlab.url
# Go subpackages may be in the form of namespace/project/path1/path2/../pathN
# We can just ignore the paths and leave the namespace/project
path_info = request.env["PATH_INFO"]
path_info.sub!(/^\//, '')
project_path = path_info.split('/').first(2).join('/')
request_url = URI.join(base_url, project_path)
domain_path = strip_url(request_url.to_s)
"<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n";
end
def strip_url(url)
url.gsub(/\Ahttps?:\/\//, '')
end
end
end
end
...@@ -2,8 +2,8 @@ module Gitlab ...@@ -2,8 +2,8 @@ module Gitlab
class ProjectSearchResults < SearchResults class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref attr_reader :project, :repository_ref
def initialize(project_id, query, repository_ref = nil) def initialize(project, query, repository_ref = nil)
@project = Project.find(project_id) @project = project
@repository_ref = if repository_ref.present? @repository_ref = if repository_ref.present?
repository_ref repository_ref
else else
...@@ -73,7 +73,7 @@ module Gitlab ...@@ -73,7 +73,7 @@ module Gitlab
end end
def notes def notes
Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC') project.notes.user.search(query).order('updated_at DESC')
end end
def commits def commits
...@@ -84,8 +84,8 @@ module Gitlab ...@@ -84,8 +84,8 @@ module Gitlab
end end
end end
def limit_project_ids def project_ids_relation
[project.id] project
end end
end end
end end
...@@ -2,12 +2,12 @@ module Gitlab ...@@ -2,12 +2,12 @@ module Gitlab
class SearchResults class SearchResults
attr_reader :query attr_reader :query
# Limit search results by passed project ids # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
attr_reader :limit_project_ids attr_reader :limit_projects
def initialize(limit_project_ids, query) def initialize(limit_projects, query)
@limit_project_ids = limit_project_ids || Project.all @limit_projects = limit_projects || Project.all
@query = Shellwords.shellescape(query) if query.present? @query = Shellwords.shellescape(query) if query.present?
end end
...@@ -27,7 +27,8 @@ module Gitlab ...@@ -27,7 +27,8 @@ module Gitlab
end end
def total_count def total_count
@total_count ||= projects_count + issues_count + merge_requests_count + milestones_count @total_count ||= projects_count + issues_count + merge_requests_count +
milestones_count
end end
def projects_count def projects_count
...@@ -53,27 +54,29 @@ module Gitlab ...@@ -53,27 +54,29 @@ module Gitlab
private private
def projects def projects
Project.where(id: limit_project_ids).search(query) limit_projects.search(query)
end end
def issues def issues
issues = Issue.where(project_id: limit_project_ids) issues = Issue.where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/ if query =~ /#(\d+)\z/
issues = issues.where(iid: $1) issues = issues.where(iid: $1)
else else
issues = issues.full_search(query) issues = issues.full_search(query)
end end
issues.order('updated_at DESC') issues.order('updated_at DESC')
end end
def milestones def milestones
milestones = Milestone.where(project_id: limit_project_ids) milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query) milestones = milestones.search(query)
milestones.order('updated_at DESC') milestones.order('updated_at DESC')
end end
def merge_requests def merge_requests
merge_requests = MergeRequest.in_projects(limit_project_ids) merge_requests = MergeRequest.in_projects(project_ids_relation)
if query =~ /[#!](\d+)\z/ if query =~ /[#!](\d+)\z/
merge_requests = merge_requests.where(iid: $1) merge_requests = merge_requests.where(iid: $1)
else else
...@@ -89,5 +92,9 @@ module Gitlab ...@@ -89,5 +92,9 @@ module Gitlab
def per_page def per_page
20 20
end end
def project_ids_relation
limit_projects.select(:id).reorder(nil)
end
end end
end end
...@@ -2,10 +2,10 @@ module Gitlab ...@@ -2,10 +2,10 @@ module Gitlab
class SnippetSearchResults < SearchResults class SnippetSearchResults < SearchResults
include SnippetsHelper include SnippetsHelper
attr_reader :limit_snippet_ids attr_reader :limit_snippets
def initialize(limit_snippet_ids, query) def initialize(limit_snippets, query)
@limit_snippet_ids = limit_snippet_ids @limit_snippets = limit_snippets
@query = query @query = query
end end
...@@ -35,11 +35,11 @@ module Gitlab ...@@ -35,11 +35,11 @@ module Gitlab
private private
def snippet_titles def snippet_titles
Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC') limit_snippets.search(query).order('updated_at DESC')
end end
def snippet_blobs def snippet_blobs
Snippet.where(id: limit_snippet_ids).search_code(query).order('updated_at DESC') limit_snippets.search_code(query).order('updated_at DESC')
end end
def default_scope def default_scope
......
...@@ -46,20 +46,11 @@ namespace :spec do ...@@ -46,20 +46,11 @@ namespace :spec do
run_commands(cmds) run_commands(cmds)
end end
desc 'GitLab | Rspec | Run benchmark specs'
task :benchmark do
cmds = [
%W(rake gitlab:setup),
%W(rspec spec --tag @benchmark)
]
run_commands(cmds)
end
desc 'GitLab | Rspec | Run other specs' desc 'GitLab | Rspec | Run other specs'
task :other do task :other do
cmds = [ cmds = [
%W(rake gitlab:setup), %W(rake gitlab:setup),
%W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services --tag ~@benchmark) %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
] ]
run_commands(cmds) run_commands(cmds)
end end
...@@ -69,7 +60,7 @@ desc "GitLab | Run specs" ...@@ -69,7 +60,7 @@ desc "GitLab | Run specs"
task :spec do task :spec do
cmds = [ cmds = [
%W(rake gitlab:setup), %W(rake gitlab:setup),
%W(rspec spec --tag ~@benchmark), %W(rspec spec),
] ]
run_commands(cmds) run_commands(cmds)
end end
......
require 'spec_helper'
describe IssuesFinder, benchmark: true do
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:label1) { create(:label, project: project, title: 'A') }
let(:label2) { create(:label, project: project, title: 'B') }
before do
10.times do |n|
issue = create(:issue, author: user, project: project)
if n > 4
create(:label_link, label: label1, target: issue)
create(:label_link, label: label2, target: issue)
end
end
end
describe 'retrieving issues without labels' do
let(:finder) do
IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
state: 'opened')
end
benchmark_subject { finder.execute }
it { is_expected.to iterate_per_second(2000) }
end
describe 'retrieving issues with labels' do
let(:finder) do
IssuesFinder.new(user, scope: 'all', label_name: label1.title,
state: 'opened')
end
benchmark_subject { finder.execute }
it { is_expected.to iterate_per_second(1000) }
end
describe 'retrieving issues for a single project' do
let(:finder) do
IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
state: 'opened', project_id: project.id)
end
benchmark_subject { finder.execute }
it { is_expected.to iterate_per_second(2000) }
end
end
end
require 'spec_helper'
describe TrendingProjectsFinder, benchmark: true do
describe '#execute' do
let(:finder) { described_class.new }
let(:user) { create(:user) }
# to_a is used to force actually running the query (instead of just building
# it).
benchmark_subject { finder.execute(user).non_archived.to_a }
it { is_expected.to iterate_per_second(500) }
end
end
require 'spec_helper'
describe Banzai::Filter::ReferenceFilter, benchmark: true do
let(:input) do
html = <<-EOF
<p>Hello @alice and @bob, how are you doing today?</p>
<p>This is simple @dummy text to see how the @ReferenceFilter class performs
when @processing HTML.</p>
EOF
Nokogiri::HTML.fragment(html)
end
let(:project) { create(:empty_project) }
let(:filter) { described_class.new(input, project: project) }
describe '#replace_text_nodes_matching' do
let(:iterations) { 6000 }
describe 'with identical input and output HTML' do
benchmark_subject do
filter.replace_text_nodes_matching(User.reference_pattern) do |content|
content
end
end
it { is_expected.to iterate_per_second(iterations) }
end
describe 'with different input and output HTML' do
benchmark_subject do
filter.replace_text_nodes_matching(User.reference_pattern) do |content|
'@eve'
end
end
it { is_expected.to iterate_per_second(iterations) }
end
end
end
require 'spec_helper'
describe Milestone, benchmark: true do
describe '#sort_issues' do
let(:milestone) { create(:milestone) }
let(:issue1) { create(:issue, milestone: milestone) }
let(:issue2) { create(:issue, milestone: milestone) }
let(:issue3) { create(:issue, milestone: milestone) }
let(:issue_ids) { [issue3.id, issue2.id, issue1.id] }
benchmark_subject { milestone.sort_issues(issue_ids) }
it { is_expected.to iterate_per_second(500) }
end
end
require 'spec_helper'
describe Project, benchmark: true do
describe '.trending' do
let(:group) { create(:group) }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :public, group: group) }
let(:iterations) { 500 }
before do
2.times do
create(:note_on_commit, project: project1)
end
create(:note_on_commit, project: project2)
end
describe 'without an explicit start date' do
benchmark_subject { described_class.trending.to_a }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'with an explicit start date' do
let(:date) { 1.month.ago }
benchmark_subject { described_class.trending(date).to_a }
it { is_expected.to iterate_per_second(iterations) }
end
end
describe '.find_with_namespace' do
let(:group) { create(:group, name: 'sisinmaru') }
let(:project) { create(:project, name: 'maru', namespace: group) }
describe 'using a capitalized namespace' do
benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') }
it { is_expected.to iterate_per_second(600) }
end
describe 'using a lowercased namespace' do
benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') }
it { is_expected.to iterate_per_second(600) }
end
end
end
require 'spec_helper'
describe ProjectTeam, benchmark: true do
describe '#max_member_access' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
5.times do
project.team << [create(:user), :reporter]
project.group.add_user(create(:user), :reporter)
end
end
benchmark_subject { project.team.max_member_access(user.id) }
it { is_expected.to iterate_per_second(35000) }
end
end
require 'spec_helper'
describe User, benchmark: true do
describe '.all' do
before do
10.times { create(:user) }
end
benchmark_subject { User.all.to_a }
it { is_expected.to iterate_per_second(500) }
end
describe '.by_login' do
before do
%w{Alice Bob Eve}.each do |name|
create(:user,
email: "#{name}@gitlab.com",
username: name,
name: name)
end
end
# The iteration count is based on the query taking little over 1 ms when
# using PostgreSQL.
let(:iterations) { 900 }
describe 'using a capitalized username' do
benchmark_subject { User.by_login('Alice') }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'using a lowercase username' do
benchmark_subject { User.by_login('alice') }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'using a capitalized Email address' do
benchmark_subject { User.by_login('Alice@gitlab.com') }
it { is_expected.to iterate_per_second(iterations) }
end
describe 'using a lowercase Email address' do
benchmark_subject { User.by_login('alice@gitlab.com') }
it { is_expected.to iterate_per_second(iterations) }
end
end
describe '.find_by_any_email' do
let(:user) { create(:user) }
describe 'using a user with only a single Email address' do
let(:email) { user.email }
benchmark_subject { User.find_by_any_email(email) }
it { is_expected.to iterate_per_second(1000) }
end
describe 'using a user with multiple Email addresses' do
let(:email) { user.emails.first.email }
benchmark_subject { User.find_by_any_email(email) }
before do
10.times do
user.emails.create(email: FFaker::Internet.email)
end
end
it { is_expected.to iterate_per_second(1000) }
end
end
end
require 'spec_helper'
describe Projects::CreateService, benchmark: true do
describe '#execute' do
let(:user) { create(:user, :admin) }
let(:group) do
group = create(:group)
create(:group_member, group: group, user: user)
group
end
benchmark_subject do
name = SecureRandom.hex
service = described_class.new(user,
name: name,
path: name,
namespace_id: group.id,
visibility_level: Gitlab::VisibilityLevel::PUBLIC)
service.execute
end
it { is_expected.to iterate_per_second(0.5) }
end
end
...@@ -2,14 +2,24 @@ require "spec_helper" ...@@ -2,14 +2,24 @@ require "spec_helper"
describe Projects::RepositoriesController do describe Projects::RepositoriesController do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) }
describe "GET archive" do describe "GET archive" do
context 'as a guest' do
it 'responds with redirect in correct format' do
get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
expect(response.content_type).to start_with 'text/html'
expect(response).to be_redirect
end
end
context 'as a user' do
let(:user) { create(:user) }
before do before do
sign_in(user)
project.team << [user, :developer] project.team << [user, :developer]
sign_in(user)
end end
it "uses Gitlab::Workhorse" do it "uses Gitlab::Workhorse" do
expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip") expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
...@@ -29,4 +39,5 @@ describe Projects::RepositoriesController do ...@@ -29,4 +39,5 @@ describe Projects::RepositoriesController do
end end
end end
end end
end
end end
...@@ -9,19 +9,6 @@ describe ProjectsController do ...@@ -9,19 +9,6 @@ describe ProjectsController do
describe "GET show" do describe "GET show" do
context "when requested by `go get`" do
render_views
it "renders the go-import meta tag" do
get :show, "go-get" => "1", namespace_id: "bogus_namespace", id: "bogus_project"
expect(response.body).to include("name='go-import'")
content = "localhost/bogus_namespace/bogus_project git http://localhost/bogus_namespace/bogus_project.git"
expect(response.body).to include("content='#{content}'")
end
end
context "rendering default project view" do context "rendering default project view" do
render_views render_views
......
...@@ -22,7 +22,7 @@ describe 'Comments', feature: true do ...@@ -22,7 +22,7 @@ describe 'Comments', feature: true do
it 'should be valid' do it 'should be valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1) is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
expect(find('.js-main-target-form input[type=submit]').value). expect(find('.js-main-target-form input[type=submit]').value).
to eq('Add Comment') to eq('Comment')
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
expect(page).not_to have_link('Cancel') expect(page).not_to have_link('Cancel')
end end
...@@ -49,7 +49,7 @@ describe 'Comments', feature: true do ...@@ -49,7 +49,7 @@ describe 'Comments', feature: true do
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
fill_in 'note[note]', with: 'This is awsome!' fill_in 'note[note]', with: 'This is awsome!'
find('.js-md-preview-button').click find('.js-md-preview-button').click
click_button 'Add Comment' click_button 'Comment'
end end
end end
...@@ -202,7 +202,7 @@ describe 'Comments', feature: true do ...@@ -202,7 +202,7 @@ describe 'Comments', feature: true do
before do before do
page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do
fill_in 'note[note]', with: 'Another comment on line 10' fill_in 'note[note]', with: 'Another comment on line 10'
click_button('Add Comment') click_button('Comment')
end end
end end
......
...@@ -427,6 +427,45 @@ module Ci ...@@ -427,6 +427,45 @@ module Ci
end end
end end
describe "YAML Alias/Anchor" do
it "is correctly supported for jobs" do
config = <<EOT
job1: &JOBTMPL
script: execute-script-for-job
job2: *JOBTMPL
EOT
config_processor = GitlabCiYamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(2)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
except: nil,
stage: "test",
stage_idx: 1,
name: :job1,
only: nil,
commands: "\nexecute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false
})
expect(config_processor.builds_for_stage_and_ref("test", "master").second).to eq({
except: nil,
stage: "test",
stage_idx: 1,
name: :job2,
only: nil,
commands: "\nexecute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false
})
end
end
describe "Error handling" do describe "Error handling" do
it "fails to parse YAML" do it "fails to parse YAML" do
expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
......
...@@ -127,34 +127,6 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -127,34 +127,6 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
end end
describe '#cross_project?' do
context 'when source, and target repositories are the same' do
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.cross_project?).to eq false
end
end
context 'when source repo is a fork' do
let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns true' do
expect(pull_request.cross_project?).to eq true
end
end
context 'when target repo is a fork' do
let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns true' do
expect(pull_request.cross_project?).to eq true
end
end
end
describe '#number' do describe '#number' do
let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
...@@ -166,7 +138,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -166,7 +138,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
describe '#valid?' do describe '#valid?' do
let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') } let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') }
context 'when source and target branches exists' do context 'when source, and target repositories are the same' do
context 'and source and target branches exists' do
let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) } let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
it 'returns true' do it 'returns true' do
...@@ -174,7 +147,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -174,7 +147,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
end end
context 'when source branch doesn not exists' do context 'and source branch doesn not exists' do
let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) } let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
it 'returns false' do it 'returns false' do
...@@ -182,7 +155,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -182,7 +155,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
end end
context 'when target branch doesn not exists' do context 'and target branch doesn not exists' do
let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) } let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
it 'returns false' do it 'returns false' do
...@@ -190,4 +163,23 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -190,4 +163,23 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
end end
end end
context 'when source repo is a fork' do
let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
end
end
context 'when target repo is a fork' do
let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
end
end
end
end end
require 'spec_helper'
describe Gitlab::Middleware::Go, lib: true do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
describe '#call' do
describe 'when go-get=0' do
it 'skips go-import generation' do
env = { 'rack.input' => '',
'QUERY_STRING' => 'go-get=0' }
expect(app).to receive(:call).with(env).and_return('no-go')
middleware.call(env)
end
end
describe 'when go-get=1' do
it 'returns a document' do
env = { 'rack.input' => '',
'QUERY_STRING' => 'go-get=1',
'PATH_INFO' => '/group/project/path' }
resp = middleware.call(env)
expect(resp[0]).to eq(200)
expect(resp[1]['Content-Type']).to eq('text/html')
expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n"
expect(resp[2].body).to eq([expected_body])
end
end
end
end
...@@ -5,7 +5,7 @@ describe Gitlab::ProjectSearchResults, lib: true do ...@@ -5,7 +5,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:query) { 'hello world' } let(:query) { 'hello world' }
describe 'initialize with empty ref' do describe 'initialize with empty ref' do
let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, '') } let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') }
it { expect(results.project).to eq(project) } it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil } it { expect(results.repository_ref).to be_nil }
...@@ -14,7 +14,7 @@ describe Gitlab::ProjectSearchResults, lib: true do ...@@ -14,7 +14,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
describe 'initialize with ref' do describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' } let(:ref) { 'refs/heads/test' }
let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, ref) } let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) }
it { expect(results.project).to eq(project) } it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) } it { expect(results.repository_ref).to eq(ref) }
......
require 'spec_helper'
describe Gitlab::SearchResults do
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
let!(:merge_request) do
create(:merge_request, source_project: project, title: 'foo')
end
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:results) { described_class.new(Project.all, 'foo') }
describe '#total_count' do
it 'returns the total amount of search hits' do
expect(results.total_count).to eq(4)
end
end
describe '#projects_count' do
it 'returns the total amount of projects' do
expect(results.projects_count).to eq(1)
end
end
describe '#issues_count' do
it 'returns the total amount of issues' do
expect(results.issues_count).to eq(1)
end
end
describe '#merge_requests_count' do
it 'returns the total amount of merge requests' do
expect(results.merge_requests_count).to eq(1)
end
end
describe '#milestones_count' do
it 'returns the total amount of milestones' do
expect(results.milestones_count).to eq(1)
end
end
describe '#empty?' do
it 'returns true when there are no search results' do
allow(results).to receive(:total_count).and_return(0)
expect(results.empty?).to eq(true)
end
it 'returns false when there are search results' do
expect(results.empty?).to eq(false)
end
end
end
require 'spec_helper'
describe Gitlab::SnippetSearchResults do
let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
let(:results) { described_class.new(Snippet.all, 'foo') }
describe '#total_count' do
it 'returns the total amount of search hits' do
expect(results.total_count).to eq(2)
end
end
describe '#snippet_titles_count' do
it 'returns the amount of matched snippet titles' do
expect(results.snippet_titles_count).to eq(1)
end
end
describe '#snippet_blobs_count' do
it 'returns the amount of matched snippet blobs' do
expect(results.snippet_blobs_count).to eq(1)
end
end
end
...@@ -132,4 +132,32 @@ describe Ci::Runner, models: true do ...@@ -132,4 +132,32 @@ describe Ci::Runner, models: true do
expect(runner.belongs_to_one_project?).to be_truthy expect(runner.belongs_to_one_project?).to be_truthy
end end
end end
describe '#search' do
let(:runner) { create(:ci_runner, token: '123abc') }
it 'returns runners with a matching token' do
expect(described_class.search(runner.token)).to eq([runner])
end
it 'returns runners with a partially matching token' do
expect(described_class.search(runner.token[0..2])).to eq([runner])
end
it 'returns runners with a matching token regardless of the casing' do
expect(described_class.search(runner.token.upcase)).to eq([runner])
end
it 'returns runners with a matching description' do
expect(described_class.search(runner.description)).to eq([runner])
end
it 'returns runners with a partially matching description' do
expect(described_class.search(runner.description[0..2])).to eq([runner])
end
it 'returns runners with a matching description regardless of the casing' do
expect(described_class.search(runner.description.upcase)).to eq([runner])
end
end
end end
...@@ -32,9 +32,54 @@ describe Issue, "Issuable" do ...@@ -32,9 +32,54 @@ describe Issue, "Issuable" do
describe ".search" do describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") } let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
it "matches by title" do it 'returns notes with a matching title' do
expect(described_class.search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
expect(described_class.search('able')).to eq([searchable_issue]) expect(described_class.search('able')).to eq([searchable_issue])
end end
it 'returns notes with a matching title regardless of the casing' do
expect(described_class.search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
end
describe ".full_search" do
let!(:searchable_issue) do
create(:issue, title: "Searchable issue", description: 'kittens')
end
it 'returns notes with a matching title' do
expect(described_class.full_search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
expect(described_class.full_search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
expect(described_class.full_search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
it 'returns notes with a matching description' do
expect(described_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching description' do
expect(described_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a matching description regardless of the casing' do
expect(described_class.full_search(searchable_issue.description.upcase)).
to eq([searchable_issue])
end
end end
describe "#today?" do describe "#today?" do
......
...@@ -103,4 +103,30 @@ describe Group, models: true do ...@@ -103,4 +103,30 @@ describe Group, models: true do
expect(group.avatar_type).to eq(["only images allowed"]) expect(group.avatar_type).to eq(["only images allowed"])
end end
end end
describe '.search' do
it 'returns groups with a matching name' do
expect(described_class.search(group.name)).to eq([group])
end
it 'returns groups with a partially matching name' do
expect(described_class.search(group.name[0..2])).to eq([group])
end
it 'returns groups with a matching name regardless of the casing' do
expect(described_class.search(group.name.upcase)).to eq([group])
end
it 'returns groups with a matching path' do
expect(described_class.search(group.path)).to eq([group])
end
it 'returns groups with a partially matching path' do
expect(described_class.search(group.path[0..2])).to eq([group])
end
it 'returns groups with a matching path regardless of the casing' do
expect(described_class.search(group.path.upcase)).to eq([group])
end
end
end end
...@@ -80,6 +80,12 @@ describe MergeRequest, models: true do ...@@ -80,6 +80,12 @@ describe MergeRequest, models: true do
it { is_expected.to respond_to(:merge_when_build_succeeds) } it { is_expected.to respond_to(:merge_when_build_succeeds) }
end end
describe '.in_projects' do
it 'returns the merge requests for a set of projects' do
expect(described_class.in_projects(Project.all)).to eq([subject])
end
end
describe '#to_reference' do describe '#to_reference' do
it 'returns a String reference to the object' do it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}" expect(subject.to_reference).to eq "!#{subject.iid}"
......
...@@ -181,4 +181,34 @@ describe Milestone, models: true do ...@@ -181,4 +181,34 @@ describe Milestone, models: true do
expect(issue4.position).to eq(42) expect(issue4.position).to eq(42)
end end
end end
describe '.search' do
let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
it 'returns milestones with a matching title' do
expect(described_class.search(milestone.title)).to eq([milestone])
end
it 'returns milestones with a partially matching title' do
expect(described_class.search(milestone.title[0..2])).to eq([milestone])
end
it 'returns milestones with a matching title regardless of the casing' do
expect(described_class.search(milestone.title.upcase)).to eq([milestone])
end
it 'returns milestones with a matching description' do
expect(described_class.search(milestone.description)).to eq([milestone])
end
it 'returns milestones with a partially matching description' do
expect(described_class.search(milestone.description[0..2])).
to eq([milestone])
end
it 'returns milestones with a matching description regardless of the casing' do
expect(described_class.search(milestone.description.upcase)).
to eq([milestone])
end
end
end end
...@@ -41,13 +41,32 @@ describe Namespace, models: true do ...@@ -41,13 +41,32 @@ describe Namespace, models: true do
it { expect(namespace.human_name).to eq(namespace.owner_name) } it { expect(namespace.human_name).to eq(namespace.owner_name) }
end end
describe :search do describe '.search' do
before do let(:namespace) { create(:namespace) }
@namespace = create :namespace
it 'returns namespaces with a matching name' do
expect(described_class.search(namespace.name)).to eq([namespace])
end
it 'returns namespaces with a partially matching name' do
expect(described_class.search(namespace.name[0..2])).to eq([namespace])
end
it 'returns namespaces with a matching name regardless of the casing' do
expect(described_class.search(namespace.name.upcase)).to eq([namespace])
end
it 'returns namespaces with a matching path' do
expect(described_class.search(namespace.path)).to eq([namespace])
end end
it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) } it 'returns namespaces with a partially matching path' do
it { expect(Namespace.search('unknown')).to eq([]) } expect(described_class.search(namespace.path[0..2])).to eq([namespace])
end
it 'returns namespaces with a matching path regardless of the casing' do
expect(described_class.search(namespace.path.upcase)).to eq([namespace])
end
end end
describe :move_dir do describe :move_dir do
......
...@@ -140,10 +140,16 @@ describe Note, models: true do ...@@ -140,10 +140,16 @@ describe Note, models: true do
end end
end end
describe :search do describe '.search' do
let!(:note) { create(:note, note: "WoW") } let(:note) { create(:note, note: 'WoW') }
it { expect(Note.search('wow')).to include(note) } it 'returns notes with matching content' do
expect(described_class.search(note.note)).to eq([note])
end
it 'returns notes with matching content regardless of the casing' do
expect(described_class.search('WOW')).to eq([note])
end
end end
describe :grouped_awards do describe :grouped_awards do
...@@ -220,4 +226,12 @@ describe Note, models: true do ...@@ -220,4 +226,12 @@ describe Note, models: true do
expect(note.is_award?).to be_falsy expect(note.is_award?).to be_falsy
end end
end end
describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ')
expect { note.valid? }.to change(note, :line_code).to(nil)
end
end
end end
...@@ -666,6 +666,58 @@ describe Project, models: true do ...@@ -666,6 +666,58 @@ describe Project, models: true do
end end
end end
describe '.search' do
let(:project) { create(:project, description: 'kitten mittens') }
it 'returns projects with a matching name' do
expect(described_class.search(project.name)).to eq([project])
end
it 'returns projects with a partially matching name' do
expect(described_class.search(project.name[0..2])).to eq([project])
end
it 'returns projects with a matching name regardless of the casing' do
expect(described_class.search(project.name.upcase)).to eq([project])
end
it 'returns projects with a matching description' do
expect(described_class.search(project.description)).to eq([project])
end
it 'returns projects with a partially matching description' do
expect(described_class.search('kitten')).to eq([project])
end
it 'returns projects with a matching description regardless of the casing' do
expect(described_class.search('KITTEN')).to eq([project])
end
it 'returns projects with a matching path' do
expect(described_class.search(project.path)).to eq([project])
end
it 'returns projects with a partially matching path' do
expect(described_class.search(project.path[0..2])).to eq([project])
end
it 'returns projects with a matching path regardless of the casing' do
expect(described_class.search(project.path.upcase)).to eq([project])
end
it 'returns projects with a matching namespace name' do
expect(described_class.search(project.namespace.name)).to eq([project])
end
it 'returns projects with a partially matching namespace name' do
expect(described_class.search(project.namespace.name[0..2])).to eq([project])
end
it 'returns projects with a matching namespace name regardless of the casing' do
expect(described_class.search(project.namespace.name.upcase)).to eq([project])
end
end
describe '#rename_repo' do describe '#rename_repo' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:gitlab_shell) { Gitlab::Shell.new } let(:gitlab_shell) { Gitlab::Shell.new }
...@@ -728,4 +780,20 @@ describe Project, models: true do ...@@ -728,4 +780,20 @@ describe Project, models: true do
project.expire_caches_before_rename('foo') project.expire_caches_before_rename('foo')
end end
end end
describe '.search_by_title' do
let(:project) { create(:project, name: 'kittens') }
it 'returns projects with a matching name' do
expect(described_class.search_by_title(project.name)).to eq([project])
end
it 'returns projects with a partially matching name' do
expect(described_class.search_by_title('kitten')).to eq([project])
end
it 'returns projects with a matching name regardless of the casing' do
expect(described_class.search_by_title('KITTENS')).to eq([project])
end
end
end end
...@@ -59,4 +59,48 @@ describe Snippet, models: true do ...@@ -59,4 +59,48 @@ describe Snippet, models: true do
expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}"
end end
end end
describe '.search' do
let(:snippet) { create(:snippet) }
it 'returns snippets with a matching title' do
expect(described_class.search(snippet.title)).to eq([snippet])
end
it 'returns snippets with a partially matching title' do
expect(described_class.search(snippet.title[0..2])).to eq([snippet])
end
it 'returns snippets with a matching title regardless of the casing' do
expect(described_class.search(snippet.title.upcase)).to eq([snippet])
end
it 'returns snippets with a matching file name' do
expect(described_class.search(snippet.file_name)).to eq([snippet])
end
it 'returns snippets with a partially matching file name' do
expect(described_class.search(snippet.file_name[0..2])).to eq([snippet])
end
it 'returns snippets with a matching file name regardless of the casing' do
expect(described_class.search(snippet.file_name.upcase)).to eq([snippet])
end
end
describe '#search_code' do
let(:snippet) { create(:snippet, content: 'class Foo; end') }
it 'returns snippets with matching content' do
expect(described_class.search_code(snippet.content)).to eq([snippet])
end
it 'returns snippets with partially matching content' do
expect(described_class.search_code('class')).to eq([snippet])
end
it 'returns snippets with matching content regardless of the casing' do
expect(described_class.search_code('FOO')).to eq([snippet])
end
end
end end
...@@ -476,17 +476,43 @@ describe User, models: true do ...@@ -476,17 +476,43 @@ describe User, models: true do
end end
end end
describe 'search' do describe '.search' do
let(:user1) { create(:user, username: 'James', email: 'james@testing.com') } let(:user) { create(:user) }
let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') }
it 'returns users with a matching name' do
it "should be case insensitive" do expect(described_class.search(user.name)).to eq([user])
expect(User.search(user1.username.upcase).to_a).to eq([user1]) end
expect(User.search(user1.username.downcase).to_a).to eq([user1])
expect(User.search(user2.username.upcase).to_a).to eq([user2]) it 'returns users with a partially matching name' do
expect(User.search(user2.username.downcase).to_a).to eq([user2]) expect(described_class.search(user.name[0..2])).to eq([user])
expect(User.search(user1.username.downcase).to_a.size).to eq(2) end
expect(User.search(user2.username.downcase).to_a.size).to eq(1)
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search(user.name.upcase)).to eq([user])
end
it 'returns users with a matching Email' do
expect(described_class.search(user.email)).to eq([user])
end
it 'returns users with a partially matching Email' do
expect(described_class.search(user.email[0..2])).to eq([user])
end
it 'returns users with a matching Email regardless of the casing' do
expect(described_class.search(user.email.upcase)).to eq([user])
end
it 'returns users with a matching username' do
expect(described_class.search(user.username)).to eq([user])
end
it 'returns users with a partially matching username' do
expect(described_class.search(user.username[0..2])).to eq([user])
end
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search(user.username.upcase)).to eq([user])
end end
end end
......
...@@ -14,7 +14,6 @@ require File.expand_path("../../config/environment", __FILE__) ...@@ -14,7 +14,6 @@ require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails' require 'rspec/rails'
require 'shoulda/matchers' require 'shoulda/matchers'
require 'sidekiq/testing/inline' require 'sidekiq/testing/inline'
require 'benchmark/ips'
require 'rspec/retry' require 'rspec/retry'
# Requires supporting ruby files with custom matchers and macros, etc, # Requires supporting ruby files with custom matchers and macros, etc,
...@@ -38,7 +37,6 @@ RSpec.configure do |config| ...@@ -38,7 +37,6 @@ RSpec.configure do |config|
config.include ActiveJob::TestHelper config.include ActiveJob::TestHelper
config.include StubGitlabCalls config.include StubGitlabCalls
config.include StubGitlabData config.include StubGitlabData
config.include BenchmarkMatchers, benchmark: true
config.infer_spec_type_from_file_location! config.infer_spec_type_from_file_location!
config.raise_errors_for_deprecations! config.raise_errors_for_deprecations!
......
module BenchmarkMatchers
extend RSpec::Matchers::DSL
def self.included(into)
into.extend(ClassMethods)
end
matcher :iterate_per_second do |min_iterations|
supports_block_expectations
match do |block|
@max_stddev ||= 30
@entry = benchmark(&block)
expect(@entry.ips).to be >= min_iterations
expect(@entry.stddev_percentage).to be <= @max_stddev
end
chain :with_maximum_stddev do |value|
@max_stddev = value
end
description do
"run at least #{min_iterations} iterations per second"
end
failure_message do
ips = @entry.ips.round(2)
stddev = @entry.stddev_percentage.round(2)
"expected at least #{min_iterations} iterations per second " \
"with a maximum stddev of #{@max_stddev}%, instead of " \
"#{ips} iterations per second with a stddev of #{stddev}%"
end
end
# Benchmarks the given block and returns a Benchmark::IPS::Report::Entry.
def benchmark(&block)
report = Benchmark.ips(quiet: true) do |bench|
bench.report do
instance_eval(&block)
end
end
report.entries[0]
end
module ClassMethods
# Wraps around rspec's subject method so you can write:
#
# benchmark_subject { SomeClass.some_method }
#
# instead of:
#
# subject { -> { SomeClass.some_method } }
def benchmark_subject(&block)
subject { block }
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment