Commit 436caf4e authored by Baldinof's avatar Baldinof

Merge branch 'master' into fix_remove_fork_link

parents e8c72354 ca3fc229
......@@ -15,6 +15,7 @@
.sass-cache/
.secret
.vagrant
.byebug_history
Vagrantfile
backups/*
config/aws.yml
......
......@@ -71,15 +71,6 @@ spec:services:
- ruby
- mysql
spec:benchmark:
stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
- ruby
- mysql
allow_failure: true
spec:other:
stage: test
script:
......@@ -243,22 +234,6 @@ spec:services:ruby22:
- ruby
- 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:
stage: test
image: ruby:2.2
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
- Support Golang subpackage fetching (Stan Hu)
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
......@@ -9,6 +10,7 @@ v 8.6.0 (unreleased)
- Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- 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)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Strip leading and trailing spaces in URL validator (evuez)
......@@ -16,9 +18,14 @@ v 8.6.0 (unreleased)
- Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Rewrite logo to simplify SVG code (Sean Lang)
- Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
- Ignore jobs that start with `.` (hidden jobs)
- Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
- Refactor and greatly improve search performance
- Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
- Allow to define on which builds the current one depends on
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip)
......@@ -27,12 +34,15 @@ v 8.6.0 (unreleased)
- 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
- Remove fork link closes all merge requests opened on source project (Florent Baldino)
- Move group activity to separate page
- Continue parameters are checked to ensure redirection goes to the same instance
v 8.5.5
- Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
- Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4
- Do not cache requests for badges (including builds badge)
......
......@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0'
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-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
......
......@@ -358,7 +358,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.0)
gitlab_git (9.0.1)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -532,8 +532,8 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-saml (1.4.2)
omniauth (~> 1.1)
omniauth-saml (1.5.0)
omniauth (~> 1.3)
ruby-saml (~> 1.1, >= 1.1.1)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
......@@ -692,7 +692,7 @@ GEM
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.7.5)
ruby-saml (1.1.1)
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby2ruby (2.2.0)
......@@ -975,7 +975,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.4.2)
omniauth-saml (~> 1.5.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
......
......@@ -108,6 +108,8 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
bootstrapBreakpoint = bp.getBreakpointSize()
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
......@@ -256,35 +258,14 @@ $ ->
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
bootstrapBreakpoint = undefined;
checkBootstrapBreakpoints = ->
if $('.device-xs').is(':visible')
bootstrapBreakpoint = "xs"
else if $('.device-sm').is(':visible')
bootstrapBreakpoint = "sm"
else if $('.device-md').is(':visible')
bootstrapBreakpoint = "md"
else if $('.device-lg').is(':visible')
bootstrapBreakpoint = "lg"
setBootstrapBreakpoints = ->
if $('.device-xs').length
return
$("body")
.append('<div class="device-xs visible-xs"></div>'+
'<div class="device-sm visible-sm"></div>'+
'<div class="device-md visible-md"></div>'+
'<div class="device-lg visible-lg"></div>')
checkBootstrapBreakpoints()
fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint
checkBootstrapBreakpoints()
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
......@@ -293,6 +274,5 @@ $ ->
.on "resize", (e) ->
fitSidebarForSize()
setBootstrapBreakpoints()
checkInitialSidebarSize()
new Aside()
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event) =>
$(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
......@@ -9,27 +9,46 @@ class @AwardsHandler
$("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide()
$(".emoji-menu").removeClass "is-visible"
$(".awards")
.off "click"
.on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock()
@setupSearch()
handleClick: (e) ->
e.preventDefault()
emoji = $(this)
.find(".icon")
.data "emoji"
awards_handler.addAward emoji
showEmojiMenu: ->
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()
else
$.get "/emojis", (response) ->
$(".add-award").after response
$(".emoji-menu").show()
$('.js-add-award').addClass "is-loading"
$.get "/emojis", (response) =>
$('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus()
@setupSearch()
, 200
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
$(".emoji-menu").hide()
$(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
......@@ -39,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji)
@decrementCounter(emoji)
else
counter = @findEmojiIcon(emoji).siblings(".counter")
counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
......@@ -53,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
......@@ -70,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) ->
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)
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)
addMeToAuthorList: (emoji) ->
......@@ -98,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
nodes.push("<div class='counter'>1</div>")
nodes.push("</div>")
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
$(".award").tooltip()
nodes.push(
"<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
"<span class='award-control-text js-counter'>1</span>",
"</button>"
)
emoji_node = $(nodes.join("\n"))
.insertBefore(".js-award-holder")
.find(".emoji-icon")
.data("emoji", emoji)
$('.award-control').tooltip()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
......@@ -128,7 +155,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
$(".award [data-emoji='#{emoji}']")
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
......@@ -164,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val()
# Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove()
$("ul.emoji-menu-search, h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
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").append(h5).append(ul)
else
......
class @Breakpoints
instance = null;
class BreakpointInstance
BREAKPOINTS = ["xs", "sm", "md", "lg"]
constructor: ->
@setup()
setup: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}"
return if $(allDeviceSelector.join(",")).length
# Create all the elements
els = $.map BREAKPOINTS, (breakpoint) ->
"<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
$("body").append els.join('')
getBreakpointSize: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}"
$visibleDevice = $(allDeviceSelector.join(",")).filter(":visible")
return $visibleDevice.attr("class").split("visible-")[1]
@get: ->
return instance ?= new BreakpointInstance
$ =>
@bp = Breakpoints.get()
......@@ -74,8 +74,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length
when 'groups:show'
when 'groups:activity'
new Activities()
when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
......@@ -103,6 +104,8 @@ class Dispatcher
new ProjectFork()
when 'projects:artifacts:browse'
new BuildArtifacts()
when 'projects:group_links:index'
new GroupsSelect()
switch path.first()
when 'admin'
......
......@@ -238,13 +238,15 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if !value?
field.remove()
if @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
field.remove()
# Toggle active class for the tick mark
el.toggleClass "is-active"
......
$(document).on("click", '.toggle-nav-collapse', (e) ->
e.preventDefault()
collapsed = 'page-sidebar-collapsed'
expanded = 'page-sidebar-expanded'
collapsed = 'page-sidebar-collapsed'
expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
......@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
niceScrollBars.updateScrollBar();
), 300
$(document).on("click", '.toggle-nav-collapse', (e) ->
e.preventDefault()
toggleSidebar()
)
$ ->
size = bp.getBreakpointSize()
if size is "xs" or size is "sm"
if $('.page-with-sidebar').hasClass(expanded)
toggleSidebar()
......@@ -157,3 +157,7 @@
float: right;
}
}
.content-block-small {
padding: 10px 0;
}
......@@ -294,7 +294,7 @@
}
.dropdown-content {
max-height: 200px;
max-height: 215px;
overflow-y: scroll;
}
......
......@@ -141,22 +141,18 @@ header {
margin-left: $sidebar_collapsed_width;
}
@media (max-width: $screen-md-max) {
.header-collapsed {
.header-collapsed {
margin-left: $sidebar_collapsed_width;
}
.header-expanded {
margin-left: $sidebar_width;
@media (min-width: $screen-md-min) {
@include collapsed-header;
}
}
@media(min-width: $screen-md-max) {
.header-collapsed {
@include collapsed-header;
}
.header-expanded {
margin-left: $sidebar_collapsed_width;
.header-expanded {
@media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
}
......
......@@ -63,7 +63,7 @@
border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
@media (max-width: $screen-sm-max) {
width: 100%;
}
}
......
......@@ -39,7 +39,7 @@
}
.sidebar-wrapper {
z-index: 99;
z-index: 999;
background: $background-color;
}
......@@ -203,7 +203,11 @@
}
@mixin expanded-sidebar {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
......
......@@ -152,3 +152,10 @@ $dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Award emoji
*/
$award-emoji-menu-bg: #FFF;
$award-emoji-menu-border: #F1F2F4;
$award-emoji-new-btn-icon-color: #DCDCDC;
.awards {
@include clearfix;
line-height: 34px;
.emoji-icon {
width: 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;
top: 100%;
left: 0;
margin-top: 3px;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-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);
background-color: $award-emoji-menu-bg;
border: 1px solid $award-emoji-menu-border;
border-radius: $border-radius-base;
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 {
padding: $gl-padding;
......@@ -90,36 +37,97 @@
height: 300px;
overflow-y: scroll;
h5 {
clear: left;
input.emoji-search{
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{
background: image-url("icon-search.png") 240px no-repeat;
}
.emoji-menu-list {
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;
width: 30px;
height: 30px;
text-align: center;
float: left;
margin: 3px;
list-decorate: none;
@include border-radius(5px);
padding: 0;
background: none;
border: 0;
border-radius: $border-radius-base;
transition: transform .15s cubic-bezier(.3, 0, .2, 2);
&: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;
}
}
module ContinueParams
extend ActiveSupport::Concern
def continue_params
continue_params = params[:continue]
return nil unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
return unless continue_params[:to] && continue_params[:to].start_with?('/')
continue_params
end
end
......@@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController
# Load group projects
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
......@@ -46,6 +46,8 @@ class GroupsController < Groups::ApplicationController
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@shared_projects = @group.shared_projects
respond_to do |format|
format.html
......@@ -62,8 +64,10 @@ class GroupsController < Groups::ApplicationController
end
end
def events
def activity
respond_to do |format|
format.html
format.json do
load_events
pager_json("events/_events", @events.count)
......@@ -131,7 +135,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
params.require(:group).permit(:name, :description, :path, :avatar, :public)
params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
end
def load_events
......
class Projects::ForksController < Projects::ApplicationController
include ContinueParams
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
......@@ -53,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController
render :error
end
end
private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
end
class Projects::GroupLinksController < Projects::ApplicationController
layout 'project_settings'
before_action :authorize_admin_project!
def index
@group_links = project.project_group_links.all
end
def create
link = project.project_group_links.new
link.group_id = params[:link_group_id]
link.group_access = params[:link_group_access]
link.save
redirect_to namespace_project_group_links_path(project.namespace, project)
end
def destroy
project.project_group_links.find(params[:id]).destroy
redirect_to namespace_project_group_links_path(project.namespace, project)
end
end
class Projects::ImportsController < Projects::ApplicationController
include ContinueParams
# Authorize
before_action :authorize_admin_project!
before_action :require_no_repo, only: [:new, :create]
......@@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController
private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
def finished_notice
if @project.forked?
'The project was successfully forked.'
......
......@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
@project_member = @project.project_members.new
@project_group_links = @project.project_group_links
end
def create
......
class ProjectsController < ApplicationController
include ExtractsPath
prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
......@@ -242,16 +241,6 @@ class ProjectsController < ApplicationController
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?
project.repository_exists? && !project.empty_repo?
end
......
......@@ -244,10 +244,17 @@ class IssuableFinder
items
end
def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming'
end
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
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
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
......
......@@ -43,7 +43,8 @@ class ProjectsFinder
if current_user
[
group_projects_for_user(current_user, group),
group.projects.public_and_internal_only
group.projects.public_and_internal_only,
group.shared_projects.visible_to_user(current_user)
]
else
[group.projects.public_only]
......@@ -52,7 +53,10 @@ class ProjectsFinder
def all_projects(current_user)
if current_user
[current_user.authorized_projects, public_and_internal_projects]
[
current_user.authorized_projects,
public_and_internal_projects
]
else
[Project.public_only]
end
......
......@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
user = User.find_by(email: user_or_email.try(:downcase))
user = User.find_by_any_email(user_or_email.try(:downcase))
end
if user
......
......@@ -59,6 +59,7 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end
......
......@@ -46,9 +46,23 @@ module Ci
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)
where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
query: "%#{query.try(:downcase)}%")
t = arel_table
pattern = "%#{query}%"
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end
def set_default_values
......
......@@ -61,12 +61,29 @@ module Issuable
end
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)
where("LOWER(title) like :query", query: "%#{query.downcase}%")
where(arel_table[:title].matches("%#{query}%"))
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)
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
def sort(method)
......
......@@ -23,6 +23,8 @@ class Group < Namespace
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
......@@ -33,8 +35,18 @@ class Group < Namespace
after_destroy :post_destroy_hook
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)
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
def sort(method)
......
......@@ -135,7 +135,6 @@ class MergeRequest < ActiveRecord::Base
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
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 :from_project, ->(project) { where(source_project_id: project.id) }
scope :merged, -> { with_state(:merged) }
......@@ -162,6 +161,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/)
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)
reference = "#{self.class.reference_prefix}#{iid}"
......
......@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId
include Sortable
......@@ -58,9 +59,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
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)
query = "%#{query}%"
where("title like ? or description like ?", query, query)
t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
end
......@@ -72,6 +82,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/)
end
def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
......
......@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
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)
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
def clean_path(path)
......
......@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
before_validation :clear_blank_line_code!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
......@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) }
scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
......@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
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)
where("LOWER(note) like :query", query: "%#{query.downcase}%")
table = arel_table
pattern = "%#{query}%"
where(table[:note].matches(pattern))
end
def grouped_awards
......@@ -162,26 +173,29 @@ class Note < ActiveRecord::Base
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end
# Check if such line of code exists in merge request diff
# If exists - its active discussion
# If not - its outdated diff
# Check if this note is part of an "active" discussion
#
# This will always return true for anything except MergeRequest noteables,
# which have special logic.
#
# If the note's current diff cannot be matched in the MergeRequest's current
# diff, it's considered inactive.
def active?
return true unless self.diff
return false unless noteable
return @active if defined?(@active)
diffs = noteable.diffs(Commit.max_diff_options)
notable_diff = diffs.find { |d| d.new_path == self.diff.new_path }
noteable_diff = find_noteable_diff
return @active = false if notable_diff.nil?
if noteable_diff
parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
parsed_lines = Gitlab::Diff::Parser.new.parse(notable_diff.diff.each_line)
# We cannot use ||= because @active may be false
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
else
@active = false
end
def outdated?
!active?
@active
end
def diff_file_index
......@@ -365,6 +379,16 @@ class Note < ActiveRecord::Base
private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
# Find the diff on noteable that matches our own
def find_noteable_diff
diffs = noteable.diffs(Commit.max_diff_options)
diffs.find { |d| d.new_path == self.diff.new_path }
end
def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line?
end
......
......@@ -151,6 +151,8 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
......@@ -266,13 +268,31 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
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)
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).
where('LOWER(projects.name) LIKE :query OR
LOWER(projects.path) LIKE :query OR
LOWER(namespaces.name) LIKE :query OR
LOWER(projects.description) LIKE :query',
query: "%#{query.try(:downcase)}%")
where(ntable[:name].matches(pattern))
union = Gitlab::SQL::Union.new([projects, namespaces])
where("projects.id IN (#{union.to_sql})")
end
def search_by_visibility(level)
......@@ -280,7 +300,10 @@ class Project < ActiveRecord::Base
end
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
def find_with_namespace(id)
......@@ -878,6 +901,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active
end
def allowed_to_share_with_group?
!namespace.share_with_group_lock
end
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
......@@ -919,13 +946,13 @@ class Project < ActiveRecord::Base
end
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
# 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
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
def build_coverage_enabled?
......
class ProjectGroupLink < ActiveRecord::Base
GUEST = 10
REPORTER = 20
DEVELOPER = 30
MASTER = 40
belongs_to :project
belongs_to :group
validates :project_id, presence: true
validates :group_id, presence: true
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
def self.access_options
Gitlab::Access.options
end
def self.default_access
DEVELOPER
end
def human_access
self.class.access_options.key(self.group_access)
end
private
def different_group
if self.group && self.project && self.project.group == self.group
errors.add(:base, "Project cannot be shared with the project it is in.")
end
end
end
......@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci'
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
def supported_events
......
......@@ -160,7 +160,27 @@ class ProjectTeam
end
end
access.max
if project.invited_groups.any? && project.allowed_to_share_with_group?
access << max_invited_level(user_id)
end
access.compact.max
end
def max_invited_level(user_id)
project.project_group_links.map do |group_link|
invited_group = group_link.group
access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
# If group member has higher access level we should restrict it
# to max allowed access level
if access && access > group_link.group_access
access = group_link.group_access
end
access
end.compact.max
end
private
......@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
invited_members = []
if project.invited_groups.any? && project.allowed_to_share_with_group?
project.project_group_links.each do |group_link|
invited_group = group_link.group
im = invited_group.group_members
if level
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
# Skip group members if we ask for masters
# but max group access is developers
next if int_level > group_link.group_access
# If we ask for developers and max
# group access is developers we need to provide
# both group master, developers as devs
if int_level == group_link.group_access
im.where("access_level >= ?)", group_link.group_access)
else
im.send(level)
end
end
invited_members << im
end
invited_members = invited_members.flatten.compact
end
if level
project_members = project_members.send(level)
......@@ -175,6 +224,7 @@ class ProjectTeam
end
user_ids = project_members.pluck(:user_id)
user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
......
......@@ -113,12 +113,32 @@ class Snippet < ActiveRecord::Base
end
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)
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
# 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)
where('(content LIKE :query)', query: "%#{query}%")
table = Snippet.arel_table
pattern = "%#{query}%"
where(table[:content].matches(pattern))
end
def accessible_to(user)
......
......@@ -286,8 +286,22 @@ class User < ActiveRecord::Base
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)
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
def by_login(login)
......@@ -818,7 +832,8 @@ class User < ActiveRecord::Base
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
projects.select(:id)])
projects.select(:id),
groups.joins(:shared_projects).select(:project_id)])
end
def ci_projects_union
......
......@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
project_ids = projects.pluck(:id)
Gitlab::SearchResults.new(project_ids, params[:search])
Gitlab::SearchResults.new(projects, params[:search])
end
end
end
......@@ -7,7 +7,7 @@ module Search
end
def execute
Gitlab::ProjectSearchResults.new(project.id,
Gitlab::ProjectSearchResults.new(project,
params[:search],
params[:repository_ref])
end
......
......@@ -7,8 +7,9 @@ module Search
end
def execute
snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
snippets = Snippet.accessible_to(current_user)
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
end
end
......@@ -50,6 +50,22 @@
.panel-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
- if @group.shared_projects.any?
.panel.panel-default
.panel-heading
Projects shared with #{@group.name}
%span.badge
#{@group.shared_projects.count}
%ul.well-list
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
%span.label.label-gray
= repository_size(project)
%span.pull-right.light
%span.monospace= project.path_with_namespace + ".git"
.col-md-6
- if can?(current_user, :admin_group_member, @group)
.panel.panel-default
......
......@@ -2,8 +2,10 @@
.emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis|
%h5= AwardEmoji::CATEGORIES[category]
%ul
%h5.emoji-menu-title
= AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- 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"])
.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
- if projects.present?
.panel.panel-default
.panel-heading
Projects shared with
%strong #{@group.name}
(#{projects.count})
%ul.well-list
- projects.each do |project|
%li.project-row
= link_to namespace_project_path(project.namespace, project), class: dom_class(project) do
%span.namespace-name
- if project.namespace
= project.namespace.human_name
\/
%span.project-name
= truncate(project.name, length: 25)
%span.arrow
%i.icon-angle-right
= 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'
......@@ -23,6 +23,15 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
.form-group
%hr
= f.label :share_with_group_lock, class: 'control-label' do
Share with group lock
.col-sm-10
.checkbox
= f.check_box :share_with_group_lock
%span.descr Prevent sharing a project with another group within this group
.form-actions
= f.submit 'Save group', class: "btn btn-save"
......
......@@ -30,28 +30,22 @@
%ul.nav-links
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
%li
= link_to "#projects", 'data-toggle' => 'tab' do
Projects
- if @shared_projects.present?
%li
= link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
.tab-content
.tab-pane.active#activity
.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
.tab-pane.active#projects
= render "projects", projects: @projects
.tab-pane#shared
= render "shared_projects", projects: @shared_projects
- else
%p.nav-links.no-top
No projects to show
......@@ -9,10 +9,15 @@
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
= icon('dashboard fw')
= icon('group fw')
%span
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
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
......
......@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('home fw')
= icon('bookmark fw')
%span
Project
= nav_link(path: 'projects#activity') do
......
......@@ -13,6 +13,12 @@
= icon('pencil-square-o fw')
%span
Project Settings
- if @project.allowed_to_share_with_group?
= nav_link(controller: :group_links) do
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
= icon('share-square-o fw')
%span
Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
......
!!! 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"}
- page_title "Groups"
%h3.page_title Share project with other groups
%p.light
Projects can be stored in only one group at once. However you can share a project with other groups here.
%hr
- if @group_links.present?
.enabled-groups.panel.panel-default
.panel-heading
Already shared with
%ul.well-list
- @group_links.each do |group_link|
- group = group_link.group
%li
.pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do
%i.icon-remove
disable sharing
= link_to group do
%strong
%i.icon-folder-open
= group.name
%br
.light up to #{group_link.human_access}
.available-groups
%h4
Can be shared with
%div
= form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do
.form-group
= label_tag :link_group_id, 'Group', class: 'control-label'
.col-sm-10
= groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
.form-group
= label_tag :link_group_access, 'Max access level', class: 'control-label'
.col-sm-10
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control"
.form-actions
= submit_tag "Share", class: "btn btn-create"
......@@ -71,7 +71,7 @@
.merge-requests
= render 'merge_requests'
.content-block
.content-block.content-block-small
= render 'votes/votes_block', votable: @issue
.row
......
......@@ -68,7 +68,7 @@
.tab-content
#notes.notes.tab-pane.voting_notes
.content-block.oneline-block
.content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request
.row
......
- @project_group_links.each do |group_links|
- shared_group = group_links.group
- shared_group_users_count = group_links.group.group_members.count
.panel.panel-default
.panel-heading
Shared with
%strong #{shared_group.name}
group, members with
%strong #{group_links.human_access}
role (#{shared_group_users_count})
- if current_user.can?(:admin_group, shared_group)
.panel-head-actions
= link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
%i.fa.fa-pencil-square-o
Edit group members
%ul.content-list
- shared_group.group_members.order('access_level DESC').limit(20).each do |member|
= render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
- if shared_group_users_count > 20
%li
and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
......@@ -18,3 +18,6 @@
- if @group
= render "group_members", members: @group_members
- if @project_group_links.any? && @project.allowed_to_share_with_group?
= render "shared_group_members"
......@@ -6,13 +6,20 @@
- else
Any
%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
= 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
%li.divider
- current_user.authorized_groups.sort_by(&:name).each do |group|
%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
.dropdown.inline.prepend-left-10.project-filter
......@@ -23,11 +30,18 @@
- else
Any
%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
= 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
%li.divider
- current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
%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
......@@ -15,7 +15,7 @@
.filter-item.inline
- if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
.filter-item.inline.milestone-filter
......
.awards.votes-block
- 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)
.counter
%span.award-control-text.js-counter
= notes.count
- if current_user
.awards-controls
%a.add-award{"href" => "#"}
= icon('smile-o')
%div.award-menu-holder.js-award-holder
%a.btn.award-control.js-add-award{"href" => "#"}
= 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
:javascript
......@@ -23,17 +26,3 @@
noteable_id,
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
config.encoding = "utf-8"
# 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.
config.active_support.escape_html_entities_in_json = true
......
......@@ -203,11 +203,11 @@ Devise.setup do |config|
# 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.
#
# config.warden do |manager|
# manager.failure_app = AnotherApp
config.warden do |manager|
manager.failure_app = Gitlab::DeviseFailure
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
end
if Gitlab::LDAP::Config.enabled?
Gitlab.config.ldap.servers.values.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
......@@ -382,7 +382,7 @@ Rails.application.routes.draw do
get :issues
get :merge_requests
get :projects
get :events
get :activity
end
scope module: :groups do
......@@ -701,6 +701,8 @@ Rails.application.routes.draw do
end
end
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do
delete :delete_attachment
......
class CreateProjectGroupLinks < ActiveRecord::Migration
def change
create_table :project_group_links do |t|
t.integer :project_id, null: false
t.integer :group_id, null: false
t.timestamps
end
end
end
class AddAccessToProjectGroupLink < ActiveRecord::Migration
def change
add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
end
end
class AddGroupShareLock < ActiveRecord::Migration
def change
add_column :namespaces, :share_with_group_lock, :boolean, default: false
end
end
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
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
create_table "abuse_reports", force: :cascade do |t|
t.integer "reporter_id"
......@@ -258,6 +259,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "architecture"
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|
t.string "type"
t.string "title"
......@@ -417,11 +421,13 @@ ActiveRecord::Schema.define(version: 20160309140734) do
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"], 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", ["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", ["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_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t|
t.integer "user_id"
......@@ -543,12 +549,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", ["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", ["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", ["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", ["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", ["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|
t.string "title", null: false
......@@ -562,10 +570,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
end
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", ["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", ["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|
t.string "name", null: false
......@@ -576,12 +586,15 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "type"
t.string "description", default: "", null: false
t.string "avatar"
t.boolean "share_with_group_lock", default: false
end
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_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
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_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
......@@ -607,6 +620,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
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", ["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_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
......@@ -656,6 +670,14 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
end
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
......@@ -705,9 +727,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
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", ["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", ["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", ["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", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
......@@ -749,9 +774,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "type"
t.string "title"
t.integer "project_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "active", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "active", default: false, null: false
t.text "properties"
t.boolean "template", default: false
t.boolean "push_events", default: true
......@@ -785,7 +810,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
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"], 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", ["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", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
......@@ -919,9 +946,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", ["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_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_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", ["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|
t.integer "project_id", null: false
......
......@@ -145,6 +145,7 @@ Parameters:
"state": "active",
"created_at": "2013-09-30T13:46:01Z"
},
"expires_at": null,
"updated_at": "2013-10-02T07:34:20Z",
"created_at": "2013-10-02T07:34:20Z"
}
......
......@@ -51,6 +51,7 @@ Parameters:
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z"
}
......
......@@ -619,6 +619,20 @@ Revoking team membership for a user who is not currently a team member is consid
Please note that the returned JSON currently differs slightly. Thus you should not
rely on the returned JSON structure.
### Share project with group
Allow to share project with group.
```
POST /projects/:id/share
```
Parameters:
- `id` (required) - The ID of a project
- `group_id` (required) - The ID of a group
- `group_access` (required) - Level of permissions for sharing
## Hooks
Also called Project Hooks and Webhooks.
......
......@@ -3,6 +3,7 @@
### CI User documentation
- [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 `.gitlab-ci.yml` works](yaml/README.md)
- [Configure a Runner, the application that runs your builds](runners/README.md)
......@@ -14,24 +15,4 @@
- [Build artifacts](build_artifacts/README.md)
- [User permissions](permissions/README.md)
- [API](api/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)
- [CI services (linked docker containers)](services/README.md)
......@@ -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:
```
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`.
## 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 Python application to Heroku](test-and-deploy-python-application-to-heroku.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
comprehensive documentation and comes with a test repository hosted on
GitLab.com.
- [Testing PHP](php.md)
- [Blost post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
......@@ -223,20 +223,13 @@ You can access a builds badge image using following link:
http://example.gitlab.com/namespace/project/badges/branch/build.svg
```
Awesome! You started using CI in GitLab!
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
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
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md
......
......@@ -116,7 +116,8 @@ Alias for [stages](#stages).
### variables
_**Note:** Introduced in GitLab Runner v0.5.0._
>**Note:**
Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
environment. The variables are stored in the git repository and are meant to
......@@ -153,7 +154,8 @@ cache:
#### cache:key
_**Note:** Introduced in GitLab Runner v1.0.0._
>**Note:**
Introduced in GitLab Runner v1.0.0.
The `key` directive allows you to define the affinity of caching
between jobs, allowing to have a single cache for all jobs,
......@@ -234,13 +236,14 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner |
| stage | no (default: `test`) | Defines a build stage |
| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created |
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
......@@ -393,15 +396,18 @@ The above script will:
### artifacts
_**Note:** Introduced in GitLab Runner v0.7.0 for non-Windows platforms._
_**Note:** Limited Windows support was added in GitLab Runner v.1.0.0.
Currently not all executors are supported._
_**Note:** Build artifacts are only collected for successful builds._
>**Notes:**
>
> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
> - Limited Windows support was added in GitLab Runner v.1.0.0.
> - Currently not all executors are supported.
> - Build artifacts are only collected for successful builds.
`artifacts` is used to specify list of files and directories which should be
attached to build after success. Below are some examples.
attached to build after success. To pass artifacts between different builds,
see [dependencies](#dependencies).
Below are some examples.
Send all files in `binaries` and `.config`:
......@@ -453,9 +459,130 @@ release-job:
The artifacts will be sent to GitLab after a successful build and will
be available for download in the GitLab UI.
#### artifacts:name
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
The `name` directive allows you to define the name of the created artifacts
archive. That way, you can have a unique name of every archive which could be
useful when you'd like to download the archive from GitLab. The `artifacts:name`
variable can make use of any of the [predefined variables](../variables/README.md).
---
**Example configurations**
To create an archive with a name of the current build:
```yaml
job:
artifacts:
name: "$CI_BUILD_NAME"
```
To create an archive with a name of the current branch or tag including only
the files that are untracked by Git:
```yaml
job:
artifacts:
name: "$CI_BUILD_REF_NAME"
untracked: true
```
To create an archive with a name of the current build and the current branch or
tag including only the files that are untracked by Git:
```yaml
job:
artifacts:
name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
untracked: true
```
To create an archive with a name of the current [stage](#stages) and branch name:
```yaml
job:
artifacts:
name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
untracked: true
```
---
If you use **Windows Batch** to run your shell scripts you need to replace
`$` with `%`:
```yaml
job:
artifacts:
name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
untracked: true
```
### dependencies
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
This feature should be used in conjunction with [`artifacts`](#artifacts) and
allows you to define the artifacts to pass between different builds.
Note that `artifacts` from previous [stages](#stages) are passed by default.
To use this feature, define `dependencies` in context of the job and pass
a list of all previous builds from which the artifacts should be downloaded.
You can only define builds from stages that are executed before the current one.
An error will be shown if you define builds from the current stage or next ones.
---
In the following example, we define two jobs with artifacts, `build:osx` and
`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
will be downloaded and extracted in the context of the build. The same happens
for `test:linux` and artifacts from `build:linux`.
The job `deploy` will download artifacts from all previous builds because of
the [stage](#stages) precedence:
```yaml
build:osx:
stage: build
script: make build:osx
artifacts:
paths:
- binaries/
build:linux:
stage: build
script: make build:linux
artifacts:
paths:
- binaries/
test:osx:
stage: test
script: make test:osx
dependencies:
- build:osx
test:linux:
stage: test
script: make test:linux
dependencies:
- build:linux
deploy:
stage: deploy
script: make deploy
```
### cache
_**Note:** Introduced in GitLab Runner v0.7.0._
>**Note:**
Introduced in GitLab Runner v0.7.0.
`cache` is used to specify list of files and directories which should be cached
between builds. Below are some examples:
......@@ -509,6 +636,155 @@ rspec:
The cache is provided on best effort basis, so don't expect that cache will be
always present. For implementation details please check GitLab Runner.
## Hidden jobs
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
use this feature to ignore jobs, or use the
[special YAML features](#special-yaml-features) and transform the hidden jobs
into templates.
In the following example, `.job_name` will be ignored:
```yaml
.job_name:
script:
- rake spec
```
## Special YAML features
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
and map merging (`<<`), which will allow you to greatly reduce the complexity
of `.gitlab-ci.yml`.
Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
### Anchors
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
YAML also has a handy feature called 'anchors', which let you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
to provide templates for your jobs.
The following example uses anchors and map merging. It will create two jobs,
`test1` and `test2`, that will inherit the parameters of `.job_template`, each
having their own custom `script` defined:
```yaml
.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
image: ruby:2.1
services:
- postgres
- redis
test1:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test1 project
test2:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test2 project
```
`&` sets up the name of the anchor (`job_definition`), `<<` means "merge the
given hash into the current one", and `*` includes the named anchor
(`job_definition` again). The expanded version looks like this:
```yaml
.job_template:
image: ruby:2.1
services:
- postgres
- redis
test1:
image: ruby:2.1
services:
- postgres
- redis
script:
- test1 project
test2:
image: ruby:2.1
services:
- postgres
- redis
script:
- test2 project
```
Let's see another one example. This time we will use anchors to define two sets
of services. This will create two jobs, `test:postgres` and `test:mysql`, that
will share the `script` directive defined in `.job_template`, and the `services`
directive defined in `.postgres_services` and `.mysql_services` respectively:
```yaml
.job_template: &job_definition
script:
- test project
.postgres_services:
services: &postgres_definition
- postgres
- ruby
.mysql_services:
services: &mysql_definition
- mysql
- ruby
test:postgres:
<< *job_definition
services: *postgres_definition
test:mysql:
<< *job_definition
services: *mysql_definition
```
The expanded version looks like this:
```yaml
.job_template:
script:
- test project
.postgres_services:
services:
- postgres
- ruby
.mysql_services:
services:
- mysql
- ruby
test:postgres:
script:
- test project
services:
- postgres
- ruby
test:mysql:
script:
- test project
services:
- mysql
- ruby
```
You can see that the hidden jobs are conveniently used as templates.
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
......
# Development
- [Architecture](architecture.md) of GitLab
- [Benchmarking](benchmarking.md)
- [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid
- [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
......@@ -233,9 +233,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-5-stable gitlab
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab
**Note:** You can change `8-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
......
......@@ -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.
### 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 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
Click the icon to begin the authentication process. If everything goes well the user
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
### 500 error after login
......
# From 8.5 to 8.6
### 1. Stop server
sudo service gitlab stop
### 2. Backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
### 3. Get latest code
```bash
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
For GitLab Community Edition:
```bash
sudo -u git -H git checkout 8-6-stable
```
OR
For GitLab Enterprise Edition:
```bash
sudo -u git -H git checkout 8-6-stable-ee
```
### 4. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all
sudo -u git -H git checkout v2.6.11
```
### 5. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
sudo -u git -H git checkout 0.6.5
sudo -u git -H make
```
### 6. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 7. Update configuration files
#### New configuration options for `gitlab.yml`
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
git diff origin/8-5-stable:config/gitlab.yml.example origin/8-6-stable:config/gitlab.yml.example
```
#### Nginx configuration
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
# For HTTPS configurations
git diff origin/8-5-stable:lib/support/nginx/gitlab-ssl origin/8-6-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
git diff origin/8-5-stable:lib/support/nginx/gitlab origin/8-6-stable:lib/support/nginx/gitlab
```
If you are using Apache instead of NGINX please see the updated [Apache templates].
Also note that because Apache does not support upstreams behind Unix sockets you
will need to let gitlab-workhorse listen on a TCP port. You can do this
via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-6-stable/lib/support/init.d/gitlab.default.example#L37
#### Init script
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
### 8. Updates for PostgreSQL Users
Starting with 8.6 users using GitLab in combination with PostgreSQL are required
to have the `pg_trgm` extension enabled for all GitLab databases. If you're
using GitLab's Omnibus packages there's nothing you'll need to do manually as
this extension is enabled automatically. Users who install GitLab without using
Omnibus (e.g. by building from source) have to enable this extension manually.
To enable this extension run the following SQL command as a PostgreSQL super
user for _every_ GitLab database:
```sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
```
Certain operating systems might require the installation of extra packages for
this extension to be available. For example, users using Ubuntu will have to
install the `postgresql-contrib` package in order for this extension to be
available.
### 9. Start application
sudo service gitlab start
sudo service nginx restart
### 10. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
To make sure you didn't miss anything run a more thorough check:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (8.5)
### 1. Revert the code to the previous version
Follow the [upgrade guide from 8.4 to 8.5](8.4-to-8.5.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
......@@ -582,6 +582,7 @@ X-Gitlab-Event: Note Hook
"created_at": "2015-04-09 02:40:38 UTC",
"updated_at": "2015-04-09 02:40:38 UTC",
"file_name": "test.rb",
"expires_at": null,
"type": "ProjectSnippet",
"visibility_level": 0
}
......
......@@ -13,6 +13,8 @@
- [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.md)
- [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
......
# Share Projects with other Groups
In GitLab Enterprise Edition you can share projects with other groups.
This makes it possible to add a group of users to a project with a single action.
## Groups as collections of users
In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
## Sharing a project with a group of users
The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
But what if 'Project Acme' already belongs to another group, say 'Open Source'?
This is where the (Enterprise Edition only) group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png)
## Maximum access level
!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png)
In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
# Sharing a project with a group
If you want to share a single project in a group with another group,
you can do so easily. By setting the permission you can quickly
give a select group of users access to a project in a restricted manner.
In a project go to the project settings -> groups.
Now you can select a group that you want to share this project with and with
which maximum access level. Users in that group are able to access this project
with their set group access level, up to the maximum level that you've set.
![Share a project with a group](share_with_group.png)
......@@ -21,6 +21,11 @@ Feature: Admin Groups
When I select user "John Doe" from user list as "Reporter"
Then I should see "John Doe" in team list in every project as "Reporter"
Scenario: Shared projects
Given group has shared projects
When I visit group page
Then I should see project shared with group
@javascript
Scenario: Remove user from group
Given we have user "John Doe" in group
......
......@@ -15,6 +15,10 @@ Feature: Groups
Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page
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
Scenario: I should see group "Owned" issues list
......
Feature: Project Group Links
Background:
Given I sign in as a user
And I own project "Shop"
And project "Shop" is shared with group "Ops"
And project "Shop" is not shared with group "Market"
And I visit project group links page
Scenario: I should see list of groups
Then I should see project already shared with group "Ops"
Then I should see project is not shared with group "Market"
@javascript
Scenario: I share project with group
When I select group "Market" for share
Then I should see project is shared with group "Market"
......@@ -34,9 +34,10 @@ Feature: Project Network Graph
@javascript
Scenario: I should filter selected tag
When I switch ref to "v1.0.0"
Then page should have "v1.0.0" in title
Then page should have content not containing "v1.0.0"
When click "Show only selected branch" checkbox
Then page should not have content not containing "v1.0.0"
Then page should only have content from "v1.0.0"
When click "Show only selected branch" checkbox
Then page should have content not containing "v1.0.0"
......
......@@ -39,3 +39,8 @@ Feature: Project Team Management
And I click link "Import team from another project"
And I submit "Website" project for import team
Then I should see "Mike" in team list as "Reporter"
Scenario: See all members of projects shared group
Given I share project with group "OpenSource"
And I visit project "Shop" team page
Then I should see "Opensource" group user listing
......@@ -73,6 +73,21 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
end
end
step 'group has shared projects' do
share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
share_link.group_id = current_group.id
share_link.save!
end
step 'I visit group page' do
visit admin_group_path(current_group)
end
step 'I should see project shared with group' do
expect(page).to have_content(shared_project.name_with_namespace)
expect(page).to have_content "Projects shared with"
end
step 'we have user "John Doe" in group' do
current_group.add_reporter(user_john)
end
......@@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
@group ||= Group.first
end
def shared_project
@shared_project ||= create(:empty_project)
end
def user_john
@user_john ||= User.find_by(name: "John Doe")
end
......
......@@ -36,22 +36,17 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
execute_script('$("#assignee_id").val("")')
execute_script('$(".js-user-search").first().click()')
sleep 1
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
sleep 1
find("#assignee_id").set("")
find(".js-author-search", match: :first).click
find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
execute_script('$(".js-user-search").first().click()')
sleep 1
execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
sleep 1
execute_script('$(".js-user-search").eq(1).click()')
sleep 1
execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
sleep 1
find('.js-author-search').click
find('.dropdown-menu-user-full-name', match: :first).click
find('.js-assignee-search').click
find('.dropdown-menu-user-full-name', match: :first).click
end
def should_see(issue)
......
......@@ -40,22 +40,16 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
execute_script('$("#assignee_id").val("")')
execute_script('$(".js-user-search").first().click()')
sleep 0.5
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
sleep 2
find("#assignee_id").set("")
find(".js-author-search", match: :first).click
find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
execute_script('$(".js-user-search").first().click()')
sleep 0.5
execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
sleep 2
execute_script('$(".js-user-search").eq(1).click()')
sleep 0.5
execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
sleep 2
find(".js-author-search").click
find(".dropdown-menu-author li a", match: :first).click
find(".js-assignee-search").click
find(".dropdown-menu-assignee li a", match: :first).click
end
def should_see(merge_request)
......
......@@ -10,31 +10,30 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I click the thumbsup award Emoji' do
page.within '.awards' do
thumbsup = page.find('.award .emoji-1F44D')
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
sleep 0.3
end
end
step 'I click to emoji-picker' do
page.within '.awards-controls' do
page.find('.add-award').click
page.within '.awards' do
page.find('.js-add-award').click
end
end
step 'I click to emoji in the picker' do
page.within '.emoji-menu-content' do
page.first('.emoji-icon').click
page.first('.js-emoji-btn').click
end
end
step 'I can remove it by clicking to icon' do
page.within '.awards' do
expect do
page.find('.award.active').click
page.find('.js-emoji-btn.active').click
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
......@@ -46,26 +45,24 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I have award added' do
sleep 0.2
page.within '.awards' do
expect(page).to have_selector '.award'
expect(page.find('.award.active .counter')).to have_content '1'
expect(page.find('.award.active')['data-original-title']).to eq('me')
expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
end
end
step 'I have no awards added' do
page.within '.awards' do
expect(page).to have_selector '.award'
expect(page.all('.award').size).to eq(2)
expect(page).to have_selector '.award-control.js-emoji-btn'
expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
# Check tooltip data
page.all('.award').each do |element|
page.all('.award-control.js-emoji-btn').each do |element|
expect(element['title']).to eq("")
end
page.all('.award .counter').each do |element|
page.all('.award-control .js-counter').each do |element|
expect(element).to have_content '0'
end
end
......
......@@ -41,17 +41,14 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
When 'I switch ref to "feature"' do
select 'feature', from: 'ref'
sleep 2
end
When 'I switch ref to "v1.0.0"' do
select 'v1.0.0', from: 'ref'
sleep 2
end
When 'click "Show only selected branch" checkbox' do
find('#filter_ref').click
sleep 2
end
step 'page should have content not containing "v1.0.0"' do
......@@ -60,7 +57,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
end
step 'page should not have content not containing "v1.0.0"' do
step 'page should have "v1.0.0" in title' do
expect(page).to have_css 'title', text: 'Network · v1.0.0', visible: false
end
step 'page should only have content from "v1.0.0"' do
page.within '.network-graph' do
expect(page).not_to have_content 'Change some files'
end
......
class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
include Select2Helper
step 'I should see project already shared with group "Ops"' do
page.within '.enabled-groups' do
expect(page).to have_content "Ops"
end
end
step 'I should see project is not shared with group "Market"' do
page.within '.enabled-groups' do
expect(page).not_to have_content "Market"
end
end
step 'I select group "Market" for share' do
group = Group.find_by(path: 'market')
select2(group.id, from: "#link_group_id")
select "Master", from: 'link_group_access'
click_button "Share"
end
step 'I should see project is shared with group "Market"' do
page.within '.enabled-groups' do
expect(page).to have_content "Market"
end
end
step 'project "Shop" is shared with group "Ops"' do
group = create(:group, name: 'Ops')
share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
share_link.group_id = group.id
share_link.save!
end
step 'project "Shop" is not shared with group "Market"' do
create(:group, name: 'Market', path: 'market')
end
step 'I visit project group links page' do
visit namespace_project_group_links_path(project.namespace, project)
end
def project
@project ||= Project.find_by_name "Shop"
end
end
......@@ -123,4 +123,23 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
click_link('Remove user from team')
end
end
step 'I share project with group "OpenSource"' do
project = Project.find_by(name: 'Shop')
os_group = create(:group, name: 'OpenSource')
create(:project, group: os_group)
@os_user1 = create(:user)
@os_user2 = create(:user)
os_group.add_owner(@os_user1)
os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER)
share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
share_link.group_id = os_group.id
share_link.save!
end
step 'I should see "Opensource" group user listing' do
expect(page).to have_content("Shared with OpenSource group, members with Master role (2)")
expect(page).to have_content(@os_user1.name)
expect(page).to have_content(@os_user2.name)
end
end
......@@ -27,6 +27,10 @@ module SharedPaths
visit group_path(Group.find_by(name: "Owned"))
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
visit issues_group_path(Group.find_by(name: "Owned"))
end
......
......@@ -144,6 +144,9 @@ module API
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
# TODO (rspeicher): Deprecated; remove in 9.0
expose(:expires_at) { |snippet| nil }
end
class ProjectEntity < Grape::Entity
......@@ -243,6 +246,10 @@ module API
end
end
class ProjectGroupLink < Grape::Entity
expose :id, :project_id, :group_id, :group_access
end
class Namespace < Grape::Entity
expose :id, :path, :kind
end
......
......@@ -290,6 +290,33 @@ module API
end
end
# Share project with group
#
# Parameters:
# id (required) - The ID of a project
# group_id (required) - The ID of a group
# group_access (required) - Level of permissions for sharing
#
# Example Request:
# POST /projects/:id/share
post ":id/share" do
authorize! :admin_project, user_project
required_attributes! [:group_id, :group_access]
unless user_project.allowed_to_share_with_group?
return render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new
link.group_id = params[:group_id]
link.group_access = params[:group_access]
if link.save
present link, with: Entities::ProjectGroupLink
else
render_api_error!(link.errors.full_messages.first, 409)
end
end
# Upload a file
#
# Parameters:
......
......@@ -5,7 +5,9 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
:dependencies]
attr_reader :before_script, :image, :services, :variables, :path, :cache
......@@ -60,6 +62,7 @@ module Ci
@jobs = {}
@config.each do |key, job|
next if key.to_s.start_with?('.')
stage = job[:stage] || job[:type] || DEFAULT_STAGE
@jobs[key] = { stage: stage }.merge(job)
end
......@@ -81,6 +84,7 @@ module Ci
services: job[:services] || @services,
artifacts: job[:artifacts],
cache: job[:cache] || @cache,
dependencies: job[:dependencies],
}.compact
}
end
......@@ -143,6 +147,7 @@ module Ci
validate_job_stage!(name, job) if job[:stage]
validate_job_cache!(name, job) if job[:cache]
validate_job_artifacts!(name, job) if job[:artifacts]
validate_job_dependencies!(name, job) if job[:dependencies]
end
private
......@@ -216,6 +221,10 @@ module Ci
end
def validate_job_artifacts!(name, job)
if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
end
if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
end
......@@ -225,6 +234,22 @@ module Ci
end
end
def validate_job_dependencies!(name, job)
if !validate_array_of_strings(job[:dependencies])
raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
end
stage_index = stages.index(job[:stage])
job[:dependencies].each do |dependency|
raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
unless stages.index(@jobs[dependency][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
end
def validate_array_of_strings(values)
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
......
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
direction: :asc).each do |raw_data|
pull_request = PullRequestFormatter.new(project, raw_data)
if !pull_request.cross_project? && pull_request.valid?
merge_request = MergeRequest.create!(pull_request.attributes)
if pull_request.valid?
merge_request = MergeRequest.new(pull_request.attributes)
if merge_request.save
import_comments(pull_request.number, merge_request)
import_comments_on_diff(pull_request.number, merge_request)
end
end
end
true
rescue ActiveRecord::RecordInvalid => e
......
......@@ -17,16 +17,12 @@ module Gitlab
}
end
def cross_project?
source_repo.id != target_repo.id
end
def number
raw_data.number
end
def valid?
source_branch.present? && target_branch.present?
!cross_project? && source_branch.present? && target_branch.present?
end
private
......@@ -53,6 +49,10 @@ module Gitlab
raw_data.body || ""
end
def cross_project?
source_repo.present? && target_repo.present? && source_repo.id != target_repo.id
end
def description
formatter.author_line(author) + body
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
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
def initialize(project_id, query, repository_ref = nil)
@project = Project.find(project_id)
def initialize(project, query, repository_ref = nil)
@project = project
@repository_ref = if repository_ref.present?
repository_ref
else
......@@ -73,7 +73,7 @@ module Gitlab
end
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
def commits
......@@ -84,8 +84,8 @@ module Gitlab
end
end
def limit_project_ids
[project.id]
def project_ids_relation
project
end
end
end
......@@ -2,12 +2,12 @@ module Gitlab
class SearchResults
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
attr_reader :limit_project_ids
attr_reader :limit_projects
def initialize(limit_project_ids, query)
@limit_project_ids = limit_project_ids || Project.all
def initialize(limit_projects, query)
@limit_projects = limit_projects || Project.all
@query = Shellwords.shellescape(query) if query.present?
end
......@@ -27,7 +27,8 @@ module Gitlab
end
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
def projects_count
......@@ -53,27 +54,29 @@ module Gitlab
private
def projects
Project.where(id: limit_project_ids).search(query)
limit_projects.search(query)
end
def issues
issues = Issue.where(project_id: limit_project_ids)
issues = Issue.where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
else
issues = issues.full_search(query)
end
issues.order('updated_at DESC')
end
def milestones
milestones = Milestone.where(project_id: limit_project_ids)
milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query)
milestones.order('updated_at DESC')
end
def merge_requests
merge_requests = MergeRequest.in_projects(limit_project_ids)
merge_requests = MergeRequest.in_projects(project_ids_relation)
if query =~ /[#!](\d+)\z/
merge_requests = merge_requests.where(iid: $1)
else
......@@ -89,5 +92,9 @@ module Gitlab
def per_page
20
end
def project_ids_relation
limit_projects.select(:id).reorder(nil)
end
end
end
......@@ -2,10 +2,10 @@ module Gitlab
class SnippetSearchResults < SearchResults
include SnippetsHelper
attr_reader :limit_snippet_ids
attr_reader :limit_snippets
def initialize(limit_snippet_ids, query)
@limit_snippet_ids = limit_snippet_ids
def initialize(limit_snippets, query)
@limit_snippets = limit_snippets
@query = query
end
......@@ -35,11 +35,11 @@ module Gitlab
private
def snippet_titles
Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC')
limit_snippets.search(query).order('updated_at DESC')
end
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
def default_scope
......
......@@ -46,20 +46,11 @@ namespace :spec do
run_commands(cmds)
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'
task :other do
cmds = [
%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)
end
......@@ -69,7 +60,7 @@ desc "GitLab | Run specs"
task :spec do
cmds = [
%W(rake gitlab:setup),
%W(rspec spec --tag ~@benchmark),
%W(rspec spec),
]
run_commands(cmds)
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
......@@ -19,7 +19,7 @@ describe Projects::ImportsController do
end
it 'sets flash.now if params is present' do
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'Started' }
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' }
expect(flash.now[:notice]).to eq 'Started'
end
......@@ -45,7 +45,7 @@ describe Projects::ImportsController do
end
it 'sets flash.now if params is present' do
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'In progress' }
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' }
expect(flash.now[:notice]).to eq 'In progress'
end
......
......@@ -2,14 +2,24 @@ require "spec_helper"
describe Projects::RepositoriesController do
let(:project) { create(:project) }
let(:user) { create(:user) }
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
sign_in(user)
project.team << [user, :developer]
sign_in(user)
end
it "uses Gitlab::Workhorse" do
expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
......@@ -29,4 +39,5 @@ describe Projects::RepositoriesController do
end
end
end
end
end
......@@ -9,19 +9,6 @@ describe ProjectsController 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
render_views
......
FactoryGirl.define do
factory :project_group_link do
project
group
end
end
......@@ -17,6 +17,10 @@ describe ProjectsFinder do
create(:project, :public, group: group, name: 'C', path: 'C')
end
let!(:shared_project) do
create(:project, :private, name: 'D', path: 'D')
end
let(:finder) { described_class.new }
describe 'without a group' do
......@@ -56,8 +60,36 @@ describe ProjectsFinder do
describe 'with a user' do
subject { finder.execute(user, group: group) }
describe 'without shared projects' do
it { is_expected.to eq([public_project, internal_project]) }
end
describe 'with shared projects and group membership' do
before do
group.add_user(user, Gitlab::Access::DEVELOPER)
shared_project.project_group_links.
create(group_access: Gitlab::Access::MASTER, group: group)
end
it do
is_expected.to eq([shared_project, public_project, internal_project])
end
end
describe 'with shared projects and project membership' do
before do
shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
shared_project.project_group_links.
create(group_access: Gitlab::Access::MASTER, group: group)
end
it do
is_expected.to eq([shared_project, public_project, internal_project])
end
end
end
end
end
end
......@@ -397,7 +397,7 @@ module Ci
services: ["mysql"],
before_script: ["pwd"],
rspec: {
artifacts: { paths: ["logs/", "binaries/"], untracked: true },
artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
script: "rspec"
}
})
......@@ -417,6 +417,7 @@ module Ci
image: "ruby:2.1",
services: ["mysql"],
artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"],
untracked: true
}
......@@ -427,6 +428,73 @@ module Ci
end
end
describe "Dependencies" do
let(:config) do
{
build1: { stage: 'build', script: 'test' },
build2: { stage: 'build', script: 'test' },
test1: { stage: 'test', script: 'test', dependencies: dependencies },
test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' }
}
end
subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
context 'no dependencies' do
let(:dependencies) { }
it { expect { subject }.to_not raise_error }
end
context 'dependencies to builds' do
let(:dependencies) { [:build1, :build2] }
it { expect { subject }.to_not raise_error }
end
context 'undefined dependency' do
let(:dependencies) { [:undefined] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
end
context 'dependencies to deploy' do
let(:dependencies) { [:deploy] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
end
end
describe "Hidden jobs" do
let(:config) do
YAML.dump({
'.hidden_job' => { script: 'test' },
'normal_job' => { script: 'test' }
})
end
let(:config_processor) { GitlabCiYamlProcessor.new(config) }
subject { config_processor.builds_for_stage_and_ref("test", "master") }
it "doesn't create jobs that starts with dot" do
expect(subject.size).to eq(1)
expect(subject.first).to eq({
except: nil,
stage: "test",
stage_idx: 1,
name: :normal_job,
only: nil,
commands: "\ntest",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false
})
end
end
describe "YAML Alias/Anchor" do
it "is correctly supported for jobs" do
config = <<EOT
......@@ -629,6 +697,13 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
end
it "returns errors if job artifacts:name is not an a string" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
end
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
......@@ -684,6 +759,13 @@ EOT
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
end
it "returns errors if job dependencies is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
end
end
end
end
......@@ -127,34 +127,6 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
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
let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
......@@ -166,7 +138,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
describe '#valid?' do
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)) }
it 'returns true' do
......@@ -174,7 +147,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
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)) }
it 'returns false' do
......@@ -182,7 +155,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
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)) }
it 'returns false' do
......@@ -190,4 +163,23 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
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
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
let(:query) { 'hello world' }
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.repository_ref).to be_nil }
......@@ -14,7 +14,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
describe 'initialize with ref' do
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.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
expect(runner.belongs_to_one_project?).to be_truthy
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
......@@ -32,9 +32,54 @@ describe Issue, "Issuable" do
describe ".search" do
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])
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
describe "#today?" do
......
......@@ -103,4 +103,30 @@ describe Group, models: true do
expect(group.avatar_type).to eq(["only images allowed"])
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
......@@ -80,6 +80,12 @@ describe MergeRequest, models: true do
it { is_expected.to respond_to(:merge_when_build_succeeds) }
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
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}"
......
......@@ -181,4 +181,34 @@ describe Milestone, models: true do
expect(issue4.position).to eq(42)
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
......@@ -41,13 +41,32 @@ describe Namespace, models: true do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
describe :search do
before do
@namespace = create :namespace
describe '.search' do
let(: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
it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) }
it { expect(Namespace.search('unknown')).to eq([]) }
it 'returns namespaces with a partially matching path' do
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
describe :move_dir do
......
......@@ -140,13 +140,19 @@ describe Note, models: true do
end
end
describe :search do
let!(:note) { create(:note, note: "WoW") }
describe '.search' do
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
describe :grouped_awards do
it 'returns notes with matching content regardless of the casing' do
expect(described_class.search('WOW')).to eq([note])
end
end
describe '.grouped_awards' do
before do
create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true
......@@ -163,6 +169,66 @@ describe Note, models: true do
end
end
describe '#active?' do
it 'is always true when the note has no associated diff' do
note = build(:note)
expect(note).to receive(:diff).and_return(nil)
expect(note).to be_active
end
it 'is never true when the note has no noteable associated' do
note = build(:note)
expect(note).to receive(:diff).and_return(double)
expect(note).to receive(:noteable).and_return(nil)
expect(note).not_to be_active
end
it 'returns the memoized value if defined' do
note = build(:note)
expect(note).to receive(:diff).and_return(double)
expect(note).to receive(:noteable).and_return(double)
note.instance_variable_set(:@active, 'foo')
expect(note).not_to receive(:find_noteable_diff)
expect(note.active?).to eq 'foo'
end
context 'for a merge request noteable' do
it 'is false when noteable has no matching diff' do
merge = build_stubbed(:merge_request, :simple)
note = build(:note, noteable: merge)
allow(note).to receive(:diff).and_return(double)
expect(note).to receive(:find_noteable_diff).and_return(nil)
expect(note).not_to be_active
end
it 'is true when noteable has a matching diff' do
merge = create(:merge_request, :simple)
# Generate a real line_code value so we know it will match. We use a
# random line from a random diff just for funsies.
diff = merge.diffs.to_a.sample
line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
# We're persisting in order to trigger the set_diff callback
note = create(:note, noteable: merge, line_code: code)
# Make sure we don't get a false positive from a guard clause
expect(note).to receive(:find_noteable_diff).and_call_original
expect(note).to be_active
end
end
end
describe "editable?" do
it "returns true" do
note = build(:note)
......@@ -220,4 +286,12 @@ describe Note, models: true do
expect(note.is_award?).to be_falsy
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
require 'spec_helper'
describe ProjectGroupLink do
describe "Associations" do
it { should belong_to(:group) }
it { should belong_to(:project) }
end
describe "Validation" do
let!(:project_group_link) { create(:project_group_link) }
it { should validate_presence_of(:project_id) }
it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
it { should validate_presence_of(:group_id) }
it { should validate_presence_of(:group_access) }
end
end
......@@ -582,7 +582,58 @@ describe Project, models: true do
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
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
......@@ -666,4 +717,20 @@ describe Project, models: true do
fork_project.unlink_fork(user)
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
......@@ -67,6 +67,50 @@ describe ProjectTeam, models: true do
end
end
describe :max_invited_level do
let(:group) { create(:group) }
let(:project) { create(:empty_project) }
before do
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::DEVELOPER
)
group.add_user(master, Gitlab::Access::MASTER)
group.add_user(reporter, Gitlab::Access::REPORTER)
end
it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
end
describe :max_member_access do
let(:group) { create(:group) }
let(:project) { create(:empty_project) }
before do
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::DEVELOPER
)
group.add_user(master, Gitlab::Access::MASTER)
group.add_user(reporter, Gitlab::Access::REPORTER)
end
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
it "does not have an access" do
project.namespace.update(share_with_group_lock: true)
expect(project.team.max_member_access(master.id)).to be_nil
expect(project.team.max_member_access(reporter.id)).to be_nil
end
end
describe "#human_max_access" do
it 'returns Master role' do
user = create(:user)
......
......@@ -59,4 +59,48 @@ describe Snippet, models: true do
expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}"
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
......@@ -463,17 +463,43 @@ describe User, models: true do
end
end
describe 'search' do
let(:user1) { create(:user, username: 'James', email: 'james@testing.com') }
let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') }
it "should be case insensitive" do
expect(User.search(user1.username.upcase).to_a).to eq([user1])
expect(User.search(user1.username.downcase).to_a).to eq([user1])
expect(User.search(user2.username.upcase).to_a).to eq([user2])
expect(User.search(user2.username.downcase).to_a).to eq([user2])
expect(User.search(user1.username.downcase).to_a.size).to eq(2)
expect(User.search(user2.username.downcase).to_a.size).to eq(1)
describe '.search' do
let(:user) { create(:user) }
it 'returns users with a matching name' do
expect(described_class.search(user.name)).to eq([user])
end
it 'returns users with a partially matching name' do
expect(described_class.search(user.name[0..2])).to eq([user])
end
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
......
require 'rails_helper'
describe API::API, api: true do
include ApiHelpers
describe 'GET /projects/:project_id/snippets/:id' do
# TODO (rspeicher): Deprecated; remove in 9.0
it 'always exposes expires_at as nil' do
admin = create(:admin)
snippet = create(:project_snippet, author: admin)
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
expect(json_response).to have_key('expires_at')
expect(json_response['expires_at']).to be_nil
end
end
end
......@@ -747,6 +747,42 @@ describe API::API, api: true do
end
end
describe "POST /projects/:id/share" do
let(:group) { create(:group) }
it "should share project with group" do
expect do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
end.to change { ProjectGroupLink.count }.by(1)
expect(response.status).to eq 201
expect(json_response['group_id']).to eq group.id
expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
end
it "should return a 400 error when group id is not given" do
post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
expect(response.status).to eq 400
end
it "should return a 400 error when access level is not given" do
post api("/projects/#{project.id}/share", user), group_id: group.id
expect(response.status).to eq 400
end
it "should return a 400 error when sharing is disabled" do
project.namespace.update(share_with_group_lock: true)
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
expect(response.status).to eq 400
end
it "should return a 409 error when wrong params passed" do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
expect(response.status).to eq 409
expect(json_response['message']).to eq 'Group access is not included in the list'
end
end
describe 'GET /projects/search/:query' do
let!(:query) { 'query'}
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
......
......@@ -14,7 +14,6 @@ require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'sidekiq/testing/inline'
require 'benchmark/ips'
require 'rspec/retry'
# Requires supporting ruby files with custom matchers and macros, etc,
......@@ -38,7 +37,6 @@ RSpec.configure do |config|
config.include ActiveJob::TestHelper
config.include StubGitlabCalls
config.include StubGitlabData
config.include BenchmarkMatchers, benchmark: true
config.infer_spec_type_from_file_location!
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