summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG18
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock28
-rw-r--r--app/assets/javascripts/application.js.coffee23
-rw-r--r--app/assets/javascripts/ci/build.coffee91
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js.coffee17
-rw-r--r--app/assets/javascripts/labels_select.js.coffee7
-rw-r--r--app/assets/javascripts/logo.js.coffee2
-rw-r--r--app/assets/javascripts/project_new.js.coffee19
-rw-r--r--app/assets/javascripts/sidebar.js.coffee9
-rw-r--r--app/assets/javascripts/users_select.js.coffee8
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss26
-rw-r--r--app/assets/stylesheets/framework/header.scss51
-rw-r--r--app/assets/stylesheets/framework/nav.scss6
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss85
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/awards.scss10
-rw-r--r--app/assets/stylesheets/pages/builds.scss91
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/assets/stylesheets/pages/xterm.scss19
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/profiles/notifications_controller.rb23
-rw-r--r--app/controllers/projects/avatars_controller.rb5
-rw-r--r--app/controllers/projects/builds_controller.rb2
-rw-r--r--app/controllers/projects/git_http_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb7
-rw-r--r--app/controllers/projects/raw_controller.rb5
-rw-r--r--app/controllers/projects/repositories_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/snippets_finder.rb2
-rw-r--r--app/helpers/branches_helper.rb4
-rw-r--r--app/helpers/dropdowns_helper.rb4
-rw-r--r--app/helpers/nav_helper.rb10
-rw-r--r--app/helpers/notifications_helper.rb19
-rw-r--r--app/helpers/workhorse_helper.rb24
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/issue.rb12
-rw-r--r--app/models/merge_request.rb21
-rw-r--r--app/models/note.rb19
-rw-r--r--app/models/notification_setting.rb1
-rw-r--r--app/models/project.rb90
-rw-r--r--app/models/project_services/bamboo_service.rb44
-rw-r--r--app/models/project_services/issue_tracker_service.rb18
-rw-r--r--app/models/project_services/teamcity_service.rb37
-rw-r--r--app/models/project_team.rb10
-rw-r--r--app/models/service.rb10
-rw-r--r--app/models/user.rb43
-rw-r--r--app/services/ci/create_trigger_request_service.rb2
-rw-r--r--app/services/notification_service.rb18
-rw-r--r--app/views/dashboard/issues.atom.builder4
-rw-r--r--app/views/groups/issues.atom.builder4
-rw-r--r--app/views/issues/_issue.atom.builder20
-rw-r--r--app/views/layouts/_collapse_button.html.haml5
-rw-r--r--app/views/layouts/_page.html.haml10
-rw-r--r--app/views/layouts/ci/_page.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/nav/_admin.html.haml42
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml23
-rw-r--r--app/views/layouts/nav/_explore.html.haml8
-rw-r--r--app/views/layouts/nav/_group.html.haml6
-rw-r--r--app/views/layouts/nav/_profile.html.haml10
-rw-r--r--app/views/layouts/nav/_project.html.haml29
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml28
-rw-r--r--app/views/projects/_merge_request_settings.html.haml11
-rw-r--r--app/views/projects/artifacts/browse.html.haml1
-rw-r--r--app/views/projects/builds/_header.html.haml16
-rw-r--r--app/views/projects/builds/_sidebar.html.haml93
-rw-r--r--app/views/projects/builds/_user.html.haml4
-rw-r--r--app/views/projects/builds/show.html.haml189
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/issues/_head.html.haml25
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/issues/index.atom.builder4
-rw-r--r--app/views/projects/issues/index.html.haml33
-rw-r--r--app/views/projects/labels/index.html.haml63
-rw-r--r--app/views/projects/merge_requests/_head.html.haml5
-rw-r--r--app/views/projects/merge_requests/index.html.haml26
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml27
-rw-r--r--app/views/projects/merge_requests/widget/open/_build_failed.html.haml6
-rw-r--r--app/views/projects/milestones/index.html.haml29
-rw-r--r--app/views/projects/pipelines/_head.html.haml2
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/icons/_activity.svg15
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml2
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/initializers/metrics.rb17
-rw-r--r--config/mail_room.yml2
-rw-r--r--db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb15
-rw-r--r--db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb10
-rw-r--r--db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb11
-rw-r--r--db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb6
-rw-r--r--db/migrate/20160610201627_migrate_users_notification_level.rb21
-rw-r--r--db/migrate/20160610301627_remove_notification_level_from_users.rb7
-rw-r--r--db/schema.rb43
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/container_registry.md4
-rw-r--r--doc/administration/logs.md137
-rw-r--r--doc/api/merge_requests.md13
-rw-r--r--doc/ci/yaml/README.md27
-rw-r--r--doc/development/doc_styleguide.md52
-rw-r--r--doc/logs/logs.md93
-rw-r--r--doc/workflow/merge_requests.md11
-rw-r--r--doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.pngbin0 -> 17552 bytes
-rw-r--r--features/project/active_tab.feature12
-rw-r--r--features/steps/profile/profile.rb1
-rw-r--r--features/steps/project/active_tab.rb16
-rw-r--r--features/steps/project/builds/artifacts.rb4
-rw-r--r--features/steps/shared/active_tab.rb4
-rw-r--r--lib/api/entities.rb5
-rw-r--r--lib/api/helpers.rb18
-rw-r--r--lib/api/merge_requests.rb7
-rw-r--r--lib/api/repositories.rb10
-rw-r--r--lib/api/session.rb2
-rw-r--r--lib/backup/manager.rb21
-rw-r--r--lib/ci/api/entities.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb26
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/backend/grack_auth.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb13
-rw-r--r--lib/gitlab/sanitizers/svg.rb48
-rw-r--r--lib/gitlab/workhorse.rb18
-rwxr-xr-xscripts/prepare_build.sh4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb19
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb2
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb5
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb51
-rw-r--r--spec/features/builds_spec.rb23
-rw-r--r--spec/features/issues/bulk_assigment_labels_spec.rb17
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb15
-rw-r--r--spec/features/issues/filter_issues_spec.rb36
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb105
-rw-r--r--spec/features/profiles/preferences_spec.rb8
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb1
-rw-r--r--spec/helpers/issues_helper_spec.rb16
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb12
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb4
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb25
-rw-r--r--spec/lib/gitlab/auth_spec.rb26
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb13
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/sanitizers/svg_spec.rb94
-rw-r--r--spec/lib/gitlab/search_results_spec.rb16
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb2
-rw-r--r--spec/models/build_spec.rb2
-rw-r--r--spec/models/concerns/milestoneish_spec.rb14
-rw-r--r--spec/models/event_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb153
-rw-r--r--spec/models/note_spec.rb15
-rw-r--r--spec/models/notification_setting_spec.rb1
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb14
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb12
-rw-r--r--spec/models/project_spec.rb96
-rw-r--r--spec/models/project_team_spec.rb6
-rw-r--r--spec/models/service_spec.rb33
-rw-r--r--spec/requests/api/issues_spec.rb25
-rw-r--r--spec/requests/api/merge_requests_spec.rb24
-rw-r--r--spec/requests/api/milestones_spec.rb13
-rw-r--r--spec/requests/ci/api/builds_spec.rb2
-rw-r--r--spec/requests/jwt_controller_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb312
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb12
-rw-r--r--spec/services/todo_service_spec.rb18
176 files changed, 2575 insertions, 1141 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3dc48a89463..83a906932d0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,7 +7,8 @@ services:
cache:
key: "ruby21"
paths:
- - vendor
+ - vendor/apt
+ - vendor/ruby
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
@@ -91,9 +92,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- - knapsack spinach "-r rerun"
- # retry failed tests 3 times
- - retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
+ - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
paths:
- knapsack/
diff --git a/.rubocop.yml b/.rubocop.yml
index c637f5e12f5..dbdabbb9d4c 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -349,7 +349,7 @@ Style/MultilineArrayBraceLayout:
# Avoid multi-line chains of blocks.
Style/MultilineBlockChain:
- Enabled: false
+ Enabled: true
# Ensures newlines after multiline block do statements.
Style/MultilineBlockLayout:
diff --git a/CHANGELOG b/CHANGELOG
index b00c149a753..2aed8eb322b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,16 +1,21 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
+ - Fix Error 500 when using closes_issues API with an external issue tracker
+ - Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
- Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
+ - Fix issue with arrow keys not working in search autocomplete dropdown
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository
+ - Don't show tags for revert and cherry-pick operations
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
- Allow customisable text on the 'nearly there' page after a user signs up
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
+ - Fix SVG sanitizer to allow more elements
- Allow forking projects with restricted visibility level
- Added descriptions to notification settings dropdown
- Improve note validation to prevent errors when creating invalid note via API
@@ -31,7 +36,11 @@ v 8.9.0 (unreleased)
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Links from a wiki page to other wiki pages should be rewritten as expected
+ - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
- Fix issues filter when ordering by milestone
+ - Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3
+ - Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
+ - TeamCity Service: Fix URL handling when base URL contains a path
- Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
@@ -53,7 +62,9 @@ v 8.9.0 (unreleased)
- Improve error handling importing projects
- Remove duplicated notification settings
- Put project Files and Commits tabs under Code tab
+ - Decouple global notification level from user model
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
+ - Add workhorse controller and API helpers
- An indicator is now displayed at the top of the comment field for confidential issues.
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Improve issuables APIs performance when accessing notes !4471
@@ -61,15 +72,20 @@ v 8.9.0 (unreleased)
- Markdown editor now correctly resets the input value on edit cancellation !4175
- Toggling a task list item in a issue/mr description does not creates a Todo for mentions
- Improved UX of date pickers on issue & milestone forms
+ - Cache on the database if a project has an active external issue tracker.
+ - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
+ - All classes in the Banzai::ReferenceParser namespace are now instrumented
+ - Remove deprecated issues_tracker and issues_tracker_id from project model
+ - Allow users to create confidential issues in private projects
v 8.8.5 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds
- - Fix issue with arrow keys not working in search autocomplete dropdown
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
- Import GitHub repositories respecting the API rate limit
- Fix importer for GitHub comments on diff
- Disable Webhooks before proceeding with the GitHub import
+ - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace
v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493
diff --git a/Gemfile b/Gemfile
index b2660144f2b..6d8a33c2eef 100644
--- a/Gemfile
+++ b/Gemfile
@@ -245,7 +245,7 @@ end
group :development do
gem "foreman"
- gem 'brakeman', '~> 3.2.0', require: false
+ gem 'brakeman', '~> 3.3.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'quiet_assets', '~> 1.0.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index dfc15700494..2ba2676efa1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -97,16 +97,7 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
- brakeman (3.2.1)
- erubis (~> 2.6)
- haml (>= 3.0, < 5.0)
- highline (>= 1.6.20, < 2.0)
- ruby2ruby (~> 2.3.0)
- ruby_parser (~> 3.8.1)
- safe_yaml (>= 1.0)
- sass (~> 3.0)
- slim (>= 1.3.6, < 4.0)
- terminal-table (~> 1.4)
+ brakeman (3.3.2)
browser (2.0.3)
builder (3.2.2)
bullet (5.0.0)
@@ -338,7 +329,6 @@ GEM
hashie (3.4.3)
health_check (1.5.1)
rails (>= 2.3.0)
- highline (1.7.8)
hipchat (1.5.2)
httparty
mimemagic
@@ -642,10 +632,7 @@ GEM
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
- ruby2ruby (2.3.0)
- ruby_parser (~> 3.1)
- sexp_processor (~> 4.0)
- ruby_parser (3.8.1)
+ ruby_parser (3.8.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
@@ -655,7 +642,7 @@ GEM
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
- sass (3.4.21)
+ sass (3.4.22)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
sass (~> 3.1)
@@ -704,9 +691,6 @@ GEM
tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1)
- slim (3.0.6)
- temple (~> 0.7.3)
- tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
spinach (0.8.10)
colorize
@@ -747,10 +731,8 @@ GEM
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
- temple (0.7.6)
term-ansicolor (1.3.2)
tins (~> 1.0)
- terminal-table (1.5.2)
test_after_commit (0.4.2)
activerecord (>= 3.2)
thin (1.6.4)
@@ -759,7 +741,7 @@ GEM
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
- tilt (2.0.2)
+ tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tinder (1.10.1)
@@ -848,7 +830,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
- brakeman (~> 3.2.0)
+ brakeman (~> 3.3.0)
browser (~> 2.0.3)
bullet
bundler-audit
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index b28327ce12d..69d4c4f5dd3 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -162,19 +162,6 @@ $ ->
$el.data('placement') || 'bottom'
)
- $('.header-logo .home').tooltip(
- placement: (_, el) ->
- $el = $(el)
- if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom'
- container: 'body'
- )
-
- $('.page-with-sidebar').tooltip(
- selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user'
- placement: 'right'
- container: 'body'
- )
-
# Form submitter
$('.trigger-submit').on 'change', ->
$(@).parents('form').submit()
@@ -207,6 +194,7 @@ $ ->
$('.navbar-toggle').on 'click', ->
$('.header-content .title').toggle()
+ $('.header-content .header-logo').toggle()
$('.header-content .navbar-collapse').toggle()
$('.navbar-toggle').toggleClass('active')
$('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left")
@@ -241,7 +229,6 @@ $ ->
$this.attr 'value', $this.val()
$sidebarGutterToggle = $('.js-sidebar-toggle')
- $navIconToggle = $('.toggle-nav-collapse')
$(document)
.off 'breakpoint:change'
@@ -251,10 +238,6 @@ $ ->
if $gutterIcon.hasClass('fa-angle-double-right')
$sidebarGutterToggle.trigger('click')
- $navIcon = $navIconToggle.find('.fa')
- if $navIcon.hasClass('fa-angle-left')
- $navIconToggle.trigger('click')
-
fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint
bootstrapBreakpoint = bp.getBreakpointSize()
@@ -267,8 +250,8 @@ $ ->
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
$(window)
- .off "resize"
- .on "resize", (e) ->
+ .off "resize.app"
+ .on "resize.app", (e) ->
fitSidebarForSize()
gl.awardsHandler = new AwardsHandler()
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index 98d05e41273..f763ba96e33 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -1,19 +1,31 @@
-class CiBuild
+class @CiBuild
@interval: null
@state: null
- constructor: (build_url, build_status, build_state) ->
+ constructor: (@build_url, @build_status, @state) ->
clearInterval(CiBuild.interval)
- @state = build_state
+ # Init breakpoint checker
+ @bp = Breakpoints.get()
+ @hideSidebar()
+ $('.js-build-sidebar').niceScroll()
+ $(document)
+ .off 'click', '.js-sidebar-build-toggle'
+ .on 'click', '.js-sidebar-build-toggle', @toggleSidebar
- @initScrollButtonAffix()
+ $(window)
+ .off 'resize.build'
+ .on 'resize.build', @hideSidebar
- if build_status == "running" || build_status == "pending"
+ if $('#build-trace').length
+ @getInitialBuildTrace()
+ @initScrollButtonAffix()
+
+ if @build_status is "running" or @build_status is "pending"
#
# Bind autoscroll button to follow build output
#
- $("#autoscroll-button").bind "click", ->
+ $('#autoscroll-button').on 'click', ->
state = $(this).data("state")
if "enabled" is state
$(this).data "state", "disabled"
@@ -27,26 +39,37 @@ class CiBuild
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
- if window.location.href.split("#").first() is build_url
- last_state = @state
- $.ajax
- url: build_url + "/trace.json?state=" + encodeURIComponent(@state)
- dataType: "json"
- success: (log) =>
- return unless last_state is @state
-
- if log.state and log.status is "running"
- @state = log.state
- if log.append
- $('.fa-refresh').before log.html
- else
- $('#build-trace code').html log.html
- $('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
- @checkAutoscroll()
- else if log.status isnt build_status
- Turbolinks.visit build_url
+ if window.location.href.split("#").first() is @build_url
+ @getBuildTrace()
, 4000
+ getInitialBuildTrace: ->
+ $.ajax
+ url: @build_url
+ dataType: 'json'
+ success: (build_data) ->
+ $('.js-build-output').html build_data.trace_html
+
+ if build_data.status is 'success' or build_data.status is 'failed'
+ $('.js-build-refresh').remove()
+
+ getBuildTrace: ->
+ $.ajax
+ url: "#{@build_url}/trace.json?state=#{encodeURIComponent(@state)}"
+ dataType: "json"
+ success: (log) =>
+ if log.state
+ @state = log.state
+
+ if log.status is "running"
+ if log.append
+ $('.js-build-output').append log.html
+ else
+ $('.js-build-output').html log.html
+ @checkAutoscroll()
+ else if log.status isnt @build_status
+ Turbolinks.visit @build_url
+
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
@@ -61,4 +84,22 @@ class CiBuild
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
)
-@CiBuild = CiBuild
+ shouldHideSidebar: ->
+ bootstrapBreakpoint = @bp.getBreakpointSize()
+
+ bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm'
+
+ toggleSidebar: =>
+ if @shouldHideSidebar()
+ $('.js-build-sidebar')
+ .toggleClass 'right-sidebar-expanded right-sidebar-collapsed'
+
+ hideSidebar: =>
+ if @shouldHideSidebar()
+ $('.js-build-sidebar')
+ .removeClass 'right-sidebar-expanded'
+ .addClass 'right-sidebar-collapsed'
+ else
+ $('.js-build-sidebar')
+ .removeClass 'right-sidebar-collapsed'
+ .addClass 'right-sidebar-expanded'
diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee
index 16d023dd391..9dc3529a17f 100644
--- a/app/assets/javascripts/issues-bulk-assignment.js.coffee
+++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee
@@ -97,13 +97,22 @@ class @IssuableBulkActions
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) ->
- labelIds.push $(label).val() if label
+ labelIds.push parseInt($(label).val()) if label
labelIds
###*
- * Just an alias of @getUnmarkedIndeterminedLabels
- * @return {Array} Array of labels
+ * Returns Label IDs that will be removed from issue selection
+ * @return {Array} Array of labels IDs
###
getLabelsToRemove: ->
- @getUnmarkedIndeterminedLabels()
+ result = []
+ indeterminatedLabels = @getUnmarkedIndeterminedLabels()
+ labelsToApply = @getLabelsToApply()
+
+ indeterminatedLabels.map (id) ->
+ # We need to exclude label IDs that will be applied
+ # By not doing this will cause issues from selection to not add labels at all
+ result.push(id) if labelsToApply.indexOf(id) is -1
+
+ result
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index ec74dfaae1a..9ca88f1226e 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -95,8 +95,11 @@ class @LabelsSelect
$newLabelCreateButton.enable()
if label.message?
+ errors = _.map label.message, (value, key) ->
+ "#{key} #{value[0]}"
+
$newLabelError
- .text label.message
+ .html errors.join("<br/>")
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
@@ -254,7 +257,7 @@ class @LabelsSelect
search:
fields: ['title']
selectable: true
-
+ filterable: true
toggleLabel: (selected, el) ->
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
index d14b7139237..9fdc27a9787 100644
--- a/app/assets/javascripts/logo.js.coffee
+++ b/app/assets/javascripts/logo.js.coffee
@@ -47,4 +47,4 @@ $ ->
# Make logo clickable as part of a workaround for Safari visited
# link behaviour (See !2690).
$('#logo').on 'click', ->
- $('#js-shortcuts-home').get(0).click()
+ Turbolinks.visit('/')
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
index 63dee4ed5d7..e48343a19b5 100644
--- a/app/assets/javascripts/project_new.js.coffee
+++ b/app/assets/javascripts/project_new.js.coffee
@@ -7,12 +7,17 @@ class @ProjectNew
@toggleSettingsOnclick()
- toggleSettings: ->
- checked = $("#project_builds_enabled").prop("checked")
- if checked
- $('.builds-feature').show()
- else
- $('.builds-feature').hide()
+ toggleSettings: =>
+ @_showOrHide('#project_builds_enabled', '.builds-feature')
+ @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature')
toggleSettingsOnclick: ->
- $("#project_builds_enabled").on 'click', @toggleSettings
+ $('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings
+
+ _showOrHide: (checkElement, container) ->
+ $container = $(container)
+
+ if $(checkElement).prop('checked')
+ $container.show()
+ else
+ $container.hide()
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index ea4ac52da31..2ce63c16428 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -4,8 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
- $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
- $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
setTimeout ( ->
niceScrollBars = $('.nicescroll').niceScroll();
@@ -17,10 +15,3 @@ $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
toggleSidebar()
)
-
-$ ->
- size = bp.getBreakpointSize()
-
- if size is "xs" or size is "sm"
- if $('.page-with-sidebar').hasClass(expanded)
- toggleSidebar()
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index de0eae58bff..88246b0feb8 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -95,7 +95,7 @@ class @UsersSelect
data: (term, callback) =>
isAuthorFilter = $('.js-author-search')
- @users term, term is '' and isAuthorFilter, (users) =>
+ @users term, (users) =>
if term.length is 0
showDivider = 0
@@ -221,7 +221,7 @@ class @UsersSelect
multiple: $(select).hasClass('multiselect')
minimumInputLength: 0
query: (query) =>
- @users query.term, @projectId?, (users) =>
+ @users query.term, (users) =>
data = { results: users }
if query.term.length == 0
@@ -304,7 +304,7 @@ class @UsersSelect
# Return users list. Filtered by query
# Only active users retrieved
- users: (query, fromProject, callback) =>
+ users: (query, callback) =>
url = @buildUrl(@usersPath)
$.ajax(
@@ -313,7 +313,7 @@ class @UsersSelect
search: query
per_page: 20
active: true
- project_id: @projectId if fromProject
+ project_id: @projectId
group_id: @groupId
current_user: @showCurrentUser
author_id: @authorId
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 2540ff497f2..408d4a68e1e 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -8,34 +8,16 @@
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
- .header-logo {
- background: $color-darker;
- a {
- color: $color-light;
-
- h3 {
- color: $color-light;
- }
- }
+ .collapse-nav a {
+ color: $color-light;
+ background: $color;
&:hover {
- background-color: $color-dark;
- a {
- color: $white-light;
-
- h3 {
- color: $white-light;
- }
- }
+ color: $white-light;
}
}
- .collapse-nav a {
- color: $white-light;
- background: $color;
- }
-
.sidebar-wrapper {
background: $color-darker;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index c46d6b14782..63996ea44f6 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -109,10 +109,8 @@ header {
position: relative;
height: $header-height;
padding-right: 40px;
-
- @media (max-width: $screen-xs-min) {
- padding-left: 40px;
- }
+ padding-left: 30px;
+ transition-duration: .3s;
@media (min-width: $screen-sm-min) {
padding-right: 0;
@@ -122,9 +120,29 @@ header {
margin-top: -5px;
}
+ .header-logo {
+ position: absolute;
+ left: 50%;
+ margin-left: -18px;
+ top: 7px;
+ transition-duration: .3s;
+ z-index: 999;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ right: 25px;
+ left: auto;
+ }
+ }
+
.title {
margin: 0;
font-size: 19px;
+ max-width: 400px;
+ display: inline-block;
line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
@@ -133,6 +151,10 @@ header {
vertical-align: top;
white-space: nowrap;
+ @media (max-width: $screen-sm-max) {
+ max-width: 190px;
+ }
+
a {
color: $gl-text-color;
&:hover {
@@ -160,6 +182,10 @@ header {
.navbar-collapse {
float: right;
border-top: none;
+
+ @media (max-width: $screen-xs-max) {
+ float: none;
+ }
}
}
@@ -176,17 +202,20 @@ header {
margin-left: 0;
.header-content {
- padding-left: 30px;
- transition-duration: .3s;
+
+ @media (min-width: $screen-sm-max) {
+ padding-left: 30px;
+ transition-duration: .3s;
+ }
}
}
-.header-expanded {
- margin-left: 0;
+.tanuki-shape {
+ transition: all 0.8s;
- .header-content {
- padding-left: $sidebar_width;
- transition-duration: .3s;
+ &:hover, &.highlight {
+ fill: rgb(255, 255, 255);
+ transition: all 0.1s;
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index a036799e15a..4de89daeb36 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,6 +1,7 @@
@mixin fade($gradient-direction, $rgba, $gradient-color) {
visibility: visible;
opacity: 1;
+ z-index: 2;
position: absolute;
bottom: 12px;
width: 43px;
@@ -68,6 +69,7 @@
}
&.sub-nav {
+ text-align: center;
background-color: $background-color;
.container-fluid {
@@ -171,7 +173,6 @@
> form {
display: inline-block;
margin-top: -1px;
- margin-bottom: 12px;
}
.icon-label {
@@ -250,6 +251,7 @@
background: $background-color;
border-bottom: 1px solid $border-color;
transition-duration: .3s;
+ text-align: center;
.container-fluid {
position: relative;
@@ -352,7 +354,7 @@
.fade-right {
@media (min-width: $screen-xs-max) {
- right: 67px;
+ right: 68px;
}
@media (max-width: $screen-xs-min) {
right: 0;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 94985413746..b7ec3f70bfb 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -35,24 +35,11 @@
}
.sidebar-wrapper {
- .header-logo {
- height: $header-height;
- padding: 8px 26px;
- width: $sidebar_width;
- position: fixed;
- z-index: 999;
- overflow: hidden;
- transition-duration: .3s;
-
- &:hover {
- background-color: #eee;
- }
- }
.sidebar-user {
padding: 15px 22px;
position: fixed;
- bottom: 40px;
+ bottom: 0;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
@@ -97,10 +84,10 @@
}
a {
- text-align: center;
- padding: 8px;
+ width: $sidebar_width;
+ padding: 7px 15px 7px 23px;
font-size: $gl-font-size;
- color: $gray;
+ line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
@@ -118,10 +105,9 @@
font-size: 16px;
}
- .nav-link-text {
- margin-top: 3px;
- font-size: 13px;
- line-height: 18px;
+ i,
+ svg {
+ margin-right: 13px;
}
&.back-link i {
@@ -129,6 +115,12 @@
}
}
}
+
+ .count {
+ float: right;
+ padding: 0 8px;
+ @include border-radius(6px);
+ }
}
.sidebar-subnav {
@@ -143,11 +135,12 @@
.collapse-nav a {
width: $sidebar_width;
position: fixed;
- bottom: 0;
+ top: 0;
left: 0;
- font-size: 13px;
+ padding: 5px 0;
+ font-size: 18px;
background: transparent;
- height: 40px;
+ height: 50px;
text-align: center;
line-height: 40px;
transition-duration: .3s;
@@ -170,25 +163,8 @@
.sidebar-wrapper {
width: 0;
- .header-logo {
- width: 0;
- padding: 8px 0;
-
- a {
- padding-left: ($sidebar_collapsed_width - 36) / 2;
-
- .gitlab-text-container {
- display: none;
- }
- }
- }
-
- #logo {
- display: none;
- }
-
.nav-sidebar {
- width: $sidebar_collapsed_width;
+ width: 0;
li {
width: auto;
@@ -203,6 +179,10 @@
.collapse-nav a {
width: 0;
+
+ i {
+ display: none;
+ }
}
.sidebar-user {
@@ -218,9 +198,8 @@
}
.page-sidebar-expanded {
- padding-left: $sidebar_width;
- @media (max-width: $screen-xs-min) {
+ @media (max-width: $screen-sm-max) {
padding-left: 0;
}
@@ -241,20 +220,6 @@
}
}
}
-
- .layout-nav {
- @media (max-width: $screen-xs-min) {
- padding-right: 0;
- }
-
- @media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
- padding-right: 90px;
- }
-
- @media (min-width: $screen-md-min) {
- padding-right: $sidebar_width;
- }
- }
}
.right-sidebar-collapsed {
@@ -273,7 +238,9 @@
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- padding-right: $sidebar_collapsed_width;
+ &:not(.build-sidebar) {
+ padding-right: $sidebar_collapsed_width;
+ }
}
@media (min-width: $screen-md-min) {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 99e3df119ed..752d8ec8788 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -2,7 +2,7 @@
* Layout
*/
$sidebar_collapsed_width: 62px;
-$sidebar_width: 90px;
+$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
@@ -260,3 +260,6 @@ $calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
+
+$ci-output-bg: #1d1f21;
+$ci-text-color: #c5c8c6;
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 05d1ee5b998..6211f3a52eb 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -101,13 +101,21 @@
line-height: 20px;
outline: 0;
+ &:hover,
&.active,
&:active {
- background-color: $white-dark;
+ background-color: $row-hover;
+ border-color: $row-hover-border;
box-shadow: none;
outline: 0;
}
+ &.btn {
+ &:focus {
+ outline: 0;
+ }
+ }
+
&.is-loading {
.award-control-icon-normal,
.emoji-icon {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 44222e8e8a4..e8f1935d239 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -53,37 +53,92 @@
left: 70px;
}
}
+}
- .build-widget {
- padding: 10px;
- background: $background-color;
- margin-bottom: 20px;
- border-radius: 4px;
+.build-header {
+ position: relative;
+ padding-right: 40px;
- .title {
- margin-top: 0;
- color: #666;
- line-height: 1.5;
- }
- .attr-name {
- color: #777;
- }
+ @media (min-width: $screen-sm-min) {
+ padding-right: 0;
}
- .alert-disabled {
- background: $background-color;
+ a {
+ color: $gl-gray;
- a {
- color: #3084bb !important;
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
}
}
+
+ code {
+ color: $code-color;
+ }
+
+ .avatar {
+ float: none;
+ margin-right: 2px;
+ margin-left: 2px;
+ }
}
table.builds {
-
.build-link {
a {
color: $gl-dark-link-color;
}
}
}
+
+.build-trace {
+ background: $ci-output-bg;
+ color: $ci-text-color;
+ white-space: pre;
+ overflow-x: auto;
+ font-size: 12px;
+
+ .fa-refresh {
+ font-size: 24px;
+ }
+
+ .bash {
+ display: block;
+ }
+}
+
+.right-sidebar.build-sidebar {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+
+ &.right-sidebar-collapsed {
+ display: none;
+ }
+
+ .block {
+ width: 100%;
+ }
+
+ .build-sidebar-header {
+ padding-top: 0;
+
+ .gutter-toggle {
+ margin-top: 0;
+ }
+ }
+}
+
+.build-detail-row {
+ margin-bottom: 5px;
+}
+
+.build-light-text {
+ color: $gl-placeholder-color;
+}
+
+.build-gutter-toggle {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ margin-top: -17px;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 787c387379e..ea453ce356a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -29,7 +29,7 @@
}
}
-.issuable-sidebar {
+.right-sidebar {
a {
color: inherit;
}
@@ -74,6 +74,10 @@
}
}
+ .block-first {
+ padding-top: 0;
+ }
+
.title {
color: $gl-text-color;
margin-bottom: 10px;
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 3f28e402929..8d855ce99b0 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -11,18 +11,15 @@
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
- $l-black: #7f7f7f;
- $l-red: #f00;
- $l-green: #0f0;
- $l-yellow: #ff0;
- $l-blue: #5c5cff;
- $l-magenta: #f0f;
- $l-cyan: #0ff;
- $l-white: #fff;
+ $l-black: #373b41;
+ $l-red: #c66;
+ $l-green: #b5bd68;
+ $l-yellow: #f0c674;
+ $l-blue: #81a2be;
+ $l-magenta: #b294bb;
+ $l-cyan: #8abeb7;
+ $l-white: $ci-text-color;
- .term-bold {
- font-weight: bold;
- }
.term-italic {
font-style: italic;
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 62f63701799..cd6ae507cf1 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
+ include WorkhorseHelper
before_action :authenticate_user_from_token!
before_action :authenticate_user!
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 131a16dad9b..014b9b43ff2 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -42,7 +42,7 @@ class JwtController < ApplicationController
end
def authenticate_user(login, password)
- user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
+ user = Gitlab::Auth.find_with_user_password(login, password)
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
user
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 18ee55c839a..40d1906a53f 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -1,12 +1,13 @@
class Profiles::NotificationsController < Profiles::ApplicationController
def show
- @user = current_user
- @group_notifications = current_user.notification_settings.for_groups
- @project_notifications = current_user.notification_settings.for_projects
+ @user = current_user
+ @group_notifications = current_user.notification_settings.for_groups
+ @project_notifications = current_user.notification_settings.for_projects
+ @global_notification_setting = current_user.global_notification_setting
end
def update
- if current_user.update_attributes(user_params)
+ if current_user.update_attributes(user_params) && update_notification_settings
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
@@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email, :notification_level)
+ params.require(:user).permit(:notification_email)
+ end
+
+ def global_notification_setting_params
+ params.require(:global_notification_setting).permit(:level)
+ end
+
+ private
+
+ def update_notification_settings
+ return true unless global_notification_setting_params
+
+ current_user.global_notification_setting.update_attributes(global_notification_setting_params)
end
end
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 72921b3aa14..5962f74c39b 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController
return if cached_blob?
- headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
- headers['Content-Disposition'] = 'inline'
- headers['Content-Type'] = safe_content_type(@blob)
- head :ok # 'render nothing: true' messes up the Content-Type
+ send_git_blob @repository, @blob
else
render_404
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 9b80efa5f11..14c82826342 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -41,7 +41,7 @@ class Projects::BuildsController < Projects::ApplicationController
def trace
respond_to do |format|
format.json do
- render json: @build.trace_with_state(params[:state]).merge!(id: @build.id, status: @build.status)
+ render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
end
end
end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 348d6cf4d96..f907d63258b 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -43,7 +43,7 @@ class Projects::GitHttpController < Projects::ApplicationController
return if project && project.public? && upload_pack?
authenticate_or_request_with_http_basic do |login, password|
- auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip)
+ auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
if auth_result.type == :ci && upload_pack?
@ci = true
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 06a114dcbe8..67e7187c10d 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -61,12 +61,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.json { render json: @merge_request }
format.patch { render text: @merge_request.to_patch }
format.diff do
- headers.store(*Gitlab::Workhorse.send_git_diff(@project.repository,
- @merge_request.diff_base_commit.id,
- @merge_request.last_commit.id))
- headers['Content-Disposition'] = 'inline'
+ return render_404 unless @merge_request.diff_refs
- head :ok
+ send_git_diff @project.repository, @merge_request.diff_refs
end
end
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 10de0e60530..10d24da16d7 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController
if @blob.lfs_pointer?
send_lfs_object
else
- headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
- headers['Content-Disposition'] = 'inline'
- headers['Content-Type'] = safe_content_type(@blob)
- head :ok # 'render nothing: true' messes up the Content-Type
+ send_git_blob @repository, @blob
end
else
render_404
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index bb7a6b6a5ab..d5af0341d18 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -11,8 +11,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
- headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
- head :ok
+ send_git_archive @repository, ref: params[:ref], format: params[:format]
rescue => ex
logger.error("#{self.class.name}: #{ex}")
return git_not_found!
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3af62c7696c..a6479c42d94 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -234,7 +234,7 @@ class ProjectsController < Projects::ApplicationController
:issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds,
+ :public_builds, :only_allow_merge_if_build_succeeds
)
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 01cbf91c658..00ff1611039 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -51,7 +51,7 @@ class SnippetsFinder
snippets = project.snippets.fresh
if current_user
- if project.team.member?(current_user.id) || current_user.admin?
+ if project.team.member?(current_user) || current_user.admin?
snippets
else
snippets.public_and_internal
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index e39548e17e1..3ee3fc74f0c 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -14,4 +14,8 @@ module BranchesHelper
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end
+
+ def project_branches
+ options_for_select(@project.repository.branch_names, @project.default_branch)
+ end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 14697f774cc..6b617e1730a 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -67,9 +67,9 @@ module DropdownsHelper
end
end
- def dropdown_filter(placeholder)
+ def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
- filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
+ filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search', class: "dropdown-input-search")
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index f685e547537..469accf3142 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -30,17 +30,13 @@ module NavHelper
else
"page-gutter right-sidebar-expanded"
end
+ elsif current_path?('builds#show')
+ "page-gutter build-sidebar right-sidebar-expanded"
end
end
def nav_header_class
- class_name =
- if nav_menu_collapsed?
- "header-collapsed"
- else
- "header-expanded"
- end
- class_name += " with-horizontal-nav" if defined?(nav) && nav
+ class_name = " with-horizontal-nav" if defined?(nav) && nav
class_name
end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index b8e64b3890a..50c21fc0d49 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -61,4 +61,23 @@ module NotificationsHelper
end
end
end
+
+ def notification_level_radio_buttons
+ html = ""
+
+ NotificationSetting.levels.each_key do |level|
+ level = level.to_sym
+ next if level == :global
+
+ html << content_tag(:div, class: "radio") do
+ content_tag(:label, { value: level }) do
+ radio_button_tag(:"global_notification_setting[level]", level, @global_notification_setting.level.to_sym == level) +
+ content_tag(:div, level.to_s.capitalize, class: "level-title") +
+ content_tag(:p, notification_description(level))
+ end
+ end
+ end
+
+ html.html_safe
+ end
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
new file mode 100644
index 00000000000..2bd0dbfd095
--- /dev/null
+++ b/app/helpers/workhorse_helper.rb
@@ -0,0 +1,24 @@
+# Helpers to send Git blobs, diffs or archives through Workhorse.
+# Workhorse will also serve files when using `send_file`.
+module WorkhorseHelper
+ # Send a Git blob through Workhorse
+ def send_git_blob(repository, blob)
+ headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
+ headers['Content-Disposition'] = 'inline'
+ headers['Content-Type'] = safe_content_type(blob)
+ head :ok # 'render nothing: true' messes up the Content-Type
+ end
+
+ # Send a Git diff through Workhorse
+ def send_git_diff(repository, diff_refs)
+ headers.store(*Gitlab::Workhorse.send_git_diff(repository, diff_refs))
+ headers['Content-Disposition'] = 'inline'
+ head :ok
+ end
+
+ # Archive a Git repository and send it through Workhorse
+ def send_git_archive(repository, ref:, format:)
+ headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
+ head :ok
+ end
+end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 44515550d9e..aea946f9224 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -533,7 +533,7 @@ class Ability
def filter_confidential_issues_abilities(user, issue, rules)
return rules if user.admin? || !issue.confidential?
- unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
+ unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
rules.delete(:admin_issue)
rules.delete(:read_issue)
rules.delete(:update_issue)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b8ada6361ac..6a64ca451f7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -194,7 +194,7 @@ module Ci
def trace_length
if raw_trace
- raw_trace.length
+ raw_trace.bytesize
else
0
end
@@ -216,7 +216,7 @@ module Ci
recreate_trace_dir
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
- File.open(path_to_trace, 'a') do |f|
+ File.open(path_to_trace, 'ab') do |f|
f.write(trace_part)
end
end
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 59fc9951d11..b69ae37668c 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -3,7 +3,7 @@ module Ci
extend Ci::Model
belongs_to :trigger, class_name: 'Ci::Trigger'
- belongs_to :commit, class_name: 'Ci::Pipeline', foreign_key: :commit_id
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
has_many :builds, class_name: 'Ci::Build'
serialize :variables
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 235922710ad..1bdf9c011b2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -51,10 +51,18 @@ class Issue < ActiveRecord::Base
end
def self.visible_to_user(user)
- return where(confidential: false) if user.blank?
+ return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return all if user.admin?
- where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
+ where('
+ issues.confidential IS NULL
+ OR issues.confidential IS FALSE
+ OR (issues.confidential = TRUE
+ AND (issues.author_id = :user_id
+ OR issues.assignee_id = :user_id
+ OR issues.project_id IN(:project_ids)))',
+ user_id: user.id,
+ project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
def self.reference_prefix
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b0ed8182855..7b8858b24d6 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -260,19 +260,20 @@ class MergeRequest < ActiveRecord::Base
end
def mergeable?
- return false unless open? && !work_in_progress? && !broken?
+ return false unless mergeable_state?
check_if_can_be_merged
can_be_merged?
end
- def gitlab_merge_status
- if work_in_progress?
- "work_in_progress"
- else
- merge_status_name
- end
+ def mergeable_state?
+ return false unless open?
+ return false if work_in_progress?
+ return false if broken?
+ return false unless mergeable_ci_state?
+
+ true
end
def can_cancel_merge_when_build_succeeds?(current_user)
@@ -481,6 +482,12 @@ class MergeRequest < ActiveRecord::Base
::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
end
+ def mergeable_ci_state?
+ return true unless project.only_allow_merge_if_build_succeeds?
+
+ !pipeline || pipeline.success?
+ end
+
def state_human_name
if merged?
"Merged"
diff --git a/app/models/note.rb b/app/models/note.rb
index 585d8c4ad84..58133f1581f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -88,22 +88,9 @@ class Note < ActiveRecord::Base
table = arel_table
pattern = "%#{query}%"
- found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id').
- where(table[:note].matches(pattern))
-
- if as_user
- found_notes.where('
- issues.confidential IS NULL
- OR issues.confidential IS FALSE
- OR (issues.confidential IS TRUE
- AND (issues.author_id = :user_id
- OR issues.assignee_id = :user_id
- OR issues.project_id IN(:project_ids)))',
- user_id: as_user.id,
- project_ids: as_user.authorized_projects.select(:id))
- else
- found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
- end
+ Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
+ where(table[:note].matches(pattern)).
+ merge(Issue.visible_to_user(as_user))
end
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 17fb15b08df..0ce87968e46 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -7,7 +7,6 @@ class NotificationSetting < ActiveRecord::Base
belongs_to :source, polymorphic: true
validates :user, presence: true
- validates :source, presence: true
validates :level, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
diff --git a/app/models/project.rb b/app/models/project.rb
index f47ef8a81de..dfa99fe0df2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -146,7 +146,6 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
- validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
@@ -253,20 +252,69 @@ class Project < ActiveRecord::Base
non_archived.where(table[:name].matches(pattern))
end
- def find_with_namespace(id)
- namespace_path, project_path = id.split('/', 2)
+ # Finds a single project for the given path.
+ #
+ # path - The full project path (including namespace path).
+ #
+ # Returns a Project, or nil if no project could be found.
+ def find_with_namespace(path)
+ where_paths_in([path]).reorder(nil).take
+ end
- return nil if !namespace_path || !project_path
+ # Builds a relation to find multiple projects by their full paths.
+ #
+ # Each path must be in the following format:
+ #
+ # namespace_path/project_path
+ #
+ # For example:
+ #
+ # gitlab-org/gitlab-ce
+ #
+ # Usage:
+ #
+ # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
+ #
+ # This would return the projects with the full paths matching the values
+ # given.
+ #
+ # paths - An Array of full paths (namespace path + project path) for which
+ # to find the projects.
+ #
+ # Returns an ActiveRecord::Relation.
+ def where_paths_in(paths)
+ wheres = []
+ cast_lower = Gitlab::Database.postgresql?
+
+ paths.each do |path|
+ namespace_path, project_path = path.split('/', 2)
+
+ next unless namespace_path && project_path
+
+ namespace_path = connection.quote(namespace_path)
+ project_path = connection.quote(project_path)
+
+ where = "(namespaces.path = #{namespace_path}
+ AND projects.path = #{project_path})"
+
+ if cast_lower
+ where = "(
+ #{where}
+ OR (
+ LOWER(namespaces.path) = LOWER(#{namespace_path})
+ AND LOWER(projects.path) = LOWER(#{project_path})
+ )
+ )"
+ end
- # Use of unscoped ensures we're not secretly adding any ORDER BYs, which
- # have a negative impact on performance (and aren't needed for this
- # query).
- projects = unscoped.
- joins(:namespace).
- iwhere('namespaces.path' => namespace_path)
+ wheres << where
+ end
- projects.find_by('projects.path' => project_path) ||
- projects.iwhere('projects.path' => project_path).take
+ if wheres.empty?
+ none
+ else
+ joins(:namespace).where(wheres.join(' OR '))
+ end
end
def visibility_levels
@@ -523,13 +571,21 @@ class Project < ActiveRecord::Base
end
def external_issue_tracker
- return @external_issue_tracker if defined?(@external_issue_tracker)
- @external_issue_tracker ||=
- services.issue_trackers.active.without_defaults.first
+ if has_external_issue_tracker.nil? # To populate existing projects
+ cache_has_external_issue_tracker
+ end
+
+ if has_external_issue_tracker?
+ return @external_issue_tracker if defined?(@external_issue_tracker)
+
+ @external_issue_tracker = services.external_issue_trackers.first
+ else
+ nil
+ end
end
- def can_have_issues_tracker_id?
- self.issues_enabled && !self.default_issues_tracker?
+ def cache_has_external_issue_tracker
+ update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
def build_missing_services
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 1d1780dcfbf..b5c76e4d4fe 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,6 +1,4 @@
class BambooService < CiService
- include HTTParty
-
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated?
@@ -61,18 +59,7 @@ class BambooService < CiService
end
def build_info(sha)
- url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s
-
- if username.blank? && password.blank?
- @response = HTTParty.get(url, verify: false)
- else
- url << '&os_authType=basic'
- auth = {
- username: username,
- password: password
- }
- @response = HTTParty.get(url, verify: false, basic_auth: auth)
- end
+ @response = get_path("rest/api/latest/result?label=#{sha}")
end
def build_page(sha, ref)
@@ -80,11 +67,11 @@ class BambooService < CiService
if @response.code != 200 || @response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
- URI.join(bamboo_url, "/browse/#{build_key}").to_s
+ URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key']
- URI.join(bamboo_url, "/browse/#{result_key}").to_s
+ URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end
end
@@ -112,8 +99,27 @@ class BambooService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
- # Bamboo requires a GET and does not take any data.
- url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
- self.class.get(url, verify: false)
+ get_path("updateAndBuild.action?buildKey=#{build_key}")
+ end
+
+ private
+
+ def build_url(path)
+ URI.join("#{bamboo_url}/", path).to_s
+ end
+
+ def get_path(path)
+ url = build_url(path)
+
+ if username.blank? && password.blank?
+ HTTParty.get(url, verify: false)
+ else
+ url << '&os_authType=basic'
+ HTTParty.get(url, verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
+ end
end
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 6ae9b16d3ce..87ecb3b8b86 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -38,9 +38,9 @@ class IssueTrackerService < Service
if enabled_in_gitlab_config
self.properties = {
title: issues_tracker['title'],
- project_url: add_issues_tracker_id(issues_tracker['project_url']),
- issues_url: add_issues_tracker_id(issues_tracker['issues_url']),
- new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url'])
+ project_url: issues_tracker['project_url'],
+ issues_url: issues_tracker['issues_url'],
+ new_issue_url: issues_tracker['new_issue_url']
}
else
self.properties = {}
@@ -83,16 +83,4 @@ class IssueTrackerService < Service
def issues_tracker
Gitlab.config.issues_tracker[to_param]
end
-
- def add_issues_tracker_id(url)
- if self.project
- id = self.project.issues_tracker_id
-
- if id
- url = url.gsub(":issues_tracker_id", id)
- end
- end
-
- url
- end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b0dcb52eba1..a4a967c9bc9 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -1,6 +1,4 @@
class TeamcityService < CiService
- include HTTParty
-
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated?
@@ -64,15 +62,7 @@ class TeamcityService < CiService
end
def build_info(sha)
- url = URI.join(
- teamcity_url,
- "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
- ).to_s
- auth = {
- username: username,
- password: password
- }
- @response = HTTParty.get(url, verify: false, basic_auth: auth)
+ @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
end
def build_page(sha, ref)
@@ -81,14 +71,11 @@ class TeamcityService < CiService
if @response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
- URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s
+ build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = @response['build']['id']
- URI.join(
- teamcity_url,
- "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
- ).to_s
+ build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end
@@ -123,8 +110,8 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref])
- self.class.post(
- URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
+ HTTParty.post(
+ build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=\"#{branch}\">"\
"<buildType id=\"#{build_type}\"/>"\
'</build>',
@@ -132,4 +119,18 @@ class TeamcityService < CiService
basic_auth: auth
)
end
+
+ private
+
+ def build_url(path)
+ URI.join("#{teamcity_url}/", path).to_s
+ end
+
+ def get_path(path)
+ HTTParty.get(build_url(path), verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
+ end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 70a8bbaba65..e29e854860a 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -131,8 +131,14 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user_id)
- !!find_member(user_id)
+ def member?(user, min_member_access = nil)
+ member = !!find_member(user.id)
+
+ if min_member_access
+ member && max_member_access(user.id) >= min_member_access
+ else
+ member
+ end
end
def human_max_access(user_id)
diff --git a/app/models/service.rb b/app/models/service.rb
index de3fd24584a..bf352397509 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -16,6 +16,7 @@ class Service < ActiveRecord::Base
after_initialize :initialize_properties
after_commit :reset_updated_properties
+ after_commit :cache_project_has_external_issue_tracker
belongs_to :project
has_one :service_hook
@@ -34,6 +35,7 @@ class Service < ActiveRecord::Base
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
+ scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
default_value_for :category, 'common'
@@ -192,4 +194,12 @@ class Service < ActiveRecord::Base
service.project_id = project_id
service if service.save
end
+
+ private
+
+ def cache_project_has_external_issue_tracker
+ if project && !project.destroyed?
+ project.cache_has_external_issue_tracker
+ end
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index e0987e07e1f..a5b3c8afe51 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -10,6 +10,8 @@ class User < ActiveRecord::Base
include CaseSensitivity
include TokenAuthenticatable
+ DEFAULT_NOTIFICATION_LEVEL = :participating
+
add_authentication_token_field :authentication_token
default_value_for :admin, false
@@ -99,7 +101,6 @@ class User < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- validates :notification_level, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? }
@@ -133,13 +134,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files]
- # Notification level
- # Note: When adding an option, it MUST go on the end of the array.
- #
- # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813
- # Because user.notification_disabled? is much better than user.disabled?
- enum notification_level: [:disabled, :participating, :watch, :global, :mention]
-
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true
@@ -411,8 +405,8 @@ class User < ActiveRecord::Base
end
# Returns projects user is authorized to access.
- def authorized_projects
- Project.where("projects.id IN (#{projects_union.to_sql})")
+ def authorized_projects(min_access_level = nil)
+ Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
end
def viewable_starred_projects
@@ -800,6 +794,17 @@ class User < ActiveRecord::Base
notification_settings.find_or_initialize_by(source: source)
end
+ # Lazy load global notification setting
+ # Initializes User setting with Participating level if setting not persisted
+ def global_notification_setting
+ return @global_notification_setting if defined?(@global_notification_setting)
+
+ @global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
+ @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
+
+ @global_notification_setting
+ end
+
def assigned_open_merge_request_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
assigned_merge_requests.opened.count
@@ -819,11 +824,19 @@ class User < ActiveRecord::Base
private
- def projects_union
- Gitlab::SQL::Union.new([personal_projects.select(:id),
- groups_projects.select(:id),
- projects.select(:id),
- groups.joins(:shared_projects).select(:project_id)])
+ def projects_union(min_access_level = nil)
+ relations = [personal_projects.select(:id),
+ groups_projects.select(:id),
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)]
+
+
+ if min_access_level
+ scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
+ relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
+ end
+
+ Gitlab::SQL::Union.new(relations)
end
def ci_projects_union
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index c3194f45b10..1e629cf119a 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -11,7 +11,7 @@ module Ci
trigger_request = trigger.trigger_requests.create!(
variables: variables,
- commit: pipeline,
+ pipeline: pipeline,
)
if pipeline.create_builds(nil, trigger_request)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 91ca82ed3b7..875a3f4fab6 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -279,10 +279,11 @@ class NotificationService
end
def users_with_global_level_watch(ids)
- User.where(
- id: ids,
- notification_level: NotificationSetting.levels[:watch]
- ).pluck(:id)
+ NotificationSetting.where(
+ user_id: ids,
+ source_type: nil,
+ level: NotificationSetting.levels[:watch]
+ ).pluck(:user_id)
end
# Build a list of users based on project notifcation settings
@@ -352,7 +353,9 @@ class NotificationService
users = users.reject(&:blocked?)
users.reject do |user|
- next user.notification_level == level unless project
+ global_notification_setting = user.global_notification_setting
+
+ next global_notification_setting.level == level unless project
setting = user.notification_settings_for(project)
@@ -361,13 +364,13 @@ class NotificationService
end
# reject users who globally set mention notification and has no setting per project/group
- next user.notification_level == level unless setting
+ next global_notification_setting.level == level unless setting
# reject users who set mention notification in project
next true if setting.level == level
# reject users who have mention level in project and disabled in global settings
- setting.global? && user.notification_level == level
+ setting.global? && global_notification_setting.level == level
end
end
@@ -456,7 +459,6 @@ class NotificationService
def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
recipients = target.participants(current_user)
-
recipients = add_project_watchers(recipients, project)
recipients = reject_mention_users(recipients, project)
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 83c0c6da21b..0404d0728ea 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.xmlschema if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
- xml << render(partial: 'issues/issue', collection: @issues) if @issues.any?
+ xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index c19671295af..b1628040325 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
- xml.updated @issues.first.created_at.xmlschema if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
- xml << render(partial: 'issues/issue', collection: @issues) if @issues.any?
+ xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 68a2d19e58d..96831874144 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -5,10 +5,28 @@ xml.entry do
xml.updated issue.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
- xml.author do |author|
+ xml.author do
xml.name issue.author_name
xml.email issue.author_email
end
xml.summary issue.title
+ xml.description issue.description if issue.description
+ xml.milestone issue.milestone.title if issue.milestone
+ xml.due_date issue.due_date if issue.due_date
+
+ unless issue.labels.empty?
+ xml.labels do
+ issue.labels.each do |label|
+ xml.label label.name
+ end
+ end
+ end
+
+ if issue.assignee
+ xml.assignee do
+ xml.name issue.assignee.name
+ xml.email issue.assignee.email
+ end
+ end
end
diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml
index 2ed51d87ca1..e4fab897377 100644
--- a/app/views/layouts/_collapse_button.html.haml
+++ b/app/views/layouts/_collapse_button.html.haml
@@ -1,4 +1 @@
-- if nav_menu_collapsed?
- = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
-- else
- = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
+= link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 261038ef940..f89e8582792 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,9 +1,5 @@
-.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
+.page-with-sidebar.page-sidebar-collapsed{ class: "#{page_gutter_class}" }
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
- = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
- .header-logo
- #logo
- = brand_header_logo
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
@@ -16,7 +12,9 @@
= render partial: 'layouts/collapse_button'
- if current_user
= link_to current_user, class: 'sidebar-user', title: "Profile", data: {user: current_user.username} do
- = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s46'
+ = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
+ .username
+ = current_user.username
- if defined?(nav) && nav
.layout-nav
.container-fluid
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
index a13241bebee..2e56d0ac6a3 100644
--- a/app/views/layouts/ci/_page.html.haml
+++ b/app/views/layouts/ci/_page.html.haml
@@ -1,12 +1,6 @@
.page-with-sidebar{ class: page_sidebar_class }
= render "layouts/broadcast"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
- .header-logo
- %a#logo
- = brand_header_logo
- = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
- .gitlab-text-container
- %h3 GitLab
- if defined?(sidebar) && sidebar
= render "layouts/ci/#{sidebar}"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index c33740e23fa..ad30a367fc5 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+%header.navbar.navbar-fixed-top.navbar-gitlab.header-collapsed{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
.header-content
%button.side-nav-toggle{type: 'button'}
@@ -50,6 +50,10 @@
%h1.title= title
+ .header-logo
+ #logo
+ = brand_header_logo
+
= yield :header_content
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index de2276e75e4..f292730fe45 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -2,102 +2,106 @@
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview' do
= icon('dashboard fw')
- .nav-link-text
+ %span
Overview
= nav_link(controller: [:admin, :projects]) do
= link_to admin_namespaces_projects_path, title: 'Projects' do
= icon('cube fw')
- .nav-link-text
+ %span
Projects
= nav_link(controller: :users) do
= link_to admin_users_path, title: 'Users' do
= icon('user fw')
- .nav-link-text
+ %span
Users
= nav_link(controller: :groups) do
= link_to admin_groups_path, title: 'Groups' do
= icon('group fw')
- .nav-link-text
+ %span
Groups
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
= icon('key fw')
- .nav-link-text
+ %span
Deploy Keys
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
= icon('cog fw')
- .nav-link-text
+ %span
Runners
+ %span.count= number_with_delimiter(Ci::Runner.count(:all))
= nav_link path: 'builds#index' do
= link_to admin_builds_path, title: 'Builds' do
= icon('link fw')
- .nav-link-text
+ %span
Builds
+ %span.count= number_with_delimiter(Ci::Build.count(:all))
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
= icon('file-text fw')
- .nav-link-text
+ %span
Logs
= nav_link(controller: :health_check) do
= link_to admin_health_check_path, title: 'Health Check' do
= icon('medkit fw')
- .nav-link-text
+ %span
Health Check
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do
= icon('bullhorn fw')
- .nav-link-text
+ %span
Messages
= nav_link(controller: :hooks) do
= link_to admin_hooks_path, title: 'Hooks' do
= icon('external-link fw')
- .nav-link-text
+ %span
Hooks
= nav_link(controller: :background_jobs) do
= link_to admin_background_jobs_path, title: 'Background Jobs' do
= icon('cog fw')
- .nav-link-text
+ %span
Background Jobs
= nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do
= icon('image')
- .nav-link-text
+ %span
Appearance
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
= icon('cloud fw')
- .nav-link-text
+ %span
Applications
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do
= icon('copy fw')
- .nav-link-text
+ %span
Service Templates
= nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do
= icon('tags fw')
- .nav-link-text
+ %span
Labels
= nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
= icon('exclamation-circle fw')
- .nav-link-text
+ %span
Abuse Reports
+ %span.count= number_with_delimiter(AbuseReport.count(:all))
- if askimet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
= icon('exclamation-triangle fw')
- .nav-link-text
+ %span
Spam Logs
+ %span.count= number_with_delimiter(SpamLog.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw')
- .nav-link-text
+ %span
Settings
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index b73fde7797f..18cae5bf87f 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -2,50 +2,53 @@
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
= navbar_icon('project')
- .nav-link-text
+ %span
Projects
= nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do
= icon('bell fw')
- .nav-link-text
+ %span
Todos
+ %span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
= navbar_icon('activity')
- .nav-link-text
+ %span
Activity
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
= navbar_icon('group')
- .nav-link-text
+ %span
Groups
= nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
= navbar_icon('milestones')
- .nav-link-text
+ %span
Milestones
= nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
= navbar_icon('issues')
- .nav-link-text
+ %span
Issues
+ %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
= navbar_icon('mr')
- .nav-link-text
+ %span
Merge Requests
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
- .nav-link-text
+ %span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
= icon('question-circle fw')
- .nav-link-text
+ %span
Help
= nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw')
- .nav-link-text
+ %span
Profile Settings
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 46fcf1545f2..3b40006a0cc 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -2,20 +2,20 @@
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do
= icon('bookmark fw')
- .nav-link-text
+ %span
Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups' do
= icon('group fw')
- .nav-link-text
+ %span
Groups
= nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
- .nav-link-text
+ %span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
= icon('question-circle fw')
- .nav-link-text
+ %span
Help
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 9cbee0aa363..66361a644dd 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -5,36 +5,30 @@
.fade-left
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
- = navbar_icon('group')
%span
Group
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: 'Activity' do
- = navbar_icon('activity')
%span
Activity
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
- = navbar_icon('milestones')
%span
Milestones
= nav_link(path: 'groups#issues') do
= link_to issues_group_path(@group), title: 'Issues' do
- = navbar_icon('issues')
%span
Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(issues.count)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- = navbar_icon('mr')
%span
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
- = navbar_icon('members')
%span
Members
.fade-right
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 09d9f0184be..d4b1f477f3f 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -2,51 +2,41 @@
.fade-left
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
- = icon('user fw')
%span
Profile
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account' do
- = icon('gear fw')
%span
Account
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
- = icon('cloud fw')
%span
Applications
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
- = icon('envelope-o fw')
%span
Emails
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
- = icon('lock fw')
%span
Password
= nav_link(controller: :notifications) do
= link_to profile_notifications_path, title: 'Notifications' do
- = icon('inbox fw')
%span
Notifications
= nav_link(controller: :keys) do
= link_to profile_keys_path, title: 'SSH Keys' do
- = icon('key fw')
%span
SSH Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
- -# TODO (rspeicher): Better icon?
- = icon('image fw')
%span
Preferences
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path, title: 'Audit Log' do
- = icon('history fw')
%span
Audit Log
.fade-right
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 2a58ef224b3..53d1fcc30a6 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -24,55 +24,41 @@
.fade-left
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- = navbar_icon('project')
%span
Project
= nav_link(path: 'projects#activity') do
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
- = navbar_icon('activity')
%span
Activity
-
+
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
= link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do
- = icon('code fw')
%span
Code
- if project_nav_tab? :pipelines
= nav_link(controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- = navbar_icon('pipelines')
%span
Pipelines
- if project_nav_tab? :container_registry
= nav_link(controller: %w(container_registry)) do
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
- = icon('hdd-o fw')
%span
Registry
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
= link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do
- = icon('area-chart fw')
%span
Graphs
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- = navbar_icon('milestones')
- %span
- Milestones
-
- if project_nav_tab? :issues
- = nav_link(controller: :issues) do
+ = nav_link(controller: [:issues, :labels, :milestones]) do
= link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do
- = navbar_icon('issues')
%span
Issues
- if @project.default_issues_tracker?
@@ -81,29 +67,19 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- = navbar_icon('mr')
%span
Merge Requests
%span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- = icon('tags fw')
- %span
- Labels
-
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
- = navbar_icon('wiki')
%span
Wiki
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do
- = icon('clipboard fw')
%span
Snippets
@@ -129,5 +105,4 @@
%li.hidden
= link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
Commits
-
.fade-right
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 89ae7ffda2b..f0cf82afe83 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -1,7 +1,7 @@
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
- = notification_icon(current_user.notification_level)
+ = notification_icon(current_user.global_notification_setting.level)
- else
= notification_icon(setting.level)
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index 17c097154da..e0fad555c09 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -1,7 +1,7 @@
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
- = notification_icon(current_user.notification_level)
+ = notification_icon(current_user.global_notification_setting.level)
- else
= notification_icon(setting.level)
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 7696f112bb3..f2659ac14b5 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -26,33 +26,7 @@
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
.form-group
= f.label :notification_level, class: 'label-light'
- .radio
- = f.label :notification_level, value: :disabled do
- = f.radio_button :notification_level, :disabled
- .level-title
- Disabled
- %p You will not get any notifications via email
-
- .radio
- = f.label :notification_level, value: :mention do
- = f.radio_button :notification_level, :mention
- .level-title
- On Mention
- %p You will receive notifications only for comments in which you were @mentioned
-
- .radio
- = f.label :notification_level, value: :participating do
- = f.radio_button :notification_level, :participating
- .level-title
- Participating
- %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
-
- .radio
- = f.label :notification_level, value: :watch do
- = f.radio_button :notification_level, :watch
- .level-title
- Watch
- %p You will receive notifications for any activity
+ = notification_level_radio_buttons
.prepend-top-default
= f.submit 'Update settings', class: "btn btn-create"
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
new file mode 100644
index 00000000000..da522b53417
--- /dev/null
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -0,0 +1,11 @@
+%fieldset.builds-feature
+ %h5.prepend-top-0
+ Merge Requests
+ .form-group
+ .checkbox
+ = f.label :only_allow_merge_if_build_succeeds do
+ = f.check_box :only_allow_merge_if_build_succeeds
+ %strong Only allow merge requests to be merged if the build succeeds
+ .help-block
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('workflow', 'merge_requests#only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index ede01dcc1aa..539d07d634a 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,4 +1,5 @@
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+- header_title project_title(@project, "Builds", project_builds_path(@project))
.top-block.row-content-block.clearfix
.pull-right
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
new file mode 100644
index 00000000000..51b5bd9db42
--- /dev/null
+++ b/app/views/projects/builds/_header.html.haml
@@ -0,0 +1,16 @@
+.content-block.build-header
+ = ci_status_with_icon(@build.status)
+ Build
+ %strong ##{@build.id}
+ for commit
+ = link_to ci_status_path(@build.pipeline) do
+ %strong= @build.pipeline.short_sha
+ from
+ = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
+ %code
+ = @build.ref
+ - if @build.user
+ = render "user"
+ = time_ago_with_tooltip(@build.created_at)
+ %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
new file mode 100644
index 00000000000..5d931389dfb
--- /dev/null
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -0,0 +1,93 @@
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+ .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
+ Build
+ %strong ##{@build.id}
+ %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
+ = icon('angle-double-right')
+ - if @build.coverage
+ .block.block-first
+ .title
+ Test coverage
+ %p.build-detail-row
+ #{@build.coverage}%
+
+ - if can?(current_user, :read_build, @project) && @build.artifacts?
+ .block{ class: ("block-first" if !@build.coverage) }
+ .title
+ Build artifacts
+ .btn-group.btn-group-justified{ role: :group }
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Download
+
+ - if @build.artifacts_metadata?
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Browse
+
+ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && @build.artifacts?)) }
+ .title
+ Build details
+ - if @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
+ - if @build.merge_request
+ %p.build-detail-row
+ %span.build-light-text Merge Request:
+ = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
+ - if @build.duration
+ %p.build-detail-row
+ %span.build-light-text Duration:
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+ - if @build.finished_at
+ %p.build-detail-row
+ %span.build-light-text Finished:
+ #{time_ago_with_tooltip(@build.finished_at)}
+ - if @build.erased_at
+ %p.build-detail-row
+ %span.build-light-text Erased:
+ #{time_ago_with_tooltip(@build.erased_at)}
+ %p.build-detail-row
+ %span.build-light-text Runner:
+ - if @build.runner && current_user && current_user.admin
+ = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
+ - elsif @build.runner
+ \##{@build.runner.id}
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.has_trace?
+ = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
+ - if can?(current_user, :update_build, @project) && @build.erasable?
+ = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+ class: "btn btn-sm btn-default", method: :post,
+ data: { confirm: "Are you sure you want to erase this build?" } do
+ Erase
+
+ - if @build.trigger_request
+ .build-widget
+ %h4.title
+ Trigger
+
+ %p
+ %span.build-light-text Token:
+ #{@build.trigger_request.trigger.short_token}
+
+ - if @build.trigger_request.variables
+ %p
+ %span.build-light-text Variables:
+
+ %code
+ - @build.trigger_request.variables.each do |key, value|
+ #{key}=#{value}
+
+ .block
+ .title
+ Commit message
+ %p.build-light-text.append-bottom-0
+ #{@build.pipeline.git_commit_message}
+
+ - if @build.tags.any?
+ .block
+ .title
+ Tags
+ - @build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml
new file mode 100644
index 00000000000..2642de8021d
--- /dev/null
+++ b/app/views/projects/builds/_user.html.haml
@@ -0,0 +1,4 @@
+by
+%a{ href: user_path(@build.user) }
+ = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
+ %strong= @build.user.to_reference
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 5477fc65c2b..a26f8aeb315 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,18 +1,10 @@
- page_title "#{@build.name} (##{@build.id})", "Builds"
- trace_with_state = @build.trace_with_state
+- header_title project_title(@project, "Builds", project_builds_path(@project))
.build-page
- .row-content-block.top-block
- Build ##{@build.id} for commit
- %strong.monospace= link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline)
- from
- = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
- - merge_request = @build.merge_request
- - if merge_request
- via
- = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request)
+ = render "header"
- #up-build-trace
- builds = @build.pipeline.builds.latest.to_a
- if builds.size > 1
%ul.nav-links.no-top.no-bottom
@@ -33,18 +25,6 @@
&middot;
%i.fa.fa-warning
This build was retried.
-
- .row-content-block.middle-block
- .build-head
- .clearfix
- = ci_status_with_icon(@build.status)
- - if @build.duration
- %span
- %i.fa.fa-time
- #{duration_in_words(@build.finished_at, @build.started_at)}
- .pull-right
- #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
-
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
@@ -64,158 +44,27 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
- .row.prepend-top-default
- .col-md-9
- .clearfix
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- .clearfix
+ .prepend-top-default
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
#js-build-scroll.scroll-controls
- = link_to '#up-build-trace', class: 'btn' do
+ = link_to '#build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
+ - if @build.erased?
+ .erased.alert.alert-warning
+ - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
+ Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+ - else
+ %pre.build-trace#build-trace
+ %code.bash.js-build-output
+ = icon("refresh spin", class: "js-build-refresh")
- - if @build.erased?
- .erased.alert.alert-warning
- - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
- Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- - else
- %pre.trace#build-trace
- %code.bash
- = preserve do
- = raw trace_with_state[:html]
- - if @build.active?
- %i{:class => "fa fa-refresh fa-spin"}
-
- %div#down-build-trace
-
- .col-md-3
- - if @build.coverage
- .build-widget
- %h4.title
- Test coverage
- %h1 #{@build.coverage}%
-
- - if can?(current_user, :read_build, @project) && @build.artifacts?
- .build-widget.artifacts
- %h4.title Build artifacts
- .center
- .btn-group{ role: :group }
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
- = icon('download')
- Download
-
- - if @build.artifacts_metadata?
- = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
- = icon('folder-open')
- Browse
-
- .build-widget.build-controls
- %h4.title
- Build ##{@build.id}
- - if can?(current_user, :update_build, @project)
- .center
- .btn-group{ role: :group }
- - if @build.active?
- = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
- - elsif @build.retryable?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
-
- - if @build.erasable?
- = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
- class: 'btn btn-sm btn-warning', method: :post,
- data: { confirm: 'Are you sure you want to erase this build?' } do
- = icon('eraser')
- Erase
- - if @build.has_trace?
- = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build),
- class: 'btn btn-sm btn-success', target: '_blank'
-
- .clearfix
- - if @build.duration
- %p
- %span.attr-name Duration:
- #{duration_in_words(@build.finished_at, @build.started_at)}
- %p
- %span.attr-name Created:
- #{time_ago_with_tooltip(@build.created_at)}
- - if @build.finished_at
- %p
- %span.attr-name Finished:
- #{time_ago_with_tooltip(@build.finished_at)}
- - if @build.erased_at
- %p
- %span.attr-name Erased:
- #{time_ago_with_tooltip(@build.erased_at)}
- %p
- %span.attr-name Runner:
- - if @build.runner && current_user && current_user.admin
- = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- - elsif @build.runner
- \##{@build.runner.id}
-
- - if @build.trigger_request
- .build-widget
- %h4.title
- Trigger
-
- %p
- %span.attr-name Token:
- #{@build.trigger_request.trigger.short_token}
-
- - if @build.trigger_request.variables
- %p
- %span.attr-name Variables:
-
- %code
- - @build.trigger_request.variables.each do |key, value|
- #{key}=#{value}
-
- .build-widget
- %h4.title
- Commit
- .pull-right
- %small
- = link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace"
- %p
- %span.attr-name Branch:
- = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
- %p
- %span.attr-name Author:
- #{@build.pipeline.git_author_name}
- %p
- %span.attr-name Message:
- #{@build.pipeline.git_commit_message}
-
- - if @build.tags.any?
- .build-widget
- %h4.title
- Tags
- - @build.tag_list.each do |tag|
- %span.label.label-primary
- = tag
-
- - if @builds.present?
- .build-widget
- %h4.title #{pluralize(@builds.count(:id), "other build")} for
- = succeed ":" do
- = link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace"
- %table.table.builds
- - @builds.each_with_index do |build, i|
- %tr.build
- %td
- = ci_icon_for_status(build.status)
- %td
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
- - if build.name
- = build.name
- - else
- %span ##{build.id}
-
- %td.status= build.status
+ #down-build-trace
+= render "sidebar"
- :javascript
- new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}")
+:javascript
+ new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}")
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 44ef1fdbbe3..d9b800a4ded 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -17,7 +17,7 @@
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
- = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch"
+ = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch"
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 18b125ff9d4..8449fe1e4e0 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -84,6 +84,8 @@
%br
%span.descr Enable Container Registry for this repository
%hr
+ = render 'merge_request_settings', f: f
+ %hr
= render 'builds_settings', f: f
%hr
%fieldset.features.append-bottom-default
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
new file mode 100644
index 00000000000..166dae248b6
--- /dev/null
+++ b/app/views/projects/issues/_head.html.haml
@@ -0,0 +1,25 @@
+%ul.nav-links.sub-nav
+ %div{ class: (container_class) }
+ - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+ = nav_link(controller: :issues) do
+ = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do
+ %span
+ Issues
+
+ - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ Merge Requests
+
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
+
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 75f36579b11..d8075371853 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -24,8 +24,6 @@
MERGED
- elsif merge_request.closed?
CLOSED
- %li
- = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
- if @closed_by_merge_requests.present?
%li
= render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index b9bb6fe559d..c6fc499a7b8 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -6,7 +6,7 @@
%li
- sha = @project.repository.find_branch(branch).target
- pipeline = @project.pipeline(sha, branch) if sha
- - if ci_copipelinemmit
+ - if pipeline
%span.related-branch-ci-status
= render_pipeline_status(pipeline)
%span.related-branch-info
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 7ad7c9c87e8..36957560de0 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
- xml.updated @issues.first.created_at.xmlschema if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
- xml << render(partial: 'issues/issue', collection: @issues) if @issues.any?
+ xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 7c1457553d9..cd876b5ea62 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,23 +1,26 @@
+- @no_container = true
- page_title "Issues"
+= render "projects/issues/head"
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
-.top-area
- = render 'shared/issuable/nav', type: :issues
- .nav-controls
- - if current_user
- = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
- = icon('rss')
- %span.icon-label
- Subscribe
- = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
- New Issue
+%div{ class: (container_class) }
+ .top-area
+ = render 'shared/issuable/nav', type: :issues
+ .nav-controls
+ - if current_user
+ = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
+ = icon('rss')
+ %span.icon-label
+ Subscribe
+ = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
+ - if can? current_user, :create_issue, @project
+ = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
+ New Issue
-= render 'shared/issuable/filter', type: :issues
+ = render 'shared/issuable/filter', type: :issues
-.issues-holder
- = render "issues"
+ .issues-holder
+ = render "issues"
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 93583c92609..6e1baa46b05 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,35 +1,38 @@
+- @no_container = true
- page_title "Labels"
- hide_class = ''
+= render "projects/issues/head"
-.top-area
- .nav-text
- Labels can be applied to issues and merge requests.
- .nav-controls
- - if can?(current_user, :admin_label, @project)
- = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
- New label
+%div{ class: (container_class) }
+ .top-area
+ .nav-text
+ Labels can be applied to issues and merge requests.
+ .nav-controls
+ - if can?(current_user, :admin_label, @project)
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
+ New label
-.labels
- - if can?(current_user, :admin_label, @project)
- -# Only show it in the first page
- - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1')
- .prioritized-labels{ class: ('hide' if hide) }
- %h5 Prioritized Labels
- %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
- - if @prioritized_labels.present?
- = render @prioritized_labels
- - else
- %p.empty-message No prioritized labels yet
- .other-labels
+ .labels
- if can?(current_user, :admin_label, @project)
- %h5{ class: ('hide' if hide) } Other Labels
- - if @labels.present?
- %ul.content-list.manage-labels-list.js-other-labels
- = render @labels
- = paginate @labels, theme: 'gitlab'
- - else
- .nothing-here-block
- - if can?(current_user, :admin_label, @project)
- Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
- - else
- No labels created
+ -# Only show it in the first page
+ - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1')
+ .prioritized-labels{ class: ('hide' if hide) }
+ %h5 Prioritized Labels
+ %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
+ - if @prioritized_labels.present?
+ = render @prioritized_labels
+ - else
+ %p.empty-message No prioritized labels yet
+ .other-labels
+ - if can?(current_user, :admin_label, @project)
+ %h5{ class: ('hide' if hide) } Other Labels
+ - if @labels.present?
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render @labels
+ = paginate @labels, theme: 'gitlab'
+ - else
+ .nothing-here-block
+ - if can?(current_user, :admin_label, @project)
+ Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
+ - else
+ No labels created
diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml
deleted file mode 100644
index 19e4dab874b..00000000000
--- a/app/views/projects/merge_requests/_head.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.top-tabs
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), class: "tab #{'active' if current_page?(namespace_project_merge_requests_path(@project.namespace, @project)) }" do
- %span
- Merge Requests
-
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index c8653cb0c30..9f948d41dda 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,18 +1,20 @@
+- @no_container = true
- page_title "Merge Requests"
-
+= render "projects/issues/head"
= render 'projects/last_push'
-.top-area
- = render 'shared/issuable/nav', type: :merge_requests
- .nav-controls
- = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
+%div{ class: (container_class) }
+ .top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ .nav-controls
+ = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- - if merge_project
- = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
- New Merge Request
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
+ New Merge Request
-= render 'shared/issuable/filter', type: :merge_requests
+ = render 'shared/issuable/filter', type: :merge_requests
-.merge-requests-holder
- = render 'merge_requests'
+ .merge-requests-holder
+ = render 'merge_requests'
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 13359abede7..0e0af57d76e 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -17,6 +17,8 @@
= render 'projects/merge_requests/widget/open/merge_when_build_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
= render 'projects/merge_requests/widget/open/not_allowed'
+ - elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed?
+ = render 'projects/merge_requests/widget/open/build_failed'
- elsif @merge_request.can_be_merged?
= render 'projects/merge_requests/widget/open/accept'
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index 60d7d6ff1f5..941513febbd 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -10,19 +10,20 @@
%span.btn-group
= button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do
Merge When Build Succeeds
- = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
- %span.caret
- %span.sr-only
- Select Merge Moment
- %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- %li
- = link_to "#", class: "merge_when_build_succeeds" do
- = icon('check fw')
- Merge When Build Succeeds
- %li
- = link_to "#", class: "accept_merge_request" do
- = icon('warning fw')
- Merge Immediately
+ - unless @project.only_allow_merge_if_build_succeeds?
+ = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
+ %span.caret
+ %span.sr-only
+ Select Merge Moment
+ %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
+ %li
+ = link_to "#", class: "merge_when_build_succeeds" do
+ = icon('check fw')
+ Merge When Build Succeeds
+ %li
+ = link_to "#", class: "accept_merge_request" do
+ = icon('warning fw')
+ Merge Immediately
- else
= f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do
Accept Merge Request
diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
new file mode 100644
index 00000000000..14f51af5360
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
@@ -0,0 +1,6 @@
+%h4
+ = icon('exclamation-triangle')
+ The build for this merge request failed
+
+%p
+ Please retry the build or push a new commit to fix the failure.
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 60a5b83434e..b0e0bdfff5a 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,19 +1,22 @@
+- @no_container = true
- page_title "Milestones"
+= render "projects/issues/head"
-.top-area
- = render 'shared/milestones_filter'
+%div{ class: (container_class) }
+ .top-area
+ = render 'shared/milestones_filter'
- .nav-controls
- - if can?(current_user, :admin_milestone, @project)
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
- New Milestone
+ .nav-controls
+ - if can?(current_user, :admin_milestone, @project)
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
+ New Milestone
-.milestones
- %ul.content-list
- = render @milestones
+ .milestones
+ %ul.content-list
+ = render @milestones
- - if @milestones.blank?
- %li
- .nothing-here-block No milestones to show
+ - if @milestones.blank?
+ %li
+ .nothing-here-block No milestones to show
- = paginate @milestones, theme: "gitlab"
+ = paginate @milestones, theme: "gitlab"
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index f278d4e0538..d0ba0d27d7c 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -5,11 +5,9 @@
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
- %span.badge.count.ci_counter= number_with_delimiter(@project.pipelines.running_or_pending.count)
- if project_nav_tab? :builds
= nav_link(controller: %w(builds)) do
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
%span
Builds
- %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count)
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index e74fc36c797..ca3178395c1 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-- if @merge_requests.any?
+- if @merge_requests.reorder(nil).any?
- @merge_requests.group_by(&:target_project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg
index c87794b9062..d465504b154 100644
--- a/app/views/shared/icons/_activity.svg
+++ b/app/views/shared/icons/_activity.svg
@@ -3,13 +3,14 @@
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>path-1</title>
<desc>Created with Sketch.</desc>
- <defs>
- <path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 Z" id="path-1"></path>
- </defs>
+ <defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <mask id="mask-2" fill="white">
- <use xlink:href="#path-1"></use>
- </mask>
- <use id="path-1" fill="#D8D8D8" xlink:href="#path-1"></use>
+ <g id="_activity" fill="#7E7D7D">
+ <g id="Page-1">
+ <g id="path-1">
+ <path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 L11,11 Z"></path>
+ </g>
+ </g>
+ </g>
</g>
</svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 17e2a7e9290..c30bdb0ae91 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -35,13 +35,13 @@
.clearfix
.error-alert
-- if issuable.is_a?(Issue) && !issuable.project.private?
+- if issuable.is_a?(Issue)
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :confidential do
= f.check_box :confidential
- This issue is confidential and should only be visible to team members
+ This issue is confidential and should only be visible to team members with at least Reporter access.
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
- has_due_date = issuable.has_attribute?(:due_date)
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 4e280c371ac..0acb8253139 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -4,7 +4,7 @@
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
.dropdown-page-one
= dropdown_title(title)
- = dropdown_filter(filter_placeholder)
+ = dropdown_filter(filter_placeholder, search_id: "label-name")
= dropdown_content
- if @project && show_footer
= dropdown_footer do
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 0510e7df597..1048ef6e243 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -181,7 +181,7 @@ production: &base
# host: registry.example.com
# port: 5005
# api_url: http://localhost:5000/ # internal address to the registry, will be used by GitLab to directly communicate with API
- # key_path: config/registry.key
+ # key: config/registry.key
# path: shared/registry
# issuer: gitlab-issuer
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 8dc8e270afc..618dba74151 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -12,7 +12,7 @@ Doorkeeper.configure do
end
resource_owner_from_credentials do |routes|
- Gitlab::Auth.find_in_gitlab_or_ldap(params[:username], params[:password])
+ Gitlab::Auth.find_with_user_password(params[:username], params[:password])
end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 2673093b96a..f6509ee43f1 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -96,13 +96,18 @@ if Gitlab::Metrics.enabled?
config.instrument_instance_methods(const)
end
- # Instruments all Banzai filters
- Dir[Rails.root.join('lib', 'banzai', 'filter', '*.rb')].each do |file|
- klass = File.basename(file, File.extname(file)).camelize
- const = Banzai::Filter.const_get(klass)
+ # Instruments all Banzai filters and reference parsers
+ {
+ Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
+ ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
+ }.each do |const_name, path|
+ Dir[path].each do |file|
+ klass = File.basename(file, File.extname(file)).camelize
+ const = Banzai.const_get(const_name).const_get(klass)
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
+ config.instrument_methods(const)
+ config.instrument_instance_methods(const)
+ end
end
config.instrument_methods(Banzai::Renderer)
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 761a32adb9e..7cab24b295e 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -2,7 +2,7 @@
<%
require "yaml"
require "json"
-require_relative "lib/gitlab/redis"
+require_relative "lib/gitlab/redis" unless defined?(Gitlab::Redis)
rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
new file mode 100644
index 00000000000..69d64ccd006
--- /dev/null
+++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
@@ -0,0 +1,15 @@
+class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:projects,
+ :only_allow_merge_if_build_succeeds,
+ :boolean,
+ default: false)
+ end
+
+ def down
+ remove_column(:projects, :only_allow_merge_if_build_succeeds)
+ end
+end
diff --git a/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb
new file mode 100644
index 00000000000..be295f0181d
--- /dev/null
+++ b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb
@@ -0,0 +1,10 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddHasExternalIssueTrackerToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column(:projects, :has_external_issue_tracker, :boolean)
+ end
+end
diff --git a/db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb b/db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb
new file mode 100644
index 00000000000..259abb08e47
--- /dev/null
+++ b/db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb
@@ -0,0 +1,11 @@
+class RemoveNotificationSettingNotNullConstraints < ActiveRecord::Migration
+ def up
+ change_column :notification_settings, :source_type, :string, null: true
+ change_column :notification_settings, :source_id, :integer, null: true
+ end
+
+ def down
+ change_column :notification_settings, :source_type, :string, null: false
+ change_column :notification_settings, :source_id, :integer, null: false
+ end
+end
diff --git a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
new file mode 100644
index 00000000000..477b2106dea
--- /dev/null
+++ b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
@@ -0,0 +1,6 @@
+class RemoveDeprecatedIssuesTrackerColumnsFromProjects < ActiveRecord::Migration
+ def change
+ remove_column :projects, :issues_tracker, :string, default: 'gitlab', null: false
+ remove_column :projects, :issues_tracker_id, :string
+ end
+end
diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb
new file mode 100644
index 00000000000..760b766828e
--- /dev/null
+++ b/db/migrate/20160610201627_migrate_users_notification_level.rb
@@ -0,0 +1,21 @@
+class MigrateUsersNotificationLevel < ActiveRecord::Migration
+ # Migrates only users who changed their default notification level :participating
+ # creating a new record on notification settings table
+
+ def up
+ execute(%Q{
+ INSERT INTO notification_settings
+ (user_id, level, created_at, updated_at)
+ (SELECT id, notification_level, created_at, updated_at FROM users WHERE notification_level != 1)
+ })
+ end
+
+ # Migrates from notification settings back to user notification_level
+ # If no value is found the default level of 1 will be used
+ def down
+ execute(%Q{
+ UPDATE users u SET
+ notification_level = COALESCE((SELECT level FROM notification_settings WHERE user_id = u.id AND source_type IS NULL), 1)
+ })
+ end
+end
diff --git a/db/migrate/20160610301627_remove_notification_level_from_users.rb b/db/migrate/20160610301627_remove_notification_level_from_users.rb
new file mode 100644
index 00000000000..8afb14df2cf
--- /dev/null
+++ b/db/migrate/20160610301627_remove_notification_level_from_users.rb
@@ -0,0 +1,7 @@
+class RemoveNotificationLevelFromUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ remove_column :users, :notification_level, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b7adf48fdb4..3dccbbd50ba 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160608155312) do
+ActiveRecord::Schema.define(version: 20160610301627) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -670,8 +670,8 @@ ActiveRecord::Schema.define(version: 20160608155312) do
create_table "notification_settings", force: :cascade do |t|
t.integer "user_id", null: false
- t.integer "source_id", null: false
- t.string "source_type", null: false
+ t.integer "source_id"
+ t.string "source_type"
t.integer "level", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -747,38 +747,38 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
- t.string "issues_tracker", default: "gitlab", null: false
- t.string "issues_tracker_id"
- t.boolean "snippets_enabled", default: true, null: false
+ t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
- t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
t.string "import_type"
t.string "import_source"
- t.integer "commit_count", default: 0
+ t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
- t.boolean "builds_enabled", default: true, null: false
- t.boolean "shared_runners_enabled", default: true, null: false
+ t.boolean "builds_enabled", default: true, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
t.string "build_coverage_regex"
- t.boolean "build_allow_git_fetch", default: true, null: false
- t.integer "build_timeout", default: 3600, null: false
- t.boolean "pending_delete", default: false
- t.boolean "public_builds", default: true, null: false
- t.integer "pushes_since_gc", default: 0
+ t.boolean "build_allow_git_fetch", default: true, null: false
+ t.integer "build_timeout", default: 3600, null: false
+ t.boolean "pending_delete", default: false
+ t.boolean "public_builds", default: true, null: false
+ t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
t.boolean "container_registry_enabled"
+ t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
+ t.boolean "has_external_issue_tracker"
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
@@ -986,7 +986,6 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.boolean "can_create_team", default: true, null: false
t.string "state"
t.integer "color_scheme_id", default: 1, null: false
- t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at"
t.integer "created_by_id"
t.datetime "last_credential_check_at"
diff --git a/doc/README.md b/doc/README.md
index d1345ab2493..5d89d0c9821 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -28,7 +28,7 @@
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
-- [Log system](logs/logs.md) Log system.
+- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index caf9a5bef2c..7870669fa77 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -62,7 +62,7 @@ registry:
host: registry.gitlab.example.com
port: 5005
api_url: http://localhost:5000/
- key_path: config/registry.key
+ key: config/registry.key
path: shared/registry
issuer: gitlab-issuer
```
@@ -75,7 +75,7 @@ where:
| `host` | The host URL under which the Registry will run and the users will be able to use. |
| `port` | The port under which the external Registry domain will listen on. |
| `api_url` | The internal API URL under which the Registry is exposed to. It defaults to `http://localhost:5000`. |
-| `key_path`| The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. |
+| `key` | The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. |
| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. This path needs to be readable by the GitLab user, the web-server user and the Registry user. Read more in [#container-registry-storage-path](#container-registry-storage-path). |
| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. |
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
new file mode 100644
index 00000000000..737b39db16c
--- /dev/null
+++ b/doc/administration/logs.md
@@ -0,0 +1,137 @@
+## Log system
+
+GitLab has an advanced log system where everything is logged so that you
+can analyze your instance using various system log files. In addition to
+system log files, GitLab Enterprise Edition comes with Audit Events.
+Find more about them [in Audit Events
+documentation](http://docs.gitlab.com/ee/administration/audit_events.html)
+
+System log files are typically plain text in a standard log file format.
+This guide talks about how to read and use these system log files.
+
+### production.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
+omnibus package or in `/home/git/gitlab/log/production.log` for
+installations from source.
+
+It contains information about all performed requests. You can see the
+URL and type of request, IP address and what exactly parts of code were
+involved to service this particular request. Also you can see all SQL
+request that have been performed and how much time it took. This task is
+more useful for GitLab contributors and developers. Use part of this log
+file when you are going to report bug. For example:
+
+```
+Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200
+Processing by Projects::TreeController#show as HTML
+ Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"}
+
+ ... [CUT OUT]
+
+ Namespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]]
+ CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]]
+ CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members".
+ (1.4ms) SELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened')) [["target_project_id", 18]]
+ Rendered layouts/nav/_project.html.haml (28.0ms)
+ Rendered layouts/_collapse_button.html.haml (0.2ms)
+ Rendered layouts/_flash.html.haml (0.1ms)
+ Rendered layouts/_page.html.haml (32.9ms)
+Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms)
+```
+
+In this example we can see that server processed an HTTP request with URL
+`/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12
+19:34:53 +0200. Also we can see that request was processed by
+`Projects::TreeController`.
+
+### application.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/application.log` for
+omnibus package or in `/home/git/gitlab/log/application.log` for
+installations from source.
+
+It helps you discover events happening in your instance such as user creation,
+project removing and so on. For example:
+
+```
+October 06, 2014 11:56: User "Administrator" (admin@example.com) was created
+October 06, 2014 11:56: Documentcloud created a new project "Documentcloud / Underscore"
+October 06, 2014 11:56: Gitlab Org created a new project "Gitlab Org / Gitlab Ce"
+October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was removed
+October 07, 2014 11:25: Project "project133" was removed
+```
+
+### githost.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for
+omnibus package or in `/home/git/gitlab/log/githost.log` for
+installations from source.
+
+GitLab has to interact with Git repositories but in some rare cases
+something can go wrong and in this case you will know what exactly
+happened. This log file contains all failed requests from GitLab to Git
+repositories. In the majority of cases this file will be useful for developers
+only. For example:
+
+```
+December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq/.git --work-tree=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq merge --no-ff -mMerge branch 'feature_conflict' into 'feature' source/feature_conflict
+
+error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git'
+```
+
+### sidekiq.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for
+omnibus package or in `/home/git/gitlab/log/sidekiq.log` for
+installations from source.
+
+GitLab uses background jobs for processing tasks which can take a long
+time. All information about processing these jobs are written down to
+this file. For example:
+
+```
+2014-06-10T07:55:20Z 2037 TID-tm504 ERROR: /opt/bitnami/apps/discourse/htdocs/vendor/bundle/ruby/1.9.1/gems/redis-3.0.7/lib/redis/client.rb:228:in `read'
+2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
+```
+
+### gitlab-shell.log
+
+This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for
+omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for
+installations from source.
+
+GitLab shell is used by Gitlab for executing Git commands and provide
+SSH access to Git repositories. For example:
+
+```
+I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at </var/opt/gitlab/git-data/repositories/root/dcdcdcdcd.git>.
+I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git.
+```
+
+### unicorn\_stderr.log
+
+This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for
+omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for
+installations from source.
+
+Unicorn is a high-performance forking Web server which is used for
+serving the GitLab application. You can look at this log if, for
+example, your application does not respond. This log contains all
+information about the state of unicorn processes at any given time.
+
+```
+I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list
+I, [2015-02-13T06:14:56.931002 #9047] INFO -- : listening on addr=127.0.0.1:8080 fd=12
+I, [2015-02-13T06:14:56.931381 #9047] INFO -- : listening on addr=/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket fd=13
+I, [2015-02-13T06:14:56.936638 #9047] INFO -- : master process ready
+I, [2015-02-13T06:14:56.946504 #9092] INFO -- : worker=0 spawned pid=9092
+I, [2015-02-13T06:14:56.946943 #9092] INFO -- : worker=0 ready
+I, [2015-02-13T06:14:56.947892 #9094] INFO -- : worker=1 spawned pid=9094
+I, [2015-02-13T06:14:56.948181 #9094] INFO -- : worker=1 ready
+W, [2015-02-13T07:16:01.312916 #9094] WARN -- : #<Unicorn::HttpServer:0x0000000208f618>: worker (pid: 9094) exceeds memory limit (320626688 bytes > 247066940 bytes)
+W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 9094) alive: 3621 sec (trial 1)
+I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9094 exit 0> worker=1
+I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379
+I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 16b892dc3b7..2930f615fc1 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -572,7 +572,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
```
-Example response:
+Example response when the GitLab issue tracker is used:
```json
[
@@ -618,6 +618,17 @@ Example response:
]
```
+Example response when an external issue tracker (e.g. JIRA) is used:
+
+```json
+[
+ {
+ "id" : "PROJECT-123",
+ "title" : "Title of this issue"
+ }
+]
+```
+
## Subscribe to a merge request
Subscribes the authenticated user to a merge request to receive notification. If
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index a3481f58c6c..0707555e393 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -30,6 +30,7 @@ If you want a quick introduction to GitLab CI, follow our
- [when](#when)
- [artifacts](#artifacts)
- [artifacts:name](#artifacts-name)
+ - [artifacts:when](#artifacts-when)
- [dependencies](#dependencies)
- [before_script and after_script](#before_script-and-after_script)
- [Hidden jobs](#hidden-jobs)
@@ -651,6 +652,32 @@ job:
untracked: true
```
+#### artifacts:when
+
+>**Note:**
+Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+
+`artifacts:when` is used to upload artifacts on build failure or despite the
+failure.
+
+`artifacts:when` can be set to one of the following values:
+
+1. `on_success` - upload artifacts only when build succeeds. This is the default
+1. `on_failure` - upload artifacts only when build fails
+1. `always` - upload artifacts despite the build status
+
+---
+
+**Example configurations**
+
+To upload artifacts only when build fails.
+
+```yaml
+job:
+ artifacts:
+ when: on_failure
+```
+
### dependencies
>**Note:**
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 8292b393757..f5d97179f8a 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -103,14 +103,14 @@ Inside the document:
- Every piece of documentation that comes with a new feature should declare the
GitLab version that feature got introduced. Right below the heading add a
- note: `_**Note:** This feature was introduced in GitLab 8.3_`
+ note: `>**Note:** This feature was introduced in GitLab 8.3`
- If possible every feature should have a link to the MR that introduced it.
The above note would be then transformed to:
- `_**Note:** This feature was [introduced][ce-1242] in GitLab 8.3_`, where
+ `>**Note:** This feature was [introduced][ce-1242] in GitLab 8.3`, where
the [link identifier](#links) is named after the repository (CE) and the MR
number
- If the feature is only in GitLab EE, don't forget to mention it, like:
- `_**Note:** This feature was introduced in GitLab EE 8.3_`. Otherwise, leave
+ `>**Note:** This feature was introduced in GitLab EE 8.3`. Otherwise, leave
this mention out
## References
@@ -141,6 +141,48 @@ Inside the document:
[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website"
+## Changing document location
+
+Changing a document's location is not to be taken lightly. Remember that the
+documentation is available to all installations under `help/` and not only to
+GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
+Documentation team beforehand.
+
+If you indeed need to change a document's location, do NOT remove the old
+document, but rather put a text in it that points to the new location, like:
+
+```
+This document was moved to [path/to/new_doc.md](path/to/new_doc.md).
+```
+
+where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
+
+---
+
+For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
+`doc/administration/lfs.md`, then the steps would be:
+
+1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
+1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
+
+ ```
+ This document was moved to [administration/lfs.md](../../administration/lfs.md).
+ ```
+
+1. Find and replace any occurrences of the old location with the new one.
+ A quick way to find them is to use `grep`:
+
+ ```
+ grep -nR "lfs_administration.md" doc/
+ ```
+
+ The above command will search in the `doc/` directory for
+ `lfs_administration.md` recursively and will print the file and the line
+ where this file is mentioned. Note that we used just the filename
+ (`lfs_administration.md`) and not the whole the relative path
+ (`workflow/lfs/lfs_administration.md`).
+
+
## API
Here is a list of must-have items. Use them in the exact order that appears
@@ -222,8 +264,8 @@ curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.
#### Post data using JSON content
-_**Note:** In this example we create a new group. Watch carefully the single
-and double quotes._
+> **Note:** In this example we create a new group. Watch carefully the single
+and double quotes.
```bash
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
diff --git a/doc/logs/logs.md b/doc/logs/logs.md
index f84060b8d07..a2eca62d691 100644
--- a/doc/logs/logs.md
+++ b/doc/logs/logs.md
@@ -1,92 +1 @@
-## Log system
-GitLab has an advanced log system where everything is logged so that you can analyze your instance using various system log files.
-In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://docs.gitlab.com/ee/administration/audit_events.html)
-
-System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files.
-
-#### production.log
-This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/log/production.log` for installations from the source.
-
-This file contains information about all performed requests. You can see url and type of request, IP address and what exactly parts of code were involved to service this particular request. Also you can see all SQL request that have been performed and how much time it took.
-This task is more useful for GitLab contributors and developers. Use part of this log file when you are going to report bug.
-
-```
-Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200
-Processing by Projects::TreeController#show as HTML
- Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"}
-
- ... [CUT OUT]
-
- amespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]]
- CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]]
- CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members".
-  (1.4ms) SELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened')) [["target_project_id", 18]]
- Rendered layouts/nav/_project.html.haml (28.0ms)
- Rendered layouts/_collapse_button.html.haml (0.2ms)
- Rendered layouts/_flash.html.haml (0.1ms)
- Rendered layouts/_page.html.haml (32.9ms)
-Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms)
-```
-In this example we can see that server processed HTTP request with url `/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12 19:34:53 +0200. Also we can see that request was processed by Projects::TreeController.
-
-#### application.log
-This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/log/application.log` for installations from the source.
-
-This log file helps you discover events happening in your instance such as user creation, project removing and so on.
-
-```
-October 06, 2014 11:56: User "Administrator" (admin@example.com) was created
-October 06, 2014 11:56: Documentcloud created a new project "Documentcloud / Underscore"
-October 06, 2014 11:56: Gitlab Org created a new project "Gitlab Org / Gitlab Ce"
-October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was removed
-October 07, 2014 11:25: Project "project133" was removed
-```
-#### githost.log
-This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/log/githost.log` for installations from the source.
-
-The GitLab has to interact with git repositories but in some rare cases something can go wrong and in this case you will know what exactly happened. This log file contains all failed requests from GitLab to git repository. In majority of cases this file will be useful for developers only.
-```
-December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq/.git --work-tree=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq merge --no-ff -mMerge branch 'feature_conflict' into 'feature' source/feature_conflict
-
-error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git'
-```
-
-#### sidekiq.log
-This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/log/sidekiq.log` for installations from the source.
-
-GitLab uses background jobs for processing tasks which can take a long time. All information about processing these jobs are writing down to this file.
-```
-2014-06-10T07:55:20Z 2037 TID-tm504 ERROR: /opt/bitnami/apps/discourse/htdocs/vendor/bundle/ruby/1.9.1/gems/redis-3.0.7/lib/redis/client.rb:228:in `read'
-2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
-```
-
-#### gitlab-shell.log
-This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from the source.
-
-gitlab-shell is using by Gitlab for executing git commands and provide ssh access to git repositories.
-
-```
-I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at </var/opt/gitlab/git-data/repositories/root/dcdcdcdcd.git>.
-I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git.
-```
-
-#### unicorn_stderr.log
-This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from the source.
-
-Unicorn is a high-performance forking Web server which is used for serving the GitLab application. You can look at this log if, for example, your application does not respond. This log contains all information about the state of unicorn processes at any given time.
-
-```
-I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list
-I, [2015-02-13T06:14:56.931002 #9047] INFO -- : listening on addr=127.0.0.1:8080 fd=12
-I, [2015-02-13T06:14:56.931381 #9047] INFO -- : listening on addr=/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket fd=13
-I, [2015-02-13T06:14:56.936638 #9047] INFO -- : master process ready
-I, [2015-02-13T06:14:56.946504 #9092] INFO -- : worker=0 spawned pid=9092
-I, [2015-02-13T06:14:56.946943 #9092] INFO -- : worker=0 ready
-I, [2015-02-13T06:14:56.947892 #9094] INFO -- : worker=1 spawned pid=9094
-I, [2015-02-13T06:14:56.948181 #9094] INFO -- : worker=1 ready
-W, [2015-02-13T07:16:01.312916 #9094] WARN -- : #<Unicorn::HttpServer:0x0000000208f618>: worker (pid: 9094) exceeds memory limit (320626688 bytes > 247066940 bytes)
-W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 9094) alive: 3621 sec (trial 1)
-I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9094 exit 0> worker=1
-I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379
-I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready
-```
+This document was moved to [administration/logs.md](../administration/logs.md).
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index 1b5718c91c1..d2ec56e6504 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -2,6 +2,17 @@
Merge requests allow you to exchange changes you made to source code
+## Only allow merge requests to be merged if the build succeeds
+
+You can prevent merge requests from being merged if their build did not succeed
+in the project settings page.
+
+![only_allow_merge_if_build_succeeds](merge_requests/only_allow_merge_if_build_succeeds.png)
+
+Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box.
+
+Please note that you need to have builds configured to enable this feature.
+
## Checkout merge requests locally
Locate the section for your GitLab remote in the `.git/config` file. It looks like this:
diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png
new file mode 100644
index 00000000000..18bebf5fe92
--- /dev/null
+++ b/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png
Binary files differ
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 26e67503021..c4f987a7923 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -107,12 +107,16 @@ Feature: Project Active Tab
Scenario: On Project Issues/Milestones
Given I visit my project's issues page
- And I click the "Milestones" tab
- Then the active main tab should be Milestones
+ And I click the "Milestones" sub tab
+ Then the active main tab should be Issues
+ Then the active sub tab should be Milestones
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Project Issues/Labels
Given I visit my project's issues page
- And I click the "Labels" tab
- Then the active main tab should be Labels
+ And I click the "Labels" sub tab
+ Then the active main tab should be Issues
+ Then the active sub tab should be Labels
And no other main tabs should be active
+ And no other sub tabs should be active
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index b1a87b96efd..9e5602dacf1 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -155,6 +155,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I click on my profile picture' do
+ find(:css, '.side-nav-toggle').click
find(:css, '.sidebar-user').click
end
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 745fd3471c4..80043463188 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -77,14 +77,14 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
# Sub Tabs: Issues
- step 'I click the "Milestones" tab' do
- page.within('.layout-nav') do
+ step 'I click the "Milestones" sub tab' do
+ page.within('.sub-nav') do
click_link('Milestones')
end
end
- step 'I click the "Labels" tab' do
- page.within('.layout-nav') do
+ step 'I click the "Labels" sub tab' do
+ page.within('.sub-nav') do
click_link('Labels')
end
end
@@ -93,11 +93,11 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
ensure_active_sub_tab('Issues')
end
- step 'the active main tab should be Milestones' do
- ensure_active_main_tab('Milestones')
+ step 'the active sub tab should be Milestones' do
+ ensure_active_sub_tab('Milestones')
end
- step 'the active main tab should be Labels' do
- ensure_active_main_tab('Labels')
+ step 'the active sub tab should be Labels' do
+ ensure_active_sub_tab('Labels')
end
end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index 1bdb57af9d1..2876e8812e9 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -5,11 +5,11 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
include RepoHelpers
step 'I click artifacts download button' do
- page.within('.artifacts') { click_link 'Download' }
+ click_link 'Download'
end
step 'I click artifacts browse button' do
- page.within('.artifacts') { click_link 'Browse' }
+ click_link 'Browse'
end
step 'I should see content of artifacts archive' do
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index ace717b9909..4eef7aff213 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -6,7 +6,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
- expect(find('div.content ul.nav-links li.active')).to have_content(content)
+ expect(find('.sub-nav li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
@@ -18,7 +18,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
- expect(page).to have_selector('div.content ul.nav-links li.active', count: 1)
+ expect(page).to have_selector('.sub-nav li.active', count: 1)
end
step 'no other sub navs should be active' do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 50d69274b2e..14370ac218d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -179,6 +179,11 @@ module API
expose :upvotes, :downvotes
end
+ class ExternalIssue < Grape::Entity
+ expose :title
+ expose :id
+ end
+
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch
expose :upvotes, :downvotes
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 2aaa0557ea3..de5959e3aae 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -408,5 +408,23 @@ module API
error!(errors[:access_level], 422) if errors[:access_level].any?
not_found!(errors)
end
+
+ def send_git_blob(repository, blob)
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ header(*Gitlab::Workhorse.send_git_blob(repository, blob))
+ end
+
+ def send_git_archive(repository, ref:, format:)
+ header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
+ end
+
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ Entities::ExternalIssue
+ else
+ Entities::Issue
+ end
+ end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 43221d5622a..0e94efd4acd 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -228,11 +228,10 @@ module API
# Merge request can not be merged
# because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user)
- not_allowed! if !merge_request.open? || merge_request.work_in_progress?
- merge_request.check_if_can_be_merged
+ not_allowed! unless merge_request.mergeable_state?
- render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged?
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
if params[:sha] && merge_request.source_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.source_sha}", 409)
@@ -330,7 +329,7 @@ module API
get "#{path}/closes_issues" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 9cb14e95ebc..f55aceed92c 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -56,8 +56,7 @@ module API
blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
not_found! "File" unless blob
- content_type 'text/plain'
- header(*Gitlab::Workhorse.send_git_blob(repo, blob))
+ send_git_blob repo, blob
end
# Get a raw blob contents by blob sha
@@ -80,10 +79,7 @@ module API
not_found! 'Blob' unless blob
- env['api.format'] = :txt
-
- content_type blob.mime_type
- header(*Gitlab::Workhorse.send_git_blob(repo, blob))
+ send_git_blob repo, blob
end
# Get a an archive of the repository
@@ -98,7 +94,7 @@ module API
authorize! :download_code, user_project
begin
- header(*Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]))
+ send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
not_found!('File')
end
diff --git a/lib/api/session.rb b/lib/api/session.rb
index 56e69b2366f..56c202f1294 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -11,7 +11,7 @@ module API
# Example Request:
# POST /session
post "/session" do
- user = Gitlab::Auth.find_in_gitlab_or_ldap(params[:email] || params[:login], params[:password])
+ user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
return unauthorized! unless user
present user, with: Entities::UserLogin
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 9dd665441a0..2ff3e3bdfb0 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -38,7 +38,6 @@ module Backup
end
def upload(tar_file)
- remote_directory = Gitlab.config.backup.upload.remote_directory
$progress.print "Uploading backup archive to remote storage #{remote_directory} ... "
connection_settings = Gitlab.config.backup.upload.connection
@@ -47,8 +46,7 @@ module Backup
return
end
- connection = ::Fog::Storage.new(connection_settings)
- directory = connection.directories.create(key: remote_directory)
+ directory = connect_to_remote_directory(connection_settings)
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
@@ -155,6 +153,23 @@ module Backup
private
+ def connect_to_remote_directory(connection_settings)
+ connection = ::Fog::Storage.new(connection_settings)
+
+ # We only attempt to create the directory for local backups. For AWS
+ # and other cloud providers, we cannot guarantee the user will have
+ # permission to create the bucket.
+ if connection.service == ::Fog::Storage::Local
+ connection.directories.create(key: remote_directory)
+ else
+ connection.directories.get(remote_directory)
+ end
+ end
+
+ def remote_directory
+ Gitlab.config.backup.upload.remote_directory
+ end
+
def backup_contents
folders_to_backup + archives_to_backup + ["backup_information.yml"]
end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index b25e0e573a8..a902ced35d7 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -56,7 +56,7 @@ module Ci
class TriggerRequest < Grape::Entity
expose :id, :variables
- expose :commit, using: Commit
+ expose :pipeline, using: Commit, as: :commit
end
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 130f5b0892e..40a5d180fd0 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -8,6 +8,8 @@ module Ci
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
:dependencies, :before_script, :after_script, :variables]
+ ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
+ ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when]
attr_reader :before_script, :after_script, :image, :services, :path, :cache
@@ -135,6 +137,12 @@ module Ci
end
def validate_global_cache!
+ @cache.keys.each do |key|
+ unless ALLOWED_CACHE_KEYS.include? key
+ raise ValidationError, "#{name} cache unknown parameter #{key}"
+ end
+ end
+
if @cache[:key] && !validate_string(@cache[:key])
raise ValidationError, "cache:key parameter should be a string"
end
@@ -200,7 +208,7 @@ module Ci
raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
end
- if job[:when] && !job[:when].in?(%w(on_success on_failure always))
+ if job[:when] && !job[:when].in?(%w[on_success on_failure always])
raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
end
end
@@ -233,6 +241,12 @@ module Ci
end
def validate_job_cache!(name, job)
+ job[:cache].keys.each do |key|
+ unless ALLOWED_CACHE_KEYS.include? key
+ raise ValidationError, "#{name} job: cache unknown parameter #{key}"
+ end
+ end
+
if job[:cache][:key] && !validate_string(job[:cache][:key])
raise ValidationError, "#{name} job: cache:key parameter should be a string"
end
@@ -247,6 +261,12 @@ module Ci
end
def validate_job_artifacts!(name, job)
+ job[:artifacts].keys.each do |key|
+ unless ALLOWED_ARTIFACTS_KEYS.include? key
+ raise ValidationError, "#{name} job: artifacts unknown parameter #{key}"
+ end
+ end
+
if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
end
@@ -258,6 +278,10 @@ module Ci
if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths])
raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings"
end
+
+ if job[:artifacts][:when] && !job[:artifacts][:when].in?(%w[on_success on_failure always])
+ raise ValidationError, "#{name} job: artifacts:when parameter should be on_success, on_failure or always"
+ end
end
def validate_job_dependencies!(name, job)
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 076e2af7d38..db1704af75e 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -3,14 +3,14 @@ module Gitlab
Result = Struct.new(:user, :type)
class << self
- def find(login, password, project:, ip:)
+ def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
result = Result.new
if valid_ci_request?(login, password, project)
result.type = :ci
- elsif result.user = find_in_gitlab_or_ldap(login, password)
+ elsif result.user = find_with_user_password(login, password)
result.type = :gitlab_or_ldap
elsif result.user = oauth_access_token_check(login, password)
result.type = :oauth
@@ -20,7 +20,7 @@ module Gitlab
result
end
- def find_in_gitlab_or_ldap(login, password)
+ def find_with_user_password(login, password)
user = User.by_login(login)
# If no user is found, or it's an LDAP server, try LDAP.
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 9e09d2e118d..adbf5941a96 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -95,7 +95,7 @@ module Grack
end
def authenticate_user(login, password)
- user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
+ user = Gitlab::Auth.find_with_user_password(login, password)
unless user
user = oauth_access_token_check(login, password)
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 978c3f7896d..dd3ff0ab18b 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -31,8 +31,6 @@ module Gitlab
# Any data inserted while running this method (or after it has finished
# running) is _not_ updated automatically.
#
- # This method _only_ updates rows where the column's value is set to NULL.
- #
# table - The name of the table.
# column - The name of the column to update.
# value - The value for the column.
@@ -55,10 +53,10 @@ module Gitlab
first['count'].
to_i
- # Update in batches of 5%
+ # Update in batches of 5% until we run out of any rows to update.
batch_size = ((total / 100.0) * 5.0).ceil
- while processed < total
+ loop do
start_row = exec_query(%Q{
SELECT id
FROM #{quoted_table}
@@ -66,6 +64,9 @@ module Gitlab
LIMIT 1 OFFSET #{processed}
}).to_hash.first
+ # There are no more rows to process
+ break unless start_row
+
stop_row = exec_query(%Q{
SELECT id
FROM #{quoted_table}
@@ -126,6 +127,8 @@ module Gitlab
begin
transaction do
update_column_in_batches(table, column, default)
+
+ change_column_null(table, column, false) unless allow_null
end
# We want to rescue _all_ exceptions here, even those that don't inherit
# from StandardError.
@@ -134,8 +137,6 @@ module Gitlab
raise error
end
-
- change_column_null(table, column, false) unless allow_null
end
end
end
diff --git a/lib/gitlab/sanitizers/svg.rb b/lib/gitlab/sanitizers/svg.rb
index 5e95f6c0529..8304b9a482c 100644
--- a/lib/gitlab/sanitizers/svg.rb
+++ b/lib/gitlab/sanitizers/svg.rb
@@ -12,23 +12,45 @@ module Gitlab
def scrub(node)
unless Whitelist::ALLOWED_ELEMENTS.include?(node.name)
node.unlink
- else
- node.attributes.each do |attr_name, attr|
- valid_attributes = Whitelist::ALLOWED_ATTRIBUTES[node.name]
-
- unless valid_attributes && valid_attributes.include?(attr_name)
- if Whitelist::ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) &&
- attr_name.start_with?('data-')
- # Arbitrary data attributes are allowed. Verify that the attribute
- # is a valid data attribute.
- attr.unlink unless attr_name =~ DATA_ATTR_PATTERN
- else
- attr.unlink
- end
+ return
+ end
+
+ valid_attributes = Whitelist::ALLOWED_ATTRIBUTES[node.name]
+ return unless valid_attributes
+
+ node.attribute_nodes.each do |attr|
+ attr_name = attribute_name_with_namespace(attr)
+
+ if valid_attributes.include?(attr_name)
+ attr.unlink if unsafe_href?(attr)
+ else
+ # Arbitrary data attributes are allowed.
+ unless allows_data_attribute?(node) && data_attribute?(attr)
+ attr.unlink
end
end
end
end
+
+ def attribute_name_with_namespace(attr)
+ if attr.namespace
+ "#{attr.namespace.prefix}:#{attr.name}"
+ else
+ attr.name
+ end
+ end
+
+ def allows_data_attribute?(node)
+ Whitelist::ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name)
+ end
+
+ def unsafe_href?(attr)
+ attribute_name_with_namespace(attr) == 'xlink:href' && !attr.value.start_with?('#')
+ end
+
+ def data_attribute?(attr)
+ attr.name.start_with?('data-') && attr.name =~ DATA_ATTR_PATTERN && attr.namespace.nil?
+ end
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 56af739b1ef..388f84dbe0e 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -21,27 +21,29 @@ module Gitlab
[
SEND_DATA_HEADER,
- "git-blob:#{encode(params)}",
+ "git-blob:#{encode(params)}"
]
end
- def send_git_archive(project, ref, format)
+ def send_git_archive(repository, ref:, format:)
format ||= 'tar.gz'
format.downcase!
- params = project.repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
+ params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
raise "Repository or ref not found" if params.empty?
[
SEND_DATA_HEADER,
- "git-archive:#{encode(params)}",
+ "git-archive:#{encode(params)}"
]
end
- def send_git_diff(repository, from, to)
+ def send_git_diff(repository, diff_refs)
+ from, to = diff_refs
+
params = {
- 'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => from,
- 'ShaTo' => to
+ 'RepoPath' => repository.path_to_repo,
+ 'ShaFrom' => from.sha,
+ 'ShaTo' => to.sha
}
[
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index d6fb1a34e8c..7e71a030901 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -16,10 +16,10 @@ retry() {
}
if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
- mkdir -p vendor
+ mkdir -p vendor/apt
# Install phantomjs package
- pushd vendor
+ pushd vendor/apt
if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then
wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
fi
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 78be7e3dc35..cbaa3e0b7b2 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -105,6 +105,15 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to eq [issue]
end
+ it 'should not list confidential issues for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
it 'should list confidential issues for author' do
sign_in(author)
get_issues
@@ -148,7 +157,7 @@ describe Projects::IssuesController do
shared_examples_for 'restricted action' do |http_status|
it 'returns 404 for guests' do
- sign_out :user
+ sign_out(:user)
go(id: unescaped_parameter_value.to_param)
expect(response).to have_http_status :not_found
@@ -161,6 +170,14 @@ describe Projects::IssuesController do
expect(response).to have_http_status :not_found
end
+ it 'returns 404 for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
it "returns #{http_status[:success]} for author" do
sign_in(author)
go(id: unescaped_parameter_value.to_param)
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 1301574f489..4b408c03703 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -91,7 +91,7 @@ describe Projects::MergeRequestsController do
id: merge_request.iid,
format: :diff)
- expect(response.headers['Gitlab-Workhorse-Send-Data']).to start_with("git-diff:")
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index fb29274c687..33c35161da3 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -17,6 +17,7 @@ describe Projects::RawController do
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).
to eq("inline")
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
end
end
@@ -31,6 +32,7 @@ describe Projects::RawController do
expect(response.status).to eq(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 0ddbec9eac2..aad62cf20e3 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -20,10 +20,11 @@ describe Projects::RepositoriesController do
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 "uses Gitlab::Workhorse" do
get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
context "when the service raises an error" do
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index da8d97c9f82..5c8ddbebf0d 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -67,9 +67,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
}
)
-
- project.issues_tracker = 'redmine'
- project.issues_tracker_id = 'project_name_in_redmine'
end
end
@@ -84,9 +81,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
}
)
-
- project.issues_tracker = 'jira'
- project.issues_tracker_id = 'project_name_in_jira'
end
end
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index b710cb3c72f..4dd9548cfc5 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -5,8 +5,6 @@ describe "Dashboard Issues Feed", feature: true do
let!(:user) { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }
- let!(:issue1) { create(:issue, author: user, assignee: user, project: project1) }
- let!(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
before do
project1.team << [user, :master]
@@ -14,16 +12,51 @@ describe "Dashboard Issues Feed", feature: true do
end
describe "atom feed" do
- it "should render atom feed via private token" do
+ it "renders atom feed via private token" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
- expect(response_headers['Content-Type']).
- to have_content('application/atom+xml')
+ expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
- expect(body).to have_selector('author email', text: issue1.author_email)
- expect(body).to have_selector('entry summary', text: issue1.title)
- expect(body).to have_selector('author email', text: issue2.author_email)
- expect(body).to have_selector('entry summary', text: issue2.title)
+ end
+
+ context "issue with basic fields" do
+ let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+
+ it "renders issue fields" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue2.author_email)
+ expect(entry).to have_selector('assignee email', text: issue2.author_email)
+ expect(entry).not_to have_selector('labels')
+ expect(entry).not_to have_selector('milestone')
+ expect(entry).to have_selector('description', text: issue2.description)
+ end
+ end
+
+ context "issue with label and milestone" do
+ let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
+ let!(:label1) { create(:label, project: project1, title: 'label1') }
+ let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+
+ before do
+ issue1.labels << label1
+ end
+
+ it "renders issue label and milestone info" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue1.author_email)
+ expect(entry).to have_selector('assignee email', text: issue1.author_email)
+ expect(entry).to have_selector('labels label', text: label1.title)
+ expect(entry).to have_selector('milestone', text: milestone1.title)
+ expect(entry).not_to have_selector('description')
+ end
end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index df221ab1f3b..b8ecc356b4d 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -93,9 +93,7 @@ describe "Builds" do
end
it 'has button to download artifacts' do
- page.within('.artifacts') do
- expect(page).to have_content 'Download'
- end
+ expect(page).to have_content 'Download'
end
end
@@ -107,9 +105,7 @@ describe "Builds" do
end
it do
- page.within('.build-controls') do
- expect(page).to have_link 'Raw'
- end
+ expect(page).to have_link 'Raw'
end
end
end
@@ -165,15 +161,10 @@ describe "Builds" do
end
describe "GET /:project/builds/:id/download" do
- context "Build from project" do
- before do
- @build.update_attributes(artifacts_file: artifacts_file)
- visit namespace_project_build_path(@project.namespace, @project, @build)
- page.within('.artifacts') { click_link 'Download' }
- end
-
- it { expect(page.status_code).to eq(200) }
- it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file)
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link 'Download'
end
context "Build from other project" do
@@ -193,7 +184,7 @@ describe "Builds" do
@build.run!
@build.trace = 'BUILD TRACE'
visit namespace_project_build_path(@project.namespace, @project, @build)
- page.within('.build-controls') { click_link 'Raw' }
+ page.within('.js-build-sidebar') { click_link 'Raw' }
end
it 'sends the right headers' do
diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb
index c58b87281a3..0fbc2062e39 100644
--- a/spec/features/issues/bulk_assigment_labels_spec.rb
+++ b/spec/features/issues/bulk_assigment_labels_spec.rb
@@ -83,6 +83,23 @@ feature 'Issues > Labels bulk assignment', feature: true do
end
end
+ context 'can assign a label to all issues when label is present' do
+ before do
+ issue2.labels << bug
+ issue2.labels << feature
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check 'check_all_issues'
+ open_labels_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'bug'
+ end
+ end
+
context 'can bulk un-assign' do
context 'all labels to all issues' do
before do
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 0ec8b6b180a..16c619c9288 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -199,4 +199,19 @@ feature 'Issue filtering by Labels', feature: true do
end
end
end
+
+ context 'dropdown filtering', js: true do
+ it 'should filter by label name' do
+ page.within '.labels-filter' do
+ click_button 'Label'
+ wait_for_ajax
+ fill_in 'label-name', with: 'bug'
+
+ page.within '.dropdown-content' do
+ expect(page).not_to have_content 'enhancement'
+ expect(page).to have_content 'bug'
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 7efbaaa048c..1f0594e6b02 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -294,40 +294,4 @@ describe 'Filter issues', feature: true do
end
end
end
-
- describe 'filter by any author', js: true do
- before do
- user2 = create(:user, name: "tester")
- create(:issue, project: project, author: user)
- create(:issue, project: project, author: user2)
-
- visit namespace_project_issues_path(project.namespace, project)
- end
-
- it 'should show filter by any author link' do
- click_button "Author"
- fill_in "Search authors", with: "tester"
-
- page.within ".dropdown-menu-author" do
- expect(page).to have_content "tester"
- end
- end
-
- it 'should show filter issues by any author' do
- page.within '.issues-list' do
- expect(page).to have_selector ".issue", count: 2
- end
-
- click_button "Author"
- fill_in "Search authors", with: "tester"
-
- page.within ".dropdown-menu-author" do
- click_link "tester"
- end
-
- page.within '.issues-list' do
- expect(page).to have_selector ".issue", count: 1
- end
- end
- end
end
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
new file mode 100644
index 00000000000..65e9185ec24
--- /dev/null
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+feature 'Only allow merge requests to be merged if the build succeeds', feature: true do
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project) }
+
+ before do
+ login_as merge_request.author
+
+ project.team << [merge_request.author, :master]
+ end
+
+ context 'project does not have CI enabled' do
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when project has CI enabled' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
+
+ context 'when merge requests can only be merged if the build succeeds' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, true)
+ end
+
+ context 'when CI is running' do
+ before { pipeline.update_column(:status, :running) }
+
+ it 'does not allow to merge immediately' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Build Succeeds'
+ expect(page).not_to have_button 'Select Merge Moment'
+ end
+ end
+
+ context 'when CI failed' do
+ before { pipeline.update_column(:status, :failed) }
+
+ it 'does not allow MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).not_to have_button 'Accept Merge Request'
+ expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ end
+ end
+
+ context 'when CI succeeded' do
+ before { pipeline.update_column(:status, :success) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+
+ context 'when merge requests can be merged when the build failed' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, false)
+ end
+
+ context 'when CI is running' do
+ before { pipeline.update_column(:status, :running) }
+
+ it 'allows MR to be merged immediately', js: true do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Build Succeeds'
+
+ click_button 'Select Merge Moment'
+ expect(page).to have_content 'Merge Immediately'
+ end
+ end
+
+ context 'when CI failed' do
+ before { pipeline.update_column(:status, :failed) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when CI succeeded' do
+ before { pipeline.update_column(:status, :success) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+ end
+
+ def visit_merge_request(merge_request)
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 8f645438cff..787bf42d048 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -54,7 +54,7 @@ describe 'Profile > Preferences', feature: true do
end
end
- describe 'User changes their default dashboard' do
+ describe 'User changes their default dashboard', js: true do
it 'creates a flash message' do
select 'Starred Projects', from: 'user_dashboard'
click_button 'Save'
@@ -66,8 +66,10 @@ describe 'Profile > Preferences', feature: true do
select 'Starred Projects', from: 'user_dashboard'
click_button 'Save'
- click_link 'Dashboard'
- expect(page.current_path).to eq starred_dashboard_projects_path
+ allowing_for_delay do
+ find('#logo').click
+ expect(page.current_path).to eq starred_dashboard_projects_path
+ end
click_link 'Your Projects'
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 0559b02f321..f88c0616b52 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -16,6 +16,7 @@ describe 'Cherry-pick Commits' do
it do
visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
+ expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index eae61a54dfc..831ae7fb69c 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -7,10 +7,7 @@ describe IssuesHelper do
describe "url_for_project_issues" do
let(:project_url) { ext_project.external_issue_tracker.project_url }
- let(:ext_expected) do
- project_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project]) }
it "should return internal path if used internal tracker" do
@@ -56,11 +53,7 @@ describe IssuesHelper do
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
- let(:ext_expected) do
- issues_url.gsub(':id', issue.iid.to_s)
- .gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
it "should return internal path if used internal tracker" do
@@ -106,10 +99,7 @@ describe IssuesHelper do
describe 'url_for_new_issue' do
let(:issues_url) { ext_project.external_issue_tracker.new_issue_url }
- let(:ext_expected) do
- issues_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) }
it "should return internal path if used internal tracker" do
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index 697d10bbf70..f181125156b 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -69,6 +69,18 @@ describe Banzai::Filter::RedactorFilter, lib: true do
expect(doc.css('a').length).to eq 0
end
+ it 'removes references for project members with guest role' do
+ member = create(:user)
+ project = create(:empty_project, :public)
+ project.team << [member, :guest]
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
+ doc = filter(link, current_user: member)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
it 'allows references for author' do
author = create(:user)
project = create(:empty_project, :public)
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index ea4ab2c852e..72bc6a0b704 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -52,8 +52,8 @@ describe Banzai::Pipeline::WikiPipeline do
end
describe "Links" do
- let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
- let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
+ let(:namespace) { create(:namespace, name: "wiki_link_ns") }
+ let(:project) { create(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 7375539cf17..304290d6608 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -501,6 +501,7 @@ module Ci
})
config_processor = GitlabCiYamlProcessor.new(config, path)
+
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
@@ -601,6 +602,23 @@ module Ci
allow_failure: false
})
end
+
+ %w[on_success on_failure always].each do |when_state|
+ it "returns artifacts for when #{when_state} defined" do
+ config = YAML.dump({
+ rspec: {
+ script: "rspec",
+ artifacts: { paths: ["logs/", "binaries/"], when: when_state }
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ builds = config_processor.builds_for_stage_and_ref("test", "master")
+ expect(builds.size).to eq(1)
+ expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
+ end
+ end
end
describe "Dependencies" do
@@ -967,6 +985,13 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
end
+ it "returns errors if job artifacts:when is not an a predefined value" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always")
+ 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
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index a814ad2a4e7..7bec1367156 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Auth, lib: true do
let(:gl_auth) { described_class }
- describe 'find' do
+ describe 'find_for_git_client' do
it 'recognizes CI' do
token = '123'
project = create(:empty_project)
@@ -11,7 +11,7 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
- expect(gl_auth.find('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
+ expect(gl_auth.find_for_git_client('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
end
it 'recognizes master passwords' do
@@ -19,7 +19,7 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
- expect(gl_auth.find(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
end
it 'recognizes OAuth tokens' do
@@ -29,7 +29,7 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
- expect(gl_auth.find("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
end
it 'returns double nil for invalid credentials' do
@@ -37,11 +37,11 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
- expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
end
end
- describe 'find_in_gitlab_or_ldap' do
+ describe 'find_with_user_password' do
let!(:user) do
create(:user,
username: username,
@@ -52,25 +52,25 @@ describe Gitlab::Auth, lib: true do
let(:password) { 'my-secret' }
it "should find user by valid login/password" do
- expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).to eql user
end
it 'should find user by valid email/password with case-insensitive email' do
- expect(gl_auth.find_in_gitlab_or_ldap(user.email.upcase, password)).to eql user
+ expect(gl_auth.find_with_user_password(user.email.upcase, password)).to eql user
end
it 'should find user by valid username/password with case-insensitive username' do
- expect(gl_auth.find_in_gitlab_or_ldap(username.upcase, password)).to eql user
+ expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user
end
it "should not find user with invalid password" do
password = 'wrong'
- expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
it "should not find user with invalid login" do
user = 'wrong'
- expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
context "with ldap enabled" do
@@ -81,13 +81,13 @@ describe Gitlab::Auth, lib: true do
it "tries to autheticate with db before ldap" do
expect(Gitlab::LDAP::Authentication).not_to receive(:login)
- gl_auth.find_in_gitlab_or_ldap(username, password)
+ gl_auth.find_with_user_password(username, password)
end
it "uses ldap as fallback to for authentication" do
expect(Gitlab::LDAP::Authentication).to receive(:login)
- gl_auth.find_in_gitlab_or_ldap('ldap_user', 'password')
+ gl_auth.find_with_user_password('ldap_user', 'password')
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 83ddabe6b0b..1ec539066a7 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -120,6 +120,19 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
model.add_column_with_default(:projects, :foo, :integer, default: 10)
end.to raise_error(RuntimeError)
end
+
+ it 'removes the added column whenever changing a column NULL constraint fails' do
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false).
+ and_raise(RuntimeError)
+
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
end
context 'inside a transaction' do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index db0ff95b4f5..270b89972d7 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -43,6 +43,18 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ results = described_class.new(member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 1
+ end
+
it 'should list project confidential issues for author' do
results = described_class.new(author, project, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb
new file mode 100644
index 00000000000..030c2063ab2
--- /dev/null
+++ b/spec/lib/gitlab/sanitizers/svg_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Sanitizers::SVG do
+ let(:scrubber) { Gitlab::Sanitizers::SVG::Scrubber.new }
+ let(:namespace) { double(Nokogiri::XML::Namespace, prefix: 'xlink', href: 'http://www.w3.org/1999/xlink') }
+ let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: '#awesome_id') }
+
+ describe '.clean' do
+ let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
+ let(:data) { open(input_svg_path).read }
+ let(:sanitized_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
+ let(:sanitized) { open(sanitized_svg_path).read }
+
+ it 'delegates sanitization to scrubber' do
+ expect_any_instance_of(Gitlab::Sanitizers::SVG::Scrubber).to receive(:scrub).at_least(:once)
+ described_class.clean(data)
+ end
+
+ it 'returns sanitized data' do
+ expect(described_class.clean(data)).to eq(sanitized)
+ end
+ end
+
+ context 'scrubber' do
+ describe '#scrub' do
+ let(:invalid_element) { double(Nokogiri::XML::Node, name: 'invalid', value: 'invalid') }
+ let(:invalid_attribute) { double(Nokogiri::XML::Attr, name: 'invalid', namespace: nil) }
+ let(:valid_element) { double(Nokogiri::XML::Node, name: 'use') }
+
+ it 'removes an invalid element' do
+ expect(invalid_element).to receive(:unlink)
+
+ scrubber.scrub(invalid_element)
+ end
+
+ it 'removes an invalid attribute' do
+ allow(valid_element).to receive(:attribute_nodes) { [invalid_attribute] }
+ expect(invalid_attribute).to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+
+ it 'accepts valid element' do
+ allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] }
+ expect(valid_element).not_to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+
+ it 'accepts valid namespaced attributes' do
+ allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] }
+ expect(namespaced_attr).not_to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+ end
+
+ describe '#attribute_name_with_namespace' do
+ it 'returns name with prefix when attribute is namespaced' do
+ expect(scrubber.attribute_name_with_namespace(namespaced_attr)).to eq('xlink:href')
+ end
+ end
+
+ describe '#unsafe_href?' do
+ let(:unsafe_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: 'http://evilsite.example.com/random.svg') }
+
+ it 'returns true if href attribute is an external url' do
+ expect(scrubber.unsafe_href?(unsafe_attr)).to be_truthy
+ end
+
+ it 'returns false if href atttribute is an internal reference' do
+ expect(scrubber.unsafe_href?(namespaced_attr)).to be_falsey
+ end
+ end
+
+ describe '#data_attribute?' do
+ let(:data_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: nil, value: 'gitlab is awesome') }
+ let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: namespace, value: 'gitlab is awesome') }
+ let(:other_attr) { double(Nokogiri::XML::Attr, name: 'something', namespace: nil, value: 'content') }
+
+ it 'returns true if is a valid data attribute' do
+ expect(scrubber.data_attribute?(data_attr)).to be_truthy
+ end
+
+ it 'returns false if attribute is namespaced' do
+ expect(scrubber.data_attribute?(namespaced_attr)).to be_falsey
+ end
+
+ it 'returns false if not a data attribute' do
+ expect(scrubber.data_attribute?(other_attr)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index f4afe597e8d..1bb444bf34f 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -86,6 +86,22 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 1
end
+ it 'should not list confidential issues for project members with guest role' do
+ project_1.team << [member, :guest]
+ project_2.team << [member, :guest]
+
+ results = described_class.new(member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 1
+ end
+
it 'should list confidential issues for author' do
results = described_class.new(author, limit_projects, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d940bf05061..c5c1402e8fc 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Workhorse, lib: true do
end
it "raises an error" do
- expect { subject.send_git_archive(project, "master", "zip") }.to raise_error(RuntimeError)
+ expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
end
end
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 7660ea2659c..2beb6cc598d 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -219,7 +219,7 @@ describe Ci::Build, models: true do
context 'and trigger variables' do
let(:trigger) { create(:ci_trigger, project: project) }
- let(:trigger_request) { create(:ci_trigger_request_with_variables, commit: pipeline, trigger: trigger) }
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
let(:trigger_variables) do
[
{ key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 47c3be673c5..7e9ab8940cf 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -5,6 +5,7 @@ describe Milestone, 'Milestoneish' do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -21,6 +22,7 @@ describe Milestone, 'Milestoneish' do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
describe '#closed_items_count' do
@@ -28,6 +30,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.closed_items_count(non_member)).to eq 2
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.closed_items_count(guest)).to eq 2
+ end
+
it 'should count confidential issues for author' do
expect(milestone.closed_items_count(author)).to eq 4
end
@@ -50,6 +56,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.total_items_count(non_member)).to eq 4
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.total_items_count(guest)).to eq 4
+ end
+
it 'should count confidential issues for author' do
expect(milestone.total_items_count(author)).to eq 7
end
@@ -85,6 +95,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.percent_complete(non_member)).to eq 50
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.percent_complete(guest)).to eq 50
+ end
+
it 'should count confidential issues for author' do
expect(milestone.percent_complete(author)).to eq 57
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index b0e76fec693..166a1dc4ddb 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -50,6 +50,7 @@ describe Event, models: true do
let(:project) { create(:empty_project, :public) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
@@ -61,6 +62,7 @@ describe Event, models: true do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
context 'issue event' do
@@ -71,6 +73,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -81,6 +84,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
@@ -93,6 +97,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -103,6 +108,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 1b7cbc3efda..3b199f4d98d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -455,4 +455,157 @@ describe MergeRequest, models: true do
expect(user2.assigned_open_merge_request_count).to eq(1)
end
end
+
+ describe '#check_if_can_be_merged' do
+ let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) }
+
+ subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
+
+ context 'when it is not broken and has no conflicts' do
+ it 'is marked as mergeable' do
+ allow(subject).to receive(:broken?) { false }
+ allow(project).to receive_message_chain(:repository, :can_be_merged?) { true }
+
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
+ end
+ end
+
+ context 'when broken' do
+ before { allow(subject).to receive(:broken?) { true } }
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
+ end
+
+ context 'when it has conflicts' do
+ before do
+ allow(subject).to receive(:broken?) { false }
+ allow(project).to receive_message_chain(:repository, :can_be_merged?) { false }
+ end
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
+ end
+ end
+
+ describe '#mergeable?' do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ it 'returns false if #mergeable_state? is false' do
+ expect(subject).to receive(:mergeable_state?) { false }
+
+ expect(subject.mergeable?).to be_falsey
+ end
+
+ it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
+ allow(subject).to receive(:mergeable_state?) { true }
+ expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:can_be_merged?) { true }
+
+ expect(subject.mergeable?).to be_truthy
+ end
+ end
+
+ describe '#mergeable_state?' do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ it 'checks if merge request can be merged' do
+ allow(subject).to receive(:mergeable_ci_state?) { true }
+ expect(subject).to receive(:check_if_can_be_merged)
+
+ subject.mergeable?
+ end
+
+ context 'when not open' do
+ before { subject.close }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when working in progress' do
+ before { subject.title = 'WIP MR' }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when broken' do
+ before { allow(subject).to receive(:broken?) { true } }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when failed' do
+ before { allow(subject).to receive(:broken?) { false } }
+
+ context 'when project settings restrict to merge only if build succeeds and build failed' do
+ before do
+ project.only_allow_merge_if_build_succeeds = true
+ allow(subject).to receive(:mergeable_ci_state?) { false }
+ end
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#mergeable_ci_state?' do
+ let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ subject { build(:merge_request, target_project: project) }
+
+ context 'when it is only allowed to merge when build is green' do
+ context 'and a failed pipeline is associated' do
+ before do
+ pipeline.statuses << create(:commit_status, status: 'failed', project: project)
+ allow(subject).to receive(:pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_falsey }
+ end
+
+ context 'when no pipeline is associated' do
+ before do
+ allow(subject).to receive(:pipeline) { nil }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+ end
+
+ context 'when merges are not restricted to green builds' do
+ subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) }
+
+ context 'and a failed pipeline is associated' do
+ before do
+ pipeline.statuses << create(:commit_status, status: 'failed', project: project)
+ allow(subject).to receive(:pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+
+ context 'when no pipeline is associated' do
+ before do
+ allow(subject).to receive(:pipeline) { nil }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index f15e96714b2..285ab19cfaf 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -162,16 +162,23 @@ describe Note, models: true do
end
context "confidential issues" do
- let(:user) { create :user }
- let(:confidential_issue) { create(:issue, :confidential, author: user) }
- let(:confidential_note) { create :note, note: "Random", noteable: confidential_issue, project: confidential_issue.project }
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
it "returns notes with matching content if user can see the issue" do
expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note])
end
it "does not return notes with matching content if user can not see the issue" do
- user = create :user
+ user = create(:user)
+ expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
+ end
+
+ it "does not return notes with matching content for project members with guest role" do
+ user = create(:user)
+ project.team << [user, :guest]
expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 295081e9da1..4e24e89b008 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe NotificationSetting, type: :model do
subject { NotificationSetting.new(source_id: 1, source_type: 'Project') }
it { is_expected.to validate_presence_of(:user) }
- it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:level) }
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) }
end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index e771f35811e..9ae461f8c2d 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -126,25 +126,25 @@ describe BambooService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a specific URL when response has no results' do
stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a build URL when bamboo_url has no trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
it 'returns a build URL when bamboo_url has a trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
end
@@ -192,9 +192,9 @@ describe BambooService, models: true do
end
end
- def service(bamboo_url: 'http://gitlab.com')
+ def service(bamboo_url: 'http://gitlab.com/bamboo')
described_class.create(
- project: build_stubbed(:empty_project),
+ project: create(:empty_project),
properties: {
bamboo_url: bamboo_url,
username: 'mic',
@@ -205,7 +205,7 @@ describe BambooService, models: true do
end
def stub_request(status: 200, body: nil, build_state: 'success')
- bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic'
+ bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}})
WebMock.stub_request(:get, bamboo_full_url).to_return(
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index ad24b895170..474715d24c3 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -126,19 +126,19 @@ describe TeamcityService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has no trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has a trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
end
@@ -180,9 +180,9 @@ describe TeamcityService, models: true do
end
end
- def service(teamcity_url: 'http://gitlab.com')
+ def service(teamcity_url: 'http://gitlab.com/teamcity')
described_class.create(
- project: build_stubbed(:empty_project),
+ project: create(:empty_project),
properties: {
teamcity_url: teamcity_url,
username: 'mic',
@@ -193,7 +193,7 @@ describe TeamcityService, models: true do
end
def stub_request(status: 200, body: nil, build_status: 'success')
- teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
+ teamcity_full_url = 'http://mic:password@gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
WebMock.stub_request(:get, teamcity_full_url).to_return(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 553556ed326..de8815f5a38 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -53,7 +53,6 @@ describe Project, models: true do
it { is_expected.to validate_length_of(:path).is_within(0..255) }
it { is_expected.to validate_length_of(:description).is_within(0..2000) }
it { is_expected.to validate_presence_of(:creator) }
- it { is_expected.to validate_length_of(:issues_tracker_id).is_within(0..255) }
it { is_expected.to validate_presence_of(:namespace) }
it 'should not allow new projects beyond user limits' do
@@ -258,24 +257,66 @@ describe Project, models: true do
end
end
- describe :can_have_issues_tracker_id? do
+ describe :external_issue_tracker do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
- it 'should be true for projects with external issues tracker if issues enabled' do
- expect(ext_project.can_have_issues_tracker_id?).to be_truthy
+ context 'on existing projects with no value for has_external_issue_tracker' do
+ before(:each) do
+ project.update_column(:has_external_issue_tracker, nil)
+ ext_project.update_column(:has_external_issue_tracker, nil)
+ end
+
+ it 'updates the has_external_issue_tracker boolean' do
+ expect do
+ project.external_issue_tracker
+ end.to change { project.reload.has_external_issue_tracker }.to(false)
+
+ expect do
+ ext_project.external_issue_tracker
+ end.to change { ext_project.reload.has_external_issue_tracker }.to(true)
+ end
+ end
+
+ it 'returns nil and does not query services when there is no external issue tracker' do
+ project.build_missing_services
+ project.reload
+
+ expect(project).not_to receive(:services)
+
+ expect(project.external_issue_tracker).to eq(nil)
+ end
+
+ it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
+ ext_project.reload # Factory returns a project with changed attributes
+ ext_project.build_missing_services
+ ext_project.reload
+
+ expect(ext_project).to receive(:services).once.and_call_original
+
+ 2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
end
+ end
+
+ describe :cache_has_external_issue_tracker do
+ let(:project) { create(:project) }
- it 'should be false for projects with internal issue tracker if issues enabled' do
- expect(project.can_have_issues_tracker_id?).to be_falsey
+ it 'stores true if there is any external_issue_tracker' do
+ services = double(:service, external_issue_trackers: [RedmineService.new])
+ expect(project).to receive(:services).and_return(services)
+
+ expect do
+ project.cache_has_external_issue_tracker
+ end.to change { project.has_external_issue_tracker}.to(true)
end
- it 'should be always false if issues disabled' do
- project.issues_enabled = false
- ext_project.issues_enabled = false
+ it 'stores false if there is no external_issue_tracker' do
+ services = double(:service, external_issue_trackers: [])
+ expect(project).to receive(:services).and_return(services)
- expect(project.can_have_issues_tracker_id?).to be_falsey
- expect(ext_project.can_have_issues_tracker_id?).to be_falsey
+ expect do
+ project.cache_has_external_issue_tracker
+ end.to change { project.has_external_issue_tracker}.to(false)
end
end
@@ -859,4 +900,37 @@ describe Project, models: true do
it { is_expected.to be_falsey }
end
end
+
+ describe '.where_paths_in' do
+ context 'without any paths' do
+ it 'returns an empty relation' do
+ expect(Project.where_paths_in([])).to eq([])
+ end
+ end
+
+ context 'without any valid paths' do
+ it 'returns an empty relation' do
+ expect(Project.where_paths_in(%w[foo])).to eq([])
+ end
+ end
+
+ context 'with valid paths' do
+ let!(:project1) { create(:project) }
+ let!(:project2) { create(:project) }
+
+ it 'returns the projects matching the paths' do
+ projects = Project.where_paths_in([project1.path_with_namespace,
+ project2.path_with_namespace])
+
+ expect(projects).to contain_exactly(project1, project2)
+ end
+
+ it 'returns projects regardless of the casing of paths' do
+ projects = Project.where_paths_in([project1.path_with_namespace.upcase,
+ project2.path_with_namespace.upcase])
+
+ expect(projects).to contain_exactly(project1, project2)
+ end
+ end
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index bacb17a8883..8bebd6a9447 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -29,6 +29,9 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::REPORTER)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::REPORTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
@@ -64,6 +67,9 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::MASTER)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::MASTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 8592e112c50..2f000dbc01a 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -204,4 +204,37 @@ describe Service, models: true do
expect(service.bamboo_url_was).to be_nil
end
end
+
+ describe "callbacks" do
+ let(:project) { create(:project) }
+ let!(:service) do
+ RedmineService.new(
+ project: project,
+ active: true,
+ properties: {
+ project_url: 'http://redmine/projects/project_name_in_redmine',
+ issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id",
+ new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new'
+ }
+ )
+ end
+
+ describe "on create" do
+ it "updates the has_external_issue_tracker boolean" do
+ expect do
+ service.save!
+ end.to change { service.project.has_external_issue_tracker }.from(nil).to(true)
+ end
+ end
+
+ describe "on update" do
+ it "updates the has_external_issue_tracker boolean" do
+ service.save!
+
+ expect do
+ service.update_attributes(active: false)
+ end.to change { service.project.has_external_issue_tracker }.from(true).to(false)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index bb926172593..59e557c5b2a 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -5,6 +5,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
@@ -41,7 +42,10 @@ describe API::API, api: true do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
- before { project.team << [user, :reporter] }
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
describe "GET /issues" do
context "when unauthenticated" do
@@ -144,6 +148,14 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
+ it 'should return project issues without confidential issues for project members with guest role' do
+ get api("#{base_url}/issues", guest)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
it 'should return project confidential issues for author' do
get api("#{base_url}/issues", author)
expect(response.status).to eq(200)
@@ -278,6 +290,11 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+ it "should return 404 for project members with guest role" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+ expect(response.status).to eq(404)
+ end
+
it "should return confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
expect(response.status).to eq(200)
@@ -413,6 +430,12 @@ describe API::API, api: true do
expect(response.status).to eq(403)
end
+ it "should return 403 for project members with guest role" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+ expect(response.status).to eq(403)
+ end
+
it "should update a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
title: 'updated title'
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 9da69a913a8..5896b93603f 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -419,6 +419,15 @@ describe API::API, api: true do
expect(json_response['message']).to eq('405 Method Not Allowed')
end
+ it 'returns 405 if the build failed for a merge request that requires success' do
+ allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response.status).to eq(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
it "should return 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
@@ -554,6 +563,21 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
+
+ it 'handles external issues' do
+ jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
+ issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
+ merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
+
+ get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 241995041bb..0154d1c62cc 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -146,6 +146,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+
before do
public_project.team << [user, :developer]
milestone.issues << issue << confidential_issue
@@ -160,6 +161,18 @@ describe API::API, api: true do
expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
end
+ it 'does not return confidential issues to team members with guest role' do
+ member = create(:user)
+ project.team << [member, :guest]
+
+ get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ end
+
it 'does not return confidential issues to regular users' do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 88271642532..e8508f8f950 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -85,7 +85,7 @@ describe Ci::API::API do
trigger = FactoryGirl.create(:ci_trigger, project: project)
pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: pipeline, trigger: trigger)
+ trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger)
pipeline.create_builds(nil, trigger_request)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index c995993a853..d2d4a9eca18 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -44,7 +44,7 @@ describe JwtController do
let(:user) { create(:user) }
let(:headers) { { authorization: credentials('user', 'password') } }
- before { expect(Gitlab::Auth).to receive(:find_in_gitlab_or_ldap).with('user', 'password').and_return(user) }
+ before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
subject! { get '/jwt/auth', parameters, headers }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index cef5e0d8659..e871a103d42 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -72,6 +72,7 @@ describe NotificationService, services: true do
should_not_email(@u_disabled)
should_not_email(@unsubscriber)
should_not_email(@u_outsider_mentioned)
+ should_not_email(@u_lazy_participant)
end
it 'filters out "mentioned in" notes' do
@@ -80,6 +81,20 @@ describe NotificationService, services: true do
expect(Notify).not_to receive(:note_issue_email)
notification.new_note(mentioned_note)
end
+
+ context 'participating' do
+ context 'by note' do
+ before do
+ ActionMailer::Base.deliveries.clear
+ note.author = @u_lazy_participant
+ note.save
+ notification.new_note(note)
+ end
+
+
+ it { should_not_email(@u_lazy_participant) }
+ end
+ end
end
describe 'new note on issue in project that belongs to a group' do
@@ -106,6 +121,7 @@ describe NotificationService, services: true do
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
end
end
@@ -116,12 +132,14 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
it 'filters out users that can not read the issue' do
project.team << [member, :developer]
+ project.team << [guest, :guest]
expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
@@ -130,6 +148,7 @@ describe NotificationService, services: true do
notification.new_note(note)
should_not_email(non_member)
+ should_not_email(guest)
should_email(author)
should_email(assignee)
should_email(member)
@@ -235,6 +254,7 @@ describe NotificationService, services: true do
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
@@ -248,10 +268,11 @@ describe NotificationService, services: true do
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
- @u_committer.update_attributes(notification_level: :mention)
+ @u_committer = create_global_setting_for(@u_committer, :mention)
notification.new_note(note)
should_not_email(@u_committer)
end
@@ -280,10 +301,11 @@ describe NotificationService, services: true do
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
- issue.assignee.update_attributes(notification_level: :mention)
+ create_global_setting_for(issue.assignee, :mention)
notification.new_issue(issue, @u_disabled)
should_not_email(issue.assignee)
@@ -303,17 +325,20 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label = create(:label, issues: [confidential_issue])
label.toggle_subscription(non_member)
label.toggle_subscription(author)
label.toggle_subscription(assignee)
label.toggle_subscription(member)
+ label.toggle_subscription(guest)
label.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
@@ -322,6 +347,7 @@ describe NotificationService, services: true do
should_not_email(non_member)
should_not_email(author)
+ should_not_email(guest)
should_email(assignee)
should_email(member)
should_email(admin)
@@ -341,6 +367,7 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails previous assignee even if he has the "on mention" notif level' do
@@ -356,6 +383,7 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails new assignee even if he has the "on mention" notif level' do
@@ -371,6 +399,7 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails new assignee' do
@@ -386,6 +415,7 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'does not email new assignee if they are the current user' do
@@ -401,6 +431,35 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.reassigned_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reassigned_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.reassigned_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -438,6 +497,7 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
let!(:label_1) { create(:label, issues: [confidential_issue]) }
@@ -445,11 +505,13 @@ describe NotificationService, services: true do
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label_2.toggle_subscription(non_member)
label_2.toggle_subscription(author)
label_2.toggle_subscription(assignee)
label_2.toggle_subscription(member)
+ label_2.toggle_subscription(guest)
label_2.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
@@ -457,6 +519,7 @@ describe NotificationService, services: true do
notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
should_not_email(non_member)
+ should_not_email(guest)
should_email(author)
should_email(assignee)
should_email(member)
@@ -479,6 +542,35 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.close_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.close_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.close_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -495,6 +587,35 @@ describe NotificationService, services: true do
should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.reopen_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reopen_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.reopen_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
end
@@ -520,6 +641,7 @@ describe NotificationService, services: true do
should_email(@u_guest_watcher)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it "emails subscribers of the merge request's labels" do
@@ -530,6 +652,36 @@ describe NotificationService, services: true do
should_email(subscriber)
end
+
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.new_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.new_merge_request(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.new_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_not_email(@u_lazy_participant) }
+ end
+ end
end
describe '#reassigned_merge_request' do
@@ -545,6 +697,36 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.reassigned_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reassigned_merge_request(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.reassigned_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -572,6 +754,7 @@ describe NotificationService, services: true do
should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
@@ -590,6 +773,36 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.close_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.close_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.close_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -606,6 +819,36 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.merge_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.merge_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.merge_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -622,6 +865,36 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.reopen_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reopen_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.reopen_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
end
@@ -640,6 +913,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participating)
+ should_email(@u_lazy_participant)
should_not_email(@u_guest_watcher)
should_not_email(@u_disabled)
end
@@ -647,14 +921,19 @@ describe NotificationService, services: true do
end
def build_team(project)
- @u_watcher = create(:user, notification_level: :watch)
- @u_participating = create(:user, notification_level: :participating)
- @u_participant_mentioned = create(:user, username: 'participant', notification_level: :participating)
- @u_disabled = create(:user, notification_level: :disabled)
- @u_mentioned = create(:user, username: 'mention', notification_level: :mention)
- @u_committer = create(:user, username: 'committer')
- @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating)
- @u_outsider_mentioned = create(:user, username: 'outsider')
+ @u_watcher = create_global_setting_for(create(:user), :watch)
+ @u_participating = create_global_setting_for(create(:user), :participating)
+ @u_participant_mentioned = create_global_setting_for(create(:user, username: 'participant'), :participating)
+ @u_disabled = create_global_setting_for(create(:user), :disabled)
+ @u_mentioned = create_global_setting_for(create(:user, username: 'mention'), :mention)
+ @u_committer = create(:user, username: 'committer')
+ @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
+ @u_outsider_mentioned = create(:user, username: 'outsider')
+
+ # User to be participant by default
+ # This user does not contain any record in notification settings table
+ # It should be treated with a :participating notification_level
+ @u_lazy_participant = create(:user, username: 'lazy-participant')
create_guest_watcher
@@ -665,6 +944,15 @@ describe NotificationService, services: true do
project.team << [@u_mentioned, :master]
project.team << [@u_committer, :master]
project.team << [@u_not_mentioned, :master]
+ project.team << [@u_lazy_participant, :master]
+ end
+
+ def create_global_setting_for(user, level)
+ setting = user.global_notification_setting
+ setting.level = level
+ setting.save
+
+ user
end
def create_guest_watcher
@@ -677,8 +965,8 @@ describe NotificationService, services: true do
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
- @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: :participating)
- @watcher_and_subscriber = create(:user, notification_level: :watch)
+ @subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
+ @watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 6108c26a78b..0971fec2e9f 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -33,6 +33,18 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ autocomplete = described_class.new(project, non_member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
it 'should list project confidential issues for author' do
autocomplete = described_class.new(project, author)
issues = autocomplete.issues.map(&:iid)
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 489c920f19f..549a936b060 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -5,13 +5,15 @@ describe TodoService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') }
+ let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
+ project.team << [guest, :guest]
project.team << [author, :developer]
project.team << [member, :developer]
project.team << [john_doe, :developer]
@@ -41,18 +43,20 @@ describe TodoService, services: true do
service.new_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.new_issue(confidential_issue, john_doe)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
@@ -81,6 +85,7 @@ describe TodoService, services: true do
service.update_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
@@ -92,13 +97,14 @@ describe TodoService, services: true do
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.update_issue(confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
@@ -192,18 +198,20 @@ describe TodoService, services: true do
service.new_note(note, john_doe)
should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
- it 'does not create todo for non project members when leaving a note on a confidential issue' do
+ it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do
service.new_note(note_on_confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
@@ -245,6 +253,7 @@ describe TodoService, services: true do
service.new_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -256,6 +265,7 @@ describe TodoService, services: true do
service.update_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)