Commit 5174d6c1 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into feature/runner-lock-on-project

* upstream/master: (337 commits)
  Update CHANGELOG for !4659
  Center the header logo for all Devise emails
  Add previews for all customized Devise emails
  Customize the Devise `unlock_instructions` email
  Customize the Devise `reset_password_instructions` email
  Customize the Devise `password_change` emails
  Use gitlab-git 10.2.0
  Use Git cached counters on project show page
  Fix indentation scss-lint errors
  Added title attribute to enties in tree view Closes #18353
  Banzai::Filter::ExternalLinkFilter use XPath
  Reduce queries in IssueReferenceFilter
  Use gitlab_git 10.1.4
  Fixed ordering in Project.find_with_namespace
  Fix images in emails
  Banzai::Filter::UploadLinkFilter use XPath
  Turn Group#owners into a has_many association
  Make project_id nullable
  CHANGELOG [ci skip]
  CHANGELOG [ci skip]
  ...
parents 8a0aeab8 faee4763
This diff is collapsed.
...@@ -52,7 +52,7 @@ gem "browser", '~> 2.0.3' ...@@ -52,7 +52,7 @@ gem "browser", '~> 2.0.3'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem "gitlab_git", '~> 10.0' gem "gitlab_git", '~> 10.2'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
...@@ -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'
......
...@@ -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)
...@@ -124,6 +124,8 @@ GEM ...@@ -124,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)
...@@ -275,7 +277,7 @@ GEM ...@@ -275,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.2.0)
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)
...@@ -398,7 +400,7 @@ GEM ...@@ -398,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)
...@@ -414,6 +416,7 @@ GEM ...@@ -414,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)
...@@ -553,7 +556,6 @@ GEM ...@@ -553,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)
...@@ -839,6 +841,7 @@ DEPENDENCIES ...@@ -839,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)
...@@ -871,7 +874,7 @@ DEPENDENCIES ...@@ -871,7 +874,7 @@ DEPENDENCIES
github-markup (~> 1.3.1) github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0) gitlab_emoji (~> 0.3.0)
gitlab_git (~> 10.0) gitlab_git (~> 10.2)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0) gollum-lib (~> 4.1.0)
...@@ -934,7 +937,6 @@ DEPENDENCIES ...@@ -934,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'
......
class @BlobGitignoreSelector #= require blob/template_selector
constructor: (opts) ->
{
@dropdown
@editor
@$wrapper = @dropdown.closest('.gitignore-selector')
@$filenameInput = $('#file_name')
@data = @dropdown.data('filenames')
} = opts
@dropdown.glDropdown( class @BlobGitignoreSelector extends TemplateSelector
data: @data, requestFile: (query) ->
filterable: true, Api.gitignoreText query.name, @requestFileSuccess.bind(@)
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (gitignore) ->
gitignore.name
)
@toggleGitignoreSelector()
@bindEvents()
bindEvents: ->
@$filenameInput
.on 'keyup blur', (e) =>
@toggleGitignoreSelector()
toggleGitignoreSelector: ->
filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
@$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
onClick: (item, el, e) =>
e.preventDefault()
@requestIgnoreFile(item.name)
requestIgnoreFile: (name) ->
Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
requestIgnoreFileSuccess: (gitignore) ->
@editor.setValue(gitignore.content, 1)
@editor.focus()
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
dropdown: $dropdown,
editor: @editor
)
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
pattern: /(.gitignore)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)
class @BlobLicenseSelector #= require blob/template_selector
licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
constructor: (editor) -> class @BlobLicenseSelector extends TemplateSelector
@$licenseSelector = $('.js-license-selector') requestFile: (query) ->
$fileNameInput = $('#file_name') data =
project: @dropdown.data('project')
fullname: @dropdown.data('fullname')
initialFileNameValue = if $fileNameInput.length Api.licenseText query.id, data, @requestFileSuccess.bind(@)
$fileNameInput.val()
else if $('.editor-file-name').length
$('.editor-file-name').text().trim()
@toggleLicenseSelector(initialFileNameValue)
if $fileNameInput
$fileNameInput.on 'keyup blur', (e) =>
@toggleLicenseSelector($(e.target).val())
$('select.license-select').on 'change', (e) ->
data =
project: $(this).data('project')
fullname: $(this).data('fullname')
Api.licenseText $(this).val(), data, (license) ->
editor.setValue(license.content, -1)
toggleLicenseSelector: (fileName) =>
if @licenseRegex.test(fileName)
@$licenseSelector.show()
else
@$licenseSelector.hide()
class @BlobLicenseSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-license-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobLicenseSelector(
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)
...@@ -12,8 +12,9 @@ class @EditBlob ...@@ -12,8 +12,9 @@ class @EditBlob
$("#file-content").val(@editor.getValue()) $("#file-content").val(@editor.getValue())
@initModePanesAndLinks() @initModePanesAndLinks()
new BlobLicenseSelector(@editor)
new BlobGitignoreSelectors(editor: @editor) new BlobLicenseSelectors { @editor }
new BlobGitignoreSelectors { @editor }
initModePanesAndLinks: -> initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane") @$editModePanes = $(".js-edit-mode-pane")
......
class @TemplateSelector
constructor: (opts = {}) ->
{
@dropdown,
@data,
@pattern,
@wrapper,
@editor,
@fileEndpoint,
@$input = $('#file_name')
} = opts
@buildDropdown()
@bindEvents()
@onFilenameUpdate()
buildDropdown: ->
@dropdown.glDropdown(
data: @data,
filterable: true,
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (item) ->
item.name
)
bindEvents: ->
@$input.on('keyup blur', (e) =>
@onFilenameUpdate()
)
onFilenameUpdate: ->
return unless @$input.length
filenameMatches = @pattern.test(@$input.val().trim())
if not filenameMatches
@wrapper.addClass('hidden')
return
@wrapper.removeClass('hidden')
onClick: (item, el, e) =>
e.preventDefault()
@requestFile(item)
requestFile: (item) ->
# To be implemented on the extending class
# e.g.
# Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess: (file) ->
@editor.setValue(file.content, 1)
@editor.focus()
...@@ -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 ''
......
...@@ -15,6 +15,9 @@ GitLab.GfmAutoComplete = ...@@ -15,6 +15,9 @@ GitLab.GfmAutoComplete =
Members: Members:
template: '<li>${username} <small>${title}</small></li>' template: '<li>${username} <small>${title}</small></li>'
Labels:
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
# Issues and MergeRequests # Issues and MergeRequests
Issues: Issues:
template: '<li><small>${id}</small> ${title}</li>' template: '<li><small>${id}</small> ${title}</li>'
...@@ -176,6 +179,25 @@ GitLab.GfmAutoComplete = ...@@ -176,6 +179,25 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.iid} #{m.title}" search: "#{m.iid} #{m.title}"
@input.atwho
at: '~'
alias: 'labels'
searchKey: 'search'
displayTpl: @Labels.template
insertTpl: '${atwho-at}${title}'
callbacks:
beforeSave: (merges) ->
sanitizeLabelTitle = (title)->
if /\w+\s+\w+/g.test(title)
"\"#{sanitize(title)}\""
else
sanitize(title)
$.map merges, (m) ->
title: sanitizeLabelTitle(m.title)
color: m.color
search: "#{m.title}"
destroyAtWho: -> destroyAtWho: ->
@input.atwho('destroy') @input.atwho('destroy')
...@@ -195,6 +217,8 @@ GitLab.GfmAutoComplete = ...@@ -195,6 +217,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests @input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis # load emojis
@input.atwho 'load', ':', data.emojis @input.atwho 'load', ':', data.emojis
# load labels
@input.atwho 'load', '~', data.labels
# This trigger at.js again # This trigger at.js again
# otherwise we would be stuck with loading until the user types # otherwise we would be stuck with loading until the user types
......
...@@ -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
......
...@@ -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('/')
...@@ -9,7 +9,7 @@ class @MergeRequest ...@@ -9,7 +9,7 @@ class @MergeRequest
# Options: # Options:
# action - String, current controller action # action - String, current controller action
# #
constructor: (@opts) -> constructor: (@opts = {}) ->
this.$el = $('.merge-request') this.$el = $('.merge-request')
this.$('.show-all-commits').on 'click', => this.$('.show-all-commits').on 'click', =>
......
...@@ -88,7 +88,7 @@ class @MergeRequestTabs ...@@ -88,7 +88,7 @@ class @MergeRequestTabs
scrollToElement: (container) -> scrollToElement: (container) ->
if window.location.hash if window.location.hash
navBarHeight = $('.navbar-gitlab').outerHeight() navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
$el = $("#{container} #{window.location.hash}:not(.match)") $el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
......
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
......
...@@ -91,6 +91,10 @@ ...@@ -91,6 +91,10 @@
background-color: $white-light; background-color: $white-light;
border-top: none; border-top: none;
} }
&.top-block .container-fluid {
background-color: inherit;
}
} }
.cover-block { .cover-block {
......
...@@ -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;
......
...@@ -2,8 +2,19 @@ ...@@ -2,8 +2,19 @@
* Application Header * Application Header
* *
*/ */
@mixin tanuki-logo-colors($path-color) {
fill: $path-color;
transition: all 0.8s;
&:hover,
&.highlight {
fill: lighten($path-color, 25%);
transition: all 0.1s;
}
}
header { header {
transition-duration: .3s; transition: padding $sidebar-transition-duration;
&.navbar-empty { &.navbar-empty {
height: $header-height; height: $header-height;
...@@ -79,14 +90,9 @@ header { ...@@ -79,14 +90,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 +114,7 @@ header { ...@@ -108,9 +114,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,25 +202,24 @@ header { ...@@ -198,25 +202,24 @@ header {
} }
} }
.header-collapsed { #tanuki-logo {
margin-left: 0;
.header-content { #tanuki-left-ear,
#tanuki-right-ear,
@media (min-width: $screen-sm-max) { #tanuki-nose {
padding-left: 30px; @include tanuki-logo-colors($tanuki-red);
transition-duration: .3s;
}
} }
}
.tanuki-shape { #tanuki-left-eye,
transition: all 0.8s; #tanuki-right-eye {
@include tanuki-logo-colors($tanuki-orange);
}
&:hover, &.highlight { #tanuki-left-cheek,
fill: rgb(255, 255, 255); #tanuki-right-cheek {
transition: all 0.1s; @include tanuki-logo-colors($tanuki-yellow);
} }
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
......
...@@ -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,70 +39,63 @@ ...@@ -34,70 +39,63 @@
} }
} }
.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;
}
} }
} }
.nav-sidebar {
position: absolute;
top: 50px;
bottom: 65px;
width: $sidebar_width;
overflow-y: auto;
overflow-x: hidden;
.tanuki-shape { @media (min-width: $sidebar-breakpoint) {
transition: all 0.8s; bottom: 115px;
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
} }
}
.nav-sidebar {
margin-top: 22 + $header-height;
margin-bottom: 116px;
transition-duration: .3s;
list-style: none;
overflow: hidden;
&.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 +107,6 @@ ...@@ -109,10 +107,6 @@
svg { svg {
margin-right: 13px; margin-right: 13px;
} }
&.back-link i {
transition-duration: .3s;
}
} }
} }
...@@ -123,37 +117,50 @@ ...@@ -123,37 +117,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 +169,34 @@ ...@@ -162,62 +169,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
...@@ -154,6 +156,11 @@ $warning-message-border: #f0e2bb; ...@@ -154,6 +156,11 @@ $warning-message-border: #f0e2bb;
/* header */ /* header */
$light-grey-header: #faf9f9; $light-grey-header: #faf9f9;
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
$tanuki-yellow: #fca326;
/* /*
* State colors: * State colors:
*/ */
......
...@@ -38,6 +38,10 @@ table { ...@@ -38,6 +38,10 @@ table {
margin: 0 auto; margin: 0 auto;
text-align: left; text-align: left;
width: 600px; width: 600px;
& > td {
text-align: center;
}
} }
&#body { &#body {
......
...@@ -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 {
......
...@@ -66,8 +66,7 @@ ...@@ -66,8 +66,7 @@
font-family: $regular_font; font-family: $regular_font;
} }
.gitignore-selector { .gitignore-selector, .license-selector {
.dropdown { .dropdown {
line-height: 21px; line-height: 21px;
} }
......
.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;
...@@ -33,7 +28,7 @@ ...@@ -33,7 +28,7 @@
.container-fluid { .container-fluid {
position: relative; position: relative;
@media (min-width: $screen-md-max) { @media (min-width: $screen-lg-min) {
.row { .row {
display: flex; display: flex;
-ms-flex-align: center; -ms-flex-align: center;
...@@ -229,13 +224,20 @@ ...@@ -229,13 +224,20 @@
right: 16px; right: 16px;
bottom: 0; bottom: 0;
.btn { @media (max-width: $screen-md-max) {
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-md-max) {
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
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
...@@ -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
......
...@@ -143,6 +143,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -143,6 +143,7 @@ class ProjectsController < Projects::ApplicationController
issues: autocomplete.issues, issues: autocomplete.issues,
milestones: autocomplete.milestones, milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
members: participants members: participants
} }
......
...@@ -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)
...@@ -180,8 +180,8 @@ module BlobHelper ...@@ -180,8 +180,8 @@ module BlobHelper
licenses = Licensee::License.all licenses = Licensee::License.all
@licenses_for_select = { @licenses_for_select = {
Popular: licenses.select(&:featured).map { |license| [license.name, license.key] }, Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
} }
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
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
...@@ -311,7 +321,7 @@ module Ci ...@@ -311,7 +321,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?
...@@ -322,11 +332,15 @@ module Ci ...@@ -322,11 +332,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
...@@ -339,6 +353,25 @@ module Ci ...@@ -339,6 +353,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!
...@@ -346,7 +379,7 @@ module Ci ...@@ -346,7 +379,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?
...@@ -92,10 +94,13 @@ module Ci ...@@ -92,10 +94,13 @@ module Ci
end end
def create_builds(user, trigger_request = nil) def create_builds(user, trigger_request = nil)
##
# We persist pipeline only if there are builds available
#
return unless config_processor return unless config_processor
config_processor.stages.any? do |stage|
CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present? build_builds_for_stages(config_processor.stages, user,
end 'success', trigger_request) && save
end end
def create_next_builds(build) def create_next_builds(build)
...@@ -113,10 +118,10 @@ module Ci ...@@ -113,10 +118,10 @@ module Ci
prior_builds = latest_builds.where.not(stage: next_stages) prior_builds = latest_builds.where.not(stage: next_stages)
prior_status = prior_builds.status prior_status = prior_builds.status
# create builds for next stages based # build builds for next stage that has builds available
next_stages.any? do |stage| # and save pipeline if we have builds
CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present? build_builds_for_stages(next_stages, build.user, prior_status,
end build.trigger_request) && save
end end
def retried def retried
...@@ -137,10 +142,10 @@ module Ci ...@@ -137,10 +142,10 @@ module Ci
@config_processor ||= begin @config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error(e.message) self.yaml_errors = e.message
nil nil
rescue rescue
save_yaml_error("Undefined error") self.yaml_errors = 'Undefined error'
nil nil
end end
end end
...@@ -161,8 +166,23 @@ module Ci ...@@ -161,8 +166,23 @@ 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
private private
def build_builds_for_stages(stages, user, status, trigger_request)
##
# Note that `Array#any?` implements a short circuit evaluation, so we
# build builds only for the first stage that has builds available.
#
stages.any? do |stage|
CreateBuildsService.new(self)
.execute(stage, user, status, trigger_request).present?
end
end
def update_state def update_state
statuses.reload statuses.reload
self.status = if yaml_errors.blank? self.status = if yaml_errors.blank?
...@@ -175,11 +195,5 @@ module Ci ...@@ -175,11 +195,5 @@ module Ci
self.duration = statuses.latest.duration self.duration = statuses.latest.duration
save save
end end
def save_yaml_error(error)
return if self.yaml_errors?
self.yaml_errors = error
update_state
end
end end
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
......
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.
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