Commit 2a87a55f authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into refactor/ci-config-add-entry-error

* master: (189 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
  ...
parents d9ca8401 faee4763
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased) v 8.9.0 (unreleased)
- Fix pipeline status when there are no builds in pipeline
- Fix Error 500 when using closes_issues API with an external issue tracker - Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev) - Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues. - Bulk assign/unassign labels to issues.
...@@ -12,6 +13,7 @@ v 8.9.0 (unreleased) ...@@ -12,6 +13,7 @@ v 8.9.0 (unreleased)
- Fix an issue where note polling stopped working if a window was in the - Fix an issue where note polling stopped working if a window was in the
background during a refresh. background during a refresh.
- Make EmailsOnPushWorker use Sidekiq mailers queue - Make EmailsOnPushWorker use Sidekiq mailers queue
- Redesign all Devise emails. !4297
- Fix wiki page events' webhook to point to the wiki repository - Fix wiki page events' webhook to point to the wiki repository
- Don't show tags for revert and cherry-pick operations - Don't show tags for revert and cherry-pick operations
- Fix issue todo not remove when leave project !4150 (Long Nguyen) - Fix issue todo not remove when leave project !4150 (Long Nguyen)
...@@ -22,24 +24,36 @@ v 8.9.0 (unreleased) ...@@ -22,24 +24,36 @@ v 8.9.0 (unreleased)
- Added descriptions to notification settings dropdown - Added descriptions to notification settings dropdown
- Improve note validation to prevent errors when creating invalid note via API - Improve note validation to prevent errors when creating invalid note via API
- Reduce number of fog gem dependencies - Reduce number of fog gem dependencies
- Implement a fair usage of shared runners
- Remove project notification settings associated with deleted projects - Remove project notification settings associated with deleted projects
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects - Fix 404 page when viewing TODOs that contain milestones or labels in different projects
- Add a metric for the number of new Redis connections created by a transaction
- Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
- Redesign navigation for project pages - Redesign navigation for project pages
- Fix images in sign-up confirmation email
- Added shortcut 'y' for copying a files content hash URL #14470
- Fix groups API to list only user's accessible projects - Fix groups API to list only user's accessible projects
- Fix horizontal scrollbar for long commit message.
- Add Environments and Deployments
- Redesign account and email confirmation emails - Redesign account and email confirmation emails
- Don't fail builds for projects that are deleted - Don't fail builds for projects that are deleted
- Support Docker Registry manifest v1
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8 - Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0 - Use gitlab-shell v3.0.0
- Fixed alignment of download dropdown in merge requests
- Upgrade to jQuery 2 - Upgrade to jQuery 2
- Adds selected branch name to the dropdown toggle
- Use Knapsack to evenly distribute tests across multiple nodes - Use Knapsack to evenly distribute tests across multiple nodes
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load - Don't allow MRs to be merged when commits were added since the last review / page load
- Add DB index on users.state - Add DB index on users.state
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning) - Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Fix race condition on merge when build succeeds
- Links from a wiki page to other wiki pages should be rewritten as expected - Links from a wiki page to other wiki pages should be rewritten as expected
- Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos) - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
- Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393
- Fix issues filter when ordering by milestone - Fix issues filter when ordering by milestone
- Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3 - Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3
- Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid) - Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
...@@ -49,11 +63,14 @@ v 8.9.0 (unreleased) ...@@ -49,11 +63,14 @@ v 8.9.0 (unreleased)
- Add support for using Yubikeys (U2F) for two-factor authentication - Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore - Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature - Remove 'main language' feature
- Toggle whitespace button now available for compare branches diffs #17881
- Pipelines can be canceled only when there are running builds - Pipelines can be canceled only when there are running builds
- Use downcased path to container repository as this is expected path by Docker - Use downcased path to container repository as this is expected path by Docker
- Projects pending deletion will render a 404 page - Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails - Measure queue duration between gitlab-workhorse and Rails
- Added Gfm autocomplete for labels
- Make Omniauth providers specs to not modify global configuration - Make Omniauth providers specs to not modify global configuration
- Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11 - Make authentication service for Container Registry to be compatible with < Docker 1.11
- Add Application Setting to configure Container Registry token expire delay (default 5min) - Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav - Cache assigned issue and merge request counts in sidebar nav
...@@ -70,9 +87,11 @@ v 8.9.0 (unreleased) ...@@ -70,9 +87,11 @@ v 8.9.0 (unreleased)
- Replace Colorize with Rainbow for coloring console output in Rake tasks. - Replace Colorize with Rainbow for coloring console output in Rake tasks.
- Add workhorse controller and API helpers - Add workhorse controller and API helpers
- An indicator is now displayed at the top of the comment field for confidential issues. - An indicator is now displayed at the top of the comment field for confidential issues.
- Show categorised search queries in the search autocomplete
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Improve issuables APIs performance when accessing notes !4471 - Improve issuables APIs performance when accessing notes !4471
- External links now open in a new tab - External links now open in a new tab
- Prevent default actions of disabled buttons and links
- Markdown editor now correctly resets the input value on edit cancellation !4175 - Markdown editor now correctly resets the input value on edit cancellation !4175
- Toggling a task list item in a issue/mr description does not creates a Todo for mentions - Toggling a task list item in a issue/mr description does not creates a Todo for mentions
- Improved UX of date pickers on issue & milestone forms - Improved UX of date pickers on issue & milestone forms
...@@ -90,20 +109,31 @@ v 8.9.0 (unreleased) ...@@ -90,20 +109,31 @@ v 8.9.0 (unreleased)
- New custom icons for navigation - New custom icons for navigation
- Horizontally scrolling navigation on project, group, and profile settings pages - Horizontally scrolling navigation on project, group, and profile settings pages
- Hide global side navigation by default - Hide global side navigation by default
- Fix project Star/Unstar project button tooltip
- Remove tanuki logo from side navigation; center on top nav - Remove tanuki logo from side navigation; center on top nav
- Include user relationships when retrieving award_emoji - Include user relationships when retrieving award_emoji
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
v 8.8.5 (unreleased) - Set inverse_of for Project/Service association to reduce the number of queries
- Ensure branch cleanup regardless of whether the GitHub import process succeeds - Update tanuki logo highlight/loading colors
- Fix todos page throwing errors when you have a project pending deletion - Use Git cached counters for branches and tags on project page
- Reduce number of SQL queries when rendering user references
- Import GitHub repositories respecting the API rate limit v 8.8.5
- Fix importer for GitHub comments on diff - Import GitHub repositories respecting the API rate limit !4166
- Disable Webhooks before proceeding with the GitHub import - Fix todos page throwing errors when you have a project pending deletion !4300
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace - Disable Webhooks before proceeding with the GitHub import !4470
- Fix importer for GitHub comments on diff !4488
- Adjust the SAML control flow to allow LDAP identities to be added to an existing SAML user !4498
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace !4541
- Prevent unauthorized access for projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
- Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
- Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
v 8.8.4 v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493 - Fix LDAP-based login for users with 2FA enabled. !4493
- Added descriptions to notification settings dropdown
- Due date can be removed from milestones
v 8.8.3 v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
...@@ -219,6 +249,9 @@ v 8.8.0 ...@@ -219,6 +249,9 @@ v 8.8.0
v 8.7.7 v 8.7.7
- Fix import by `Any Git URL` broken if the URL contains a space - Fix import by `Any Git URL` broken if the URL contains a space
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
v 8.7.6 v 8.7.6
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
...@@ -381,6 +414,11 @@ v 8.7.0 ...@@ -381,6 +414,11 @@ v 8.7.0
- Add RAW build trace output and button on build page - Add RAW build trace output and button on build page
- Add incremental build trace update into CI API - Add incremental build trace update into CI API
v 8.6.9
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
v 8.6.8 v 8.6.8
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
...@@ -535,6 +573,10 @@ v 8.6.0 ...@@ -535,6 +573,10 @@ v 8.6.0
- Trigger a todo for mentions on commits page - Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests - Let project owners and admins soft delete issues and merge requests
v 8.5.13
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.5.12 v 8.5.12
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
...@@ -696,6 +738,10 @@ v 8.5.0 ...@@ -696,6 +738,10 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos - Add Todos
v 8.4.11
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.4.10 v 8.4.10
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
...@@ -832,6 +878,10 @@ v 8.4.0 ...@@ -832,6 +878,10 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up - Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
v 8.3.10
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.3.9 v 8.3.9
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
...@@ -950,6 +1000,10 @@ v 8.3.0 ...@@ -950,6 +1000,10 @@ v 8.3.0
- Expose Git's version in the admin area - Expose Git's version in the admin area
- Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
v 8.2.6
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.2.5 v 8.2.5
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
......
...@@ -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
...@@ -227,7 +227,6 @@ gem 'gon', '~> 6.0.1' ...@@ -227,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'
......
...@@ -277,7 +277,7 @@ GEM ...@@ -277,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)
...@@ -400,7 +400,7 @@ GEM ...@@ -400,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)
...@@ -556,7 +556,6 @@ GEM ...@@ -556,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)
...@@ -875,7 +874,7 @@ DEPENDENCIES ...@@ -875,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)
...@@ -938,7 +937,6 @@ DEPENDENCIES ...@@ -938,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')
initialFileNameValue = if $fileNameInput.length
$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 = data =
project: $(this).data('project') project: @dropdown.data('project')
fullname: $(this).data('fullname') fullname: @dropdown.data('fullname')
Api.licenseText $(this).val(), data, (license) ->
editor.setValue(license.content, -1)
toggleLicenseSelector: (fileName) => Api.licenseText query.id, data, @requestFileSuccess.bind(@)
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()
...@@ -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
......
...@@ -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) ->
......
...@@ -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)
......
((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)
...@@ -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 {
...@@ -346,6 +353,12 @@ ...@@ -346,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,50 +39,39 @@ ...@@ -34,50 +39,39 @@
} }
} }
.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;
transition-duration: .3s;
.username {
margin-left: 10px;
width: $sidebar_width - 2 * 10px;
font-size: 16px; font-size: 16px;
line-height: 34px; line-height: 36px;
} transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
}
}
@media (min-width: $sidebar-breakpoint) {
.tanuki-shape { bottom: 50px;
transition: all 0.8s;
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
} }
} }
.nav-sidebar { .nav-sidebar {
margin-top: 22 + $header-height; position: absolute;
margin-bottom: 116px; top: 50px;
transition-duration: .3s; bottom: 65px;
list-style: none; width: $sidebar_width;
overflow: hidden; overflow-y: auto;
overflow-x: hidden;
@media (min-width: $sidebar-breakpoint) {
bottom: 115px;
}
&.navbar-collapse { &.navbar-collapse {
padding: 0 !important; padding: 0 !important;
} }
li { li {
width: $sidebar_width;
&.separate-item { &.separate-item {
padding-top: 10px; padding-top: 10px;
margin-top: 10px; margin-top: 10px;
...@@ -90,20 +84,18 @@ ...@@ -90,20 +84,18 @@
} }
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;
} }
...@@ -115,10 +107,6 @@ ...@@ -115,10 +107,6 @@
svg { svg {
margin-right: 13px; margin-right: 13px;
} }
&.back-link i {
transition-duration: .3s;
}
} }
} }
...@@ -129,101 +117,86 @@ ...@@ -129,101 +117,86 @@
} }
} }
.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 {
width: 0;
}
}
.page-sidebar-collapsed {
padding-left: 0;
.sidebar-wrapper {
width: 0;
.nav-sidebar {
width: 0;
li {
width: auto;
a {
span {
display: none; display: none;
} position: absolute;
} left: 0;
} bottom: 0;
height: 50px;
width: $sidebar_width;
line-height: 30px;
@media (min-width: $sidebar-breakpoint) {
display: block;
} }
.collapse-nav a { .fa {
width: 0; transition: transform .15s;
}
i { &.is-active {
display: none; .fa {
transform: rotate(90deg);
} }
} }
}
.sidebar-user { .page-sidebar-collapsed {
width: 0;
padding-left: 0; padding-left: 0;
padding-right: 0;
.username { .sidebar-wrapper {
display: none; width: 0;
}
}
} }
} }
.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;
.commits-row { font-size: 14px;
ul {
margin: 0;
li.commit {
padding: 8px 0;
}
}
.commits-row-date { &:first-child {
font-size: 15px; border-top-width: 0;
line-height: 20px;
margin-bottom: 5px;
} }
} }
li.commit { .commit-row-title {
list-style: none; line-height: 1;
margin-bottom: 7px;
.commit-row-title {
font-size: $list-font-size;
line-height: 20px;
margin-bottom: 2px;
.btn-clipboard {
margin-top: -1px;
}
.notes_count { .notes_count {
float: right; float: right;
margin-right: 10px; margin-right: 10px;
} }
.commit_short_id {
min-width: 65px;
color: $gl-dark-link-color;
font-family: $monospace_font;
}
.str-truncated { .str-truncated {
max-width: 70%; max-width: 70%;
} }
.commit-row-message { .commit-row-message {
color: $gl-dark-link-color; color: $gl-dark-link-color;
&:hover {
text-decoration: underline;
}
} }
.text-expander { .text-expander {
background: #eee; display: inline-block;
color: #555; background: $gray-light;
color: $gl-placeholder-color;
padding: 0 5px; padding: 0 5px;
cursor: pointer; cursor: pointer;
margin-left: 4px; border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
&:hover { &:hover {
background-color: #ddd; background-color: darken($gray-light, 10%);
text-decoration: none;
}
} }
}
.commit-actions {
@media (min-width: $screen-sm-min) {
float: right;
margin-left: $gl-padding;
margin-top: 2px;
font-size: 0;
} }
.btn-transparent {
padding-left: 0;
padding-right: 0;
}
.btn {
&:not(:first-child) {
margin-left: $gl-padding;
}
}
}
.commit-short-id {
font-family: $monospace_font;
font-weight: 600;
}
.commit {
padding: 10px 0;
@media (min-width: $screen-sm-min) {
padding-left: 46px;
}
&:not(:last-child) {
border-bottom: 1px solid #eee;
}
a,
button {
color: $gl-dark-link-color;
vertical-align: baseline;
}
.avatar {
margin-left: -46px;
} }
.item-title { .item-title {
display: inline-block; display: inline-block;
@media (min-width: $screen-sm-min) {
max-width: 70%; 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;
}
}
...@@ -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-header-btn { .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;
...@@ -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 {
...@@ -259,6 +265,10 @@ ul.notes { ...@@ -259,6 +265,10 @@ ul.notes {
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,7 +224,7 @@ ...@@ -229,7 +224,7 @@
right: 16px; right: 16px;
bottom: 0; bottom: 0;
@media (max-width: $screen-lg-min) { @media (max-width: $screen-md-max) {
top: 0; top: 0;
} }
...@@ -238,7 +233,7 @@ ...@@ -238,7 +233,7 @@
right: 0; right: 0;
bottom: 61px; bottom: 61px;
@media (max-width: $screen-lg-min) { @media (max-width: $screen-md-max) {
position: relative; position: relative;
bottom: 0; bottom: 0;
margin-right: 10px; margin-right: 10px;
...@@ -376,6 +371,7 @@ a.deploy-project-label { ...@@ -376,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
......
...@@ -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?
if @merge_request.pipeline && @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request) .execute(@merge_request)
@status = :merge_when_build_succeeds @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
......
...@@ -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
......
...@@ -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?
......
...@@ -42,6 +42,10 @@ module GitlabRoutingHelper ...@@ -42,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
......
...@@ -6,12 +6,6 @@ module MembersHelper ...@@ -6,12 +6,6 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym "#{action}_#{member.type.underscore}".to_sym
end end
def can_see_member_roles?(source:, user: nil)
return false unless user
user.is_admin? || source.members.exists?(user_id: user.id)
end
def remove_member_message(member, user: nil) def remove_member_message(member, user: nil)
user = current_user if defined?(current_user) user = current_user if defined?(current_user)
......
...@@ -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
...@@ -41,7 +41,7 @@ module ProjectsHelper ...@@ -41,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
...@@ -140,6 +140,10 @@ module ProjectsHelper ...@@ -140,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
......
...@@ -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
...@@ -230,6 +230,8 @@ class Ability ...@@ -230,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
...@@ -248,6 +250,8 @@ class Ability ...@@ -248,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
...@@ -265,6 +269,8 @@ class Ability ...@@ -265,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,
...@@ -275,7 +281,9 @@ class Ability ...@@ -275,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
...@@ -319,6 +327,8 @@ class Ability ...@@ -319,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
...@@ -513,10 +523,6 @@ class Ability ...@@ -513,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?
......
...@@ -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?
......
...@@ -40,7 +40,7 @@ module Ci ...@@ -40,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
...@@ -54,6 +54,7 @@ module Ci ...@@ -54,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
...@@ -75,6 +76,17 @@ module Ci ...@@ -75,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?
...@@ -85,10 +97,6 @@ module Ci ...@@ -85,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
......
...@@ -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
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
...@@ -9,6 +9,12 @@ class Group < Namespace ...@@ -9,6 +9,12 @@ class Group < Namespace
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, -> { where(members: { requested_at: nil }) }, 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
...@@ -88,10 +94,6 @@ class Group < Namespace ...@@ -88,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)
......
class JiraIssue < ExternalIssue
end
...@@ -187,6 +187,10 @@ class Note < ActiveRecord::Base ...@@ -187,6 +187,10 @@ class Note < ActiveRecord::Base
award_emoji_supported? && contains_emoji_only? award_emoji_supported? && contains_emoji_only?
end end
def emoji_awardable?
!system?
end
def clear_blank_line_code! def clear_blank_line_code!
self.line_code = nil if self.line_code.blank? self.line_code = nil if self.line_code.blank?
end end
......
...@@ -81,7 +81,7 @@ class Project < ActiveRecord::Base ...@@ -81,7 +81,7 @@ class Project < ActiveRecord::Base
has_one :jira_service, dependent: :destroy has_one :jira_service, dependent: :destroy
has_one :redmine_service, dependent: :destroy has_one :redmine_service, dependent: :destroy
has_one :custom_issue_tracker_service, dependent: :destroy has_one :custom_issue_tracker_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy has_one :external_wiki_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
...@@ -127,6 +127,8 @@ class Project < ActiveRecord::Base ...@@ -127,6 +127,8 @@ class Project < ActiveRecord::Base
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
...@@ -260,7 +262,23 @@ class Project < ActiveRecord::Base ...@@ -260,7 +262,23 @@ class Project < ActiveRecord::Base
# #
# Returns a Project, or nil if no project could be found. # Returns a Project, or nil if no project could be found.
def find_with_namespace(path) def find_with_namespace(path)
where_paths_in([path]).reorder(nil).take namespace_path, project_path = path.split('/', 2)
return unless namespace_path && project_path
namespace_path = connection.quote(namespace_path)
project_path = connection.quote(project_path)
# On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
# any literal matches come first, for this we have to use "BINARY".
# Without this there's still no guarantee in what order MySQL will return
# rows.
binary = Gitlab::Database.mysql? ? 'BINARY' : ''
order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
"AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
where_paths_in([path]).reorder(order_sql).take
end end
# Builds a relation to find multiple projects by their full paths. # Builds a relation to find multiple projects by their full paths.
......
...@@ -243,7 +243,7 @@ class Repository ...@@ -243,7 +243,7 @@ class Repository
end end
def cache_keys def cache_keys
%i(size branch_names tag_names commit_count %i(size branch_names tag_names branch_count tag_count commit_count
readme version contribution_guide changelog readme version contribution_guide changelog
license_blob license_key gitignore) license_blob license_key gitignore)
end end
...@@ -446,7 +446,7 @@ class Repository ...@@ -446,7 +446,7 @@ class Repository
def blob_at(sha, path) def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha) unless Gitlab::Git.blank_ref?(sha)
Gitlab::Git::Blob.find(self, sha, path) Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end end
end end
......
...@@ -18,7 +18,7 @@ class Service < ActiveRecord::Base ...@@ -18,7 +18,7 @@ class Service < ActiveRecord::Base
after_commit :reset_updated_properties after_commit :reset_updated_properties
after_commit :cache_project_has_external_issue_tracker after_commit :cache_project_has_external_issue_tracker
belongs_to :project belongs_to :project, inverse_of: :services
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: Proc.new { |service| service.template? } validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
......
...@@ -2,10 +2,11 @@ module Ci ...@@ -2,10 +2,11 @@ module Ci
class CreateBuildsService class CreateBuildsService
def initialize(pipeline) def initialize(pipeline)
@pipeline = pipeline @pipeline = pipeline
@config = pipeline.config_processor
end end
def execute(stage, user, status, trigger_request = nil) def execute(stage, user, status, trigger_request = nil)
builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request) builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
# check when to create next build # check when to create next build
builds_attrs = builds_attrs.select do |build_attrs| builds_attrs = builds_attrs.select do |build_attrs|
...@@ -19,33 +20,37 @@ module Ci ...@@ -19,33 +20,37 @@ module Ci
end end
end end
builds_attrs.map do |build_attrs|
# don't create the same build twice # don't create the same build twice
unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag, builds_attrs.reject! do |build_attrs|
trigger_request: trigger_request, name: build_attrs[:name]) @pipeline.builds.find_by(ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: trigger_request,
name: build_attrs[:name])
end
builds_attrs.map do |build_attrs|
build_attrs.slice!(:name, build_attrs.slice!(:name,
:commands, :commands,
:tag_list, :tag_list,
:options, :options,
:allow_failure, :allow_failure,
:stage, :stage,
:stage_idx) :stage_idx,
:environment)
build_attrs.merge!(ref: @pipeline.ref, build_attrs.merge!(pipeline: @pipeline,
ref: @pipeline.ref,
tag: @pipeline.tag, tag: @pipeline.tag,
trigger_request: trigger_request, trigger_request: trigger_request,
user: user, user: user,
project: @pipeline.project) project: @pipeline.project)
@pipeline.builds.create!(build_attrs) ##
end # We do not persist new builds here.
end # Those will be persisted when @pipeline is saved.
#
@pipeline.builds.new(build_attrs)
end end
private
def config_processor
@config_processor ||= @pipeline.config_processor
end end
end end
end end
...@@ -8,7 +8,9 @@ module Ci ...@@ -8,7 +8,9 @@ module Ci
return pipeline return pipeline
end end
unless commit if commit
pipeline.sha = commit.id
else
pipeline.errors.add(:base, 'Commit not found') pipeline.errors.add(:base, 'Commit not found')
return pipeline return pipeline
end end
...@@ -18,22 +20,18 @@ module Ci ...@@ -18,22 +20,18 @@ module Ci
return pipeline return pipeline
end end
begin
Ci::Pipeline.transaction do
pipeline.sha = commit.id
unless pipeline.config_processor unless pipeline.config_processor
pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file') pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
raise ActiveRecord::Rollback return pipeline
end end
pipeline.save! pipeline.save!
pipeline.create_builds(current_user)
end unless pipeline.create_builds(current_user)
rescue pipeline.errors.add(:base, 'No builds for this pipeline.')
pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
end end
pipeline.save
pipeline pipeline
end end
......
...@@ -7,15 +7,19 @@ module Ci ...@@ -7,15 +7,19 @@ module Ci
builds = builds =
if current_runner.shared? if current_runner.shared?
# don't run projects which have not enables shared runners builds.
builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }) # don't run projects which have not enabled shared runners
joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }).
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else else
# do run projects which are only assigned to this runner # do run projects which are only assigned to this runner (FIFO)
builds.where(project: current_runner.projects.where(builds_enabled: true)) builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC')
end end
builds = builds.order('created_at ASC')
build = builds.find do |build| build = builds.find do |build|
build.can_be_served?(current_runner) build.can_be_served?(current_runner)
end end
...@@ -35,5 +39,12 @@ module Ci ...@@ -35,5 +39,12 @@ module Ci
rescue StateMachines::InvalidTransition rescue StateMachines::InvalidTransition
nil nil
end end
private
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared).
group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
end
end end
end end
class CreateCommitBuildsService class CreateCommitBuildsService
def execute(project, user, params) def execute(project, user, params)
return false unless project.builds_enabled? return unless project.builds_enabled?
before_sha = params[:checkout_sha] || params[:before] before_sha = params[:checkout_sha] || params[:before]
sha = params[:checkout_sha] || params[:after] sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref] origin_ref = params[:ref]
unless origin_ref && sha.present?
return false
end
ref = Gitlab::Git.ref_name(origin_ref) ref = Gitlab::Git.ref_name(origin_ref)
tag = Gitlab::Git.tag_ref?(origin_ref) tag = Gitlab::Git.tag_ref?(origin_ref)
...@@ -18,23 +14,50 @@ class CreateCommitBuildsService ...@@ -18,23 +14,50 @@ class CreateCommitBuildsService
return false return false
end end
pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) @pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating pipeline when no gitlab-ci.yml is found ##
unless pipeline.ci_yaml_file # Skip creating pipeline if no gitlab-ci.yml is found
#
unless @pipeline.ci_yaml_file
return false return false
end end
# Create a new pipeline ##
pipeline.save!
# Skip creating builds for commits that have [ci skip] # Skip creating builds for commits that have [ci skip]
unless pipeline.skip_ci? # but save pipeline object
# Create builds for commit #
pipeline.create_builds(user) if @pipeline.skip_ci?
return save_pipeline!
end
##
# Skip creating builds when CI config is invalid
# but save pipeline object
#
unless @pipeline.config_processor
return save_pipeline!
end end
pipeline.touch ##
pipeline # Skip creating pipeline object if there are no builds for it.
#
unless @pipeline.create_builds(user)
@pipeline.errors.add(:base, 'No builds created')
return false
end
save_pipeline!
end
private
##
# Create a new pipeline and touch object to calculate status
#
def save_pipeline!
@pipeline.save!
@pipeline.touch
@pipeline
end end
end end
require_relative 'base_service'
class CreateDeploymentService < BaseService
def execute(deployable = nil)
environment = project.environments.find_or_create_by(
name: params[:environment]
)
project.deployments.create(
environment: environment,
ref: params[:ref],
tag: params[:tag],
sha: params[:sha],
user: current_user,
deployable: deployable
)
end
end
...@@ -3,7 +3,7 @@ class GitHooksService ...@@ -3,7 +3,7 @@ class GitHooksService
def execute(user, repo_path, oldrev, newrev, ref) def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path @repo_path = repo_path
@user = Gitlab::ShellEnv.gl_id(user) @user = Gitlab::GlId.gl_id(user)
@oldrev = oldrev @oldrev = oldrev
@newrev = newrev @newrev = newrev
@ref = ref @ref = ref
......
...@@ -11,5 +11,9 @@ module Projects ...@@ -11,5 +11,9 @@ module Projects
def merge_requests def merge_requests
@project.merge_requests.opened.select([:iid, :title]) @project.merge_requests.opened.select([:iid, :title])
end end
def labels
@project.labels.select([:title, :color])
end
end end
end end
.nav-links.sub-nav
%ul{ class: (container_class) }
= nav_link(controller: :background_jobs) do
= link_to admin_background_jobs_path, title: 'Background Jobs' do
%span
Background Jobs
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
%span
Logs
= nav_link(controller: :health_check) do
= link_to admin_health_check_path, title: 'Health Check' do
%span
Health Check
- @no_container = true
- page_title "Background Jobs" - page_title "Background Jobs"
%h3.page-title Background Jobs = render 'admin/background_jobs/head'
%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
%hr %div{ class: (container_class) }
%h3.page-title Background Jobs
%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
.panel.panel-default %hr
.panel.panel-default
.panel-heading Sidekiq running processes .panel-heading Sidekiq running processes
.panel-body .panel-body
- if @sidekiq_processes.empty? - if @sidekiq_processes.empty?
...@@ -42,5 +46,5 @@ ...@@ -42,5 +46,5 @@
.panel.panel-default .panel.panel-default
%iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
.top-area - @no_container = true
= render "admin/dashboard/head"
%div{ class: (container_class) }
.top-area
%ul.nav-links %ul.nav-links
%li{class: ('active' if @scope.nil?)} %li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do = link_to admin_builds_path do
...@@ -19,10 +24,10 @@ ...@@ -19,10 +24,10 @@
- if @all_builds.running_or_pending.any? - if @all_builds.running_or_pending.any?
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
.row-content-block.second-block .row-content-block.second-block
#{(@scope || 'all').capitalize} builds #{(@scope || 'all').capitalize} builds
%ul.content-list %ul.content-list
- if @builds.blank? - if @builds.blank?
%li %li
.nothing-here-block No builds to show .nothing-here-block No builds to show
......
.nav-links.sub-nav
%ul{ class: (container_class) }
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview' do
%span
Overview
= nav_link(controller: [:admin, :projects]) do
= link_to admin_namespaces_projects_path, title: 'Projects' do
%span
Projects
= nav_link(controller: :users) do
= link_to admin_users_path, title: 'Users' do
%span
Users
= nav_link(controller: :groups) do
= link_to admin_groups_path, title: 'Groups' do
%span
Groups
= nav_link path: 'builds#index' do
= link_to admin_builds_path, title: 'Builds' do
%span
Builds
.admin-dashboard.prepend-top-default - @no_container = true
= render "admin/dashboard/head"
%div{ class: (container_class) }
.admin-dashboard.prepend-top-default
.row .row
.col-md-4 .col-md-4
%h4 Statistics %h4 Statistics
......
- @no_container = true
- page_title "Groups" - page_title "Groups"
%h3.page-title = render "admin/dashboard/head"
%div{ class: (container_class) }
%h3.page-title
Groups (#{number_with_delimiter(@groups.total_count)}) Groups (#{number_with_delimiter(@groups.total_count)})
%p.light %p.light
Group allows you to keep projects organized. Group allows you to keep projects organized.
Use groups for uniting related projects. Use groups for uniting related projects.
.top-area .top-area
.nav-search .nav-search
= form_tag admin_groups_path, method: :get, class: 'form-inline' do = form_tag admin_groups_path, method: :get, class: 'form-inline' do
= hidden_field_tag :sort, @sort = hidden_field_tag :sort, @sort
...@@ -34,8 +38,8 @@ ...@@ -34,8 +38,8 @@
= sort_title_oldest_updated = sort_title_oldest_updated
= link_to 'New Group', new_admin_group_path, class: "btn btn-new" = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
%ul.content-list %ul.content-list
- @groups.each do |group| - @groups.each do |group|
= render 'group', group: group = render 'group', group: group
= paginate @groups, theme: "gitlab" = paginate @groups, theme: "gitlab"
- @no_container = true
- page_title "Health Check" - page_title "Health Check"
= render 'admin/background_jobs/head'
%h3.page-title %div{ class: (container_class) }
%h3.page-title
Health Check Health Check
.bs-callout.clearfix .bs-callout.clearfix
.pull-left .pull-left
%p %p
Access token is Access token is
...@@ -12,7 +15,7 @@ ...@@ -12,7 +15,7 @@
data: { confirm: 'Are you sure you want to reset the health check token?' } do data: { confirm: 'Are you sure you want to reset the health check token?' } do
= icon('refresh') = icon('refresh')
Reset health check access token Reset health check access token
%p.light %p.light
Health information can be retrieved as plain text, JSON, or XML using: Health information can be retrieved as plain text, JSON, or XML using:
%ul %ul
%li %li
...@@ -22,7 +25,7 @@ ...@@ -22,7 +25,7 @@
%li %li
%code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml)
%p.light %p.light
You can also ask for the status of specific services: You can also ask for the status of specific services:
%ul %ul
%li %li
...@@ -32,8 +35,8 @@ ...@@ -32,8 +35,8 @@
%li %li
%code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations)
%hr %hr
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Current Status: Current Status:
- if @errors.blank? - if @errors.blank?
......
- @no_container = true
- page_title "Logs" - page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger, - loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger, Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger] Gitlab::RepositoryCheckLogger]
%ul.nav-links.log-tabs = render 'admin/background_jobs/head'
%div{ class: (container_class) }
%ul.nav-links.log-tabs
- loggers.each do |klass| - loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}", = link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab' 'data-toggle' => 'tab'
.row-content-block .row-content-block
To prevent performance issues admin logs output the last 2000 lines To prevent performance issues admin logs output the last 2000 lines
.tab-content .tab-content
- loggers.each do |klass| - loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
id: klass::file_name_noext } id: klass::file_name_noext }
......
- @no_container = true
- page_title "Projects" - page_title "Projects"
= render 'shared/show_aside' = render 'shared/show_aside'
= render "admin/dashboard/head"
.row.prepend-top-default %div{ class: (container_class) }
.row.prepend-top-default
%aside.col-md-3 %aside.col-md-3
.panel.admin-filter .panel.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do = form_tag admin_namespaces_projects_path, method: :get, class: '' do
......
- @no_container = true
- page_title "Users" - page_title "Users"
= render 'shared/show_aside' = render 'shared/show_aside'
= render "admin/dashboard/head"
.admin-filter %div{ class: (container_class) }
.admin-filter
%ul.nav-links %ul.nav-links
%li{class: "#{'active' unless params[:filter]}"} %li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do = link_to admin_users_path do
...@@ -68,7 +71,7 @@ ...@@ -68,7 +71,7 @@
%i.fa.fa-search %i.fa.fa-search
.panel.panel-default .panel.panel-default
%ul.well-list %ul.well-list
- @users.each do |user| - @users.each do |user|
%li %li
...@@ -104,4 +107,4 @@ ...@@ -104,4 +107,4 @@
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- if user.can_be_removed? - if user.can_be_removed?
= link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
= paginate @users, theme: "gitlab" = paginate @users, theme: "gitlab"
.center
#content
%h2 Hello, #{@resource.name}!
%p
The password for your GitLab account on
#{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}
has successfully been changed.
%p
If you did not initiate this change, please contact your administrator
immediately.
Hello, <%= @resource.name %>!
The password for your GitLab account on <%= Gitlab.config.gitlab.url %>
has successfully been changed.
If you did not initiate this change, please contact your administrator
immediately.
<p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
.center
#content
%h2 Hello, #{@resource.name}!
%p
Someone, hopefully you, has requested to reset the password for your
GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}.
%p
If you did not perform this request, you can safely ignore this email.
%p
Otherwise, click the link below to complete the process.
#cta
= link_to('Reset password', edit_password_url(@resource, reset_password_token: @token))
Hello, <%= @resource.name %>!
Someone, hopefully you, has requested to reset the password for your GitLab
account on <%= Gitlab.config.gitlab.url %>
If you did not perform this request, you can safely ignore this email.
Otherwise, click the link below to complete the process:
<%= edit_password_url(@resource, reset_password_token: @token) %>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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