summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBaldinof <baldinof@gmail.com>2016-03-14 21:51:06 +0100
committerBaldinof <baldinof@gmail.com>2016-03-14 21:51:06 +0100
commit436caf4e8b7beb8cb61bb1045273488477841880 (patch)
tree99e0793f063507d05f71d901cb94955afcfe1660
parente8c723543cfc4c1d905a5794a2da1bef7689d784 (diff)
parentca3fc2296f13f8dc7c89c4361b448ed46708cab7 (diff)
downloadgitlab-ce-436caf4e8b7beb8cb61bb1045273488477841880.tar.gz
Merge branch 'master' into fix_remove_fork_link
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml27
-rw-r--r--CHANGELOG10
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock10
-rw-r--r--app/assets/javascripts/application.js.coffee28
-rw-r--r--app/assets/javascripts/awards_handler.coffee77
-rw-r--r--app/assets/javascripts/breakpoints.coffee33
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee5
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee4
-rw-r--r--app/assets/javascripts/sidebar.js.coffee18
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss20
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss7
-rw-r--r--app/assets/stylesheets/pages/awards.scss210
-rw-r--r--app/controllers/concerns/continue_params.rb13
-rw-r--r--app/controllers/groups_controller.rb10
-rw-r--r--app/controllers/projects/forks_controller.rb13
-rw-r--r--app/controllers/projects/group_links_controller.rb23
-rw-r--r--app/controllers/projects/imports_controller.rb12
-rw-r--r--app/controllers/projects/project_members_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/finders/issuable_finder.rb7
-rw-r--r--app/finders/projects_finder.rb8
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/milestones_helper.rb1
-rw-r--r--app/models/ci/runner.rb20
-rw-r--r--app/models/concerns/issuable.rb21
-rw-r--r--app/models/group.rb14
-rw-r--r--app/models/merge_request.rb19
-rw-r--r--app/models/milestone.rb18
-rw-r--r--app/models/namespace.rb12
-rw-r--r--app/models/note.rb52
-rw-r--r--app/models/project.rb45
-rw-r--r--app/models/project_group_link.rb36
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/project_team.rb52
-rw-r--r--app/models/snippet.rb24
-rw-r--r--app/models/user.rb19
-rw-r--r--app/services/search/global_service.rb3
-rw-r--r--app/services/search/project_service.rb2
-rw-r--r--app/services/search/snippet_service.rb5
-rw-r--r--app/views/admin/groups/show.html.haml16
-rw-r--r--app/views/emojis/index.html.haml10
-rw-r--r--app/views/groups/_activities.html.haml12
-rw-r--r--app/views/groups/_shared_projects.html.haml18
-rw-r--r--app/views/groups/activity.html.haml9
-rw-r--r--app/views/groups/edit.html.haml9
-rw-r--r--app/views/groups/show.html.haml22
-rw-r--r--app/views/layouts/nav/_group.html.haml7
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml6
-rw-r--r--app/views/projects/go_import.html.haml5
-rw-r--r--app/views/projects/group_links/index.html.haml41
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml21
-rw-r--r--app/views/projects/project_members/index.html.haml3
-rw-r--r--app/views/search/_filter.html.haml46
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/votes/_votes_block.html.haml27
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/devise.rb10
-rw-r--r--config/initializers/go_get.rb1
-rw-r--r--config/initializers/mysql_ignore_postgresql_options.rb49
-rw-r--r--config/initializers/postgresql_opclasses_support.rb188
-rw-r--r--config/routes.rb4
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb10
-rw-r--r--db/migrate/20130820102832_add_access_to_project_group_link.rb5
-rw-r--r--db/migrate/20150930110012_add_group_share_lock.rb5
-rw-r--r--db/migrate/20160226114608_add_trigram_indexes_for_searching.rb53
-rw-r--r--db/migrate/20160307221555_disallow_blank_line_code_on_note.rb9
-rw-r--r--db/schema.rb42
-rw-r--r--doc/api/notes.md1
-rw-r--r--doc/api/project_snippets.md1
-rw-r--r--doc/api/projects.md14
-rw-r--r--doc/ci/README.md23
-rw-r--r--doc/ci/enable_or_disable_ci.md2
-rw-r--r--doc/ci/examples/README.md16
-rw-r--r--doc/ci/quick_start/README.md11
-rw-r--r--doc/ci/yaml/README.md298
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/benchmarking.md69
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/requirements.md11
-rw-r--r--doc/integration/saml.md52
-rw-r--r--doc/update/8.5-to-8.6.md164
-rw-r--r--doc/web_hooks/web_hooks.md1
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/groups/max_access_level.pngbin0 -> 135354 bytes
-rw-r--r--doc/workflow/groups/other_group_sees_shared_project.pngbin0 -> 118382 bytes
-rw-r--r--doc/workflow/groups/share_project_with_groups.pngbin0 -> 118868 bytes
-rw-r--r--doc/workflow/share_projects_with_other_groups.md30
-rw-r--r--doc/workflow/share_with_group.md13
-rw-r--r--doc/workflow/share_with_group.pngbin0 -> 53784 bytes
-rw-r--r--features/admin/groups.feature5
-rw-r--r--features/groups.feature4
-rw-r--r--features/project/group_links.feature16
-rw-r--r--features/project/network_graph.feature3
-rw-r--r--features/project/team_management.feature5
-rw-r--r--features/steps/admin/groups.rb19
-rw-r--r--features/steps/dashboard/issues.rb21
-rw-r--r--features/steps/dashboard/merge_requests.rb20
-rw-r--r--features/steps/project/issues/award_emoji.rb29
-rw-r--r--features/steps/project/network_graph.rb9
-rw-r--r--features/steps/project/project_group_links.rb50
-rw-r--r--features/steps/project/team_management.rb19
-rw-r--r--features/steps/shared/paths.rb4
-rw-r--r--lib/api/entities.rb7
-rw-r--r--lib/api/projects.rb27
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb27
-rw-r--r--lib/gitlab/devise_failure.rb23
-rw-r--r--lib/gitlab/github_import/importer.rb11
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb10
-rw-r--r--lib/gitlab/middleware/go.rb50
-rw-r--r--lib/gitlab/project_search_results.rb10
-rw-r--r--lib/gitlab/search_results.rb25
-rw-r--r--lib/gitlab/snippet_search_results.rb10
-rw-r--r--lib/tasks/spec.rake13
-rw-r--r--spec/benchmarks/finders/issues_finder_spec.rb55
-rw-r--r--spec/benchmarks/finders/trending_projects_finder_spec.rb14
-rw-r--r--spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb41
-rw-r--r--spec/benchmarks/models/milestone_spec.rb17
-rw-r--r--spec/benchmarks/models/project_spec.rb50
-rw-r--r--spec/benchmarks/models/project_team_spec.rb23
-rw-r--r--spec/benchmarks/models/user_spec.rb78
-rw-r--r--spec/benchmarks/services/projects/create_service_spec.rb28
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb4
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb37
-rw-r--r--spec/controllers/projects_controller_spec.rb13
-rw-r--r--spec/factories/project_group_links.rb6
-rw-r--r--spec/finders/projects_finder_spec.rb34
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb84
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb64
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb30
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/search_results_spec.rb55
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb25
-rw-r--r--spec/models/ci/runner_spec.rb28
-rw-r--r--spec/models/concerns/issuable_spec.rb47
-rw-r--r--spec/models/group_spec.rb26
-rw-r--r--spec/models/merge_request_spec.rb6
-rw-r--r--spec/models/milestone_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb29
-rw-r--r--spec/models/note_spec.rb82
-rw-r--r--spec/models/project_group_link_spec.rb17
-rw-r--r--spec/models/project_spec.rb67
-rw-r--r--spec/models/project_team_spec.rb44
-rw-r--r--spec/models/snippet_spec.rb44
-rw-r--r--spec/models/user_spec.rb48
-rw-r--r--spec/requests/api/project_snippets_spec.rb18
-rw-r--r--spec/requests/api/projects_spec.rb36
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/matchers/benchmark_matchers.rb61
158 files changed, 2902 insertions, 1030 deletions
diff --git a/.gitignore b/.gitignore
index 1eb785451f4..8f861d76a37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
.sass-cache/
.secret
.vagrant
+.byebug_history
Vagrantfile
backups/*
config/aws.yml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d21785f7af2..bd013d50faa 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.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
@@ -332,4 +307,4 @@ notify:slack:
- master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- - tags@gitlab-org/gitlab-ee \ No newline at end of file
+ - tags@gitlab-org/gitlab-ee
diff --git a/CHANGELOG b/CHANGELOG
index 84739fab82b..4324da1fc61 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
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)
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index a04abec9149..bc02b8685c1 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.10
+2.6.11
diff --git a/Gemfile b/Gemfile
index 7e70761a77a..1550afb1b56 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 432bdc344ad..d4e28db00d6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 1212e89975b..e9c6196e926 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -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()
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 8f89d3e61a2..03a44874161 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,6 +1,6 @@
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()
- $("#emoji_search").focus()
- else
- $.get "/emojis", (response) ->
- $(".add-award").after response
- $(".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
+ $('.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
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
new file mode 100644
index 00000000000..1ffaaddb055
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.coffee
@@ -0,0 +1,33 @@
+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()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 54b28f2dd8d..1be86e3b820 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -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'
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 8e1449bc59c..b94de4c7b5e 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -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"
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index cff309c5972..eea3f5ee910 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -1,8 +1,7 @@
-$(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()
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index e6609ac7108..6edabe20136 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -157,3 +157,7 @@
float: right;
}
}
+
+.content-block-small {
+ padding: 10px 0;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 009d621fc74..5b647fc6176 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -294,7 +294,7 @@
}
.dropdown-content {
- max-height: 200px;
+ max-height: 215px;
overflow-y: scroll;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index e624982c5c9..4c4033e3ae7 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -141,22 +141,18 @@ header {
margin-left: $sidebar_collapsed_width;
}
-@media (max-width: $screen-md-max) {
- .header-collapsed {
- margin-left: $sidebar_collapsed_width;
- }
-
- .header-expanded {
- margin-left: $sidebar_width;
- }
-}
+.header-collapsed {
+ margin-left: $sidebar_collapsed_width;
-@media(min-width: $screen-md-max) {
- .header-collapsed {
+ @media (min-width: $screen-md-min) {
@include collapsed-header;
}
+}
+
+.header-expanded {
+ margin-left: $sidebar_collapsed_width;
- .header-expanded {
+ @media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 7de874c8bcd..b2fbc95e043 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -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%;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 6b382e4b1b2..26df9acd2ae 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -34,12 +34,12 @@
@media (min-width: $screen-sm-min) {
padding-right: $gutter_width;
}
-
+
}
}
.sidebar-wrapper {
- z-index: 99;
+ z-index: 999;
background: $background-color;
}
@@ -203,7 +203,11 @@
}
@mixin expanded-sidebar {
- padding-left: $sidebar_width;
+ 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) */
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 835364b2990..0261c384a58 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 87dd30f4111..28994e60baa 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,125 +1,133 @@
.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;
+.emoji-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin-top: 3px;
+ z-index: 1000;
+ min-width: 160px;
+ font-size: 14px;
+ 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);
+ }
- &:hover {
- background-color: #dce0e5;
+ .emoji-menu-content {
+ padding: $gl-padding;
+ width: 300px;
+ height: 300px;
+ overflow-y: scroll;
+
+ 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;
}
+ }
+}
- &.active {
- border-color: $border-gray-light;
- background-color: $gray-light;
-
- &:hover {
- background-color: #dce0e5;
- }
+.emoji-menu-list {
+ list-style: none;
+ padding-left: 0;
+ margin-bottom: 0;
+}
- .counter {
- font-weight: bold;
- }
- }
+.emoji-menu-list-item {
+ padding: 3px;
+ margin-left: 1px;
+ margin-right: 1px;
+}
- .icon {
- float: left;
- margin-right: 10px;
- }
+.emoji-menu-btn {
+ display: block;
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-base;
+ transition: transform .15s cubic-bezier(.3, 0, .2, 2);
+
+ &:hover {
+ background-color: transparent;
+ outline: 0;
+ transform: scale(1.3);
+ }
- .counter {
- float: left;
- }
+ &:focus,
+ &:active {
+ outline: 0;
}
- .awards-controls {
+ .emoji-icon {
+ display: inline-block;
position: relative;
- margin-left: 10px;
- float: left;
+ top: 3px;
+ }
+}
- .add-award {
- font-size: 24px;
- color: $gl-gray;
- position: relative;
- top: 2px;
+.award-menu-holder {
+ display: inline-block;
+ position: relative;
+}
- &:hover,
- &:link {
- text-decoration: none;
- }
- }
+.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;
+ }
- .emoji-menu{
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
+ &.is-loading {
+ .award-control-icon {
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);
- box-shadow: 0 6px 12px rgba(0,0,0,.175);
-
- .emoji-menu-content {
- padding: $gl-padding;
- width: 300px;
- height: 300px;
- overflow-y: scroll;
-
- h5 {
- clear: left;
- }
-
- 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;
- }
-
- li {
- cursor: pointer;
- width: 30px;
- height: 30px;
- text-align: center;
- float: left;
- margin: 3px;
- list-decorate: none;
- @include border-radius(5px);
-
- &:hover {
- background-color: #ccc;
- }
- }
- }
}
+
+ .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;
}
}
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
new file mode 100644
index 00000000000..0a995c45bdf
--- /dev/null
+++ b/app/controllers/concerns/continue_params.rb
@@ -0,0 +1,13 @@
+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
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index f05c29e9974..06c5c8be9a5 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -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
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 7b202f3862f..a1b8632df98 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -1,4 +1,6 @@
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
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
new file mode 100644
index 00000000000..4159e53bfa9
--- /dev/null
+++ b/app/controllers/projects/group_links_controller.rb
@@ -0,0 +1,23 @@
+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
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 196996f1752..7756f0f0ed3 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,4 +1,6 @@
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.'
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 8364fc293b7..e7bddc4a6f1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -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
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a26d11459f0..94789702d65 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,7 +1,6 @@
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
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index c88a420b412..19e8c7a92be 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -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] })
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 3b4e0362e04..2c55f088594 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 368969c6472..d1b1c61b710 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index e3e7daa49c5..e8ac8788d9d 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -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
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index e725a6d468c..90349a07594 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -23,7 +23,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
-
+
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
@@ -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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 27b97944e38..3c42f582937 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -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)
diff --git a/app/models/group.rb b/app/models/group.rb
index 76042b3e3fd..9919ca112dc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -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)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 18ec48b57f4..b4a6731ff20 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -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}"
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e3969f32dd6..374590ba0c5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -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("]", "\\]")
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index bdb33f37495..55842df1e2d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -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)
diff --git a/app/models/note.rb b/app/models/note.rb
index 3b20d5d22b6..b0c33f2eec5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -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 }
- end
+ @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
diff --git a/app/models/project.rb b/app/models/project.rb
index 859758293e1..8d9908128e2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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)
- 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)}%")
+ 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(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?
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
new file mode 100644
index 00000000000..e52a6bd7c84
--- /dev/null
+++ b/app/models/project_group_link.rb
@@ -0,0 +1,36 @@
+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
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index e10b5529b42..d9f0849d147 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -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
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 9629c7e1bb9..70a8bbaba65 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -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)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index dd3925c7a7d..b9e835a4486 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -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)
diff --git a/app/models/user.rb b/app/models/user.rb
index 505a547d8ec..8871b0ab9fa 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index e904cb6c6fc..e1e94c5cc38 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -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
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index f630c0a3790..c08881dce4b 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -7,7 +7,7 @@ module Search
end
def execute
- Gitlab::ProjectSearchResults.new(project.id,
+ Gitlab::ProjectSearchResults.new(project,
params[:search],
params[:repository_ref])
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 8ca0877321d..0b3e713e220 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -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
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f7fd156b84a..264fa1bf0cd 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -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
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
index b66e513e4d2..3443a8e2307 100644
--- a/app/views/emojis/index.html.haml
+++ b/app/views/emojis/index.html.haml
@@ -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
- = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) \ No newline at end of file
+ %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"])
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
new file mode 100644
index 00000000000..dc76599b776
--- /dev/null
+++ b/app/views/groups/_activities.html.haml
@@ -0,0 +1,12 @@
+.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
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
new file mode 100644
index 00000000000..d707ad4272d
--- /dev/null
+++ b/app/views/groups/_shared_projects.html.haml
@@ -0,0 +1,18 @@
+- 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
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
new file mode 100644
index 00000000000..f73e1d9e865
--- /dev/null
+++ b/app/views/groups/activity.html.haml
@@ -0,0 +1,9 @@
+= 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'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 3430f56a9c9..83936d39b16 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -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"
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 6148d8cb3d2..de314a4190c 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -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
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index e5e2a59eaed..59411ae1da1 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -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
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 319974e12c5..0ae83ee01eb 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -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
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 3359716202f..dc3050f02e5 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -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')
diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml
deleted file mode 100644
index 87ac75a350f..00000000000
--- a/app/views/projects/go_import.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-!!! 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"}
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
new file mode 100644
index 00000000000..13f5fc141fa
--- /dev/null
+++ b/app/views/projects/group_links/index.html.haml
@@ -0,0 +1,41 @@
+- 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"
+
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f5bb5d998bb..0242276cd84 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -71,7 +71,7 @@
.merge-requests
= render 'merge_requests'
- .content-block
+ .content-block.content-block-small
= render 'votes/votes_block', votable: @issue
.row
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index b262892ac65..ee5b9fd95a8 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -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
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
new file mode 100644
index 00000000000..62888e41935
--- /dev/null
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -0,0 +1,21 @@
+- @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)}
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 0f8848a5cbe..ebcfc907ebb 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -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"
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index ec478a5963d..4ef544136a8 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -6,14 +6,21 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(group_id: nil) do
- Any
- - current_user.authorized_groups.sort_by(&:name).each do |group|
- %li
- = link_to search_filter_path(group_id: group.id, project_id: nil) do
- = group.name
+ .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), 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), class: ("is-active" if params[:group_id] == group.id.to_s) do
+ = group.name
.dropdown.inline.prepend-left-10.project-filter
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
@@ -23,11 +30,18 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(project_id: nil) do
- Any
- - 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
- = project.name_with_namespace
+ .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), 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), class: ("is-active" if params[:project_id] == project.id.to_s) do
+ = project.name_with_namespace
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c3fbba2ba54..4df96a06dbe 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -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
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 176fd29cb57..20d2d5f317b 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,14 +1,17 @@
.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});
diff --git a/config/application.rb b/config/application.rb
index d8d1e7b4679..2b103c4592d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index d82cfb3ec0c..31dceaebcad 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -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
- # manager.intercept_401 = false
- # manager.default_strategies(scope: :user).unshift :some_external_strategy
- # end
+ config.warden do |manager|
+ manager.failure_app = Gitlab::DeviseFailure
+ # manager.intercept_401 = false
+ # manager.default_strategies(scope: :user).unshift :some_external_strategy
+ end
if Gitlab::LDAP::Config.enabled?
Gitlab.config.ldap.servers.values.each do |server|
diff --git a/config/initializers/go_get.rb b/config/initializers/go_get.rb
new file mode 100644
index 00000000000..7e7896b4900
--- /dev/null
+++ b/config/initializers/go_get.rb
@@ -0,0 +1 @@
+Rails.application.config.middleware.use(Gitlab::Middleware::Go)
diff --git a/config/initializers/mysql_ignore_postgresql_options.rb b/config/initializers/mysql_ignore_postgresql_options.rb
new file mode 100644
index 00000000000..835f3ec5574
--- /dev/null
+++ b/config/initializers/mysql_ignore_postgresql_options.rb
@@ -0,0 +1,49 @@
+# 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
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
new file mode 100644
index 00000000000..820cc89ef57
--- /dev/null
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -0,0 +1,188 @@
+# 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
diff --git a/config/routes.rb b/config/routes.rb
index a918b5bd3f0..536397e318a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
new file mode 100644
index 00000000000..395083f2a03
--- /dev/null
+++ b/db/migrate/20130711063759_create_project_group_links.rb
@@ -0,0 +1,10 @@
+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
diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb
new file mode 100644
index 00000000000..00e3947a6bb
--- /dev/null
+++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb
@@ -0,0 +1,5 @@
+class AddAccessToProjectGroupLink < ActiveRecord::Migration
+ def change
+ add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
+ end
+end
diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb
new file mode 100644
index 00000000000..78d1a4538f2
--- /dev/null
+++ b/db/migrate/20150930110012_add_group_share_lock.rb
@@ -0,0 +1,5 @@
+class AddGroupShareLock < ActiveRecord::Migration
+ def change
+ add_column :namespaces, :share_with_group_lock, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
new file mode 100644
index 00000000000..003169c13c6
--- /dev/null
+++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
@@ -0,0 +1,53 @@
+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
diff --git a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
new file mode 100644
index 00000000000..49e787d9a9a
--- /dev/null
+++ b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
@@ -0,0 +1,9 @@
+class DisallowBlankLineCodeOnNote < ActiveRecord::Migration
+ def up
+ execute("UPDATE notes SET line_code = NULL WHERE line_code = ''")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a74b86d8e2f..fa406f70907 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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,26 +570,31 @@ 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
- t.string "path", null: false
+ t.string "name", null: false
+ t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
- t.string "description", default: "", null: false
+ 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
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 85d4f0bafa2..d4d63e825ab 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -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"
}
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index fb802102e3a..a7acf37b5bc 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -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"
}
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 9e9486cd87a..3703f4b327a 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -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.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 2120b5b2850..4abc45bf9bb 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -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)
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index 9bd2f5aff22..c10f82054e2 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.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`.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 31f29f4a082..cc059dc4376 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,13 +1,15 @@
-## 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)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 624d9899c79..9aba4326e11 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -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
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 051eaa04152..4d27c07913d 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/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.
diff --git a/doc/development/README.md b/doc/development/README.md
index f5c3107ff44..1b281809afc 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,7 +1,6 @@
# 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)
diff --git a/doc/development/benchmarking.md b/doc/development/benchmarking.md
deleted file mode 100644
index 88e18ee95f9..00000000000
--- a/doc/development/benchmarking.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# 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
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 0fd54be58b0..4f011397269 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -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
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 8df142c531b..d59b7f0e84d 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -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.
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 148c4ac1886..1c3dc707f6d 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -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
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
new file mode 100644
index 00000000000..024f6e8a433
--- /dev/null
+++ b/doc/update/8.5-to-8.6.md
@@ -0,0 +1,164 @@
+# 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.
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 87049427139..afdf1a682e2 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -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
}
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 2ac32373ce9..25893f948ea 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -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)
diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png
new file mode 100644
index 00000000000..71106a8a5a0
--- /dev/null
+++ b/doc/workflow/groups/max_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png
new file mode 100644
index 00000000000..cbf2c3c1fdc
--- /dev/null
+++ b/doc/workflow/groups/other_group_sees_shared_project.png
Binary files differ
diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png
new file mode 100644
index 00000000000..a5dbc89fe90
--- /dev/null
+++ b/doc/workflow/groups/share_project_with_groups.png
Binary files differ
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
new file mode 100644
index 00000000000..4c59f59c587
--- /dev/null
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -0,0 +1,30 @@
+# 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'.
diff --git a/doc/workflow/share_with_group.md b/doc/workflow/share_with_group.md
new file mode 100644
index 00000000000..3b7690973cb
--- /dev/null
+++ b/doc/workflow/share_with_group.md
@@ -0,0 +1,13 @@
+# 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)
diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png
new file mode 100644
index 00000000000..a0ca6f14552
--- /dev/null
+++ b/doc/workflow/share_with_group.png
Binary files differ
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
index 2edb3964f70..ab7de7ac315 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -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
diff --git a/features/groups.feature b/features/groups.feature
index a60c3860b83..419a5d3963d 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -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
diff --git a/features/project/group_links.feature b/features/project/group_links.feature
new file mode 100644
index 00000000000..2657c4487ad
--- /dev/null
+++ b/features/project/group_links.feature
@@ -0,0 +1,16 @@
+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"
diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature
index 6cc89a15a78..89a02706bd2 100644
--- a/features/project/network_graph.feature
+++ b/features/project/network_graph.feature
@@ -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"
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 06fb45c8bde..5888662fc3f 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -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
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index 43fd91d0d4c..e1f1db2872f 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -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
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index d723300f485..f4a56865532 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -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)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 7fc0e444e86..a2adc87f8ef 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -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)
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 937fbbd34eb..ce2554bc80d 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -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
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 7a83d32a240..9b59b682676 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -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
diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb
new file mode 100644
index 00000000000..739a85e5fa4
--- /dev/null
+++ b/features/steps/project/project_group_links.rb
@@ -0,0 +1,50 @@
+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
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index caad52def79..3fbcf770b62 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -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
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index da9d1503ebc..2bd8ea745e4 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -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
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5b5b8bd044b..7204dca34ba 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -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
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6067c8b4a5e..6fcb5261e40 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -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:
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 28e074cd289..c89e1b51019 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -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
diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb
new file mode 100644
index 00000000000..a78fde9d782
--- /dev/null
+++ b/lib/gitlab/devise_failure.rb
@@ -0,0 +1,23 @@
+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
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index e2a85f29825..172c5441e36 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -45,10 +45,13 @@ 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)
- import_comments(pull_request.number, merge_request)
- import_comments_on_diff(pull_request.number, merge_request)
+ 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
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index f96fed0f5cf..4e507b090e8 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -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
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
new file mode 100644
index 00000000000..50b0dd32380
--- /dev/null
+++ b/lib/gitlab/middleware/go.rb
@@ -0,0 +1,50 @@
+# 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
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 70de6a74e76..0607a8b9592 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -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
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 2ab2d4af797..f13528a2eea 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -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
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index addda95be2b..e0e74ff8359 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -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
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 0985ef3a669..2cf7a25a0fd 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -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
diff --git a/spec/benchmarks/finders/issues_finder_spec.rb b/spec/benchmarks/finders/issues_finder_spec.rb
deleted file mode 100644
index b57a33004a4..00000000000
--- a/spec/benchmarks/finders/issues_finder_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-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
diff --git a/spec/benchmarks/finders/trending_projects_finder_spec.rb b/spec/benchmarks/finders/trending_projects_finder_spec.rb
deleted file mode 100644
index 551ce21840d..00000000000
--- a/spec/benchmarks/finders/trending_projects_finder_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-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
diff --git a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
deleted file mode 100644
index 3855763b200..00000000000
--- a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-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
diff --git a/spec/benchmarks/models/milestone_spec.rb b/spec/benchmarks/models/milestone_spec.rb
deleted file mode 100644
index a94afc4c40d..00000000000
--- a/spec/benchmarks/models/milestone_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-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
diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb
deleted file mode 100644
index cee0949edc5..00000000000
--- a/spec/benchmarks/models/project_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-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
diff --git a/spec/benchmarks/models/project_team_spec.rb b/spec/benchmarks/models/project_team_spec.rb
deleted file mode 100644
index 8b039ef7317..00000000000
--- a/spec/benchmarks/models/project_team_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-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
diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb
deleted file mode 100644
index 1be7a8d3ed9..00000000000
--- a/spec/benchmarks/models/user_spec.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-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
diff --git a/spec/benchmarks/services/projects/create_service_spec.rb b/spec/benchmarks/services/projects/create_service_spec.rb
deleted file mode 100644
index 25ed48c34fd..00000000000
--- a/spec/benchmarks/services/projects/create_service_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-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
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 0147bd2b953..2acbba469e3 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -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
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 09ec4f18f9d..0ddbec9eac2 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -2,30 +2,41 @@ require "spec_helper"
describe Projects::RepositoriesController do
let(:project) { create(:project) }
- let(:user) { create(:user) }
describe "GET archive" do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- it "uses Gitlab::Workhorse" do
- expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
+ 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"
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ expect(response.content_type).to start_with 'text/html'
+ expect(response).to be_redirect
+ end
end
- context "when the service raises an error" do
+ context 'as a user' do
+ let(:user) { create(:user) }
before do
- allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
+ 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")
- it "renders Not Found" do
get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ end
+
+ context "when the service raises an error" do
+
+ before do
+ allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
+ end
+
+ it "renders Not Found" do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
- expect(response.status).to eq(404)
+ expect(response.status).to eq(404)
+ end
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 6eee4dfe229..1893e946f5c 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -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
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
new file mode 100644
index 00000000000..e73cc05f9d7
--- /dev/null
+++ b/spec/factories/project_group_links.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :project_group_link do
+ project
+ group
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index f32641ef0f6..fae0da9d898 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -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,7 +60,35 @@ describe ProjectsFinder do
describe 'with a user' do
subject { finder.execute(user, group: group) }
- it { is_expected.to eq([public_project, internal_project]) }
+ 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
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 1e98280d045..fab6412d29f 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index 6cebcb5009a..e49dcb42342 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -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,24 +138,44 @@ 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
- let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
+ 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
- expect(pull_request.valid?).to eq true
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
+ end
+ end
+
+ 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
+ expect(pull_request.valid?).to eq false
+ end
+ end
+
+ 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
+ expect(pull_request.valid?).to eq false
+ end
end
end
- context 'when source branch doesn not exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
+ 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 branch doesn not exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
+ 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
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
new file mode 100644
index 00000000000..117a15264da
--- /dev/null
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -0,0 +1,30 @@
+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
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index efc2e5f4ef1..09adbc07dcb 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -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) }
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
new file mode 100644
index 00000000000..bb18f417858
--- /dev/null
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -0,0 +1,55 @@
+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
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
new file mode 100644
index 00000000000..e86b9ef6a63
--- /dev/null
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -0,0 +1,25 @@
+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
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index e891838672e..25e9e5eca48 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -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
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 600089802b2..aff384c2949 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -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
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 3c995053eec..c9245fc9535 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -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
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 59c40922abb..8bf68013fd2 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -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}"
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 28f13100d15..de1757bf67a 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -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
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index e0b3290e416..3c3a580942a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -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
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 33085dac4ea..6b18936edb1 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -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
+
+ 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
+ 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
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
new file mode 100644
index 00000000000..2fa6715fcaf
--- /dev/null
+++ b/spec/models/project_group_link_spec.rb
@@ -0,0 +1,17 @@
+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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ba4fb2f8222..1ca78daa5b3 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -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
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 7b63da005f0..bacb17a8883 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -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)
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 7e5b5499aea..5077ac7b62b 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 412101ac9f9..909b6796591 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -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
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
new file mode 100644
index 00000000000..3722ddf5a33
--- /dev/null
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -0,0 +1,18 @@
+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
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 9f2365a4832..a6699cdc81c 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -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) }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 159fb964171..7d939ca7509 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -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!
diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb
deleted file mode 100644
index 84f655c2119..00000000000
--- a/spec/support/matchers/benchmark_matchers.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-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