Commit 778d7266 authored by James Lopez's avatar James Lopez

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into feature/project-export

# Conflicts:
#	app/models/ci/pipeline.rb
parents 452c076a c369cc8b
...@@ -7,7 +7,8 @@ services: ...@@ -7,7 +7,8 @@ services:
cache: cache:
key: "ruby21" key: "ruby21"
paths: paths:
- vendor - vendor/apt
- vendor/ruby
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
...@@ -91,9 +92,7 @@ update-knapsack: ...@@ -91,9 +92,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
# retry failed tests 3 times
- retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
paths: paths:
- knapsack/ - knapsack/
......
...@@ -349,7 +349,7 @@ Style/MultilineArrayBraceLayout: ...@@ -349,7 +349,7 @@ Style/MultilineArrayBraceLayout:
# Avoid multi-line chains of blocks. # Avoid multi-line chains of blocks.
Style/MultilineBlockChain: Style/MultilineBlockChain:
Enabled: false Enabled: true
# Ensures newlines after multiline block do statements. # Ensures newlines after multiline block do statements.
Style/MultilineBlockLayout: Style/MultilineBlockLayout:
......
This diff is collapsed.
...@@ -210,6 +210,9 @@ gem 'mousetrap-rails', '~> 1.4.6' ...@@ -210,6 +210,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding # Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3' gem 'charlock_holmes', '~> 0.7.3'
# Parse duration
gem 'chronic_duration', '~> 0.10.6'
gem "sass-rails", '~> 5.0.0' gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0' gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2' gem "uglifier", '~> 2.7.2'
...@@ -224,7 +227,6 @@ gem 'gon', '~> 6.0.1' ...@@ -224,7 +227,6 @@ gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0' gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0' gem 'jquery-ui-rails', '~> 5.0.0'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.3.0' gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
...@@ -245,7 +247,7 @@ end ...@@ -245,7 +247,7 @@ end
group :development do group :development do
gem "foreman" gem "foreman"
gem 'brakeman', '~> 3.2.0', require: false gem 'brakeman', '~> 3.3.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'quiet_assets', '~> 1.0.2' gem 'quiet_assets', '~> 1.0.2'
......
...@@ -50,7 +50,7 @@ GEM ...@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.4) allocations (1.0.5)
arel (6.0.3) arel (6.0.3)
asana (0.4.0) asana (0.4.0)
faraday (~> 0.9) faraday (~> 0.9)
...@@ -97,16 +97,7 @@ GEM ...@@ -97,16 +97,7 @@ GEM
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.2.1) brakeman (3.3.2)
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)
browser (2.0.3) browser (2.0.3)
builder (3.2.2) builder (3.2.2)
bullet (5.0.0) bullet (5.0.0)
...@@ -133,6 +124,8 @@ GEM ...@@ -133,6 +124,8 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
cause (0.1) cause (0.1)
charlock_holmes (0.7.3) charlock_holmes (0.7.3)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
cliver (0.3.2) cliver (0.3.2)
coderay (1.1.0) coderay (1.1.0)
...@@ -284,7 +277,7 @@ GEM ...@@ -284,7 +277,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_emoji (0.3.1) gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1) gemojione (~> 2.2, >= 2.2.1)
gitlab_git (10.1.0) gitlab_git (10.1.3)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -338,7 +331,6 @@ GEM ...@@ -338,7 +331,6 @@ GEM
hashie (3.4.3) hashie (3.4.3)
health_check (1.5.1) health_check (1.5.1)
rails (>= 2.3.0) rails (>= 2.3.0)
highline (1.7.8)
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
...@@ -408,7 +400,7 @@ GEM ...@@ -408,7 +400,7 @@ GEM
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.7.0) mail_room (0.7.0)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.1) mime-types (2.99.2)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.7.0) minitest (5.7.0)
...@@ -424,6 +416,7 @@ GEM ...@@ -424,6 +416,7 @@ GEM
nokogiri (1.6.8) nokogiri (1.6.8)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7) pkg-config (~> 1.1.7)
numerizer (0.1.1)
oauth (0.4.7) oauth (0.4.7)
oauth2 (1.0.0) oauth2 (1.0.0)
faraday (>= 0.8, < 0.10) faraday (>= 0.8, < 0.10)
...@@ -563,7 +556,6 @@ GEM ...@@ -563,7 +556,6 @@ GEM
rainbow (2.1.0) rainbow (2.1.0)
raindrops (0.15.0) raindrops (0.15.0)
rake (10.5.0) rake (10.5.0)
raphael-rails (2.1.2)
rb-fsevent (0.9.6) rb-fsevent (0.9.6)
rb-inotify (0.9.5) rb-inotify (0.9.5)
ffi (>= 0.5.0) ffi (>= 0.5.0)
...@@ -642,10 +634,7 @@ GEM ...@@ -642,10 +634,7 @@ GEM
ruby-saml (1.1.2) ruby-saml (1.1.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
uuid (~> 2.3) uuid (~> 2.3)
ruby2ruby (2.3.0) ruby_parser (3.8.2)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
ruby_parser (3.8.1)
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
...@@ -655,7 +644,7 @@ GEM ...@@ -655,7 +644,7 @@ GEM
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.21) sass (3.4.22)
sass-rails (5.0.4) sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.0)
sass (~> 3.1) sass (~> 3.1)
...@@ -704,9 +693,6 @@ GEM ...@@ -704,9 +693,6 @@ GEM
tilt (>= 1.3, < 3) tilt (>= 1.3, < 3)
six (0.2.0) six (0.2.0)
slack-notifier (1.2.1) slack-notifier (1.2.1)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0) slop (3.6.0)
spinach (0.8.10) spinach (0.8.10)
colorize colorize
...@@ -747,10 +733,8 @@ GEM ...@@ -747,10 +733,8 @@ GEM
railties (>= 3.2.5, < 6) railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0) teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0) teaspoon (>= 1.0.0)
temple (0.7.6)
term-ansicolor (1.3.2) term-ansicolor (1.3.2)
tins (~> 1.0) tins (~> 1.0)
terminal-table (1.5.2)
test_after_commit (0.4.2) test_after_commit (0.4.2)
activerecord (>= 3.2) activerecord (>= 3.2)
thin (1.6.4) thin (1.6.4)
...@@ -759,7 +743,7 @@ GEM ...@@ -759,7 +743,7 @@ GEM
rack (~> 1.0) rack (~> 1.0)
thor (0.19.1) thor (0.19.1)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (2.0.2) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
tinder (1.10.1) tinder (1.10.1)
...@@ -848,7 +832,7 @@ DEPENDENCIES ...@@ -848,7 +832,7 @@ DEPENDENCIES
better_errors (~> 1.0.1) better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.2.0) brakeman (~> 3.3.0)
browser (~> 2.0.3) browser (~> 2.0.3)
bullet bullet
bundler-audit bundler-audit
...@@ -857,6 +841,7 @@ DEPENDENCIES ...@@ -857,6 +841,7 @@ DEPENDENCIES
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
connection_pool (~> 2.0) connection_pool (~> 2.0)
coveralls (~> 0.8.2) coveralls (~> 0.8.2)
...@@ -952,7 +937,6 @@ DEPENDENCIES ...@@ -952,7 +937,6 @@ DEPENDENCIES
rails (= 4.2.6) rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0) rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2)
rblineprof rblineprof
rdoc (~> 3.6) rdoc (~> 3.6)
recaptcha (~> 3.0) recaptcha (~> 3.0)
......
...@@ -42,10 +42,10 @@ class @LabelManager ...@@ -42,10 +42,10 @@ class @LabelManager
$from = @prioritizedLabels $from = @prioritizedLabels
if $from.find('li').length is 1 if $from.find('li').length is 1
$from.find('.empty-message').show() $from.find('.empty-message').removeClass('hidden')
if not $target.find('li').length if not $target.find('li').length
$target.find('.empty-message').hide() $target.find('.empty-message').addClass('hidden')
$label.detach().appendTo($target) $label.detach().appendTo($target)
...@@ -54,6 +54,9 @@ class @LabelManager ...@@ -54,6 +54,9 @@ class @LabelManager
if action is 'remove' if action is 'remove'
xhr = $.ajax url: url, type: 'DELETE' xhr = $.ajax url: url, type: 'DELETE'
# Restore empty message
$from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
else else
xhr = @savePrioritySort($label, action) xhr = @savePrioritySort($label, action)
......
...@@ -32,10 +32,6 @@ ...@@ -32,10 +32,6 @@
#= require bootstrap/tooltip #= require bootstrap/tooltip
#= require bootstrap/popover #= require bootstrap/popover
#= require select2 #= require select2
#= require raphael
#= require g.raphael
#= require g.bar
#= require branch-graph
#= require ace/ace #= require ace/ace
#= require ace/ext-searchbox #= require ace/ext-searchbox
#= require underscore #= require underscore
...@@ -125,9 +121,10 @@ window.onload = -> ...@@ -125,9 +121,10 @@ window.onload = ->
setTimeout shiftWindow, 100 setTimeout shiftWindow, 100
$ -> $ ->
gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize() bootstrapBreakpoint = bp.getBreakpointSize()
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents # Click a .js-select-on-focus field, select the contents
$(".js-select-on-focus").on "focusin", -> $(".js-select-on-focus").on "focusin", ->
...@@ -257,3 +254,31 @@ $ -> ...@@ -257,3 +254,31 @@ $ ->
gl.awardsHandler = new AwardsHandler() gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize() checkInitialSidebarSize()
new Aside() new Aside()
# Sidenav pinning
if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
$.cookie('pin_nav', 'false')
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
.removeClass('page-sidebar-pinned')
$('.navbar-fixed-top').removeClass('header-pinned-nav')
$(document)
.off 'click', '.js-nav-pin'
.on 'click', '.js-nav-pin', (e) ->
e.preventDefault()
$(this).toggleClass 'is-active'
if $.cookie('pin_nav') is 'true'
$.cookie 'pin_nav', 'false'
$('.page-with-sidebar')
.removeClass('page-sidebar-pinned')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$('.navbar-fixed-top')
.removeClass('header-pinned-nav')
.toggleClass('header-collapsed header-expanded')
else
$.cookie 'pin_nav', 'true'
$('.page-with-sidebar').addClass('page-sidebar-pinned')
$('.navbar-fixed-top').addClass('header-pinned-nav')
...@@ -40,7 +40,7 @@ class @AwardsHandler ...@@ -40,7 +40,7 @@ class @AwardsHandler
$menu = $ '.emoji-menu' $menu = $ '.emoji-menu'
if $addBtn.hasClass 'js-note-emoji' if $addBtn.hasClass 'js-note-emoji'
$addBtn.parents('.note').find('.js-awards-block').addClass 'current' $addBtn.closest('.note').find('.js-awards-block').addClass 'current'
else else
$addBtn.closest('.js-awards-block').addClass 'current' $addBtn.closest('.js-awards-block').addClass 'current'
......
...@@ -17,6 +17,8 @@ class @CiBuild ...@@ -17,6 +17,8 @@ class @CiBuild
.off 'resize.build' .off 'resize.build'
.on 'resize.build', @hideSidebar .on 'resize.build', @hideSidebar
@updateArtifactRemoveDate()
if $('#build-trace').length if $('#build-trace').length
@getInitialBuildTrace() @getInitialBuildTrace()
@initScrollButtonAffix() @initScrollButtonAffix()
...@@ -103,3 +105,10 @@ class @CiBuild ...@@ -103,3 +105,10 @@ class @CiBuild
$('.js-build-sidebar') $('.js-build-sidebar')
.removeClass 'right-sidebar-collapsed' .removeClass 'right-sidebar-collapsed'
.addClass 'right-sidebar-expanded' .addClass 'right-sidebar-expanded'
updateArtifactRemoveDate: ->
$date = $('.js-artifacts-remove')
if $date.length
date = $date.text()
$date.text $.timefor(new Date(date), ' ')
...@@ -29,6 +29,7 @@ class Dispatcher ...@@ -29,6 +29,7 @@ class Dispatcher
new Todos() new Todos()
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
new DueDateSelect()
new GLForm($('.milestone-form')) new GLForm($('.milestone-form'))
when 'groups:milestones:new' when 'groups:milestones:new'
new ZenMode() new ZenMode()
...@@ -53,9 +54,13 @@ class Dispatcher ...@@ -53,9 +54,13 @@ class Dispatcher
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
new MergedButtons()
when 'projects:merge_requests:commits', 'projects:merge_requests:builds'
new MergedButtons()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
new ZenMode() new ZenMode()
new MergedButtons()
when 'projects:merge_requests:index' when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
Issuable.init() Issuable.init()
...@@ -68,9 +73,7 @@ class Dispatcher ...@@ -68,9 +73,7 @@ class Dispatcher
new Diff() new Diff()
new ZenMode() new ZenMode()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:commits:show' when 'projects:commits:show', 'projects:activity'
shortcut_handler = new ShortcutsNavigation()
when 'projects:activity'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:show' when 'projects:show'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
...@@ -96,6 +99,7 @@ class Dispatcher ...@@ -96,6 +99,7 @@ class Dispatcher
when 'projects:blob:show', 'projects:blame:show' when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter() new LineHighlighter()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ShortcutsBlob true
when 'projects:labels:new', 'projects:labels:edit' when 'projects:labels:new', 'projects:labels:edit'
new Labels() new Labels()
when 'projects:labels:index' when 'projects:labels:index'
...@@ -129,15 +133,11 @@ class Dispatcher ...@@ -129,15 +133,11 @@ class Dispatcher
new Project() new Project()
new ProjectAvatar() new ProjectAvatar()
switch path[1] switch path[1]
when 'compare'
shortcut_handler = new ShortcutsNavigation()
when 'edit' when 'edit'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ProjectNew() new ProjectNew()
when 'new' when 'new', 'show'
new ProjectNew() new ProjectNew()
when 'show'
new ProjectShow()
when 'wikis' when 'wikis'
new Wikis() new Wikis()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
...@@ -146,9 +146,9 @@ class Dispatcher ...@@ -146,9 +146,9 @@ class Dispatcher
when 'snippets' when 'snippets'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show' new ZenMode() if path[2] == 'show'
when 'labels', 'graphs' when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
shortcut_handler = new ShortcutsNavigation() 'milestones', 'project_members', 'deploy_keys', 'builds', \
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one # If we haven't installed a custom shortcut handler, install the default one
......
class @DueDateSelect class @DueDateSelect
constructor: -> constructor: ->
# Milestone edit/new form
$datePicker = $('.datepicker')
if $datePicker.length
$dueDate = $('#milestone_due_date')
$datePicker.datepicker
dateFormat: 'yy-mm-dd'
onSelect: (dateText, inst) ->
$dueDate.val(dateText)
.datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()))
$('.js-clear-due-date').on 'click', (e) ->
e.preventDefault()
$.datepicker._clearDate($datePicker)
# Issuable sidebar
$loading = $('.js-issuable-update .due_date') $loading = $('.js-issuable-update .due_date')
.find('.block-loading') .find('.block-loading')
.hide() .hide()
...@@ -32,7 +48,7 @@ class @DueDateSelect ...@@ -32,7 +48,7 @@ class @DueDateSelect
date = new Date value.replace(new RegExp('-', 'g'), ',') date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date mediumDate = $.datepicker.formatDate 'M d, yy', date
else else
mediumDate = 'None' mediumDate = 'No due date'
data = {} data = {}
data[abilityName] = {} data[abilityName] = {}
...@@ -50,7 +66,8 @@ class @DueDateSelect ...@@ -50,7 +66,8 @@ class @DueDateSelect
$selectbox.hide() $selectbox.hide()
$value.css('display', '') $value.css('display', '')
$valueContent.html(mediumDate) cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value'
$valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>")
$sidebarValue.html(mediumDate) $sidebarValue.html(mediumDate)
if value isnt '' if value isnt ''
......
...@@ -56,13 +56,6 @@ issuable_created = false ...@@ -56,13 +56,6 @@ issuable_created = false
Issuable.filterResults $('.filter-form') Issuable.filterResults $('.filter-form')
$('.js-label-select').trigger('update.label') $('.js-label-select').trigger('update.label')
toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0
$filteredLabels.removeClass('hidden')
else
$filteredLabels.addClass('hidden')
filterResults: (form) => filterResults: (form) =>
formData = form.serialize() formData = form.serialize()
...@@ -71,58 +64,16 @@ issuable_created = false ...@@ -71,58 +64,16 @@ issuable_created = false
issuesUrl = formAction issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData issuesUrl += formData
$.ajax
type: 'GET'
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issuable.reload()
Issuable.updateStateFilters()
$filteredLabels = $('.filtered-labels')
if typeof Issuable.labelRow is 'function'
$filteredLabels.html(Issuable.labelRow(data))
Issuable.toggleLabelFilters() Turbolinks.visit(issuesUrl);
dataType: "json"
reload: ->
if Issuable.created
Issuable.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
initChecks: -> initChecks: ->
$('.check_all_issues').on 'click', -> $('.check_all_issues').off('click').on('click', ->
$('.selected_issue').prop('checked', @checked) $('.selected_issue').prop('checked', @checked)
Issuable.checkChanged() Issuable.checkChanged()
)
$('.selected_issue').on 'change', Issuable.checkChanged $('.selected_issue').off('change').on('change', Issuable.checkChanged)
updateStateFilters: ->
stateFilters = $('.issues-state-filters, .dropdown-menu-sort')
newParams = {}
paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
labelNameValues = gl.utils.getParameterValues('label_name[]')
if labelNameValues
labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
else
newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
$(this).attr 'href', newUrl
checkChanged: -> checkChanged: ->
checked_issues = $('.selected_issue:checked') checked_issues = $('.selected_issue:checked')
......
...@@ -102,6 +102,10 @@ class @IssuableForm ...@@ -102,6 +102,10 @@ class @IssuableForm
return { return {
results: data results: data
} }
data: (query) ->
{
search: query
}
formatResult: (project) -> formatResult: (project) ->
project.name_with_namespace project.name_with_namespace
formatSelection: (project) -> formatSelection: (project) ->
......
...@@ -9,6 +9,9 @@ class @IssuableBulkActions ...@@ -9,6 +9,9 @@ class @IssuableBulkActions
@bindEvents() @bindEvents()
# Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
getElement: (selector) -> getElement: (selector) ->
@container.find selector @container.find selector
...@@ -97,13 +100,22 @@ class @IssuableBulkActions ...@@ -97,13 +100,22 @@ class @IssuableBulkActions
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]') $labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) -> $labels.each (k, label) ->
labelIds.push $(label).val() if label labelIds.push parseInt($(label).val()) if label
labelIds labelIds
###* ###*
* Just an alias of @getUnmarkedIndeterminedLabels * Returns Label IDs that will be removed from issue selection
* @return {Array} Array of labels * @return {Array} Array of labels IDs
### ###
getLabelsToRemove: -> 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
...@@ -39,7 +39,7 @@ class @LabelsSelect ...@@ -39,7 +39,7 @@ class @LabelsSelect
</a> </a>
<% }); %>' <% }); %>'
) )
labelNoneHTMLTemplate = _.template('<div class="light">None</div>') labelNoneHTMLTemplate = '<span class="no-value">None</span>'
if newLabelField.length if newLabelField.length
...@@ -145,7 +145,7 @@ class @LabelsSelect ...@@ -145,7 +145,7 @@ class @LabelsSelect
template = labelHTMLTemplate(data) template = labelHTMLTemplate(data)
labelCount = data.labels.length labelCount = data.labels.length
else else
template = labelNoneHTMLTemplate() template = labelNoneHTMLTemplate
$value $value
.removeAttr('style') .removeAttr('style')
.html(template) .html(template)
......
class @LayoutNav hideEndFade = ($scrollingTabs) ->
$ -> $scrollingTabs.each ->
$('.fade-left').addClass('end-scroll') $this = $(@)
$('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this) $this
$el = $(event.target) .find('.fade-right')
currentPosition = $this.scrollLeft() .toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
size = bp.getBreakpointSize()
controlBtnWidth = $('.controls').width() $ ->
maxPosition = $this.get(0).scrollWidth - $this.parent().width() $('.fade-left').addClass('end-scroll')
maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length
hideEndFade($('.scrolling-tabs'))
$el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) $(window)
.off 'resize.nav'
.on 'resize.nav', ->
hideEndFade($('.scrolling-tabs'))
$('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this)
currentPosition = $this.scrollLeft()
maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
$this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
((w) -> ((w) ->
w.gl or= {}
w.gl.utils or= {}
w.gl.utils.isInGroupsPage = ->
return $('body').data('page').split(':')[0] is 'groups'
w.gl.utils.isInProjectPage = ->
return $('body').data('page').split(':')[0] is 'projects'
w.gl.utils.getProjectSlug = ->
return if @isInProjectPage() then $('body').data 'project' else null
w.gl.utils.getGroupSlug = ->
return if @isInGroupsPage() then $('body').data 'group' else null
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
$tooltipEl
.tooltip 'destroy'
.attr 'title', newTitle
.tooltip 'fixTitle'
gl.utils.preventDisabledButtons = ->
$('.btn').click (e) ->
if $(this).hasClass 'disabled'
e.preventDefault()
e.stopImmediatePropagation()
return false
jQuery.timefor = (time, suffix, expiredLabel) -> jQuery.timefor = (time, suffix, expiredLabel) ->
return '' unless time return '' unless time
......
...@@ -42,9 +42,3 @@ work = -> ...@@ -42,9 +42,3 @@ work = ->
$(document).on('page:fetch', start) $(document).on('page:fetch', start)
$(document).on('page:change', stop) $(document).on('page:change', stop)
$ ->
# Make logo clickable as part of a workaround for Safari visited
# link behaviour (See !2690).
$('#logo').on 'click', ->
Turbolinks.visit('/')
class @MergedButtons
constructor: ->
@$removeBranchWidget = $('.remove_source_branch_widget')
@$removeBranchProgress = $('.remove_source_branch_in_progress')
@$removeBranchFailed = $('.remove_source_branch_widget.failed')
@cleanEventListeners()
@initEventListeners()
cleanEventListeners: ->
$(document).off 'click', '.remove_source_branch'
$(document).off 'ajax:success', '.remove_source_branch'
$(document).off 'ajax:error', '.remove_source_branch'
initEventListeners: ->
$(document).on 'click', '.remove_source_branch', @removeSourceBranch
$(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess
$(document).on 'ajax:error', '.remove_source_branch', @removeBranchError
removeSourceBranch: =>
@$removeBranchWidget.hide()
@$removeBranchProgress.show()
removeBranchSuccess: ->
location.reload()
removeBranchError: ->
@$removeBranchWidget.hide()
@$removeBranchProgress.hide()
@$removeBranchFailed.show()
...@@ -24,14 +24,10 @@ class @MilestoneSelect ...@@ -24,14 +24,10 @@ class @MilestoneSelect
if issueUpdateURL if issueUpdateURL
milestoneLinkTemplate = _.template( milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"> '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>" class="bold has-tooltip" data-container="body" title="<%= remaining %>"><%= _.escape(title) %></a>'
<span class="has-tooltip" data-container="body" title="<%= remaining %>">
<%= _.escape(title) %>
</span>
</a>'
) )
milestoneLinkNoneTemplate = '<div class="light">None</div>' milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
collapsedSidebarLabelTemplate = _.template( collapsedSidebarLabelTemplate = _.template(
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left"> '<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">
...@@ -116,7 +112,7 @@ class @MilestoneSelect ...@@ -116,7 +112,7 @@ class @MilestoneSelect
.val() .val()
data = {} data = {}
data[abilityName] = {} data[abilityName] = {}
data[abilityName].milestone_id = selected data[abilityName].milestone_id = if selected? then selected else null
$loading $loading
.fadeIn() .fadeIn()
$dropdown.trigger('loading.gl.dropdown') $dropdown.trigger('loading.gl.dropdown')
......
# This is a manifest file that'll be compiled into including all the files listed below.
# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
# be included in the compiled file accessible from http://example.com/assets/application.js
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
#= require raphael
#= require g.raphael
#= require g.bar
#= require_tree .
$ ->
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
commit_url: $(".network-graph").attr('data-commit-url'),
ref: $(".network-graph").attr('data-ref'),
commit_id: $(".network-graph").attr('data-commit-id')
})
new ShortcutsNetwork(network_graph.branch_graph)
...@@ -115,12 +115,14 @@ class @Notes ...@@ -115,12 +115,14 @@ class @Notes
, @pollingInterval , @pollingInterval
refresh: => refresh: =>
return if @refreshing is true
@refreshing = true
if not document.hidden and document.URL.indexOf(@noteable_url) is 0 if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent() @getContent()
getContent: -> getContent: ->
return if @refreshing
@refreshing = true
$.ajax $.ajax
url: @notes_url url: @notes_url
data: "last_fetched_at=" + @last_fetched_at data: "last_fetched_at=" + @last_fetched_at
......
...@@ -43,6 +43,55 @@ class @Sidebar ...@@ -43,6 +43,55 @@ class @Sidebar
$('.right-sidebar') $('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' }) .hasClass('right-sidebar-collapsed'), { path: '/' })
$(document)
.off 'click', '.js-issuable-todo'
.on 'click', '.js-issuable-todo', @toggleTodo
toggleTodo: (e) =>
$this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this)
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
$.ajax(
url: "#{$this.data('url')}#{ajaxUrlExtra}"
type: ajaxType
dataType: 'json'
data:
issuable_id: $this.data('issuable')
issuable_type: $this.data('issuable-type')
beforeSend: =>
@beforeTodoSend($this, $todoLoading)
).done (data) =>
@todoUpdateDone(data, $this, $btnText, $todoLoading)
beforeTodoSend: ($btn, $todoLoading) ->
$btn.disable()
$todoLoading.removeClass 'hidden'
todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
$todoPendingCount = $('.todos-pending-count')
$todoPendingCount.text data.count
$btn.enable()
$todoLoading.addClass 'hidden'
if data.count is 0
$todoPendingCount.addClass 'hidden'
else
$todoPendingCount.removeClass 'hidden'
if data.todo?
$btn
.attr 'aria-label', $btn.data('mark-text')
.attr 'data-id', data.todo.id
$btnText.text $btn.data('mark-text')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-id'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) -> sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
...@@ -117,5 +166,3 @@ class @Sidebar ...@@ -117,5 +166,3 @@ class @Sidebar
getBlock: (name) -> getBlock: (name) ->
@sidebar.find(".block.#{name}") @sidebar.find(".block.#{name}")
...@@ -67,8 +67,12 @@ class @SearchAutocomplete ...@@ -67,8 +67,12 @@ class @SearchAutocomplete
getData: (term, callback) -> getData: (term, callback) ->
_this = @ _this = @
# Do not trigger request if input is empty unless term
return if @searchInput.val() is '' if contents = @getCategoryContents()
@searchInput.data('glDropdown').filter.options.callback contents
@enableAutocomplete()
return
# Prevent multiple ajax calls # Prevent multiple ajax calls
return if @loadingSuggestions return if @loadingSuggestions
...@@ -122,6 +126,37 @@ class @SearchAutocomplete ...@@ -122,6 +126,37 @@ class @SearchAutocomplete
).always -> ).always ->
_this.loadingSuggestions = false _this.loadingSuggestions = false
getCategoryContents: ->
userId = gon.current_user_id
{ utils, projectOptions, groupOptions, dashboardOptions } = gl
if utils.isInGroupsPage() and groupOptions
options = groupOptions[utils.getGroupSlug()]
else if utils.isInProjectPage() and projectOptions
options = projectOptions[utils.getProjectSlug()]
else if dashboardOptions
options = dashboardOptions
{ issuesPath, mrPath, name } = options
items = [
{ header: "#{name}" }
{ text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
{ text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
'separator'
{ text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
{ text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
]
items.splice 0, 1 unless name
return items
serializeState: -> serializeState: ->
{ {
# Search Criteria # Search Criteria
...@@ -209,6 +244,12 @@ class @SearchAutocomplete ...@@ -209,6 +244,12 @@ class @SearchAutocomplete
@isFocused = true @isFocused = true
@wrap.addClass('search-active') @wrap.addClass('search-active')
@getData() if @getValue() is ''
getValue: -> return @searchInput.val()
onClearInputClick: (e) => onClearInputClick: (e) =>
e.preventDefault() e.preventDefault()
@searchInput.val('').focus() @searchInput.val('').focus()
...@@ -229,6 +270,10 @@ class @SearchAutocomplete ...@@ -229,6 +270,10 @@ class @SearchAutocomplete
@locationBadgeEl.text(badgeText).show() @locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge') @wrap.addClass('has-location-badge')
hasLocationBadge: -> return @wrap.is '.has-location-badge'
restoreOriginalState: -> restoreOriginalState: ->
inputs = Object.keys @originalState inputs = Object.keys @originalState
...@@ -257,13 +302,14 @@ class @SearchAutocomplete ...@@ -257,13 +302,14 @@ class @SearchAutocomplete
@getElement("##{input}").val('') @getElement("##{input}").val('')
removeLocationBadge: -> removeLocationBadge: ->
@locationBadgeEl.hide()
# Reset state @locationBadgeEl.hide()
@resetSearchState() @resetSearchState()
@wrap.removeClass('has-location-badge') @wrap.removeClass('has-location-badge')
@disableAutocomplete()
disableAutocomplete: -> disableAutocomplete: ->
@searchInput.addClass('disabled') @searchInput.addClass('disabled')
......
class @Shortcuts class @Shortcuts
constructor: -> constructor: (skipResetBindings) ->
@enabledHelp = [] @enabledHelp = []
Mousetrap.reset() Mousetrap.reset() if not skipResetBindings
Mousetrap.bind('?', @onToggleHelp) Mousetrap.bind('?', @onToggleHelp)
Mousetrap.bind('s', Shortcuts.focusSearch) Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
......
#= require shortcuts
class @ShortcutsBlob extends Shortcuts
constructor: (skipResetBindings) ->
super skipResetBindings
Mousetrap.bind('y', ShortcutsBlob.copyToClipboard)
@copyToClipboard: ->
clipboardButton = $('.btn-clipboard')
clipboardButton.click() if clipboardButton
...@@ -3,13 +3,33 @@ expanded = 'page-sidebar-expanded' ...@@ -3,13 +3,33 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = -> toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded") $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded")
if $.cookie('pin_nav') is 'true'
$('.navbar-fixed-top').toggleClass('header-pinned-nav')
$('.page-with-sidebar').toggleClass('page-sidebar-pinned')
setTimeout ( -> setTimeout ( ->
niceScrollBars = $('.nicescroll').niceScroll(); niceScrollBars = $('.nav-sidebar').niceScroll();
niceScrollBars.updateScrollBar(); niceScrollBars.updateScrollBar();
), 300 ), 300
$(document)
.off 'click', 'body'
.on 'click', 'body', (e) ->
unless $.cookie('pin_nav') is 'true'
$target = $(e.target)
$nav = $target.closest('.sidebar-wrapper')
pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
$toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle')
if $nav.length is 0 and pageExpanded and $toggle.length is 0
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$('.navbar-fixed-top')
.toggleClass('header-collapsed header-expanded')
$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) -> $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
e.preventDefault() e.preventDefault()
......
...@@ -9,9 +9,11 @@ class @Star ...@@ -9,9 +9,11 @@ class @Star
$this.parent().find('.star-count').text data.star_count $this.parent().find('.star-count').text data.star_count
if isStarred if isStarred
$starSpan.removeClass('starred').text 'Star' $starSpan.removeClass('starred').text 'Star'
gl.utils.updateTooltipTitle $this, 'Star project'
$starIcon.removeClass('fa-star').addClass 'fa-star-o' $starIcon.removeClass('fa-star').addClass 'fa-star-o'
else else
$starSpan.addClass('starred').text 'Unstar' $starSpan.addClass('starred').text 'Unstar'
gl.utils.updateTooltipTitle $this, 'Unstar project'
$starIcon.removeClass('fa-star-o').addClass 'fa-star' $starIcon.removeClass('fa-star-o').addClass 'fa-star'
return return
......
...@@ -31,7 +31,7 @@ class @UsersSelect ...@@ -31,7 +31,7 @@ class @UsersSelect
assignTo = (selected) -> assignTo = (selected) ->
data = {} data = {}
data[abilityName] = {} data[abilityName] = {}
data[abilityName].assignee_id = selected data[abilityName].assignee_id = if selected? then selected else null
$loading $loading
.fadeIn() .fadeIn()
$dropdown.trigger('loading.gl.dropdown') $dropdown.trigger('loading.gl.dropdown')
...@@ -72,7 +72,7 @@ class @UsersSelect ...@@ -72,7 +72,7 @@ class @UsersSelect
assigneeTemplate = _.template( assigneeTemplate = _.template(
'<% if (username) { %> '<% if (username) { %>
<a class="author_link " href="/u/<%= username %>"> <a class="author_link bold" href="/u/<%= username %>">
<% if( avatar ) { %> <% if( avatar ) { %>
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>"> <img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
<% } %> <% } %>
...@@ -82,7 +82,7 @@ class @UsersSelect ...@@ -82,7 +82,7 @@ class @UsersSelect
</span> </span>
</a> </a>
<% } else { %> <% } else { %>
<span class="assign-yourself"> <span class="no-value assign-yourself">
No assignee - No assignee -
<a href="#" class="js-assign-yourself"> <a href="#" class="js-assign-yourself">
assign yourself assign yourself
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
*/ */
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar { .page-with-sidebar {
.toggle-nav-collapse,
.collapse-nav a { .pin-nav-btn {
color: $color-light; color: $color-light;
background: $color; background: $color;
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* *
*/ */
header { header {
transition-duration: .3s; transition: padding $sidebar-transition-duration;
&.navbar-empty { &.navbar-empty {
height: $header-height; height: $header-height;
...@@ -79,14 +79,9 @@ header { ...@@ -79,14 +79,9 @@ header {
&.header-collapsed { &.header-collapsed {
padding: 0 16px; padding: 0 16px;
.side-nav-toggle {
display: block;
}
} }
.side-nav-toggle { .side-nav-toggle {
display: none;
position: absolute; position: absolute;
left: -10px; left: -10px;
margin: 6px 0; margin: 6px 0;
...@@ -108,9 +103,7 @@ header { ...@@ -108,9 +103,7 @@ header {
.header-content { .header-content {
position: relative; position: relative;
height: $header-height; height: $header-height;
padding-right: 40px;
padding-left: 30px; padding-left: 30px;
transition-duration: .3s;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: 0; padding-right: 0;
...@@ -198,18 +191,6 @@ header { ...@@ -198,18 +191,6 @@ header {
} }
} }
.header-collapsed {
margin-left: 0;
.header-content {
@media (min-width: $screen-sm-max) {
padding-left: 30px;
transition-duration: .3s;
}
}
}
.tanuki-shape { .tanuki-shape {
transition: all 0.8s; transition: all 0.8s;
......
...@@ -159,7 +159,7 @@ ul.content-list { ...@@ -159,7 +159,7 @@ ul.content-list {
background-color: $gray-light; background-color: $gray-light;
border: dotted 1px $gray-dark; border: dotted 1px $gray-dark;
margin: 1px 0; margin: 1px 0;
min-height: 30px; min-height: 52px;
} }
} }
} }
......
...@@ -74,6 +74,7 @@ ...@@ -74,6 +74,7 @@
.container-fluid { .container-fluid {
background-color: $background-color; background-color: $background-color;
margin-bottom: 0;
} }
li { li {
...@@ -241,6 +242,12 @@ ...@@ -241,6 +242,12 @@
} }
} }
} }
&.adjust {
.nav-text, .nav-controls {
width: auto;
}
}
} }
.layout-nav { .layout-nav {
...@@ -250,7 +257,7 @@ ...@@ -250,7 +257,7 @@
z-index: 11; z-index: 11;
background: $background-color; background: $background-color;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
transition-duration: .3s; transition: padding $sidebar-transition-duration;
text-align: center; text-align: center;
.container-fluid { .container-fluid {
...@@ -280,11 +287,10 @@ ...@@ -280,11 +287,10 @@
} }
.dropdown { .dropdown {
margin-left: 7px; position: absolute;
top: 7px;
@media (max-width: $screen-xs-min) { right: 15px;
margin-left: 0; z-index: 2;
}
li.active { li.active {
font-weight: bold; font-weight: bold;
...@@ -347,6 +353,12 @@ ...@@ -347,6 +353,12 @@
.badge { .badge {
color: $gl-icon-color; color: $gl-icon-color;
} }
&:hover {
a, i {
color: $black;
}
}
} }
} }
......
.page-with-sidebar { .page-with-sidebar {
padding-top: $header-height; padding-top: $header-height;
transition-duration: .3s; transition: padding $sidebar-transition-duration;
.sidebar-wrapper { .sidebar-wrapper {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
overflow-y: auto;
overflow-x: hidden;
left: 0; left: 0;
height: 100%; height: 100%;
transition-duration: .3s; overflow: hidden;
transition: width $sidebar-transition-duration;
} }
} }
.sidebar-wrapper { .sidebar-wrapper {
z-index: 1000; z-index: 1000;
background: $background-color; background: $background-color;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
display: none!important;
}
} }
.content-wrapper { .content-wrapper {
width: 100%; width: 100%;
transition: padding $sidebar-transition-duration;
.container-fluid { .container-fluid {
background: #fff; background: #fff;
...@@ -34,22 +39,19 @@ ...@@ -34,22 +39,19 @@
} }
} }
.sidebar-wrapper { .sidebar-user {
padding: 15px;
.sidebar-user { position: absolute;
padding: 15px 22px; left: 0;
position: fixed; bottom: 0;
bottom: 0; width: $sidebar_width;
width: $sidebar_width; overflow: hidden;
overflow: hidden; font-size: 16px;
transition-duration: .3s; line-height: 36px;
transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
.username { @media (min-width: $sidebar-breakpoint) {
margin-left: 10px; bottom: 50px;
width: $sidebar_width - 2 * 10px;
font-size: 16px;
line-height: 34px;
}
} }
} }
...@@ -65,39 +67,46 @@ ...@@ -65,39 +67,46 @@
.nav-sidebar { .nav-sidebar {
margin-top: 22 + $header-height; position: absolute;
margin-bottom: 116px; top: 50px;
transition-duration: .3s; bottom: 65px;
list-style: none; width: $sidebar_width;
overflow: hidden; overflow-y: auto;
overflow-x: hidden;
@media (min-width: $sidebar-breakpoint) {
bottom: 115px;
}
&.navbar-collapse { &.navbar-collapse {
padding: 0 !important; padding: 0 !important;
} }
li { li {
width: $sidebar_width;
&.separate-item { &.separate-item {
padding-top: 10px; padding-top: 10px;
margin-top: 10px; margin-top: 10px;
} }
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
}
a { a {
width: $sidebar_width; padding: 7px 15px 7px 12px;
padding: 7px 15px 7px 23px;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 24px; line-height: 24px;
display: block; display: block;
text-decoration: none; text-decoration: none;
font-weight: normal; font-weight: normal;
outline: none; outline: none;
white-space: nowrap;
&:hover { &:hover,
text-decoration: none; &:active,
} &:focus {
&:active, &:focus {
text-decoration: none; text-decoration: none;
} }
...@@ -109,10 +118,6 @@ ...@@ -109,10 +118,6 @@
svg { svg {
margin-right: 13px; margin-right: 13px;
} }
&.back-link i {
transition-duration: .3s;
}
} }
} }
...@@ -123,37 +128,50 @@ ...@@ -123,37 +128,50 @@
} }
} }
.sidebar-subnav { .toggle-nav-collapse {
margin-left: 0;
padding-left: 0;
li {
list-style: none;
}
}
.collapse-nav a {
width: $sidebar_width; width: $sidebar_width;
position: fixed; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
min-height: 50px;
padding: 5px 0; padding: 5px 0;
font-size: 18px; font-size: 18px;
background: transparent; line-height: 30px;
height: 50px; }
text-align: center;
line-height: 40px; .nav-header-btn {
padding: 10px 5px;
color: inherit;
transition-duration: .3s; transition-duration: .3s;
outline: none;
&:hover { &:hover,
&:focus {
color: $white-light;
text-decoration: none; text-decoration: none;
} }
} }
.sidebar-wrapper { .pin-nav-btn {
&.hidden-nav { display: none;
width: 0; position: absolute;
left: 0;
bottom: 0;
height: 50px;
width: $sidebar_width;
line-height: 30px;
@media (min-width: $sidebar-breakpoint) {
display: block;
}
.fa {
transition: transform .15s;
}
&.is-active {
.fa {
transform: rotate(90deg);
}
} }
} }
...@@ -162,62 +180,34 @@ ...@@ -162,62 +180,34 @@
.sidebar-wrapper { .sidebar-wrapper {
width: 0; width: 0;
.nav-sidebar {
width: 0;
li {
width: auto;
a {
span {
display: none;
}
}
}
}
.collapse-nav a {
width: 0;
i {
display: none;
}
}
.sidebar-user {
width: 0;
padding-left: 0;
padding-right: 0;
.username {
display: none;
}
}
} }
} }
.page-sidebar-expanded { .page-sidebar-expanded {
@media (max-width: $screen-sm-max) {
padding-left: 0;
}
.sidebar-wrapper { .sidebar-wrapper {
width: $sidebar_width; width: $sidebar_width;
}
}
.nav-sidebar { .page-sidebar-pinned {
width: $sidebar_width; .content-wrapper,
.layout-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: $sidebar_width;
} }
}
}
.nav-sidebar li a { header.header-pinned-nav {
width: $sidebar_width; @media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
&.back-link { .side-nav-toggle {
i { display: none;
opacity: 0; }
}
} .header-content {
padding-left: 0;
} }
} }
} }
......
...@@ -6,6 +6,8 @@ $sidebar_width: 220px; ...@@ -6,6 +6,8 @@ $sidebar_width: 220px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 258px; $gutter_inner_width: 258px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1440px;
/* /*
* UI elements * UI elements
......
...@@ -7,84 +7,111 @@ ...@@ -7,84 +7,111 @@
margin-right: 9px; margin-right: 9px;
} }
.lists-separator { .commit-header {
margin: 10px 0; padding: 5px 10px;
border-color: #ddd; background-color: $background-color;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
font-size: 14px;
&:first-child {
border-top-width: 0;
}
} }
.commits-row { .commit-row-title {
ul { line-height: 1;
margin: 0; margin-bottom: 7px;
li.commit { .notes_count {
padding: 8px 0; float: right;
} margin-right: 10px;
}
.str-truncated {
max-width: 70%;
} }
.commits-row-date { .commit-row-message {
font-size: 15px; color: $gl-dark-link-color;
line-height: 20px; }
margin-bottom: 5px;
.text-expander {
display: inline-block;
background: $gray-light;
color: $gl-placeholder-color;
padding: 0 5px;
cursor: pointer;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
&:hover {
background-color: darken($gray-light, 10%);
text-decoration: none;
}
} }
} }
li.commit { .commit-actions {
list-style: none; @media (min-width: $screen-sm-min) {
float: right;
margin-left: $gl-padding;
margin-top: 2px;
font-size: 0;
}
.commit-row-title { .btn-transparent {
font-size: $list-font-size; padding-left: 0;
line-height: 20px; padding-right: 0;
margin-bottom: 2px; }
.btn-clipboard { .btn {
margin-top: -1px; &:not(:first-child) {
margin-left: $gl-padding;
} }
}
}
.notes_count { .commit-short-id {
float: right; font-family: $monospace_font;
margin-right: 10px; font-weight: 600;
} }
.commit_short_id { .commit {
min-width: 65px; padding: 10px 0;
color: $gl-dark-link-color;
font-family: $monospace_font;
}
.str-truncated { @media (min-width: $screen-sm-min) {
max-width: 70%; padding-left: 46px;
} }
.commit-row-message { &:not(:last-child) {
color: $gl-dark-link-color; border-bottom: 1px solid #eee;
}
&:hover { a,
text-decoration: underline; button {
} color: $gl-dark-link-color;
} vertical-align: baseline;
}
.text-expander { .avatar {
background: #eee; margin-left: -46px;
color: #555;
padding: 0 5px;
cursor: pointer;
margin-left: 4px;
&:hover {
background-color: #ddd;
}
}
} }
.item-title { .item-title {
display: inline-block; display: inline-block;
max-width: 70%;
@media (min-width: $screen-sm-min) {
max-width: 70%;
}
} }
.commit-row-description { .commit-row-description {
font-size: 14px; font-size: 14px;
border-left: 1px solid #eee; border-left: 1px solid #eee;
padding: 10px 15px; padding: 10px 15px;
margin: 5px 0 10px 5px; margin: 10px 0;
background: #f9f9f9; background: #f9f9f9;
display: none; display: none;
...@@ -93,6 +120,7 @@ li.commit { ...@@ -93,6 +120,7 @@ li.commit {
background: inherit; background: inherit;
padding: 0; padding: 0;
margin: 0; margin: 0;
white-space: pre-wrap;
} }
a { a {
...@@ -102,7 +130,7 @@ li.commit { ...@@ -102,7 +130,7 @@ li.commit {
.commit-row-info { .commit-row-info {
color: $gl-gray; color: $gl-gray;
line-height: 24px; line-height: 1;
a { a {
color: $gl-gray; color: $gl-gray;
...@@ -111,10 +139,6 @@ li.commit { ...@@ -111,10 +139,6 @@ li.commit {
.avatar { .avatar {
margin-right: 8px; margin-right: 8px;
} }
.committed_ago {
display: inline-block;
}
} }
&.inline-commit { &.inline-commit {
......
.environments {
.commit-title {
margin: 0;
}
}
...@@ -39,3 +39,20 @@ ...@@ -39,3 +39,20 @@
} }
} }
} }
.groups-cover-block {
.container-fluid {
position: relative;
}
.access-request-button {
@include btn-gray;
position: absolute;
right: 16px;
bottom: 32px;
padding: 3px 10px;
text-transform: none;
background-color: $background-color;
}
}
...@@ -34,6 +34,10 @@ ...@@ -34,6 +34,10 @@
color: inherit; color: inherit;
} }
.issuable-header-text {
margin-top: 7px;
}
.block { .block {
@include clearfix; @include clearfix;
padding: $gl-padding 0; padding: $gl-padding 0;
...@@ -60,10 +64,6 @@ ...@@ -60,10 +64,6 @@
margin-top: 0; margin-top: 0;
} }
.issuable-count {
margin-top: 7px;
}
.gutter-toggle { .gutter-toggle {
margin-left: 20px; margin-left: 20px;
padding-left: 10px; padding-left: 10px;
...@@ -145,7 +145,6 @@ ...@@ -145,7 +145,6 @@
.assign-yourself { .assign-yourself {
margin-top: 10px; margin-top: 10px;
font-weight: normal;
display: block; display: block;
} }
} }
...@@ -158,6 +157,10 @@ ...@@ -158,6 +157,10 @@
font-weight: normal; font-weight: normal;
} }
.no-value {
color: $gl-placeholder-color;
}
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
display: none; display: none;
} }
...@@ -248,11 +251,16 @@ ...@@ -248,11 +251,16 @@
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 10px; margin-bottom: 10px;
} }
.issuable-header-btn {
display: none;
}
} }
.issuable-pager { .issuable-header-btn {
background: $gray-normal; background: $gray-normal;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
&:hover { &:hover {
background: $gray-dark; background: $gray-dark;
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
...@@ -263,7 +271,7 @@ ...@@ -263,7 +271,7 @@
} }
} }
a:not(.issuable-pager) { a {
&:hover { &:hover {
color: $md-link-color; color: $md-link-color;
text-decoration: none; text-decoration: none;
...@@ -322,7 +330,7 @@ ...@@ -322,7 +330,7 @@
margin-left: 5px; margin-left: 5px;
a { a {
color: #8c8c8c; color: $gl-placeholder-color;
} }
} }
......
...@@ -115,6 +115,13 @@ ...@@ -115,6 +115,13 @@
} }
} }
.draggable-handler {
display: inline-block;
opacity: 0;
transition: opacity .3s;
color: $gray-darkest;
}
.prioritized-labels { .prioritized-labels {
margin-bottom: 30px; margin-bottom: 30px;
...@@ -122,6 +129,13 @@ ...@@ -122,6 +129,13 @@
display: none; display: none;
color: $gray-light; color: $gray-light;
} }
li:hover {
.draggable-handler {
display: inline-block;
opacity: 1;
}
}
} }
.other-labels { .other-labels {
......
...@@ -313,3 +313,13 @@ ...@@ -313,3 +313,13 @@
} }
} }
} }
.merged-buttons {
.btn {
float: left;
&:not(:last-child) {
margin-right: 10px;
}
}
}
...@@ -139,6 +139,12 @@ ul.notes { ...@@ -139,6 +139,12 @@ ul.notes {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: 0; padding-right: 0;
} }
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
} }
.note-emoji-button { .note-emoji-button {
...@@ -258,7 +264,11 @@ ul.notes { ...@@ -258,7 +264,11 @@ ul.notes {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
position: relative; position: relative;
} }
......
...@@ -5,10 +5,12 @@ ...@@ -5,10 +5,12 @@
font-weight: normal; font-weight: normal;
} }
} }
.no-ssh-key-message, .project-limit-message { .no-ssh-key-message, .project-limit-message {
background-color: #f28d35; background-color: #f28d35;
margin-bottom: 0; margin-bottom: 0;
} }
.new_project, .new_project,
.edit-project { .edit-project {
fieldset.features { fieldset.features {
...@@ -18,13 +20,6 @@ ...@@ -18,13 +20,6 @@
} }
} }
.project-name-holder {
.help-inline {
vertical-align: top;
padding: 7px;
}
}
.project-home-panel { .project-home-panel {
background: $white-light; background: $white-light;
text-align: left; text-align: left;
...@@ -229,13 +224,20 @@ ...@@ -229,13 +224,20 @@
right: 16px; right: 16px;
bottom: 0; bottom: 0;
.btn { @media (max-width: $screen-lg-min) {
padding: 3px 10px; top: 0;
background-color: $background-color;
} }
@media (max-width: 1304px) { .access-request-button {
top: 0; position: absolute;
right: 0;
bottom: 61px;
@media (max-width: $screen-lg-min) {
position: relative;
bottom: 0;
margin-right: 10px;
}
} }
} }
...@@ -286,10 +288,6 @@ ...@@ -286,10 +288,6 @@
color: #555; color: #555;
} }
.project_member_row form {
margin: 0;
}
.transfer-project .select2-container { .transfer-project .select2-container {
min-width: 200px; min-width: 200px;
} }
...@@ -373,6 +371,7 @@ a.deploy-project-label { ...@@ -373,6 +371,7 @@ a.deploy-project-label {
.project-import .btn { .project-import .btn {
float: left; float: left;
margin-bottom: 10px;
margin-right: 10px; margin-right: 10px;
} }
......
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
margin: 0; margin: 0;
.commit { .commit {
padding: 0; padding: 0 0 0 55px;
.commit-row-title { .commit-row-title {
.commit-row-message { .commit-row-message {
...@@ -129,4 +129,6 @@ ...@@ -129,4 +129,6 @@
.tree-controls { .tree-controls {
float: right; float: right;
margin-top: 11px; margin-top: 11px;
position: relative;
z-index: 2;
} }
...@@ -35,6 +35,7 @@ class AutocompleteController < ApplicationController ...@@ -35,6 +35,7 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id]) project = Project.find_by_id(params[:project_id])
projects = current_user.authorized_projects projects = current_user.authorized_projects
projects = projects.search(params[:search]) if params[:search].present?
projects = projects.select do |project| projects = projects.select do |project|
current_user.can?(:admin_issue, project) current_user.can?(:admin_issue, project)
end end
......
module MembershipActions
extend ActiveSupport::Concern
include MembersHelper
def request_access
membershipable.request_access(current_user)
redirect_to polymorphic_path(membershipable),
notice: 'Your request for access has been queued for review.'
end
def approve_access_request
@member = membershipable.members.request.find(params[:id])
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
@member.accept_request
redirect_to polymorphic_url([membershipable, :members])
end
def leave
@member = membershipable.members.find_by(user_id: current_user)
return render_403 unless @member
source_type = @member.real_source_type.humanize(capitalize: false)
if can?(current_user, action_member_permission(:destroy, @member), @member)
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
@member.destroy
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
else
if cannot_leave?
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
alert << " Transfer or delete the #{source_type}."
redirect_to polymorphic_url(membershipable), alert: alert
else
render_403
end
end
end
protected
def membershipable
raise NotImplementedError
end
def cannot_leave?
raise NotImplementedError
end
end
class Groups::GroupMembersController < Groups::ApplicationController class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
# Authorize # Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave] before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index def index
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members @members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present? if params[:search].present?
users = @group.users.search(params[:search]).to_a users = @group.users.search(params[:search]).to_a
...@@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
end end
def leave
@group_member = @group.group_members.find_by(user_id: current_user)
if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else
if @group.last_owner?(current_user)
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
else
return render_403
end
end
end
protected protected
def member_params def member_params
params.require(:group_member).permit(:access_level, :user_id) params.require(:group_member).permit(:access_level, :user_id)
end end
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end end
...@@ -42,7 +42,7 @@ class JwtController < ApplicationController ...@@ -42,7 +42,7 @@ class JwtController < ApplicationController
end end
def authenticate_user(login, password) 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) Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
user user
end end
......
class Profiles::NotificationsController < Profiles::ApplicationController class Profiles::NotificationsController < Profiles::ApplicationController
def show def show
@user = current_user @user = current_user
@group_notifications = current_user.notification_settings.for_groups @group_notifications = current_user.notification_settings.for_groups
@project_notifications = current_user.notification_settings.for_projects @project_notifications = current_user.notification_settings.for_projects
@global_notification_setting = current_user.global_notification_setting
end end
def update def update
if current_user.update_attributes(user_params) if current_user.update_attributes(user_params) && update_notification_settings
flash[:notice] = "Notification settings saved" flash[:notice] = "Notification settings saved"
else else
flash[:alert] = "Failed to save new settings" flash[:alert] = "Failed to save new settings"
...@@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def user_params 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
end end
class Projects::ArtifactsController < Projects::ApplicationController class Projects::ArtifactsController < Projects::ApplicationController
layout 'project' layout 'project'
before_action :authorize_read_build! before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :validate_artifacts!
def download def download
unless artifacts_file.file_storage? unless artifacts_file.file_storage?
return redirect_to artifacts_file.url return redirect_to artifacts_file.url
end end
unless artifacts_file.exists?
return render_404
end
send_file artifacts_file.path, disposition: 'attachment' send_file artifacts_file.path, disposition: 'attachment'
end end
def browse def browse
return render_404 unless build.artifacts?
directory = params[:path] ? "#{params[:path]}/" : '' directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory) @entry = build.artifacts_metadata_entry(directory)
...@@ -34,8 +30,17 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -34,8 +30,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
end end
def keep
build.keep_artifacts!
redirect_to namespace_project_build_path(project.namespace, project, build)
end
private private
def validate_artifacts!
render_404 unless build.artifacts?
end
def build def build
@build ||= project.builds.find_by!(id: params[:build_id]) @build ||= project.builds.find_by!(id: params[:build_id])
end end
......
...@@ -51,7 +51,7 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -51,7 +51,7 @@ class Projects::BuildsController < Projects::ApplicationController
return render_404 return render_404
end end
build = Ci::Build.retry(@build) build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build) redirect_to build_path(build)
end end
......
...@@ -46,7 +46,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -46,7 +46,7 @@ class Projects::CommitController < Projects::ApplicationController
def retry_builds def retry_builds
ci_builds.latest.failed.each do |build| ci_builds.latest.failed.each do |build|
if build.retryable? if build.retryable?
Ci::Build.retry(build) Ci::Build.retry(build, current_user)
end end
end end
......
class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_update_environment!, only: [:destroy]
before_action :environment, only: [:show, :destroy]
def index
@environments = project.environments
end
def show
@deployments = environment.deployments.order(id: :desc).page(params[:page])
end
def new
@environment = project.environments.new
end
def create
@environment = project.environments.create(create_params)
if @environment.persisted?
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
else
render 'new'
end
end
def destroy
if @environment.destroy
flash[:notice] = 'Environment was successfully removed.'
else
flash[:alert] = 'Failed to remove environment.'
end
redirect_to namespace_project_environments_path(project.namespace, project)
end
private
def create_params
params.require(:environment).permit(:name)
end
def environment
@environment ||= project.environments.find(params[:id])
end
end
...@@ -43,7 +43,7 @@ class Projects::GitHttpController < Projects::ApplicationController ...@@ -43,7 +43,7 @@ class Projects::GitHttpController < Projects::ApplicationController
return if project && project.public? && upload_pack? return if project && project.public? && upload_pack?
authenticate_or_request_with_http_basic do |login, password| 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? if auth_result.type == :ci && upload_pack?
@ci = true @ci = true
......
...@@ -204,10 +204,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -204,10 +204,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil) @merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active? if params[:merge_when_build_succeeds].present?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) if @merge_request.pipeline && @merge_request.pipeline.active?
.execute(@merge_request) MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
@status = :merge_when_build_succeeds .execute(@merge_request)
@status = :merge_when_build_succeeds
elsif @merge_request.pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
else
@status = :failed
end
else else
MergeWorker.perform_async(@merge_request.id, current_user.id, params) MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success @status = :success
......
...@@ -32,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -32,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def retry def retry
pipeline.retry_failed pipeline.retry_failed(current_user)
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end end
......
class Projects::ProjectMembersController < Projects::ApplicationController class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
# Authorize # Authorize
before_action :authorize_admin_project_member!, except: [:leave, :index] before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index def index
@project_members = @project.project_members @project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) @project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
if params[:search].present? if params[:search].present?
users = @project.users.search(params[:search]).to_a users = @project.users.search(params[:search]).to_a
...@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC') @project_members = @project_members.order('access_level DESC')
@group = @project.group @group = @project.group
if @group if @group
@group_members = @group.group_members @group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) @group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present? if params[:search].present?
users = @group.users.search(params[:search]).to_a users = @group.users.search(params[:search]).to_a
...@@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
end end
def leave
@project_member = @project.project_members.find_by(user_id: current_user)
if can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy
respond_to do |format|
format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
format.js { head :ok }
end
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
else
render_403
end
end
end
def apply_import def apply_import
source_project = Project.find(params[:source_project_id]) source_project = Project.find(params[:source_project_id])
...@@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def member_params def member_params
params.require(:project_member).permit(:user_id, :access_level) params.require(:project_member).permit(:user_id, :access_level)
end end
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
end end
class Projects::TodosController < Projects::ApplicationController
def create
todos = TodoService.new.mark_todo(issuable, current_user)
render json: {
todo: todos,
count: current_user.todos.pending.count,
}
end
def update
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: {
count: current_user.todos.pending.count,
}
end
private
def issuable
@issuable ||= begin
case params[:issuable_type]
when "issue"
@project.issues.find(params[:issuable_id])
when "merge_request"
@project.merge_requests.find(params[:issuable_id])
end
end
end
end
...@@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController
if @page if @page
render 'show' render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id]) elsif file = @project_wiki.find_file(params[:id], params[:version_id])
response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
if file.on_disk? if file.on_disk?
send_file file.on_disk_path, disposition: 'inline' send_file file.on_disk_path, disposition: 'inline'
else else
......
...@@ -40,7 +40,7 @@ class SessionsController < Devise::SessionsController ...@@ -40,7 +40,7 @@ class SessionsController < Devise::SessionsController
# Handle an "initial setup" state, where there's only one user, it's an admin, # Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change. # and they require a password change.
def check_initial_setup def check_initial_setup
return unless User.count == 1 return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
user = User.admins.last user = User.admins.last
......
...@@ -12,7 +12,7 @@ class NotesFinder ...@@ -12,7 +12,7 @@ class NotesFinder
when "commit" when "commit"
project.notes.for_commit_id(target_id).non_diff_notes project.notes.for_commit_id(target_id).non_diff_notes
when "issue" when "issue"
project.issues.find(target_id).notes.inc_author project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
......
...@@ -51,7 +51,7 @@ class SnippetsFinder ...@@ -51,7 +51,7 @@ class SnippetsFinder
snippets = project.snippets.fresh snippets = project.snippets.fresh
if current_user if current_user
if project.team.member?(current_user.id) || current_user.admin? if project.team.member?(current_user) || current_user.admin?
snippets snippets
else else
snippets.public_and_internal snippets.public_and_internal
......
...@@ -36,7 +36,7 @@ class TodosFinder ...@@ -36,7 +36,7 @@ class TodosFinder
private private
def action_id? def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i) action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
end end
def action_id def action_id
......
...@@ -116,7 +116,7 @@ module BlobHelper ...@@ -116,7 +116,7 @@ module BlobHelper
end end
def blob_text_viewable?(blob) def blob_text_viewable?(blob)
blob && blob.text? && !blob.lfs_pointer? blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
end end
def blob_size(blob) def blob_size(blob)
......
...@@ -14,4 +14,8 @@ module BranchesHelper ...@@ -14,4 +14,8 @@ module BranchesHelper
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end end
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
end
end end
...@@ -17,7 +17,15 @@ module ButtonHelper ...@@ -17,7 +17,15 @@ module ButtonHelper
def clipboard_button(data = {}) def clipboard_button(data = {})
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
class: 'btn btn-clipboard', class: "btn",
data: data,
type: :button
end
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data, data: data,
type: :button type: :button
end end
......
...@@ -38,10 +38,10 @@ module CiStatusHelper ...@@ -38,10 +38,10 @@ module CiStatusHelper
icon(icon_name + ' fw') icon(icon_name + ' fw')
end end
def render_commit_status(commit, tooltip_placement: 'auto left') def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
project = commit.project project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit) path = builds_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement) render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass)
end end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left') def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
...@@ -57,10 +57,10 @@ module CiStatusHelper ...@@ -57,10 +57,10 @@ module CiStatusHelper
private private
def render_status_with_link(type, status, path, tooltip_placement) def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
link_to ci_icon_for_status(status), link_to ci_icon_for_status(status),
path, path,
class: "ci-status-link ci-status-icon-#{status.dasherize}", class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
title: "#{type.titleize}: #{ci_label_for_status(status)}", title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement } data: { toggle: 'tooltip', placement: tooltip_placement }
end end
......
...@@ -16,6 +16,16 @@ module CommitsHelper ...@@ -16,6 +16,16 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer)) commit_person_link(commit, options.merge(source: :committer))
end end
def commit_author_avatar(commit, options = {})
options = options.merge(source: :author)
user = commit.send(options[:source])
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_email = user.try(:email) || source_email
image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
end
def image_diff_class(diff) def image_diff_class(diff)
if diff.deleted_file if diff.deleted_file
"deleted" "deleted"
...@@ -102,24 +112,24 @@ module CommitsHelper ...@@ -102,24 +112,24 @@ module CommitsHelper
if current_controller?(:projects, :commits) if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path) if @repo.blob_at(commit.id, @path)
return link_to( return link_to(
"Browse File »", "Browse File",
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)), tree_join(commit.id, @path)),
class: "pull-right" class: "btn btn-default"
) )
elsif @path.present? elsif @path.present?
return link_to( return link_to(
"Browse Directory »", "Browse Directory",
namespace_project_tree_path(project.namespace, project, namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)), tree_join(commit.id, @path)),
class: "pull-right" class: "btn btn-default"
) )
end end
end end
link_to( link_to(
"Browse Files", "Browse Files",
namespace_project_tree_path(project.namespace, project, commit), namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right" class: "btn btn-default"
) )
end end
...@@ -129,7 +139,7 @@ module CommitsHelper ...@@ -129,7 +139,7 @@ module CommitsHelper
tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
if can_collaborate_with_project? if can_collaborate_with_project?
btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil? btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project) elsif can?(current_user, :fork_project, @project)
continue_params = { continue_params = {
...@@ -141,7 +151,7 @@ module CommitsHelper ...@@ -141,7 +151,7 @@ module CommitsHelper
namespace_key: current_user.namespace.id, namespace_key: current_user.namespace.id,
continue: continue_params) continue: continue_params)
btn_class = "btn btn-grouped btn-close" unless btn_class.nil? btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end end
...@@ -153,7 +163,7 @@ module CommitsHelper ...@@ -153,7 +163,7 @@ module CommitsHelper
tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project? if can_collaborate_with_project?
btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil? btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project) elsif can?(current_user, :fork_project, @project)
continue_params = { continue_params = {
...@@ -187,12 +197,10 @@ module CommitsHelper ...@@ -187,12 +197,10 @@ module CommitsHelper
source_email = clean(commit.send "#{options[:source]}_email".to_sym) source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name person_name = user.try(:name) || source_name
person_email = user.try(:email) || source_email
text = text =
if options[:avatar] if options[:avatar]
avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "") %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
%Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
else else
person_name person_name
end end
......
...@@ -135,6 +135,11 @@ module DiffHelper ...@@ -135,6 +135,11 @@ module DiffHelper
toggle_whitespace_link(url, options) toggle_whitespace_link(url, options)
end end
def diff_compare_whitespace_link(project, from, to, options)
url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace)
toggle_whitespace_link(url, options)
end
private private
def hide_whitespace? def hide_whitespace?
......
...@@ -13,10 +13,23 @@ ...@@ -13,10 +13,23 @@
# merge_request_path(merge_request) # merge_request_path(merge_request)
# #
module GitlabRoutingHelper module GitlabRoutingHelper
# Project
def project_path(project, *args) def project_path(project, *args)
namespace_project_path(project.namespace, project, *args) namespace_project_path(project.namespace, project, *args)
end end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def project_files_path(project, *args) def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref) namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end end
...@@ -29,6 +42,10 @@ module GitlabRoutingHelper ...@@ -29,6 +42,10 @@ module GitlabRoutingHelper
namespace_project_pipelines_path(project.namespace, project, *args) namespace_project_pipelines_path(project.namespace, project, *args)
end end
def project_environments_path(project, *args)
namespace_project_environments_path(project.namespace, project, *args)
end
def project_builds_path(project, *args) def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args) namespace_project_builds_path(project.namespace, project, *args)
end end
...@@ -41,10 +58,6 @@ module GitlabRoutingHelper ...@@ -41,10 +58,6 @@ module GitlabRoutingHelper
activity_namespace_project_path(project.namespace, project, *args) activity_namespace_project_path(project.namespace, project, *args)
end end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def runners_path(project, *args) def runners_path(project, *args)
namespace_project_runners_path(project.namespace, project, *args) namespace_project_runners_path(project.namespace, project, *args)
end end
...@@ -65,14 +78,6 @@ module GitlabRoutingHelper ...@@ -65,14 +78,6 @@ module GitlabRoutingHelper
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def issue_url(entity, *args) def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args) namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end end
...@@ -92,4 +97,56 @@ module GitlabRoutingHelper ...@@ -92,4 +97,56 @@ module GitlabRoutingHelper
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity) toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end end
end end
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
end
def project_member_path(project_member, *args)
namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def request_access_project_members_path(project, *args)
request_access_namespace_project_project_members_path(project.namespace, project)
end
def leave_project_members_path(project, *args)
leave_namespace_project_project_members_path(project.namespace, project)
end
def approve_access_request_project_member_path(project_member, *args)
approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def resend_invite_project_member_path(project_member, *args)
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
# Groups
## Members
def group_members_url(group, *args)
group_group_members_url(group, *args)
end
def group_member_path(group_member, *args)
group_group_member_path(group_member.source, group_member)
end
def request_access_group_members_path(group, *args)
request_access_group_group_members_path(group)
end
def leave_group_members_path(group, *args)
leave_group_group_members_path(group)
end
def approve_access_request_group_member_path(group_member, *args)
approve_access_request_group_group_member_path(group_member.source, group_member)
end
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
end
end end
module GroupsHelper module GroupsHelper
def remove_user_from_group_message(group, member)
if member.user
"Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
else
"Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
end
end
def leave_group_message(group)
"Are you sure you want to leave \"#{group}\" group?"
end
def should_user_see_group_roles?(user, group)
if user
user.is_admin? || group.members.exists?(user_id: user.id)
else
false
end
end
def can_change_group_visibility_level?(group) def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group) can?(current_user, :change_visibility_level, group)
end end
......
...@@ -67,6 +67,12 @@ module IssuablesHelper ...@@ -67,6 +67,12 @@ module IssuablesHelper
end end
end end
def has_todo(issuable)
unless current_user.nil?
current_user.todos.find_by(target_id: issuable.id, state: :pending)
end
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
module MembersHelper
# Returns a `<action>_<source>_member` association, e.g.:
# - admin_project_member, update_project_member, destroy_project_member
# - admin_group_member, update_group_member, destroy_group_member
def action_member_permission(action, member)
"#{action}_#{member.type.underscore}".to_sym
end
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
text = 'Are you sure you want to '
action =
if member.request?
if member.user == user
'withdraw your access request for'
else
"deny #{member.user.name}'s request to join"
end
elsif member.invite?
"revoke the invitation for #{member.invite_email} to join"
else
"remove #{member.user.name} from"
end
text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
end
def remove_member_title(member)
text = " from #{member.real_source_type.humanize(capitalize: false)}"
text.prepend(member.request? ? 'Deny access request' : 'Remove user')
end
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
end
...@@ -12,10 +12,10 @@ module NavHelper ...@@ -12,10 +12,10 @@ module NavHelper
end end
def page_sidebar_class def page_sidebar_class
if nav_menu_collapsed? if pinned_nav?
"page-sidebar-collapsed" "page-sidebar-expanded page-sidebar-pinned"
else else
"page-sidebar-expanded" "page-sidebar-collapsed"
end end
end end
...@@ -36,7 +36,15 @@ module NavHelper ...@@ -36,7 +36,15 @@ module NavHelper
end end
def nav_header_class def nav_header_class
class_name = " with-horizontal-nav" if defined?(nav) && nav class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-expanded header-pinned-nav"
else
class_name << " header-collapsed"
end
class_name class_name
end end
...@@ -47,4 +55,8 @@ module NavHelper ...@@ -47,4 +55,8 @@ module NavHelper
def nav_control_class def nav_control_class
"nav-control" if current_user "nav-control" if current_user
end end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end end
...@@ -61,4 +61,23 @@ module NotificationsHelper ...@@ -61,4 +61,23 @@ module NotificationsHelper
end end
end 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 end
module ProjectsHelper module ProjectsHelper
def remove_from_project_team_message(project, member)
if member.user
"You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
else
"You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
end
end
def link_to_project(project) def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name') title = content_tag(:span, project.name, class: 'project-name')
...@@ -49,7 +41,7 @@ module ProjectsHelper ...@@ -49,7 +41,7 @@ module ProjectsHelper
author_html = author_html.html_safe author_html = author_html.html_safe
if opts[:name] if opts[:name]
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else else
title = opts[:title].sub(":name", sanitize(author.name)) title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
...@@ -115,14 +107,6 @@ module ProjectsHelper ...@@ -115,14 +107,6 @@ module ProjectsHelper
end end
end end
def user_max_access_in_project(user_id, project)
level = project.team.max_member_access(user_id)
if level
Gitlab::Access.options_with_owner.key(level)
end
end
def license_short_name(project) def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil? return 'LICENSE' if project.repository.license_key.nil?
...@@ -156,6 +140,10 @@ module ProjectsHelper ...@@ -156,6 +140,10 @@ module ProjectsHelper
nav_tabs << :container_registry nav_tabs << :container_registry
end end
if can?(current_user, :read_environment, project)
nav_tabs << :environments
end
if can?(current_user, :admin_project, project) if can?(current_user, :admin_project, project)
nav_tabs << :settings nav_tabs << :settings
end end
...@@ -286,10 +274,6 @@ module ProjectsHelper ...@@ -286,10 +274,6 @@ module ProjectsHelper
end end
end end
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
def new_readme_path def new_readme_path
ref = @repository.root_ref if @repository ref = @repository.root_ref if @repository
ref ||= 'master' ref ||= 'master'
......
...@@ -20,7 +20,6 @@ module TimeHelper ...@@ -20,7 +20,6 @@ module TimeHelper
end end
end end
def date_from_to(from, to) def date_from_to(from, to)
"#{from.to_s(:short)} - #{to.to_s(:short)}" "#{from.to_s(:short)} - #{to.to_s(:short)}"
end end
......
...@@ -12,6 +12,7 @@ module TodosHelper ...@@ -12,6 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your' when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'marked this as a Todo for'
end end
end end
......
module Emails
module Groups
def group_access_granted_email(group_member_id)
@group_member = GroupMember.find(group_member_id)
@group = @group_member.group
@target_url = group_url(@group)
@current_user = @group_member.user
mail(to: @group_member.user.notification_email,
subject: subject("Access to group was granted"))
end
def group_member_invited_email(group_member_id, token)
@group_member = GroupMember.find group_member_id
@group = @group_member.group
@token = token
@target_url = group_url(@group)
@current_user = @group_member.user
mail(to: @group_member.invite_email,
subject: "Invitation to join group #{@group.name}")
end
def group_invite_accepted_email(group_member_id)
@group_member = GroupMember.find group_member_id
return if @group_member.created_by.nil?
@group = @group_member.group
@target_url = group_url(@group)
@current_user = @group_member.created_by
mail(to: @group_member.created_by.notification_email,
subject: subject("Invitation accepted"))
end
def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
return if created_by_id.nil?
@group = Group.find(group_id)
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
@target_url = group_url(@group)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
end
end
module Emails
module Members
extend ActiveSupport::Concern
include MembersHelper
included do
helper_method :member_source, :member
end
def member_access_requested_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
mail(to: admins,
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_access_granted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
mail(to: member.user.notification_email,
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
end
def member_access_denied_email(member_source_type, source_id, user_id)
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
requester = User.find(user_id)
mail(to: requester.notification_email,
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
end
def member_invited_email(member_source_type, member_id, token)
@member_source_type = member_source_type
@member_id = member_id
@token = token
mail(to: member.invite_email,
subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
end
def member_invite_accepted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
return unless member.created_by
mail(to: member.created_by.notification_email,
subject: subject('Invitation accepted'))
end
def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
return unless created_by_id
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
@invite_email = invite_email
inviter = User.find(created_by_id)
mail(to: inviter.notification_email,
subject: subject('Invitation declined'))
end
def member
@member ||= Member.find(@member_id)
end
def member_source
@member_source ||= member.source
end
private
def member_source_class
@member_source_type.classify.constantize
end
end
end
module Emails module Emails
module Projects module Projects
def project_access_granted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user
mail(to: @project_member.user.notification_email,
subject: subject("Access to project was granted"))
end
def project_member_invited_email(project_member_id, token)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@token = token
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user
mail(to: @project_member.invite_email,
subject: "Invitation to join project #{@project.name_with_namespace}")
end
def project_invite_accepted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
return if @project_member.created_by.nil?
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.created_by
mail(to: @project_member.created_by.notification_email,
subject: subject("Invitation accepted"))
end
def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
return if created_by_id.nil?
@project = Project.find(project_id)
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
@target_url = namespace_project_url(@project.namespace, @project)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
def project_was_moved_email(project_id, user_id, old_path_with_namespace) def project_was_moved_email(project_id, user_id, old_path_with_namespace)
@current_user = @user = User.find user_id @current_user = @user = User.find user_id
@project = Project.find project_id @project = Project.find project_id
......
...@@ -6,13 +6,15 @@ class Notify < BaseMailer ...@@ -6,13 +6,15 @@ class Notify < BaseMailer
include Emails::Notes include Emails::Notes
include Emails::Projects include Emails::Projects
include Emails::Profile include Emails::Profile
include Emails::Groups
include Emails::Builds include Emails::Builds
include Emails::Members
add_template_helper MergeRequestsHelper add_template_helper MergeRequestsHelper
add_template_helper DiffHelper add_template_helper DiffHelper
add_template_helper BlobHelper add_template_helper BlobHelper
add_template_helper EmailsHelper add_template_helper EmailsHelper
add_template_helper MembersHelper
add_template_helper GitlabRoutingHelper
def test_email(recipient_email, subject, body) def test_email(recipient_email, subject, body)
mail(to: recipient_email, mail(to: recipient_email,
......
...@@ -9,7 +9,6 @@ class Ability ...@@ -9,7 +9,6 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject) when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject) when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject) when Issue then issue_abilities(user, subject)
when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject) when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject)
...@@ -19,6 +18,7 @@ class Ability ...@@ -19,6 +18,7 @@ class Ability
when GroupMember then group_member_abilities(user, subject) when GroupMember then group_member_abilities(user, subject)
when ProjectMember then project_member_abilities(user, subject) when ProjectMember then project_member_abilities(user, subject)
when User then user_abilities when User then user_abilities
when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
else [] else []
end.concat(global_abilities(user)) end.concat(global_abilities(user))
end end
...@@ -187,6 +187,8 @@ class Ability ...@@ -187,6 +187,8 @@ class Ability
project_report_rules project_report_rules
elsif team.guest?(user) elsif team.guest?(user)
project_guest_rules project_guest_rules
else
[]
end end
end end
...@@ -228,6 +230,8 @@ class Ability ...@@ -228,6 +230,8 @@ class Ability
:read_build, :read_build,
:read_container_image, :read_container_image,
:read_pipeline, :read_pipeline,
:read_environment,
:read_deployment
] ]
end end
...@@ -246,6 +250,8 @@ class Ability ...@@ -246,6 +250,8 @@ class Ability
:push_code, :push_code,
:create_container_image, :create_container_image,
:update_container_image, :update_container_image,
:create_environment,
:create_deployment
] ]
end end
...@@ -263,6 +269,8 @@ class Ability ...@@ -263,6 +269,8 @@ class Ability
@project_master_rules ||= project_dev_rules + [ @project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches, :push_code_to_protected_branches,
:update_project_snippet, :update_project_snippet,
:update_environment,
:update_deployment,
:admin_milestone, :admin_milestone,
:admin_project_snippet, :admin_project_snippet,
:admin_project_member, :admin_project_member,
...@@ -273,7 +281,9 @@ class Ability ...@@ -273,7 +281,9 @@ class Ability
:admin_commit_status, :admin_commit_status,
:admin_build, :admin_build,
:admin_container_image, :admin_container_image,
:admin_pipeline :admin_pipeline,
:admin_environment,
:admin_deployment
] ]
end end
...@@ -317,6 +327,8 @@ class Ability ...@@ -317,6 +327,8 @@ class Ability
unless project.builds_enabled unless project.builds_enabled
rules += named_abilities('build') rules += named_abilities('build')
rules += named_abilities('pipeline') rules += named_abilities('pipeline')
rules += named_abilities('environment')
rules += named_abilities('deployment')
end end
unless project.container_registry_enabled unless project.container_registry_enabled
...@@ -511,10 +523,6 @@ class Ability ...@@ -511,10 +523,6 @@ class Ability
end end
end end
def external_issue_abilities(user, subject)
project_abilities(user, subject.project)
end
private private
def restricted_public_level? def restricted_public_level?
...@@ -533,7 +541,7 @@ class Ability ...@@ -533,7 +541,7 @@ class Ability
def filter_confidential_issues_abilities(user, issue, rules) def filter_confidential_issues_abilities(user, issue, rules)
return rules if user.admin? || !issue.confidential? 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(:admin_issue)
rules.delete(:read_issue) rules.delete(:read_issue)
rules.delete(:update_issue) rules.delete(:update_issue)
......
...@@ -24,7 +24,7 @@ class Blob < SimpleDelegator ...@@ -24,7 +24,7 @@ class Blob < SimpleDelegator
end end
def only_display_raw? def only_display_raw?
size && size > 5.megabytes size && truncated?
end end
def svg? def svg?
......
...@@ -11,6 +11,8 @@ module Ci ...@@ -11,6 +11,8 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
...@@ -38,7 +40,7 @@ module Ci ...@@ -38,7 +40,7 @@ module Ci
new_build.save new_build.save
end end
def retry(build) def retry(build, user = nil)
new_build = Ci::Build.new(status: 'pending') new_build = Ci::Build.new(status: 'pending')
new_build.ref = build.ref new_build.ref = build.ref
new_build.tag = build.tag new_build.tag = build.tag
...@@ -52,6 +54,7 @@ module Ci ...@@ -52,6 +54,7 @@ module Ci
new_build.stage = build.stage new_build.stage = build.stage
new_build.stage_idx = build.stage_idx new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request new_build.trigger_request = build.trigger_request
new_build.user = user
new_build.save new_build.save
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build new_build
...@@ -73,6 +76,17 @@ module Ci ...@@ -73,6 +76,17 @@ module Ci
build.update_coverage build.update_coverage
build.execute_hooks build.execute_hooks
end end
after_transition any => [:success] do |build|
if build.environment.present?
service = CreateDeploymentService.new(build.project, build.user,
environment: build.environment,
sha: build.sha,
ref: build.ref,
tag: build.tag)
service.execute(build)
end
end
end end
def retryable? def retryable?
...@@ -83,10 +97,6 @@ module Ci ...@@ -83,10 +97,6 @@ module Ci
!self.pipeline.statuses.latest.include?(self) !self.pipeline.statuses.latest.include?(self)
end end
def retry
Ci::Build.retry(self)
end
def depends_on_builds def depends_on_builds
# Get builds of the same type # Get builds of the same type
latest_builds = self.pipeline.builds.latest latest_builds = self.pipeline.builds.latest
...@@ -317,7 +327,7 @@ module Ci ...@@ -317,7 +327,7 @@ module Ci
end end
def artifacts? def artifacts?
artifacts_file.exists? !artifacts_expired? && artifacts_file.exists?
end end
def artifacts_metadata? def artifacts_metadata?
...@@ -328,11 +338,15 @@ module Ci ...@@ -328,11 +338,15 @@ module Ci
Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
end end
def erase_artifacts!
remove_artifacts_file!
remove_artifacts_metadata!
end
def erase(opts = {}) def erase(opts = {})
return false unless erasable? return false unless erasable?
remove_artifacts_file! erase_artifacts!
remove_artifacts_metadata!
erase_trace! erase_trace!
update_erased!(opts[:erased_by]) update_erased!(opts[:erased_by])
end end
...@@ -345,6 +359,25 @@ module Ci ...@@ -345,6 +359,25 @@ module Ci
!self.erased_at.nil? !self.erased_at.nil?
end end
def artifacts_expired?
artifacts_expire_at && artifacts_expire_at < Time.now
end
def artifacts_expire_in
artifacts_expire_at - Time.now if artifacts_expire_at
end
def artifacts_expire_in=(value)
self.artifacts_expire_at =
if value
Time.now + ChronicDuration.parse(value)
end
end
def keep_artifacts!
self.update(artifacts_expire_at: nil)
end
private private
def erase_trace! def erase_trace!
...@@ -352,7 +385,7 @@ module Ci ...@@ -352,7 +385,7 @@ module Ci
end end
def update_erased!(user = nil) def update_erased!(user = nil)
self.update(erased_by: user, erased_at: Time.now) self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end end
def yaml_variables def yaml_variables
......
...@@ -76,8 +76,10 @@ module Ci ...@@ -76,8 +76,10 @@ module Ci
builds.running_or_pending.each(&:cancel) builds.running_or_pending.each(&:cancel)
end end
def retry_failed def retry_failed(user)
builds.latest.failed.select(&:retryable?).each(&:retry) builds.latest.failed.select(&:retryable?).each do |build|
Ci::Build.retry(build, user)
end
end end
def latest? def latest?
...@@ -161,6 +163,10 @@ module Ci ...@@ -161,6 +163,10 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end end
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
def notes def notes
Note.for_commit_id(sha) Note.for_commit_id(sha)
end end
......
# == AccessRequestable concern
#
# Contains functionality related to objects that can receive request for access.
#
# Used by Project, and Group.
#
module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
members.create(
access_level: Gitlab::Access::DEVELOPER,
user: user,
requested_at: Time.now.utc)
end
end
...@@ -5,7 +5,7 @@ module Awardable ...@@ -5,7 +5,7 @@ module Awardable
has_many :award_emoji, as: :awardable, dependent: :destroy has_many :award_emoji, as: :awardable, dependent: :destroy
if self < Participable if self < Participable
participant :award_emoji participant :award_emoji_with_associations
end end
end end
...@@ -34,8 +34,12 @@ module Awardable ...@@ -34,8 +34,12 @@ module Awardable
end end
end end
def award_emoji_with_associations
award_emoji.includes(:user)
end
def grouped_awards(with_thumbs: true) def grouped_awards(with_thumbs: true)
awards = award_emoji.group_by(&:name) awards = award_emoji_with_associations.group_by(&:name)
if with_thumbs if with_thumbs
awards[AwardEmoji::UPVOTE_NAME] ||= [] awards[AwardEmoji::UPVOTE_NAME] ||= []
......
class Deployment < ActiveRecord::Base
include InternalId
belongs_to :project, required: true, validate: true
belongs_to :environment, required: true, validate: true
belongs_to :user
belongs_to :deployable, polymorphic: true
validates :sha, presence: true
validates :ref, presence: true
delegate :name, to: :environment, prefix: true
def commit
project.commit(sha)
end
def commit_title
commit.try(:title)
end
def short_sha
Commit.truncate_sha(sha)
end
def last?
self == environment.last_deployment
end
end
class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
has_many :deployments
validates :name,
presence: true,
uniqueness: { scope: :project_id },
length: { within: 0..255 },
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
def last_deployment
deployments.last
end
end
...@@ -3,11 +3,18 @@ require 'carrierwave/orm/activerecord' ...@@ -3,11 +3,18 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace class Group < Namespace
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include AccessRequestable
include Referable include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members alias_method :members, :group_members
has_many :users, through: :group_members has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
has_many :owners,
-> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members,
source: :user
has_many :project_group_links, dependent: :destroy has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, dependent: :destroy, as: :source
...@@ -58,6 +65,10 @@ class Group < Namespace ...@@ -58,6 +65,10 @@ class Group < Namespace
"#{self.class.reference_prefix}#{name}" "#{self.class.reference_prefix}#{name}"
end end
def web_url
Gitlab::Routing.url_helpers.group_url(self)
end
def human_name def human_name
name name
end end
...@@ -83,10 +94,6 @@ class Group < Namespace ...@@ -83,10 +94,6 @@ class Group < Namespace
end end
end end
def owners
@owners ||= group_members.owners.includes(:user).map(&:user)
end
def add_users(user_ids, access_level, current_user = nil) def add_users(user_ids, access_level, current_user = nil)
user_ids.each do |user_id| user_ids.each do |user_id|
Member.add_user(self.group_members, user_id, access_level, current_user) Member.add_user(self.group_members, user_id, access_level, current_user)
......
...@@ -51,10 +51,18 @@ class Issue < ActiveRecord::Base ...@@ -51,10 +51,18 @@ class Issue < ActiveRecord::Base
end end
def self.visible_to_user(user) 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? 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 end
def self.reference_prefix def self.reference_prefix
......
...@@ -26,20 +26,28 @@ class Member < ActiveRecord::Base ...@@ -26,20 +26,28 @@ class Member < ActiveRecord::Base
allow_nil: true allow_nil: true
} }
scope :invite, -> { where(user_id: nil) } scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") } scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
scope :non_pending, -> { non_request.non_invite }
scope :guests, -> { where(access_level: GUEST) } scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) } scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) } scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) } scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) } scope :owners, -> { where(access_level: OWNER) }
scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite? after_create :send_invite, if: :invite?
after_create :create_notification_setting, unless: :invite? after_create :send_request, if: :request?
after_create :post_create_hook, unless: :invite? after_create :create_notification_setting, unless: :pending?
after_update :post_update_hook, unless: :invite? after_create :post_create_hook, unless: :pending?
after_destroy :post_destroy_hook, unless: :invite? after_update :post_update_hook, unless: :pending?
after_destroy :post_destroy_hook, unless: :pending?
after_destroy :post_decline_request, if: :request?
delegate :name, :username, :email, to: :user, prefix: true delegate :name, :username, :email, to: :user, prefix: true
...@@ -96,10 +104,31 @@ class Member < ActiveRecord::Base ...@@ -96,10 +104,31 @@ class Member < ActiveRecord::Base
end end
end end
def real_source_type
source_type
end
def invite? def invite?
self.invite_token.present? self.invite_token.present?
end end
def request?
requested_at.present?
end
def pending?
invite? || request?
end
def accept_request
return false unless request?
updated = self.update(requested_at: nil)
after_accept_request if updated
updated
end
def accept_invite!(new_user) def accept_invite!(new_user)
return false unless invite? return false unless invite?
...@@ -157,6 +186,10 @@ class Member < ActiveRecord::Base ...@@ -157,6 +186,10 @@ class Member < ActiveRecord::Base
# override in subclass # override in subclass
end end
def send_request
# override in subclass
end
def post_create_hook def post_create_hook
system_hook_service.execute_hooks_for(self, :create) system_hook_service.execute_hooks_for(self, :create)
end end
...@@ -177,6 +210,14 @@ class Member < ActiveRecord::Base ...@@ -177,6 +210,14 @@ class Member < ActiveRecord::Base
# override in subclass # override in subclass
end end
def after_accept_request
post_create_hook
end
def post_decline_request
# override in subclass
end
def system_hook_service def system_hook_service
SystemHooksService.new SystemHooksService.new
end end
......
...@@ -8,9 +8,6 @@ class GroupMember < Member ...@@ -8,9 +8,6 @@ class GroupMember < Member
validates_format_of :source_type, with: /\ANamespace\z/ validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
def self.access_level_roles def self.access_level_roles
Gitlab::Access.options_with_owner Gitlab::Access.options_with_owner
end end
...@@ -23,6 +20,11 @@ class GroupMember < Member ...@@ -23,6 +20,11 @@ class GroupMember < Member
access_level access_level
end end
# Because source_type is `Namespace`...
def real_source_type
'Group'
end
private private
def send_invite def send_invite
...@@ -31,6 +33,12 @@ class GroupMember < Member ...@@ -31,6 +33,12 @@ class GroupMember < Member
super super
end end
def send_request
notification_service.new_group_access_request(self)
super
end
def post_create_hook def post_create_hook
notification_service.new_group_member(self) notification_service.new_group_member(self)
...@@ -56,4 +64,10 @@ class GroupMember < Member ...@@ -56,4 +64,10 @@ class GroupMember < Member
super super
end end
def post_decline_request
notification_service.decline_group_access_request(self)
super
end
end end
...@@ -11,8 +11,6 @@ class ProjectMember < Member ...@@ -11,8 +11,6 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) } scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
scope :with_user, ->(user) { where(user_id: user.id) }
before_destroy :delete_member_todos before_destroy :delete_member_todos
...@@ -84,7 +82,7 @@ class ProjectMember < Member ...@@ -84,7 +82,7 @@ class ProjectMember < Member
Gitlab::Access.sym_options Gitlab::Access.sym_options
end end
def access_roles def access_level_roles
Gitlab::Access.options Gitlab::Access.options
end end
end end
...@@ -113,6 +111,12 @@ class ProjectMember < Member ...@@ -113,6 +111,12 @@ class ProjectMember < Member
super super
end end
def send_request
notification_service.new_project_access_request(self)
super
end
def post_create_hook def post_create_hook
unless owner? unless owner?
event_service.join_project(self.project, self.user) event_service.join_project(self.project, self.user)
...@@ -148,6 +152,12 @@ class ProjectMember < Member ...@@ -148,6 +152,12 @@ class ProjectMember < Member
super super
end end
def post_decline_request
notification_service.decline_project_access_request(self)
super
end
def event_service def event_service
EventCreateService.new EventCreateService.new
end end
......
...@@ -88,22 +88,9 @@ class Note < ActiveRecord::Base ...@@ -88,22 +88,9 @@ class Note < ActiveRecord::Base
table = arel_table table = arel_table
pattern = "%#{query}%" pattern = "%#{query}%"
found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id'). Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
where(table[:note].matches(pattern)) where(table[:note].matches(pattern)).
merge(Issue.visible_to_user(as_user))
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
end end
end end
...@@ -200,6 +187,10 @@ class Note < ActiveRecord::Base ...@@ -200,6 +187,10 @@ class Note < ActiveRecord::Base
award_emoji_supported? && contains_emoji_only? award_emoji_supported? && contains_emoji_only?
end end
def emoji_awardable?
!system?
end
def clear_blank_line_code! def clear_blank_line_code!
self.line_code = nil if self.line_code.blank? self.line_code = nil if self.line_code.blank?
end end
......
...@@ -7,7 +7,6 @@ class NotificationSetting < ActiveRecord::Base ...@@ -7,7 +7,6 @@ class NotificationSetting < ActiveRecord::Base
belongs_to :source, polymorphic: true belongs_to :source, polymorphic: true
validates :user, presence: true validates :user, presence: true
validates :source, presence: true
validates :level, presence: true validates :level, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id], validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source", message: "already exists in source",
......
...@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include AccessRequestable
include Referable include Referable
include Sortable include Sortable
include AfterCommitQueue include AfterCommitQueue
...@@ -80,7 +81,7 @@ class Project < ActiveRecord::Base ...@@ -80,7 +81,7 @@ class Project < ActiveRecord::Base
has_one :jira_service, dependent: :destroy has_one :jira_service, dependent: :destroy
has_one :redmine_service, dependent: :destroy has_one :redmine_service, dependent: :destroy
has_one :custom_issue_tracker_service, dependent: :destroy has_one :custom_issue_tracker_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy has_one :external_wiki_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
...@@ -102,8 +103,9 @@ class Project < ActiveRecord::Base ...@@ -102,8 +103,9 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy has_many :protected_branches, dependent: :destroy
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember' has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
has_many :users, through: :project_members alias_method :members, :project_members
has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members
has_many :deploy_keys_projects, dependent: :destroy has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy has_many :users_star_projects, dependent: :destroy
...@@ -125,6 +127,8 @@ class Project < ActiveRecord::Base ...@@ -125,6 +127,8 @@ class Project < ActiveRecord::Base
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
...@@ -146,7 +150,6 @@ class Project < ActiveRecord::Base ...@@ -146,7 +150,6 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_path_regex_message } message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled, validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] } :wiki_enabled, inclusion: { in: [true, false] }
validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
validates :namespace, presence: true validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id
...@@ -589,10 +592,6 @@ class Project < ActiveRecord::Base ...@@ -589,10 +592,6 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end end
def can_have_issues_tracker_id?
self.issues_enabled && !self.default_issues_tracker?
end
def build_missing_services def build_missing_services
services_templates = Service.where(template: true) services_templates = Service.where(template: true)
...@@ -685,16 +684,6 @@ class Project < ActiveRecord::Base ...@@ -685,16 +684,6 @@ class Project < ActiveRecord::Base
end end
end end
def project_member_by_name_or_email(name = nil, email = nil)
user = users.find_by('name like ? or email like ?', name, email)
project_members.where(user: user) if user
end
# Get Team Member record by user id
def project_member_by_id(user_id)
project_members.find_by(user_id: user_id)
end
def name_with_namespace def name_with_namespace
@name_with_namespace ||= begin @name_with_namespace ||= begin
if namespace if namespace
...@@ -704,6 +693,7 @@ class Project < ActiveRecord::Base ...@@ -704,6 +693,7 @@ class Project < ActiveRecord::Base
end end
end end
end end
alias_method :human_name, :name_with_namespace
def path_with_namespace def path_with_namespace
if namespace if namespace
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment