Commit aeb24ee8 authored by Lin Jen-Shin's avatar Lin Jen-Shin

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

* upstream/master: (353 commits)
  Put some admin settings in dropdown
  Add styleguide on configuration settings documentation
  Remove Duplicated keys add UNIQUE index to fingerprint
  Avoid autoload issue such as 'Mail::Parsers::AddressStruct'
  Move appearance settings as sub tab to application settings
  use rails root join
  fixed a couple of errors spotted in production
  Fix RangeError exceptions when referring to issues or merge requests outside of max database values
  Fix bug in `WikiLinkFilter`.
  Grammar and typographic changes to artifacts documentation
  Tweak grammar
  Small frontend code fixes and restore 8a2d88f commit
  Warn about admin privilege to disable GitHub Webhooks
  Listing GH Webhooks doesn't stop import process for non GH admin users
  fixup! updated docs for api endpoint award emoji
  Update CHANGELOG
  Ensure Todos counters doesn't count Todos for projects pending delete
  Add endpoints for award emoji on notes
  Sort API endpoints and implement feedback
  Add endpoints for Award Emoji
  ...
parents c628eeb7 764c9131
...@@ -129,56 +129,51 @@ spinach 7 10: *spinach-knapsack ...@@ -129,56 +129,51 @@ spinach 7 10: *spinach-knapsack
spinach 8 10: *spinach-knapsack spinach 8 10: *spinach-knapsack
spinach 9 10: *spinach-knapsack spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.2 # Execute all testing suites against Ruby 2.3
.ruby-23: &ruby-23
.ruby-22: &ruby-22 image: "ruby:2.3"
image: "ruby:2.2"
only: only:
- master - master
cache:
key: "ruby22"
paths:
- vendor
.rspec-knapsack-ruby22: &rspec-knapsack-ruby22 .rspec-knapsack-ruby23: &rspec-knapsack-ruby23
<<: *rspec-knapsack <<: *rspec-knapsack
<<: *ruby-22 <<: *ruby-23
.spinach-knapsack-ruby22: &spinach-knapsack-ruby22 .spinach-knapsack-ruby23: &spinach-knapsack-ruby23
<<: *spinach-knapsack <<: *spinach-knapsack
<<: *ruby-22 <<: *ruby-23
rspec 0 20 ruby22: *rspec-knapsack-ruby22 rspec 0 20 ruby23: *rspec-knapsack-ruby23
rspec 1 20 ruby22: *rspec-knapsack-ruby22 rspec 1 20 ruby23: *rspec-knapsack-ruby23
rspec 2 20 ruby22: *rspec-knapsack-ruby22 rspec 2 20 ruby23: *rspec-knapsack-ruby23
rspec 3 20 ruby22: *rspec-knapsack-ruby22 rspec 3 20 ruby23: *rspec-knapsack-ruby23
rspec 4 20 ruby22: *rspec-knapsack-ruby22 rspec 4 20 ruby23: *rspec-knapsack-ruby23
rspec 5 20 ruby22: *rspec-knapsack-ruby22 rspec 5 20 ruby23: *rspec-knapsack-ruby23
rspec 6 20 ruby22: *rspec-knapsack-ruby22 rspec 6 20 ruby23: *rspec-knapsack-ruby23
rspec 7 20 ruby22: *rspec-knapsack-ruby22 rspec 7 20 ruby23: *rspec-knapsack-ruby23
rspec 8 20 ruby22: *rspec-knapsack-ruby22 rspec 8 20 ruby23: *rspec-knapsack-ruby23
rspec 9 20 ruby22: *rspec-knapsack-ruby22 rspec 9 20 ruby23: *rspec-knapsack-ruby23
rspec 10 20 ruby22: *rspec-knapsack-ruby22 rspec 10 20 ruby23: *rspec-knapsack-ruby23
rspec 11 20 ruby22: *rspec-knapsack-ruby22 rspec 11 20 ruby23: *rspec-knapsack-ruby23
rspec 12 20 ruby22: *rspec-knapsack-ruby22 rspec 12 20 ruby23: *rspec-knapsack-ruby23
rspec 13 20 ruby22: *rspec-knapsack-ruby22 rspec 13 20 ruby23: *rspec-knapsack-ruby23
rspec 14 20 ruby22: *rspec-knapsack-ruby22 rspec 14 20 ruby23: *rspec-knapsack-ruby23
rspec 15 20 ruby22: *rspec-knapsack-ruby22 rspec 15 20 ruby23: *rspec-knapsack-ruby23
rspec 16 20 ruby22: *rspec-knapsack-ruby22 rspec 16 20 ruby23: *rspec-knapsack-ruby23
rspec 17 20 ruby22: *rspec-knapsack-ruby22 rspec 17 20 ruby23: *rspec-knapsack-ruby23
rspec 18 20 ruby22: *rspec-knapsack-ruby22 rspec 18 20 ruby23: *rspec-knapsack-ruby23
rspec 19 20 ruby22: *rspec-knapsack-ruby22 rspec 19 20 ruby23: *rspec-knapsack-ruby23
spinach 0 10 ruby22: *spinach-knapsack-ruby22 spinach 0 10 ruby23: *spinach-knapsack-ruby23
spinach 1 10 ruby22: *spinach-knapsack-ruby22 spinach 1 10 ruby23: *spinach-knapsack-ruby23
spinach 2 10 ruby22: *spinach-knapsack-ruby22 spinach 2 10 ruby23: *spinach-knapsack-ruby23
spinach 3 10 ruby22: *spinach-knapsack-ruby22 spinach 3 10 ruby23: *spinach-knapsack-ruby23
spinach 4 10 ruby22: *spinach-knapsack-ruby22 spinach 4 10 ruby23: *spinach-knapsack-ruby23
spinach 5 10 ruby22: *spinach-knapsack-ruby22 spinach 5 10 ruby23: *spinach-knapsack-ruby23
spinach 6 10 ruby22: *spinach-knapsack-ruby22 spinach 6 10 ruby23: *spinach-knapsack-ruby23
spinach 7 10 ruby22: *spinach-knapsack-ruby22 spinach 7 10 ruby23: *spinach-knapsack-ruby23
spinach 8 10 ruby22: *spinach-knapsack-ruby22 spinach 8 10 ruby23: *spinach-knapsack-ruby23
spinach 9 10 ruby22: *spinach-knapsack-ruby22 spinach 9 10 ruby23: *spinach-knapsack-ruby23
# Other generic tests # Other generic tests
......
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 error when CI job variables key specified but not defined
- Fix pipeline status when there are no builds in pipeline - 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.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters) - Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
- Show Star and Fork buttons on mobile.
- Fix endless redirections when accessing user OAuth applications when they are disabled - Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI - Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0 - Bump rouge to 1.11.0
...@@ -14,6 +16,7 @@ v 8.9.0 (unreleased) ...@@ -14,6 +16,7 @@ v 8.9.0 (unreleased)
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 - Redesign all Devise emails. !4297
- Don't show 'Leave Project' to group members
- 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)
...@@ -24,6 +27,7 @@ v 8.9.0 (unreleased) ...@@ -24,6 +27,7 @@ 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
- Add number of merge requests for a given milestone to the milestones view.
- Implement a fair usage of shared runners - 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
...@@ -34,6 +38,7 @@ v 8.9.0 (unreleased) ...@@ -34,6 +38,7 @@ v 8.9.0 (unreleased)
- Added shortcut 'y' for copying a files content hash URL #14470 - 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. - Fix horizontal scrollbar for long commit message.
- GitLab Performance Monitoring now tracks the total method execution time and call count per method
- Add Environments and Deployments - 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
...@@ -44,10 +49,13 @@ v 8.9.0 (unreleased) ...@@ -44,10 +49,13 @@ v 8.9.0 (unreleased)
- Fixed alignment of download dropdown in merge requests - Fixed alignment of download dropdown in merge requests
- Upgrade to jQuery 2 - Upgrade to jQuery 2
- Adds selected branch name to the dropdown toggle - Adds selected branch name to the dropdown toggle
- Add API endpoint for Sidekiq Metrics !4653
- Refactoring Award Emoji with API support for Issues and MergeRequests
- 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
- Limit email on push diff size to 30 files / 150 KB
- 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 - Fix race condition on merge when build succeeds
...@@ -55,10 +63,12 @@ v 8.9.0 (unreleased) ...@@ -55,10 +63,12 @@ v 8.9.0 (unreleased)
- 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 - 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
- Disable SAML account unlink feature
- 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)
- TeamCity Service: Fix URL handling when base URL contains a path - TeamCity Service: Fix URL handling when base URL contains a path
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Validate only and except regexp
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
- POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project - POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project
- Add support for using Yubikeys (U2F) for two-factor authentication - Add support for using Yubikeys (U2F) for two-factor authentication
...@@ -66,7 +76,9 @@ v 8.9.0 (unreleased) ...@@ -66,7 +76,9 @@ v 8.9.0 (unreleased)
- Remove 'main language' feature - Remove 'main language' feature
- Toggle whitespace button now available for compare branches diffs #17881 - 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
- Allow authentication using personal access tokens
- 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
- Custom notification settings
- 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 - Added Gfm autocomplete for labels
...@@ -92,6 +104,7 @@ v 8.9.0 (unreleased) ...@@ -92,6 +104,7 @@ v 8.9.0 (unreleased)
- Show categorised search queries in the search autocomplete - 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
- Add sorting dropdown to tags page !4423
- External links now open in a new tab - External links now open in a new tab
- Prevent default actions of disabled buttons and links - 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
...@@ -99,6 +112,7 @@ v 8.9.0 (unreleased) ...@@ -99,6 +112,7 @@ v 8.9.0 (unreleased)
- Improved UX of date pickers on issue & milestone forms - Improved UX of date pickers on issue & milestone forms
- Cache on the database if a project has an active external issue tracker. - Cache on the database if a project has an active external issue tracker.
- Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
- GitLab project import and export functionality
- All classes in the Banzai::ReferenceParser namespace are now instrumented - All classes in the Banzai::ReferenceParser namespace are now instrumented
- Remove deprecated issues_tracker and issues_tracker_id from project model - Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects - Allow users to create confidential issues in private projects
...@@ -118,6 +132,10 @@ v 8.9.0 (unreleased) ...@@ -118,6 +132,10 @@ v 8.9.0 (unreleased)
- Set inverse_of for Project/Service association to reduce the number of queries - Set inverse_of for Project/Service association to reduce the number of queries
- Update tanuki logo highlight/loading colors - Update tanuki logo highlight/loading colors
- Use Git cached counters for branches and tags on project page - Use Git cached counters for branches and tags on project page
- Filter parameters for request_uri value on instrumented transactions.
- Remove duplicated keys add UNIQUE index to keys fingerprint column
- Cache user todo counts from TodoService
- Ensure Todos counters doesn't count Todos for projects pending delete
v 8.8.5 v 8.8.5
- Import GitHub repositories respecting the API rate limit !4166 - Import GitHub repositories respecting the API rate limit !4166
......
...@@ -221,7 +221,7 @@ gem 'jquery-turbolinks', '~> 2.1.0' ...@@ -221,7 +221,7 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2' gem 'font-awesome-rails', '~> 4.6.1'
gem 'gitlab_emoji', '~> 0.3.0' gem 'gitlab_emoji', '~> 0.3.0'
gem 'gon', '~> 6.0.1' gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-atwho-rails', '~> 1.3.2'
......
...@@ -246,7 +246,7 @@ GEM ...@@ -246,7 +246,7 @@ GEM
fog-xml (0.1.2) fog-xml (0.1.2)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (~> 1.5, >= 1.5.11)
font-awesome-rails (4.5.0.1) font-awesome-rails (4.6.1.0)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
thor (~> 0.19.1) thor (~> 0.19.1)
...@@ -866,7 +866,7 @@ DEPENDENCIES ...@@ -866,7 +866,7 @@ DEPENDENCIES
fog-google (~> 0.3) fog-google (~> 0.3)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
font-awesome-rails (~> 4.2) font-awesome-rails (~> 4.6.1)
foreman foreman
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
......
...@@ -27,6 +27,11 @@ class @LabelManager ...@@ -27,6 +27,11 @@ class @LabelManager
$btn = $(e.currentTarget) $btn = $(e.currentTarget)
$label = $("##{$btn.data('domId')}") $label = $("##{$btn.data('domId')}")
action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
# Make sure tooltip will hide
$tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}"
$tooltip.tooltip 'destroy'
_this.toggleLabelPriority($label, action) _this.toggleLabelPriority($label, action)
toggleLabelPriority: ($label, action, persistState = true) -> toggleLabelPriority: ($label, action, persistState = true) ->
......
...@@ -78,6 +78,7 @@ class Dispatcher ...@@ -78,6 +78,7 @@ class Dispatcher
when 'projects:show' when 'projects:show'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new NotificationsForm()
new TreeView() if $('#tree-slider').length new TreeView() if $('#tree-slider').length
when 'groups:activity' when 'groups:activity'
new Activities() new Activities()
...@@ -129,6 +130,8 @@ class Dispatcher ...@@ -129,6 +130,8 @@ class Dispatcher
shortcut_handler = new ShortcutsDashboardNavigation() shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles' when 'profiles'
new Profile() new Profile()
new NotificationsForm()
new NotificationsDropdown()
when 'projects' when 'projects'
new Project() new Project()
new ProjectAvatar() new ProjectAvatar()
...@@ -136,8 +139,12 @@ class Dispatcher ...@@ -136,8 +139,12 @@ class Dispatcher
when 'edit' when 'edit'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ProjectNew() new ProjectNew()
when 'new', 'show' when 'new'
new ProjectNew() new ProjectNew()
when 'show'
new ProjectNew()
new ProjectShow()
new NotificationsDropdown()
when 'wikis' when 'wikis'
new Wikis() new Wikis()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
......
...@@ -302,6 +302,9 @@ class GitLabDropdown ...@@ -302,6 +302,9 @@ class GitLabDropdown
if @options.setIndeterminateIds if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@) @options.setIndeterminateIds.call(@)
if @options.setActiveIds
@options.setActiveIds.call(@)
# Makes indeterminate items effective # Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData @parseData @fullData
......
...@@ -210,9 +210,21 @@ class @LabelsSelect ...@@ -210,9 +210,21 @@ class @LabelsSelect
if $dropdown.hasClass('js-filter-bulk-update') if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds indeterminate = instance.indeterminateIds
active = instance.activeIds
if indeterminate.indexOf(label.id) isnt -1 if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate' selectedClass.push 'is-indeterminate'
if active.indexOf(label.id) isnt -1
# Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf 'is-indeterminate'
selectedClass.splice i, 1 unless i is -1
selectedClass.push 'is-active'
# Add input manually
instance.addInput @fieldName, label.id
if $form.find("input[type='hidden']\ if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\ [name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length [value='#{this.id(label)}']").length
...@@ -328,6 +340,10 @@ class @LabelsSelect ...@@ -328,6 +340,10 @@ class @LabelsSelect
setIndeterminateIds: -> setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds() @indeterminateIds = _this.getIndeterminateIds()
setActiveIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@activeIds = _this.getActiveIds()
) )
@bindEvents() @bindEvents()
...@@ -352,3 +368,12 @@ class @LabelsSelect ...@@ -352,3 +368,12 @@ class @LabelsSelect
label_ids.push $("#issue_#{issue_id}").data('labels') label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids) _.flatten(label_ids)
getActiveIds: ->
label_ids = []
$('.selected_issue:checked').each (i, el) ->
issue_id = $(el).data('id')
label_ids.push $("#issue_#{issue_id}").data('labels')
_.intersection.apply _, label_ids
class @NotificationsDropdown
$ ->
$(document)
.off 'click', '.update-notification'
.on 'click', '.update-notification', (e) ->
e.preventDefault()
return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom'
notificationLevel = $(@).data 'notification-level'
label = $(@).data 'notification-title'
form = $(this).parents('.notification-form:first')
form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner'
form.find('#notification_setting_level').val(notificationLevel)
form.submit()
$(document)
.off 'ajax:success', '.notification-form'
.on 'ajax:success', '.notification-form', (e, data) ->
if data.saved
new Flash('Notification settings saved', 'notice')
$(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html)
else
new Flash('Failed to save new settings', 'alert')
class @NotificationsForm
constructor: ->
@removeEventListeners()
@initEventListeners()
removeEventListeners: ->
$(document).off 'change', '.js-custom-notification-event'
initEventListeners: ->
$(document).on 'change', '.js-custom-notification-event', @toggleCheckbox
toggleCheckbox: (e) =>
$checkbox = $(e.currentTarget)
$parent = $checkbox.closest('.checkbox')
@saveEvent($checkbox, $parent)
showCheckboxLoadingSpinner: ($parent) ->
$parent
.addClass 'is-loading'
.find '.custom-notification-event-loading'
.removeClass 'fa-check'
.addClass 'fa-spin fa-spinner'
.removeClass 'is-done'
saveEvent: ($checkbox, $parent) ->
form = $parent.parents('form:first')
$.ajax(
url: form.attr('action')
method: form.attr('method')
dataType: 'json'
data: form.serialize()
beforeSend: =>
@showCheckboxLoadingSpinner($parent)
).done (data) ->
$checkbox.enable()
if data.saved
$parent
.find '.custom-notification-event-loading'
.toggleClass 'fa-spin fa-spinner fa-check is-done'
setTimeout(->
$parent
.removeClass 'is-loading'
.find '.custom-notification-event-loading'
.toggleClass 'fa-spin fa-spinner fa-check is-done'
, 2000)
...@@ -8,6 +8,10 @@ class @Profile ...@@ -8,6 +8,10 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit() $(this).parents('form').submit()
# Automatically submit email form when it changes
$('#user_notification_email').on 'change', ->
$(this).parents('form').submit()
$('.update-username').on 'ajax:before', -> $('.update-username').on 'ajax:before', ->
$('.loading-username').show() $('.loading-username').show()
$(this).find('.update-success').hide() $(this).find('.update-success').hide()
......
...@@ -34,22 +34,6 @@ class @Project ...@@ -34,22 +34,6 @@ class @Project
$(@).parents('.no-password-message').remove() $(@).parents('.no-password-message').remove()
e.preventDefault() e.preventDefault()
$('.update-notification').on 'click', (e) ->
e.preventDefault()
notification_level = $(@).data 'notification-level'
label = $(@).data 'notification-title'
$('#notification_setting_level').val(notification_level)
$('#notification-form').submit()
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active'
$(@).parent().addClass 'active'
$('#notification-form').on 'ajax:success', (e, data) ->
if data.saved
new Flash("Notification settings saved", "notice")
else
new Flash("Failed to save new settings", "alert")
@projectSelectDropdown() @projectSelectDropdown()
......
...@@ -51,15 +51,19 @@ class @Sidebar ...@@ -51,15 +51,19 @@ class @Sidebar
$this = $(e.currentTarget) $this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading') $todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this) $btnText = $('.js-issuable-todo-text', $this)
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST' ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST'
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
if $this.attr('data-delete-path')
url = "#{$this.attr('data-delete-path')}"
else
url = "#{$this.data('url')}"
$.ajax( $.ajax(
url: "#{$this.data('url')}#{ajaxUrlExtra}" url: url
type: ajaxType type: ajaxType
dataType: 'json' dataType: 'json'
data: data:
issuable_id: $this.data('issuable') issuable_id: $this.data('issuable-id')
issuable_type: $this.data('issuable-type') issuable_type: $this.data('issuable-type')
beforeSend: => beforeSend: =>
@beforeTodoSend($this, $todoLoading) @beforeTodoSend($this, $todoLoading)
...@@ -82,15 +86,15 @@ class @Sidebar ...@@ -82,15 +86,15 @@ class @Sidebar
else else
$todoPendingCount.removeClass 'hidden' $todoPendingCount.removeClass 'hidden'
if data.todo? if data.delete_path?
$btn $btn
.attr 'aria-label', $btn.data('mark-text') .attr 'aria-label', $btn.data('mark-text')
.attr 'data-id', data.todo.id .attr 'data-delete-path', data.delete_path
$btnText.text $btn.data('mark-text') $btnText.text $btn.data('mark-text')
else else
$btn $btn
.attr 'aria-label', $btn.data('todo-text') .attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-id' .removeAttr 'data-delete-path'
$btnText.text $btn.data('todo-text') $btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) -> sidebarDropdownLoading: (e) ->
......
...@@ -6,12 +6,6 @@ class @Calendar ...@@ -6,12 +6,6 @@ class @Calendar
@daySizeWithSpace = @daySize + (@daySpace * 2) @daySizeWithSpace = @daySize + (@daySpace * 2)
@monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
@months = [] @months = []
@highestValue = 0
# Get the highest value from the timestampes
_.each timestamps, (count) =>
if count > @highestValue
@highestValue = count
# Loop through the timestamps to create a group of objects # Loop through the timestamps to create a group of objects
# The group of objects will be grouped based on the day of the week they are # The group of objects will be grouped based on the day of the week they are
...@@ -39,8 +33,8 @@ class @Calendar ...@@ -39,8 +33,8 @@ class @Calendar
i++ i++
# Init color functions # Init color functions
@color = @initColor()
@colorKey = @initColorKey() @colorKey = @initColorKey()
@color = @initColor()
# Init the svg element # Init the svg element
@renderSvg(group) @renderSvg(group)
...@@ -104,7 +98,7 @@ class @Calendar ...@@ -104,7 +98,7 @@ class @Calendar
.attr 'class', 'user-contrib-cell js-tooltip' .attr 'class', 'user-contrib-cell js-tooltip'
.attr 'fill', (stamp) => .attr 'fill', (stamp) =>
if stamp.count isnt 0 if stamp.count isnt 0
@color(stamp.count) @color(Math.min(stamp.count, 40))
else else
'#ededed' '#ededed'
.attr 'data-container', 'body' .attr 'data-container', 'body'
...@@ -164,10 +158,11 @@ class @Calendar ...@@ -164,10 +158,11 @@ class @Calendar
color color
initColor: -> initColor: ->
colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
d3.scale d3.scale
.linear() .threshold()
.range(['#acd5f2', '#254e77']) .domain([0, 10, 20, 30])
.domain([0, @highestValue]) .range(colorRange)
initColorKey: -> initColorKey: ->
d3.scale d3.scale
......
...@@ -52,6 +52,19 @@ ...@@ -52,6 +52,19 @@
.git-clone-holder { .git-clone-holder {
display: none; display: none;
} }
// Display Star and Fork buttons without counters on mobile.
.project-action-buttons {
display: block;
.count-buttons .btn {
margin: 0 10px;
}
.count-buttons .count-with-arrow {
display: none;
}
}
} }
.project-stats { .project-stats {
......
...@@ -268,5 +268,10 @@ $calendar-hover-bg: #ecf3fe; ...@@ -268,5 +268,10 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1); $calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9; $calendar-unselectable-bg: #faf9f9;
/*
* Personal Access Tokens
*/
$personal-access-tokens-disabled-label-color: #bbb;
$ci-output-bg: #1d1f21; $ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6; $ci-text-color: #c5c8c6;
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
.commit-info-row { .commit-info-row {
margin-bottom: 10px; margin-bottom: 10px;
line-height: 24px;
padding-top: 6px;
&.commit-info-row-header { &.commit-info-row-header {
line-height: 34px; line-height: 34px;
......
...@@ -136,9 +136,10 @@ ...@@ -136,9 +136,10 @@
.event-last-push { .event-last-push {
overflow: auto; overflow: auto;
width: 100%; width: 100%;
.event-last-push-text { .event-last-push-text {
@include str-truncated(100%); @include str-truncated(100%);
padding: 5px 0; padding: 4px 0;
font-size: 13px; font-size: 13px;
float: left; float: left;
margin-right: -150px; margin-right: -150px;
......
...@@ -244,6 +244,10 @@ ...@@ -244,6 +244,10 @@
.panel-footer { .panel-footer {
padding: 5px 10px; padding: 5px 10px;
.btn {
min-width: auto;
}
} }
.commit { .commit {
...@@ -252,9 +256,7 @@ ...@@ -252,9 +256,7 @@
} }
.avatar { .avatar {
width: 20px; margin-left: 0;
height: 20px;
margin-right: 5px;
} }
.commit-row-info { .commit-row-info {
......
...@@ -192,6 +192,25 @@ ...@@ -192,6 +192,25 @@
} }
} }
.personal-access-tokens-never-expires-label {
color: $personal-access-tokens-disabled-label-color;
}
.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
text-align: center;
}
.created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
display: inline;
}
.btn-clipboard {
margin-left: 5px;
}
}
.user-profile { .user-profile {
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
......
...@@ -128,11 +128,6 @@ ...@@ -128,11 +128,6 @@
} }
} }
.btn-group:not(:first-child):not(:last-child) > .btn {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
form { form {
margin-left: 10px; margin-left: 10px;
} }
...@@ -499,7 +494,8 @@ pre.light-well { ...@@ -499,7 +494,8 @@ pre.light-well {
.activity-filter-block { .activity-filter-block {
.controls { .controls {
padding-bottom: 10px; padding-bottom: 7px;
margin-top: 8px;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
} }
...@@ -603,3 +599,20 @@ pre.light-well { ...@@ -603,3 +599,20 @@ pre.light-well {
} }
} }
} }
.custom-notifications-form {
.is-loading {
.custom-notification-event-loading {
display: inline-block;
}
}
}
.custom-notification-event-loading {
display: none;
margin-left: 5px;
&.is-done {
color: $gl-text-green;
}
}
...@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base ...@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper include PageLayoutHelper
include WorkhorseHelper include WorkhorseHelper
before_action :authenticate_user_from_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :reject_blocked! before_action :reject_blocked!
...@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base ...@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
...@@ -64,17 +64,10 @@ class ApplicationController < ActionController::Base ...@@ -64,17 +64,10 @@ class ApplicationController < ActionController::Base
end end
end end
# From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example # This filter handles both private tokens and personal access tokens
# https://gist.github.com/josevalim/fb706b1e933ef01e4fb6 def authenticate_user_from_private_token!
def authenticate_user_from_token! token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user_token = if params[:authenticity_token].presence user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
params[:authenticity_token].presence
elsif params[:private_token].presence
params[:private_token].presence
elsif request.headers['PRIVATE-TOKEN'].present?
request.headers['PRIVATE-TOKEN']
end
user = user_token && User.find_by_authentication_token(user_token.to_s)
if user if user
# Notice we are passing store false, so the user is not # Notice we are passing store false, so the user is not
...@@ -326,6 +319,10 @@ class ApplicationController < ActionController::Base ...@@ -326,6 +319,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git') current_application_settings.import_sources.include?('git')
end end
def gitlab_project_import_enabled?
current_application_settings.import_sources.include?('gitlab_project')
end
def two_factor_authentication_required? def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication current_application_settings.require_two_factor_authentication
end end
......
class Dashboard::TodosController < Dashboard::ApplicationController class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy, :destroy_all] include TodosHelper
before_action :find_todos, only: [:index, :destroy_all]
def index def index
@todos = @todos.page(params[:page]) @todos = @todos.page(params[:page])
end end
def destroy def destroy
todo.done TodoService.new.mark_todos_as_done([todo], current_user)
todo_notice = 'Todo was successfully marked as done.'
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: todo_notice } format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.js { head :ok } format.js { head :ok }
format.json do format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
end end
end end
def destroy_all def destroy_all
@todos.each(&:done) TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok } format.js { head :ok }
format.json do format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
find_todos
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
end end
end end
private private
def todo def todo
@todo ||= current_user.todos.find(params[:id]) @todo ||= find_todos.find(params[:id])
end end
def find_todos def find_todos
@todos = TodosFinder.new(current_user, params).execute @todos ||= TodosFinder.new(current_user, params).execute
end end
end end
class Groups::NotificationSettingsController < Groups::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(group)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
def new
@namespace_id = project_params[:namespace_id]
@namespace_name = Namespace.find(project_params[:namespace_id]).name
@path = project_params[:path]
end
def create
unless file_is_valid?
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
File.expand_path(project_params[:file].path),
project_params[:path]).execute
if @project.saved?
redirect_to(
project_path(@project),
notice: "Project '#{@project.name}' is being imported."
)
else
redirect_to(
new_import_gitlab_project_path,
alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}"
)
end
end
private
def file_is_valid?
project_params[:file] && project_params[:file].respond_to?(:read)
end
def verify_gitlab_project_import_enabled
render_404 unless gitlab_project_import_enabled?
end
def project_params
params.permit(
:path, :namespace_id, :file
)
end
end
class NotificationSettingsController < ApplicationController
before_action :authenticate_user!
def create
project = Project.find(params[:project][:id])
return render_404 unless can?(current_user, :read_project, project)
@notification_setting = current_user.notification_settings_for(project)
@saved = @notification_setting.update_attributes(notification_setting_params)
render_response
end
def update
@notification_setting = current_user.notification_settings.find(params[:id])
@saved = @notification_setting.update_attributes(notification_setting_params)
render_response
end
private
def render_response
render json: {
html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
saved: @saved
}
end
def notification_setting_params
allowed_fields = NotificationSetting::EMAIL_EVENTS.dup
allowed_fields << :level
params.require(:notification_setting).permit(allowed_fields)
end
end
...@@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController ...@@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
def unlink def unlink
provider = params[:provider] provider = params[:provider]
current_user.identities.find_by(provider: provider).destroy current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
redirect_to profile_account_path redirect_to profile_account_path
end end
end end
class Profiles::NotificationsController < Profiles::ApplicationController class Profiles::NotificationsController < Profiles::ApplicationController
def show def show
@user = current_user @user = current_user
@group_notifications = current_user.notification_settings.for_groups @group_notifications = current_user.notification_settings.for_groups.order(:id)
@project_notifications = current_user.notification_settings.for_projects @project_notifications = current_user.notification_settings.for_projects.order(:id)
@global_notification_setting = current_user.global_notification_setting @global_notification_setting = current_user.global_notification_setting
end end
def update def update
if current_user.update_attributes(user_params) && update_notification_settings if current_user.update_attributes(user_params)
flash[:notice] = "Notification settings saved" flash[:notice] = "Notification settings saved"
else else
flash[:alert] = "Failed to save new settings" flash[:alert] = "Failed to save new settings"
...@@ -19,16 +19,4 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -19,16 +19,4 @@ class Profiles::NotificationsController < Profiles::ApplicationController
def user_params def user_params
params.require(:user).permit(:notification_email) params.require(:user).permit(:notification_email)
end end
def global_notification_setting_params
params.require(:global_notification_setting).permit(:level)
end
private
def update_notification_settings
return true unless global_notification_setting_params
current_user.global_notification_setting.update_attributes(global_notification_setting_params)
end
end end
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
before_action :load_personal_access_tokens, only: :index
def index
@personal_access_token = current_user.personal_access_tokens.build
end
def create
@personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
load_personal_access_tokens
render :index
end
end
def revoke
@personal_access_token = current_user.personal_access_tokens.find(params[:id])
if @personal_access_token.revoke!
flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
else
flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
end
redirect_to profile_personal_access_tokens_path
end
private
def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at)
end
def load_personal_access_tokens
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
end
end
class Projects::NotificationSettingsController < Projects::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(project)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
...@@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy] before_action :authorize_admin_project!, only: [:destroy]
def index def index
sorted = VersionSorter.rsort(@repository.tag_names) @sort = params[:sort] || 'name'
@tags = Kaminari.paginate_array(sorted).page(params[:page]) @tags = @repository.tags_sorted_by(@sort)
@tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags) @releases = project.releases.where(tag: @tags)
end end
......
class Projects::TodosController < Projects::ApplicationController class Projects::TodosController < Projects::ApplicationController
def create before_action :authenticate_user!, only: [:create]
todos = TodoService.new.mark_todo(issuable, current_user)
render json: { def create
todo: todos, todo = TodoService.new.mark_todo(issuable, current_user)
count: current_user.todos.pending.count,
}
end
def update
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: { render json: {
count: current_user.todos.pending.count, count: current_user.todos_pending_count,
delete_path: dashboard_todo_path(todo)
} }
end end
...@@ -22,7 +16,13 @@ class Projects::TodosController < Projects::ApplicationController ...@@ -22,7 +16,13 @@ class Projects::TodosController < Projects::ApplicationController
@issuable ||= begin @issuable ||= begin
case params[:issuable_type] case params[:issuable_type]
when "issue" when "issue"
@project.issues.find(params[:issuable_id]) issue = @project.issues.find(params[:issuable_id])
if can?(current_user, :read_issue, issue)
issue
else
render_404
end
when "merge_request" when "merge_request"
@project.merge_requests.find(params[:issuable_id]) @project.merge_requests.find(params[:issuable_id])
end end
......
...@@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize # Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping] before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :event_filter, only: [:show, :activity] before_action :event_filter, only: [:show, :activity]
layout :determine_layout layout :determine_layout
...@@ -186,6 +186,48 @@ class ProjectsController < Projects::ApplicationController ...@@ -186,6 +186,48 @@ class ProjectsController < Projects::ApplicationController
) )
end end
def export
@project.add_export_job(current_user: current_user)
redirect_to(
edit_project_path(@project),
notice: "Project export started. A download link will be sent by email."
)
end
def download_export
export_project_path = @project.export_project_path
if export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
edit_project_path(@project),
alert: "Project export link has expired. Please generate a new export from your project settings."
)
end
end
def remove_export
if @project.remove_exports
flash[:notice] = "Project export has been deleted."
else
flash[:alert] = "Project export could not be deleted."
end
redirect_to(edit_project_path(@project))
end
def generate_new_export
if @project.remove_exports
export
else
redirect_to(
edit_project_path(@project),
alert: "Project export could not be deleted."
)
end
end
def toggle_star def toggle_star
current_user.toggle_star(@project) current_user.toggle_star(@project)
@project.reload @project.reload
......
...@@ -123,7 +123,7 @@ class TodosFinder ...@@ -123,7 +123,7 @@ class TodosFinder
end end
def by_state(items) def by_state(items)
case params[:state] case params[:state].to_s
when 'done' when 'done'
items.done items.done
else else
......
...@@ -17,11 +17,21 @@ module ButtonHelper ...@@ -17,11 +17,21 @@ module ButtonHelper
def clipboard_button(data = {}) def clipboard_button(data = {})
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
class: "btn", class: "btn btn-clipboard",
data: data, data: data,
type: :button type: :button
end end
# Output a "Copy to Clipboard" button with a custom CSS class
#
# data - Data attributes passed to `content_tag`
# css_class - Class passed to the `content_tag`
#
# Examples:
#
# # Define the target element
# clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard")
# # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>"
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard') def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
......
...@@ -67,9 +67,9 @@ module IssuablesHelper ...@@ -67,9 +67,9 @@ module IssuablesHelper
end end
end end
def has_todo(issuable) def issuable_todo(issuable)
unless current_user.nil? if current_user
current_user.todos.find_by(target_id: issuable.id, state: :pending) current_user.todos.find_by(target: issuable, state: :pending)
end end
end end
......
...@@ -6,6 +6,12 @@ module MembersHelper ...@@ -6,6 +6,12 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym "#{action}_#{member.type.underscore}".to_sym
end end
def default_show_roles(member)
can?(current_user, action_member_permission(:update, member), member) ||
can?(current_user, action_member_permission(:destroy, member), member) ||
can?(current_user, action_member_permission(:admin, member), member.source)
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)
......
...@@ -34,7 +34,7 @@ module NotificationsHelper ...@@ -34,7 +34,7 @@ module NotificationsHelper
def notification_description(level) def notification_description(level)
case level.to_sym case level.to_sym
when :participating when :participating
'You will only receive notifications from related resources' 'You will only receive notifications for threads you have participated in'
when :mention when :mention
'You will receive notifications only for comments in which you were @mentioned' 'You will receive notifications only for comments in which you were @mentioned'
when :watch when :watch
...@@ -43,6 +43,8 @@ module NotificationsHelper ...@@ -43,6 +43,8 @@ module NotificationsHelper
'You will not get any notifications via email' 'You will not get any notifications via email'
when :global when :global
'Use your global notification setting' 'Use your global notification setting'
when :custom
'You will only receive notifications for the events you choose'
end end
end end
...@@ -62,22 +64,14 @@ module NotificationsHelper ...@@ -62,22 +64,14 @@ module NotificationsHelper
end end
end end
def notification_level_radio_buttons # Identifier to trigger individually dropdowns and custom settings modals in the same view
html = "" def notifications_menu_identifier(type, notification_setting)
"#{type}-#{notification_setting.user_id}-#{notification_setting.source_id}-#{notification_setting.source_type}"
NotificationSetting.levels.each_key do |level| end
level = level.to_sym
next if level == :global
html << content_tag(:div, class: "radio") do
content_tag(:label, { value: level }) do
radio_button_tag(:"global_notification_setting[level]", level, @global_notification_setting.level.to_sym == level) +
content_tag(:div, level.to_s.capitalize, class: "level-title") +
content_tag(:p, notification_description(level))
end
end
end
html.html_safe # Create hidden field to send notification setting source to controller
def hidden_setting_source_input(notification_setting)
return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}[id]", notification_setting.source_id
end end
end end
module TodosHelper module TodosHelper
def todos_pending_count def todos_pending_count
current_user.todos.pending.count TodosFinder.new(current_user, state: :pending).execute.count
end end
def todos_done_count def todos_done_count
current_user.todos.done.count TodosFinder.new(current_user, state: :done).execute.count
end end
def todo_action_name(todo) def todo_action_name(todo)
...@@ -12,7 +12,7 @@ module TodosHelper ...@@ -12,7 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your' when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'marked this as a Todo for' when Todo::MARKED then 'added a todo for'
end end
end end
......
...@@ -9,6 +9,19 @@ module Emails ...@@ -9,6 +9,19 @@ module Emails
subject: subject("Project was moved")) subject: subject("Project was moved"))
end end
def project_was_exported_email(current_user, project)
@project = project
mail(to: current_user.notification_email,
subject: subject("Project was exported"))
end
def project_was_not_exported_email(current_user, project, errors)
@project = project
@errors = errors
mail(to: current_user.notification_email,
subject: subject("Project export error"))
end
def repository_push_email(project_id, opts = {}) def repository_push_email(project_id, opts = {})
@message = @message =
Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts) Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts)
......
...@@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false, require_two_factor_authentication: false,
......
...@@ -335,6 +335,7 @@ module Ci ...@@ -335,6 +335,7 @@ module Ci
def erase_artifacts! def erase_artifacts!
remove_artifacts_file! remove_artifacts_file!
remove_artifacts_metadata! remove_artifacts_metadata!
save
end end
def erase(opts = {}) def erase(opts = {})
......
...@@ -170,6 +170,10 @@ module Ci ...@@ -170,6 +170,10 @@ module Ci
builds.where.not(environment: nil).success.pluck(:environment).uniq builds.where.not(environment: nil).success.pluck(:environment).uniq
end end
def notes
Note.for_commit_id(sha)
end
private private
def build_builds_for_stages(stages, user, status, trigger_request) def build_builds_for_stages(stages, user, status, trigger_request)
......
class CommitStatus < ActiveRecord::Base class CommitStatus < ActiveRecord::Base
include Statuseable include Statuseable
include Importable
self.table_name = 'ci_builds' self.table_name = 'ci_builds'
...@@ -7,7 +8,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -7,7 +8,7 @@ class CommitStatus < ActiveRecord::Base
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user belongs_to :user
validates :pipeline, presence: true validates :pipeline, presence: true, unless: :importing?
validates_presence_of :name validates_presence_of :name
......
module Importable
extend ActiveSupport::Concern
attr_accessor :importing
alias_method :importing?, :importing
end
...@@ -49,6 +49,10 @@ module Referable ...@@ -49,6 +49,10 @@ module Referable
raise NotImplementedError, "#{self} does not implement #{__method__}" raise NotImplementedError, "#{self} does not implement #{__method__}"
end end
def reference_valid?(reference)
true
end
def link_reference_pattern(route, pattern) def link_reference_pattern(route, pattern)
%r{ %r{
(?<url> (?<url>
......
...@@ -83,6 +83,10 @@ class Issue < ActiveRecord::Base ...@@ -83,6 +83,10 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/) @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end end
def self.reference_valid?(reference)
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def self.sort(method, excluded_labels: []) def self.sort(method, excluded_labels: [])
case method.to_s case method.to_s
when 'due_date_asc' then order_due_date_asc when 'due_date_asc' then order_due_date_asc
......
...@@ -9,7 +9,7 @@ class Key < ActiveRecord::Base ...@@ -9,7 +9,7 @@ class Key < ActiveRecord::Base
before_validation :strip_white_space, :generate_fingerprint before_validation :strip_white_space, :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { within: 0..255 }
validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
validates :key, format: { without: /\n|\r/, message: 'should be a single line' } validates :key, format: { without: /\n|\r/, message: 'should be a single line' }
validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' }
......
class Member < ActiveRecord::Base class Member < ActiveRecord::Base
include Sortable include Sortable
include Importable
include Gitlab::Access include Gitlab::Access
attr_accessor :raw_invite_token attr_accessor :raw_invite_token
...@@ -41,11 +42,11 @@ class Member < ActiveRecord::Base ...@@ -41,11 +42,11 @@ class Member < ActiveRecord::Base
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite? after_create :send_invite, if: :invite?, unless: :importing?
after_create :send_request, if: :request? after_create :send_request, if: :request?, unless: :importing?
after_create :create_notification_setting, unless: :pending? after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: :pending? after_create :post_create_hook, unless: [:pending?, :importing?]
after_update :post_update_hook, unless: :pending? after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending? after_destroy :post_destroy_hook, unless: :pending?
after_destroy :post_decline_request, if: :request? after_destroy :post_decline_request, if: :request?
......
...@@ -4,6 +4,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -4,6 +4,7 @@ class MergeRequest < ActiveRecord::Base
include Referable include Referable
include Sortable include Sortable
include Taskable include Taskable
include Importable
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
...@@ -13,7 +14,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -13,7 +14,7 @@ class MergeRequest < ActiveRecord::Base
serialize :merge_params, Hash serialize :merge_params, Hash
after_create :create_merge_request_diff after_create :create_merge_request_diff, unless: :importing
after_update :update_merge_request_diff after_update :update_merge_request_diff
delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
...@@ -95,12 +96,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -95,12 +96,12 @@ class MergeRequest < ActiveRecord::Base
end end
end end
validates :source_project, presence: true, unless: :allow_broken validates :source_project, presence: true, unless: [:allow_broken, :importing?]
validates :source_branch, presence: true validates :source_branch, presence: true
validates :target_project, presence: true validates :target_project, presence: true
validates :target_branch, presence: true validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds? validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches, unless: :allow_broken validate :validate_branches, unless: [:allow_broken, :importing?]
validate :validate_fork validate :validate_fork
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
...@@ -132,6 +133,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -132,6 +133,10 @@ class MergeRequest < ActiveRecord::Base
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/) @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end end
def self.reference_valid?(reference)
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
# Returns all the merge requests from an ActiveRecord:Relation. # Returns all the merge requests from an ActiveRecord:Relation.
# #
# This method uses a UNION as it usually operates on the result of # This method uses a UNION as it usually operates on the result of
......
class MergeRequestDiff < ActiveRecord::Base class MergeRequestDiff < ActiveRecord::Base
include Sortable include Sortable
include Importable
# Prevent store of diff if commits amount more then 500 # Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100 COMMITS_SAFE_SIZE = 100
...@@ -22,7 +23,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -22,7 +23,7 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_commits serialize :st_commits
serialize :st_diffs serialize :st_diffs
after_create :reload_content after_create :reload_content, unless: :importing?
def reload_content def reload_content
reload_commits reload_commits
......
...@@ -4,6 +4,7 @@ class Note < ActiveRecord::Base ...@@ -4,6 +4,7 @@ class Note < ActiveRecord::Base
include Participable include Participable
include Mentionable include Mentionable
include Awardable include Awardable
include Importable
default_value_for :system, false default_value_for :system, false
...@@ -28,11 +29,11 @@ class Note < ActiveRecord::Base ...@@ -28,11 +29,11 @@ class Note < ActiveRecord::Base
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
validates :noteable_type, presence: true validates :noteable_type, presence: true
validates :noteable_id, presence: true, unless: :for_commit? validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
validates :commit_id, presence: true, if: :for_commit? validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true validates :author, presence: true
validate unless: :for_commit? do |note| validate unless: [:for_commit?, :importing?] do |note|
unless note.noteable.try(:project) == note.project unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch') errors.add(:invalid_project, 'Note and noteable project mismatch')
end end
......
class NotificationSetting < ActiveRecord::Base class NotificationSetting < ActiveRecord::Base
enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0 } enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global] default_value_for :level, NotificationSetting.levels[:global]
...@@ -15,6 +15,24 @@ class NotificationSetting < ActiveRecord::Base ...@@ -15,6 +15,24 @@ class NotificationSetting < ActiveRecord::Base
scope :for_groups, -> { where(source_type: 'Namespace') } scope :for_groups, -> { where(source_type: 'Namespace') }
scope :for_projects, -> { where(source_type: 'Project') } scope :for_projects, -> { where(source_type: 'Project') }
EMAIL_EVENTS = [
:new_note,
:new_issue,
:reopen_issue,
:close_issue,
:reassign_issue,
:new_merge_request,
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
:merge_merge_request
]
store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events
before_save :events_to_boolean
def self.find_or_create_for(source) def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source) setting = find_or_initialize_by(source: source)
...@@ -24,4 +42,21 @@ class NotificationSetting < ActiveRecord::Base ...@@ -24,4 +42,21 @@ class NotificationSetting < ActiveRecord::Base
setting setting
end end
# Set all event attributes to false when level is not custom or being initialized for UX reasons
def set_events
return if custom?
EMAIL_EVENTS.each do |event|
events[event] = false
end
end
# Validates store accessors values as boolean
# It is a text field so it does not cast correct boolean values in JSON
def events_to_boolean
EMAIL_EVENTS.each do |event|
events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event])
end
end
end end
class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :token
belongs_to :user
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
def self.generate(params)
personal_access_token = self.new(params)
personal_access_token.ensure_token
personal_access_token
end
def revoke!
self.revoked = true
self.save
end
end
...@@ -367,6 +367,11 @@ class Project < ActiveRecord::Base ...@@ -367,6 +367,11 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC') joins(join_body).reorder('join_note_counts.amount DESC')
end end
# Deletes gitlab project export files older than 24 hours
def remove_gitlab_exports!
Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete))
end
end end
def team def team
...@@ -470,7 +475,7 @@ class Project < ActiveRecord::Base ...@@ -470,7 +475,7 @@ class Project < ActiveRecord::Base
end end
def import? def import?
external_import? || forked? external_import? || forked? || gitlab_project_import?
end end
def no_import? def no_import?
...@@ -501,6 +506,10 @@ class Project < ActiveRecord::Base ...@@ -501,6 +506,10 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new(import_url).masked_url Gitlab::UrlSanitizer.new(import_url).masked_url
end end
def gitlab_project_import?
import_type == 'gitlab_project'
end
def check_limit def check_limit
unless creator.can_create_project? or namespace.kind == 'group' unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit projects_limit = creator.projects_limit
...@@ -1096,4 +1105,27 @@ class Project < ActiveRecord::Base ...@@ -1096,4 +1105,27 @@ class Project < ActiveRecord::Base
ensure ensure
@errors = original_errors @errors = original_errors
end end
def add_export_job(current_user:)
job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
else
Rails.logger.error "Export job failed to start for project ID #{self.id}"
end
end
def export_path
File.join(Gitlab::ImportExport.storage_path, path_with_namespace)
end
def export_project_path
Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
end
def remove_exports
_, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
status.zero?
end
end end
...@@ -598,6 +598,21 @@ class Repository ...@@ -598,6 +598,21 @@ class Repository
end end
end end
def tags_sorted_by(value)
case value
when 'name'
# Would be better to use `sort_by` but `version_sorter` only exposes
# `sort` and `rsort`
VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) }
when 'updated_desc'
tags_sorted_by_committed_date.reverse
when 'updated_asc'
tags_sorted_by_committed_date
else
tags
end
end
def contributors def contributors
commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true) commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
...@@ -995,4 +1010,8 @@ class Repository ...@@ -995,4 +1010,8 @@ class Repository
def file_on_head(regex) def file_on_head(regex)
tree(:head).blobs.find { |file| file.name =~ regex } tree(:head).blobs.find { |file| file.name =~ regex }
end end
def tags_sorted_by_committed_date
tags.sort_by { |tag| commit(tag.target).committed_date }
end
end end
...@@ -51,6 +51,7 @@ class User < ActiveRecord::Base ...@@ -51,6 +51,7 @@ class User < ActiveRecord::Base
# Profile # Profile
has_many :keys, dependent: :destroy has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy has_many :emails, dependent: :destroy
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy has_many :u2f_registrations, dependent: :destroy
...@@ -267,6 +268,11 @@ class User < ActiveRecord::Base ...@@ -267,6 +268,11 @@ class User < ActiveRecord::Base
find_by!('lower(username) = ?', username.downcase) find_by!('lower(username) = ?', username.downcase)
end end
def find_by_personal_access_token(token_string)
personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
personal_access_token.user if personal_access_token
end
def by_username_or_id(name_or_id) def by_username_or_id(name_or_id)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end end
...@@ -821,6 +827,23 @@ class User < ActiveRecord::Base ...@@ -821,6 +827,23 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true) assigned_open_issues_count(force: true)
end end
def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
todos.done.count
end
end
def todos_pending_count(force: false)
Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
todos.pending.count
end
end
def update_todos_count_cache
todos_done_count(force: true)
todos_pending_count(force: true)
end
private private
def projects_union(min_access_level = nil) def projects_union(min_access_level = nil)
......
This diff is collapsed.
...@@ -80,16 +80,18 @@ module Projects ...@@ -80,16 +80,18 @@ module Projects
def after_create_actions def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
@project.create_wiki if @project.wiki_enabled? unless @project.gitlab_project_import?
@project.create_wiki if @project.wiki_enabled?
@project.build_missing_services @project.build_missing_services
@project.create_labels @project.create_labels
end
event_service.create_project(@project, current_user) event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create) system_hook_service.execute_hooks_for(@project, :create)
unless @project.group unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user] @project.team << [current_user, :master, current_user]
end end
end end
......
module Projects
module ImportExport
class ExportService < BaseService
def execute(_options = {})
@shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work'))
save_all
end
private
def save_all
if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
Gitlab::ImportExport::Saver.save(shared: @shared)
notify_success
else
cleanup_and_notify
end
end
def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
end
def project_tree_saver
Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
end
def uploads_saver
Gitlab::ImportExport::UploadsSaver.new(project: project, shared: @shared)
end
def repo_saver
Gitlab::ImportExport::RepoSaver.new(project: project, shared: @shared)
end
def wiki_repo_saver
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
def cleanup_and_notify
FileUtils.rm_rf(@shared.export_path)
notify_error
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
end
def notify_success
notification_service.project_exported(@project, @current_user)
end
def notify_error
notification_service.project_not_exported(@project, @current_user, @shared.errors)
end
end
end
end
...@@ -9,26 +9,31 @@ module Projects ...@@ -9,26 +9,31 @@ module Projects
'fogbugz', 'fogbugz',
'gitlab', 'gitlab',
'github', 'github',
'google_code' 'google_code',
'gitlab_project'
] ]
def execute def execute
if unknown_url? add_repository_to_project unless project.gitlab_project_import?
# In this case, we only want to import issues, not a repository.
create_repository
else
import_repository
end
import_data import_data
success success
rescue Error => e rescue => e
error(e.message) error(e.message)
end end
private private
def add_repository_to_project
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
else
import_repository
end
end
def create_repository def create_repository
unless project.create_repository unless project.create_repository
raise Error, 'The repository could not be created.' raise Error, 'The repository could not be created.'
...@@ -46,7 +51,7 @@ module Projects ...@@ -46,7 +51,7 @@ module Projects
def import_data def import_data
return unless has_importer? return unless has_importer?
project.repository.before_import project.repository.before_import unless project.gitlab_project_import?
unless importer.execute unless importer.execute
raise Error, 'The remote data could not be imported.' raise Error, 'The remote data could not be imported.'
...@@ -58,6 +63,8 @@ module Projects ...@@ -58,6 +63,8 @@ module Projects
end end
def importer def importer
return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
class_name.constantize.new(project) class_name.constantize.new(project)
end end
......
# TodoService class # TodoService class
# #
# Used for creating todos after certain user actions # Used for creating/updating todos after certain user actions
# #
# Ex. # Ex.
# TodoService.new.new_issue(issue, current_user) # TodoService.new.new_issue(issue, current_user)
...@@ -137,6 +137,15 @@ class TodoService ...@@ -137,6 +137,15 @@ class TodoService
def mark_pending_todos_as_done(target, user) def mark_pending_todos_as_done(target, user)
attributes = attributes_for_target(target) attributes = attributes_for_target(target)
pending_todos(user, attributes).update_all(state: :done) pending_todos(user, attributes).update_all(state: :done)
user.update_todos_count_cache
end
# When user marks some todos as done
def mark_todos_as_done(todos, current_user)
todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all)
todos.update_all(state: :done)
current_user.update_todos_count_cache
end end
# When user marks an issue as todo # When user marks an issue as todo
...@@ -151,6 +160,7 @@ class TodoService ...@@ -151,6 +160,7 @@ class TodoService
Array(users).map do |user| Array(users).map do |user|
next if pending_todos(user, attributes).exists? next if pending_todos(user, attributes).exists?
Todo.create(attributes.merge(user_id: user.id)) Todo.create(attributes.merge(user_id: user.id))
user.update_todos_count_cache
end end
end end
...@@ -161,11 +171,16 @@ class TodoService ...@@ -161,11 +171,16 @@ class TodoService
def update_issuable(issuable, author) def update_issuable(issuable, author)
# Skip toggling a task list item in a description # Skip toggling a task list item in a description
return if issuable.tasks? && issuable.updated_tasks.any? return if toggling_tasks?(issuable)
create_mention_todos(issuable.project, issuable, author) create_mention_todos(issuable.project, issuable, author)
end end
def toggling_tasks?(issuable)
issuable.previous_changes.include?('description') &&
issuable.tasks? && issuable.updated_tasks.any?
end
def handle_note(note, author) def handle_note(note, author)
# Skip system notes, and notes on project snippet # Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet? return if note.system? || note.for_snippet?
......
- page_title "Appearance" - page_title "Appearance"
%h3.page-title %h3.page-title
Appearance settings Appearance settings
%p.light %p.light
You can modify the look and feel of GitLab here You can modify the look and feel of GitLab here
%hr
= render 'form' = render 'form'
- page_title "Settings" - page_title "Settings"
%h3.page-title Settings %h3.page-title Settings
%hr %hr
= render 'form' = render 'form'
...@@ -20,3 +20,7 @@ ...@@ -20,3 +20,7 @@
= link_to admin_builds_path, title: 'Builds' do = link_to admin_builds_path, title: 'Builds' do
%span %span
Builds Builds
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
%p.lead.prepend-top-default - @no_container = true
%span = render "admin/dashboard/head"
To register a new runner you should enter the following registration token.
With this token the runner will request a unique runner token and use that for future communication.
Registration token is
%code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
.bs-callout.clearfix %div{ class: (container_class) }
.pull-left
%p %p.prepend-top-default
You can reset runners registration token by pressing a button below. %span
%p To register a new runner you should enter the following registration token.
= button_to reset_runners_token_admin_application_settings_path, With this token the runner will request a unique runner token and use that for future communication.
method: :put, class: 'btn btn-default', %br
data: { confirm: 'Are you sure you want to reset registration token?' } do Registration token is
= icon('refresh') %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
Reset runners registration token
.bs-callout .bs-callout.clearfix
%p .pull-left
A 'runner' is a process which runs a build. %p
You can setup as many runners as you need. You can reset runners registration token by pressing a button below.
%br %p
Runners can be placed on separate users, servers, and even on your local machine. = button_to reset_runners_token_admin_application_settings_path,
%br method: :put, class: 'btn btn-default',
data: { confirm: 'Are you sure you want to reset registration token?' } do
= icon('refresh')
Reset runners registration token
.bs-callout
%p
A 'runner' is a process which runs a build.
You can setup as many runners as you need.
%br
Runners can be placed on separate users, servers, and even on your local machine.
%br
%div %div
%span Each runner can be in one of the following states: %span Each runner can be in one of the following states:
%ul %ul
%li %li
%span.label.label-success shared %span.label.label-success shared
\- run builds from all unassigned projects \- run builds from all unassigned projects
%li %li
%span.label.label-info specific %span.label.label-info specific
\- run builds from assigned projects \- run builds from assigned projects
%li %li
%span.label.label-danger paused %span.label.label-danger paused
\- runner will not receive any new builds \- runner will not receive any new builds
.append-bottom-20.clearfix .append-bottom-20.clearfix
.pull-left .pull-left
= form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
.form-group .form-group
= search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
= submit_tag 'Search', class: 'btn' = submit_tag 'Search', class: 'btn'
.pull-right.light .pull-right.light
Runners with last contact less than a minute ago: #{@active_runners_cnt} Runners with last contact less than a minute ago: #{@active_runners_cnt}
%br %br
.table-holder .table-holder
%table.table %table.table
%thead %thead
%tr %tr
%th Type %th Type
%th Runner token %th Runner token
%th Description %th Description
%th Projects %th Projects
%th Builds %th Builds
%th Tags %th Tags
%th Last contact %th Last contact
%th %th
- @runners.each do |runner| - @runners.each do |runner|
= render "admin/runners/runner", runner: runner = render "admin/runners/runner", runner: runner
= paginate @runners = paginate @runners
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%p %p
%i.fa.fa-warning %i.fa.fa-warning
To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process. To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository.
%p.light %p.light
Select projects you want to import. Select projects you want to import.
......
- page_title "GitLab Import"
- header_title "Projects", root_path
%h3.page-title
= icon('gitlab')
Import an exported GitLab project
%hr
= form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do
%p
Project will be imported as
%strong
#{@namespace_name}/#{@path}
%p
To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
.form-group
= hidden_field_tag :namespace_id, @namespace_id
= hidden_field_tag :path, @path
= label_tag :file, class: 'control-label' do
%span GitLab project export
.col-sm-10
= file_field_tag :file, class: ''
.form-actions
= submit_tag 'Import project', class: 'btn btn-create'
%ul.nav-links.scrolling-tabs %div{ class: nav_control_class }
.fade-left = render 'layouts/nav/admin_settings'
= nav_link(controller: %w(dashboard admin projects users groups builds), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
= nav_link(controller: %w(background_jobs logs health_check)) do
= link_to admin_background_jobs_path, title: 'Monitoring' do
%span
Monitoring
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
%span
Deploy Keys
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do
%span
Messages
= nav_link(controller: :hooks) do
= link_to admin_hooks_path, title: 'Hooks' do
%span
Hooks
= nav_link(controller: :appearances) do %ul.nav-links.scrolling-tabs
= link_to admin_appearances_path, title: 'Appearances' do .fade-left
%span = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do
Appearance = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
= nav_link(controller: :applications) do Overview
= link_to admin_applications_path, title: 'Applications' do = nav_link(controller: %w(background_jobs logs health_check)) do
%span = link_to admin_background_jobs_path, title: 'Monitoring' do
Applications %span
Monitoring
= nav_link(controller: :services) do = nav_link(controller: :broadcast_messages) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do = link_to admin_broadcast_messages_path, title: 'Messages' do
%span %span
Service Templates Messages
= nav_link(controller: :hooks) do
= nav_link(controller: :labels) do = link_to admin_hooks_path, title: 'Hooks' do
= link_to admin_labels_path, title: 'Labels' do %span
%span System Hooks
Labels
= nav_link(controller: :abuse_reports) do = nav_link(controller: :applications) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do = link_to admin_applications_path, title: 'Applications' do
%span %span
Abuse Reports Applications
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- if askimet_enabled? = nav_link(controller: :abuse_reports) do
= nav_link(controller: :spam_logs) do = link_to admin_abuse_reports_path, title: "Abuse Reports" do
= link_to admin_spam_logs_path, title: "Spam Logs" do
%span %span
Spam Logs Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - if askimet_enabled?
= link_to admin_application_settings_path, title: 'Settings' do = nav_link(controller: :spam_logs) do
%span = link_to admin_spam_logs_path, title: "Spam Logs" do
Settings %span
.fade-right Spam Logs
.fade-right
.controls
.dropdown.admin-settings-dropdown
%a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'}
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
%span
Deploy Keys
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do
%span
Service Templates
= nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do
%span
Labels
= nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do
%span
Appearance
%li.divider
= nav_link(controller: :application_settings) do
= link_to admin_application_settings_path, title: 'Settings' do
%span
Settings
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
= link_to applications_profile_path, title: 'Applications' do = link_to applications_profile_path, title: 'Applications' do
%span %span
Applications Applications
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do
%span
Personal Access Tokens
= nav_link(controller: :emails) do = nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do = link_to profile_emails_path, title: 'Emails' do
%span %span
......
...@@ -5,18 +5,19 @@ ...@@ -5,18 +5,19 @@
= icon('cog') = icon('cog')
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- is_project_member = @project.users.exists?(current_user.id)
- access = @project.team.max_member_access(current_user.id) - access = @project.team.max_member_access(current_user.id)
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
= render 'layouts/nav/project_settings', access: access, can_edit: can_edit = render 'layouts/nav/project_settings', access: access, can_edit: can_edit
- if can_edit || access - if can_edit || is_project_member
%li.divider %li.divider
- if can_edit - if can_edit
%li %li
= link_to edit_project_path(@project) do = link_to edit_project_path(@project) do
Edit Project Edit Project
- if access - if is_project_member
%li %li
= link_to polymorphic_path([:leave, @project, :members]), = link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
......
%p
Project #{@project.name} was exported successfully.
%p
The project export can be downloaded from:
= link_to download_export_namespace_project_url(@project.namespace, @project) do
= @project.name_with_namespace + " export"
%p
The download link will expire in 24 hours.
Project <%= @project.name %> was exported successfully.
The project export can be downloaded from:
<%= download_export_namespace_project_url(@project.namespace, @project) %>
The download link will expire in 24 hours.
%p
Project #{@project.name} couldn't be exported.
%p
The errors we encountered were:
%ul
- @errors.each do |error|
%li
error
Project <%= @project.name %> couldn't be exported.
The errors we encountered were:
- @errors.each do |error|
<%= error %>
\ No newline at end of file
...@@ -62,10 +62,14 @@ ...@@ -62,10 +62,14 @@
.provider-btn-image .provider-btn-image
= provider_image_tag(provider) = provider_image_tag(provider)
- if auth_active?(provider) - if auth_active?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do - if provider.to_s == 'saml'
Disconnect %a.provider-btn
Active
- else
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
- else - else
= link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do = link_to user_omniauth_authorize_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
Connect Connect
%hr %hr
- if current_user.can_change_username? - if current_user.can_change_username?
......
...@@ -9,5 +9,4 @@ ...@@ -9,5 +9,4 @@
= link_to group.name, group_path(group) = link_to group.name, group_path(group)
.pull-right .pull-right
= form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f| = render 'shared/notifications/button', notification_setting: setting
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
...@@ -9,5 +9,4 @@ ...@@ -9,5 +9,4 @@
= link_to_project(project) = link_to_project(project)
.pull-right .pull-right
= form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f| = render 'shared/notifications/button', notification_setting: setting
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
...@@ -24,12 +24,15 @@ ...@@ -24,12 +24,15 @@
.form-group .form-group
= f.label :notification_email, class: "label-light" = f.label :notification_email, class: "label-light"
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
.form-group
= f.label :notification_level, class: 'label-light'
= notification_level_radio_buttons
.prepend-top-default = label_tag :global_notification_level, "Global notification level", class: "label-light"
= f.submit 'Update settings', class: "btn btn-create" %br
.clearfix
.form-group.pull-left
= render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
.clearfix
%hr %hr
%h5 %h5
Groups (#{@group_notifications.count}) Groups (#{@group_notifications.count})
......
- page_title "Personal Access Tokens"
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
You can generate a personal access token for each application you use that needs access to the GitLab API.
.col-lg-9
- if flash[:personal_access_token]
.created-personal-access-token-container
%h5.prepend-top-0
Your New Personal Access Token
.form-group
= text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
= clipboard_button(clipboard_text: flash[:personal_access_token])
%span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
%hr
%h5.prepend-top-0
Add a Personal Access Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
= form_for [:profile, @personal_access_token],
method: :post, html: { class: 'js-requires-input' } do |f|
= form_errors(@personal_access_token)
.form-group
= f.label :name, class: 'label-light'
= f.text_field :name, class: "form-control", required: true
.form-group
= f.label :expires_at, class: 'label-light'
= f.text_field :expires_at, class: "datepicker form-control", required: false
.prepend-top-default
= f.submit 'Create Personal Access Token', class: "btn btn-create"
%hr
%h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
- if @active_personal_access_tokens.present?
.table-responsive
%table.table.active-personal-access-tokens
%thead
%tr
%th Name
%th Created
%th Expires
%th
%tbody
- @active_personal_access_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
%td
- if token.expires_at.present?
= token.expires_at.to_date.to_s(:medium)
- else
%span.personal-access-tokens-never-expires-label Never
%td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
- else
.settings-message.text-center
You don't have any active tokens yet.
%hr
%h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
- if @inactive_personal_access_tokens.present?
.table-responsive
%table.table.inactive-personal-access-tokens
%thead
%tr
%th Name
%th Created
%tbody
- @inactive_personal_access_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
- else
.settings-message.text-center
There are no inactive tokens.
:javascript
var date = $('#personal_access_token_expires_at').val();
var datepicker = $(".datepicker").datepicker({
dateFormat: "yy-mm-dd",
minDate: 0
});
$("#created-personal-access-token").click(function() {
this.select();
});
$("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= link_to project_path(forked_from_project) do = link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name) = forked_from_project.namespace.try(:name)
.project-repo-buttons .project-repo-buttons.project-action-buttons
.count-buttons .count-buttons
= render 'projects/buttons/star' = render 'projects/buttons/star'
= render 'projects/buttons/fork' = render 'projects/buttons/fork'
...@@ -29,13 +29,13 @@ ...@@ -29,13 +29,13 @@
.project-clone-holder .project-clone-holder
= render "shared/clone_panel" = render "shared/clone_panel"
.project-repo-buttons.project-right-buttons .project-repo-buttons.btn-group.project-right-buttons
- if current_user - if current_user
= render 'shared/members/access_request_buttons', source: @project = render 'shared/members/access_request_buttons', source: @project
.btn-group
= render "projects/buttons/download" = render "projects/buttons/download"
= render 'projects/buttons/dropdown' = render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications' = render 'shared/notifications/button', notification_setting: @notification_setting
:javascript :javascript
new Star(); new Star();
...@@ -20,15 +20,15 @@ ...@@ -20,15 +20,15 @@
protected protected
.controls.hidden-xs .controls.hidden-xs
- if create_mr_button?(@repository.root_ref, branch.name) - if create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
Merge Request Merge Request
- if branch.name != @repository.root_ref - if branch.name != @repository.root_ref
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
Compare Compare
- if can_remove_branch?(@project, branch.name) - if can_remove_branch?(@project, branch.name)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o") = icon("trash-o")
- if branch.name != @repository.root_ref - if branch.name != @repository.root_ref
......
- if @notification_setting
= form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
= f.hidden_field :level
.dropdown.hidden-sm
%button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } }
= icon('bell')
= notification_title(@notification_setting.level)
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-large{ role: "menu" }
- NotificationSetting.levels.each do |level|
= notification_list_item(level.first, @notification_setting)
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
.pull-right.commit-action-buttons .pull-right.commit-action-buttons
- if defined?(@notes_count) && @notes_count > 0 - if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped.hidden-xs %span.btn.disabled.btn-grouped.hidden-xs.append-right-10
= icon('comment') = icon('comment')
= @notes_count = @notes_count
= link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped hidden-xs hidden-sm" do = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do
Browse Files Browse Files
.dropdown.inline .dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
......
...@@ -120,6 +120,42 @@ ...@@ -120,6 +120,42 @@
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save" method: :post, class: "btn btn-save"
%hr %hr
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Export project
%p.append-bottom-0
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
%p
Once the exported file is ready, you will receive a notification email with a download link.
.col-lg-9
- if @project.export_project_path
= link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default"
- else
= link_to 'Export project', export_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default"
.bs-callout.bs-callout-info
%p.append-bottom-0
%p
The following items will be exported:
%ul
%li Project and wiki repositories
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
%p
The following items will NOT be exported:
%ul
%li Build traces and artifacts
%li LFS objects
%hr
- if can? current_user, :archive_project, @project - if can? current_user, :archive_project, @project
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-3
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
Forking in progress. Forking in progress.
- else - else
Import in progress. Import in progress.
- unless @project.forked? - if @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url} %p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will. %p Please wait while we import the repository for you. Refresh at will.
:javascript :javascript
......
- content_for :note_actions do - content_for :note_actions do
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes #notes
= render 'projects/notes/notes_with_form' = render 'projects/notes/notes_with_form'
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%p %p
%strong Step 1. %strong Step 1.
Fetch and check out the branch for this merge request Fetch and check out the branch for this merge request
= clipboard_button(clipboard_target: 'pre#merge-info-1') = clipboard_button_with_class({clipboard_target: "pre#merge-info-1"}, css_class: "btn-clipboard")
%pre.dark#merge-info-1 %pre.dark#merge-info-1
- if @merge_request.for_fork? - if @merge_request.for_fork?
:preserve :preserve
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
%p %p
%strong Step 3. %strong Step 3.
Merge the branch and fix any conflicts that come up Merge the branch and fix any conflicts that come up
= clipboard_button(clipboard_target: 'pre#merge-info-3') = clipboard_button_with_class({clipboard_target: "pre#merge-info-3"}, css_class: "btn-clipboard")
%pre.dark#merge-info-3 %pre.dark#merge-info-3
- if @merge_request.for_fork? - if @merge_request.for_fork?
:preserve :preserve
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
%p %p
%strong Step 4. %strong Step 4.
Push the result of the merge to GitLab Push the result of the merge to GitLab
= clipboard_button(clipboard_target: 'pre#merge-info-4') = clipboard_button_with_class({clipboard_target: "pre#merge-info-4"}, css_class: "btn-clipboard")
%pre.dark#merge-info-4 %pre.dark#merge-info-4
:preserve :preserve
git push origin #{h @merge_request.target_branch} git push origin #{h @merge_request.target_branch}
......
...@@ -84,7 +84,12 @@ ...@@ -84,7 +84,12 @@
- if git_import_enabled? - if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do = link_to "#", class: 'btn js-toggle-button import_git' do
%i.fa.fa-git %i.fa.fa-git
%span Any repo by URL %span Repo by URL
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do
%i.fa.fa-gitlab
%span GitLab export
.js-toggle-content.hide .js-toggle-content.hide
= render "shared/import_form", f: f = render "shared/import_form", f: f
...@@ -115,6 +120,33 @@ ...@@ -115,6 +120,33 @@
e.preventDefault(); e.preventDefault();
var import_modal = $(this).next(".modal").show(); var import_modal = $(this).next(".modal").show();
}); });
$('.modal-header .close').bind('click', function() { $('.modal-header .close').bind('click', function() {
$(".modal").hide(); $(".modal").hide();
}); });
$('.import_gitlab_project').bind('click', function() {
var _href = $("a.import_gitlab_project").attr("href");
$(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
});
$('.import_gitlab_project').attr('disabled',true)
$('.import_gitlab_project').attr('title', 'Project path required.');
$('.import_gitlab_project').click(function( event ) {
if($('.import_gitlab_project').attr('disabled')) {
event.preventDefault();
new Flash("Please enter a path for the project to be imported to.");
}
});
$('#project_path').keyup(function(){
if($(this).val().length !=0) {
$('.import_gitlab_project').attr('disabled', false);
$('.import_gitlab_project').attr('title','');
$(".flash-container").html("")
} else {
$('.import_gitlab_project').attr('disabled',true);
$('.import_gitlab_project').attr('title', 'Project path required.');
}
})
...@@ -6,6 +6,6 @@ ...@@ -6,6 +6,6 @@
= render 'projects/notes/hints' = render 'projects/notes/hints'
.note-form-actions.clearfix .note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button' = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel Cancel
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.error-alert .error-alert
.note-form-actions.clearfix .note-form-actions.clearfix
= f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button"
= yield(:note_actions) = yield(:note_actions)
%a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}} %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
Discard draft Discard draft
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
= render 'projects/tags/download', ref: tag.name, project: @project = render 'projects/tags/download', ref: tag.name, project: @project
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes" do = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
= icon("pencil") = icon("pencil")
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
......
...@@ -11,12 +11,23 @@ ...@@ -11,12 +11,23 @@
.nav-controls .nav-controls
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
New tag New tag
.dropdown.inline
%button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
%span.light= @sort.humanize
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to namespace_project_tags_path(sort: nil) do
Name
= link_to namespace_project_tags_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
.tags .tags
- unless @tags.empty? - unless @tags.empty?
%ul.content-list %ul.content-list
- @tags.each do |tag| = render partial: 'tag', collection: @tags
= render 'tag', tag: @repository.find_tag(tag)
= paginate @tags, theme: 'gitlab' = paginate @tags, theme: 'gitlab'
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
- if params[:author_id].present? - if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id]) = hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author], field_name: "author_id", default_label: "Author" } }) placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline .filter-item.inline
- if params[:assignee_id].present? - if params[:assignee_id].present?
......
- todo = has_todo(issuable) - todo = issuable_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class } %aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar .issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
...@@ -9,12 +9,12 @@ ...@@ -9,12 +9,12 @@
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } } %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
- if current_user - if current_user
%button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", id: (todo.id unless todo.nil?), issuable: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project) } } %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
%span.js-issuable-todo-text %span.js-issuable-todo-text
- if todo.nil? - if todo
Add Todo
- else
Mark Done Mark Done
- else
Add Todo
= icon('spin spinner', class: 'hidden js-issuable-todo-loading') = icon('spin spinner', class: 'hidden js-issuable-todo-loading')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
......
- member = source.members.find_by(user_id: current_user.id) - member = source.members.find_by(user_id: current_user.id)
- group_member = source.group.members.find_by(user_id: current_user.id) if source.respond_to?(:group) && source.group
- if member - unless group_member
- if member.request? - if member
= link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), - if member.request?
method: :delete, = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
data: { confirm: remove_member_message(member) }, method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn access-request-button hidden-xs'
- else
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn access-request-button hidden-xs' class: 'btn access-request-button hidden-xs'
- else
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn access-request-button hidden-xs'
- default_show_roles = can?(current_user, action_member_permission(:update, member), member) || can?(current_user, action_member_permission(:destroy, member), member) - show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
- show_roles = local_assigns.fetch(:show_roles, default_show_roles)
- show_controls = local_assigns.fetch(:show_controls, true) - show_controls = local_assigns.fetch(:show_controls, true)
- user = member.user - user = member.user
......
...@@ -10,6 +10,13 @@ ...@@ -10,6 +10,13 @@
open and open and
%strong= milestone.issues_visible_to_user(current_user).closed.size %strong= milestone.issues_visible_to_user(current_user).closed.size
closed closed
%strong= milestone.merge_requests.size
merge requests:
%span.milestone-stat
%strong= milestone.merge_requests.opened.size
open and
%strong= milestone.merge_requests.merged.size
merged
%span.milestone-stat %span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}% %strong== #{milestone.percent_complete(current_user)}%
complete complete
......
- left_align = local_assigns[:left_align]
- if notification_setting
.dropdown.notification-dropdown.pull-right
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
%span.caret
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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