summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/LabelManager.js.coffee5
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee9
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee3
-rw-r--r--app/assets/javascripts/labels_select.js.coffee25
-rw-r--r--app/assets/javascripts/notifications_dropdown.js.coffee24
-rw-r--r--app/assets/javascripts/notifications_form.js.coffee49
-rw-r--r--app/assets/javascripts/profile.js.coffee4
-rw-r--r--app/assets/javascripts/project.js.coffee16
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee18
-rw-r--r--app/assets/javascripts/users/calendar.js.coffee17
-rw-r--r--app/assets/stylesheets/framework/mobile.scss13
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/commit.scss2
-rw-r--r--app/assets/stylesheets/pages/events.scss3
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss8
-rw-r--r--app/assets/stylesheets/pages/profile.scss19
-rw-r--r--app/assets/stylesheets/pages/projects.scss25
-rw-r--r--app/controllers/application_controller.rb23
-rw-r--r--app/controllers/dashboard/todos_controller.rb25
-rw-r--r--app/controllers/groups/notification_settings_controller.rb16
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb48
-rw-r--r--app/controllers/notification_settings_controller.rb36
-rw-r--r--app/controllers/profiles/accounts_controller.rb2
-rw-r--r--app/controllers/profiles/notifications_controller.rb18
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb42
-rw-r--r--app/controllers/projects/notification_settings_controller.rb16
-rw-r--r--app/controllers/projects/tags_controller.rb6
-rw-r--r--app/controllers/projects/todos_controller.rb24
-rw-r--r--app/controllers/projects_controller.rb44
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/button_helper.rb12
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/members_helper.rb6
-rw-r--r--app/helpers/notifications_helper.rb28
-rw-r--r--app/helpers/todos_helper.rb6
-rw-r--r--app/mailers/emails/projects.rb13
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/commit_status.rb3
-rw-r--r--app/models/concerns/importable.rb6
-rw-r--r--app/models/concerns/referable.rb4
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/merge_request.rb11
-rw-r--r--app/models/merge_request_diff.rb3
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/notification_setting.rb37
-rw-r--r--app/models/personal_access_token.rb20
-rw-r--r--app/models/project.rb34
-rw-r--r--app/models/repository.rb19
-rw-r--r--app/models/user.rb23
-rw-r--r--app/services/notification_service.rb126
-rw-r--r--app/services/projects/create_service.rb10
-rw-r--r--app/services/projects/import_export/export_service.rb57
-rw-r--r--app/services/projects/import_service.rb25
-rw-r--r--app/services/todo_service.rb19
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/runners/index.html.haml124
-rw-r--r--app/views/import/github/status.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml25
-rw-r--r--app/views/layouts/nav/_admin.html.haml6
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml5
-rw-r--r--app/views/notify/project_was_exported_email.html.haml8
-rw-r--r--app/views/notify/project_was_exported_email.text.erb6
-rw-r--r--app/views/notify/project_was_not_exported_email.html.haml9
-rw-r--r--app/views/notify/project_was_not_exported_email.text.erb6
-rw-r--r--app/views/profiles/accounts/show.html.haml10
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml3
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml3
-rw-r--r--app/views/profiles/notifications/show.html.haml13
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml105
-rw-r--r--app/views/projects/_home_panel.html.haml12
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/buttons/_notifications.html.haml11
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/edit.html.haml36
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml6
-rw-r--r--app/views/projects/new.html.haml34
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml15
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml10
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml20
-rw-r--r--app/views/shared/members/_member.html.haml3
-rw-r--r--app/views/shared/milestones/_summary.html.haml7
-rw-r--r--app/views/shared/notifications/_button.html.haml25
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml31
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml13
-rw-r--r--app/workers/gitlab_remove_project_export_worker.rb9
-rw-r--r--app/workers/project_export_worker.rb12
-rw-r--r--app/workers/repository_check/single_repository_worker.rb6
97 files changed, 1291 insertions, 358 deletions
diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee
index b06bcf0fcbf..6d8faba40d7 100644
--- a/app/assets/javascripts/LabelManager.js.coffee
+++ b/app/assets/javascripts/LabelManager.js.coffee
@@ -27,6 +27,11 @@ class @LabelManager
$btn = $(e.currentTarget)
$label = $("##{$btn.data('domId')}")
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)
toggleLabelPriority: ($label, action, persistState = true) ->
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index b560500cce6..7fbff9214cf 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -78,6 +78,7 @@ class Dispatcher
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
+ new NotificationsForm()
new TreeView() if $('#tree-slider').length
when 'groups:activity'
new Activities()
@@ -129,6 +130,8 @@ class Dispatcher
shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles'
new Profile()
+ new NotificationsForm()
+ new NotificationsDropdown()
when 'projects'
new Project()
new ProjectAvatar()
@@ -136,8 +139,12 @@ class Dispatcher
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
- when 'new', 'show'
+ when 'new'
new ProjectNew()
+ when 'show'
+ new ProjectNew()
+ new ProjectShow()
+ new NotificationsDropdown()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index b49bd4565a7..2a8a1f05b35 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -302,6 +302,9 @@ class GitLabDropdown
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
+ if @options.setActiveIds
+ @options.setActiveIds.call(@)
+
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index d350a7c0e7f..6a10db10eb1 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -210,9 +210,21 @@ class @LabelsSelect
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
+ active = instance.activeIds
+
if indeterminate.indexOf(label.id) isnt -1
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']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
@@ -328,6 +340,10 @@ class @LabelsSelect
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
+
+ setActiveIds: ->
+ if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @activeIds = _this.getActiveIds()
)
@bindEvents()
@@ -352,3 +368,12 @@ class @LabelsSelect
label_ids.push $("#issue_#{issue_id}").data('labels')
_.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
diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee
new file mode 100644
index 00000000000..74d2298c1fa
--- /dev/null
+++ b/app/assets/javascripts/notifications_dropdown.js.coffee
@@ -0,0 +1,24 @@
+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')
diff --git a/app/assets/javascripts/notifications_form.js.coffee b/app/assets/javascripts/notifications_form.js.coffee
new file mode 100644
index 00000000000..3432428702a
--- /dev/null
+++ b/app/assets/javascripts/notifications_form.js.coffee
@@ -0,0 +1,49 @@
+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)
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index 26a12423521..1583d1ba6f9 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -8,6 +8,10 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(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', ->
$('.loading-username').show()
$(this).find('.update-success').hide()
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 07be85a32a5..d12bad97a05 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -34,22 +34,6 @@ class @Project
$(@).parents('.no-password-message').remove()
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()
diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
index 8eb005b0a22..12340bbce54 100644
--- a/app/assets/javascripts/right_sidebar.js.coffee
+++ b/app/assets/javascripts/right_sidebar.js.coffee
@@ -51,15 +51,19 @@ class @Sidebar
$this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this)
- ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
- ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
+ ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST'
+
+ if $this.attr('data-delete-path')
+ url = "#{$this.attr('data-delete-path')}"
+ else
+ url = "#{$this.data('url')}"
$.ajax(
- url: "#{$this.data('url')}#{ajaxUrlExtra}"
+ url: url
type: ajaxType
dataType: 'json'
data:
- issuable_id: $this.data('issuable')
+ issuable_id: $this.data('issuable-id')
issuable_type: $this.data('issuable-type')
beforeSend: =>
@beforeTodoSend($this, $todoLoading)
@@ -82,15 +86,15 @@ class @Sidebar
else
$todoPendingCount.removeClass 'hidden'
- if data.todo?
+ if data.delete_path?
$btn
.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')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
- .removeAttr 'data-id'
+ .removeAttr 'data-delete-path'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) ->
diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee
index 26a26061539..c081f023b04 100644
--- a/app/assets/javascripts/users/calendar.js.coffee
+++ b/app/assets/javascripts/users/calendar.js.coffee
@@ -6,12 +6,6 @@ class @Calendar
@daySizeWithSpace = @daySize + (@daySpace * 2)
@monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
@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
# The group of objects will be grouped based on the day of the week they are
@@ -39,8 +33,8 @@ class @Calendar
i++
# Init color functions
- @color = @initColor()
@colorKey = @initColorKey()
+ @color = @initColor()
# Init the svg element
@renderSvg(group)
@@ -104,7 +98,7 @@ class @Calendar
.attr 'class', 'user-contrib-cell js-tooltip'
.attr 'fill', (stamp) =>
if stamp.count isnt 0
- @color(stamp.count)
+ @color(Math.min(stamp.count, 40))
else
'#ededed'
.attr 'data-container', 'body'
@@ -164,10 +158,11 @@ class @Calendar
color
initColor: ->
+ colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
d3.scale
- .linear()
- .range(['#acd5f2', '#254e77'])
- .domain([0, @highestValue])
+ .threshold()
+ .domain([0, 10, 20, 30])
+ .range(colorRange)
initColorKey: ->
d3.scale
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index d4e5cc819a4..c74682dfef4 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -52,6 +52,19 @@
.git-clone-holder {
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 {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 148b00ac853..c37574ca7a1 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -268,5 +268,10 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
+/*
+ * Personal Access Tokens
+ */
+$personal-access-tokens-disabled-label-color: #bbb;
+
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index fc3f214aba5..35ab28b3fea 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -26,6 +26,8 @@
.commit-info-row {
margin-bottom: 10px;
+ line-height: 24px;
+ padding-top: 6px;
&.commit-info-row-header {
line-height: 34px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 6fe57c737b3..6c36f603daf 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -136,9 +136,10 @@
.event-last-push {
overflow: auto;
width: 100%;
+
.event-last-push-text {
@include str-truncated(100%);
- padding: 5px 0;
+ padding: 4px 0;
font-size: 13px;
float: left;
margin-right: -150px;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 53bff508c72..e67271adfb1 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -244,6 +244,10 @@
.panel-footer {
padding: 5px 10px;
+
+ .btn {
+ min-width: auto;
+ }
}
.commit {
@@ -252,9 +256,7 @@
}
.avatar {
- width: 20px;
- height: 20px;
- margin-right: 5px;
+ margin-left: 0;
}
.commit-row-info {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 167ab40d881..46371ec6871 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -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 {
@media (max-width: $screen-xs-max) {
.cover-block {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 855d86cb238..f138a2f5387 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -128,11 +128,6 @@
}
}
- .btn-group:not(:first-child):not(:last-child) > .btn {
- border-top-right-radius: 3px;
- border-bottom-right-radius: 3px;
- }
-
form {
margin-left: 10px;
}
@@ -499,7 +494,8 @@ pre.light-well {
.activity-filter-block {
.controls {
- padding-bottom: 10px;
+ padding-bottom: 7px;
+ margin-top: 8px;
border-bottom: 1px solid $border-color;
}
}
@@ -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;
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index cd6ae507cf1..dd1bc6f5d52 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper
include WorkhorseHelper
- before_action :authenticate_user_from_token!
+ before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :reject_blocked!
@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
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|
log_exception(exception)
@@ -64,17 +64,10 @@ class ApplicationController < ActionController::Base
end
end
- # From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
- # https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
- def authenticate_user_from_token!
- user_token = if params[:authenticity_token].presence
- 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)
+ # This filter handles both private tokens and personal access tokens
+ def authenticate_user_from_private_token!
+ token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
+ user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
if user
# Notice we are passing store false, so the user is not
@@ -326,6 +319,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git')
end
+ def gitlab_project_import_enabled?
+ current_application_settings.import_sources.include?('gitlab_project')
+ end
+
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index f9a1929c117..3a2db3e6eeb 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,44 +1,39 @@
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
@todos = @todos.page(params[:page])
end
def destroy
- todo.done
-
- todo_notice = 'Todo was successfully marked as done.'
+ TodoService.new.mark_todos_as_done([todo], current_user)
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.json do
- render json: { count: @todos.size, done_count: current_user.todos.done.count }
- end
+ format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
end
end
def destroy_all
- @todos.each(&:done)
+ TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
- format.json do
- find_todos
- render json: { count: @todos.size, done_count: current_user.todos.done.count }
- end
+ format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
end
end
private
def todo
- @todo ||= current_user.todos.find(params[:id])
+ @todo ||= find_todos.find(params[:id])
end
def find_todos
- @todos = TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, params).execute
end
end
diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb
deleted file mode 100644
index de13b16ccf2..00000000000
--- a/app/controllers/groups/notification_settings_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-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
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
new file mode 100644
index 00000000000..f99aa490d3e
--- /dev/null
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -0,0 +1,48 @@
+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
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
new file mode 100644
index 00000000000..eddd03cc229
--- /dev/null
+++ b/app/controllers/notification_settings_controller.rb
@@ -0,0 +1,36 @@
+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
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 175afbf8425..69959fe3687 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
def unlink
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
end
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 40d1906a53f..b8b71d295f6 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -1,13 +1,13 @@
class Profiles::NotificationsController < Profiles::ApplicationController
def show
@user = current_user
- @group_notifications = current_user.notification_settings.for_groups
- @project_notifications = current_user.notification_settings.for_projects
+ @group_notifications = current_user.notification_settings.for_groups.order(:id)
+ @project_notifications = current_user.notification_settings.for_projects.order(:id)
@global_notification_setting = current_user.global_notification_setting
end
def update
- if current_user.update_attributes(user_params) && update_notification_settings
+ if current_user.update_attributes(user_params)
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
@@ -19,16 +19,4 @@ class Profiles::NotificationsController < Profiles::ApplicationController
def user_params
params.require(:user).permit(:notification_email)
end
-
- def global_notification_setting_params
- params.require(:global_notification_setting).permit(:level)
- end
-
- private
-
- def update_notification_settings
- return true unless global_notification_setting_params
-
- current_user.global_notification_setting.update_attributes(global_notification_setting_params)
- end
end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
new file mode 100644
index 00000000000..508b82a9a6c
--- /dev/null
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -0,0 +1,42 @@
+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
diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb
deleted file mode 100644
index 7d81cc03c73..00000000000
--- a/app/controllers/projects/notification_settings_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-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
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 46b242aa5ff..6dc495247c8 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
- sorted = VersionSorter.rsort(@repository.tag_names)
- @tags = Kaminari.paginate_array(sorted).page(params[:page])
+ @sort = params[:sort] || 'name'
+ @tags = @repository.tags_sorted_by(@sort)
+ @tags = Kaminari.paginate_array(@tags).page(params[:page])
+
@releases = project.releases.where(tag: @tags)
end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index a51bd5e2b49..23868d986e9 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -1,18 +1,12 @@
class Projects::TodosController < Projects::ApplicationController
- def create
- todos = TodoService.new.mark_todo(issuable, current_user)
+ before_action :authenticate_user!, only: [:create]
- render json: {
- todo: todos,
- count: current_user.todos.pending.count,
- }
- end
-
- def update
- current_user.todos.find_by_id(params[:id]).update(state: :done)
+ def create
+ todo = TodoService.new.mark_todo(issuable, current_user)
render json: {
- count: current_user.todos.pending.count,
+ count: current_user.todos_pending_count,
+ delete_path: dashboard_todo_path(todo)
}
end
@@ -22,7 +16,13 @@ class Projects::TodosController < Projects::ApplicationController
@issuable ||= begin
case params[:issuable_type]
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"
@project.merge_requests.find(params[:issuable_id])
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 482c11cf23c..8044c637825 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# 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]
layout :determine_layout
@@ -186,6 +186,48 @@ class ProjectsController < Projects::ApplicationController
)
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
current_user.toggle_star(@project)
@project.reload
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index aa47c6c157e..58a00f88af7 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -123,7 +123,7 @@ class TodosFinder
end
def by_state(items)
- case params[:state]
+ case params[:state].to_s
when 'done'
items.done
else
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 07a3f452460..9051a493b9b 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -17,11 +17,21 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
- class: "btn",
+ class: "btn btn-clipboard",
data: data,
type: :button
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')
content_tag :button,
icon('clipboard'),
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8dbc51a689f..8231ce49fac 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -67,9 +67,9 @@ module IssuablesHelper
end
end
- def has_todo(issuable)
- unless current_user.nil?
- current_user.todos.find_by(target_id: issuable.id, state: :pending)
+ def issuable_todo(issuable)
+ if current_user
+ current_user.todos.find_by(target: issuable, state: :pending)
end
end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 877c77050be..ec106418f2d 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -6,6 +6,12 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym
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)
user = current_user if defined?(current_user)
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 50c21fc0d49..77783cd7640 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -34,7 +34,7 @@ module NotificationsHelper
def notification_description(level)
case level.to_sym
when :participating
- 'You will only receive notifications from related resources'
+ 'You will only receive notifications for threads you have participated in'
when :mention
'You will receive notifications only for comments in which you were @mentioned'
when :watch
@@ -43,6 +43,8 @@ module NotificationsHelper
'You will not get any notifications via email'
when :global
'Use your global notification setting'
+ when :custom
+ 'You will only receive notifications for the events you choose'
end
end
@@ -62,22 +64,14 @@ module NotificationsHelper
end
end
- def notification_level_radio_buttons
- html = ""
-
- NotificationSetting.levels.each_key do |level|
- level = level.to_sym
- next if level == :global
-
- html << content_tag(:div, class: "radio") do
- content_tag(:label, { value: level }) do
- radio_button_tag(:"global_notification_setting[level]", level, @global_notification_setting.level.to_sym == level) +
- content_tag(:div, level.to_s.capitalize, class: "level-title") +
- content_tag(:p, notification_description(level))
- end
- end
- end
+ # Identifier to trigger individually dropdowns and custom settings modals in the same view
+ def notifications_menu_identifier(type, notification_setting)
+ "#{type}-#{notification_setting.user_id}-#{notification_setting.source_id}-#{notification_setting.source_type}"
+ 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
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 9adf5ef29f7..a832a6c8df7 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,10 +1,10 @@
module TodosHelper
def todos_pending_count
- current_user.todos.pending.count
+ TodosFinder.new(current_user, state: :pending).execute.count
end
def todos_done_count
- current_user.todos.done.count
+ TodosFinder.new(current_user, state: :done).execute.count
end
def todo_action_name(todo)
@@ -12,7 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
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
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 689fb3e0ffb..e0af7081411 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -9,6 +9,19 @@ module Emails
subject: subject("Project was moved"))
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 = {})
@message =
Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a744f937918..d914b0b26eb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_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'],
- 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'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 764d8e4e136..d618c84e983 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -341,6 +341,7 @@ module Ci
def erase_artifacts!
remove_artifacts_file!
remove_artifacts_metadata!
+ save
end
def erase(opts = {})
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index a5f2ac59001..5b264ecffc5 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -170,6 +170,10 @@ module Ci
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
+ def notes
+ Note.for_commit_id(sha)
+ end
+
private
def build_builds_for_stages(stages, user, status, trigger_request)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e53c483b904..ab13db4b297 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,5 +1,6 @@
class CommitStatus < ActiveRecord::Base
include Statuseable
+ include Importable
self.table_name = 'ci_builds'
@@ -7,7 +8,7 @@ class CommitStatus < ActiveRecord::Base
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user
- validates :pipeline, presence: true
+ validates :pipeline, presence: true, unless: :importing?
validates_presence_of :name
diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb
new file mode 100644
index 00000000000..019ef755849
--- /dev/null
+++ b/app/models/concerns/importable.rb
@@ -0,0 +1,6 @@
+module Importable
+ extend ActiveSupport::Concern
+
+ attr_accessor :importing
+ alias_method :importing?, :importing
+end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index ce064f675ae..dee940a3f88 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -49,6 +49,10 @@ module Referable
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
+ def reference_valid?(reference)
+ true
+ end
+
def link_reference_pattern(route, pattern)
%r{
(?<url>
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1bdf9c011b2..3c5859194b4 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -83,6 +83,10 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
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: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
diff --git a/app/models/member.rb b/app/models/member.rb
index cea6d259760..4ee3f1bb5c2 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,5 +1,6 @@
class Member < ActiveRecord::Base
include Sortable
+ include Importable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -41,11 +42,11 @@ class Member < ActiveRecord::Base
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
- after_create :send_invite, if: :invite?
- after_create :send_request, if: :request?
- after_create :create_notification_setting, unless: :pending?
- after_create :post_create_hook, unless: :pending?
- after_update :post_update_hook, unless: :pending?
+ after_create :send_invite, if: :invite?, unless: :importing?
+ after_create :send_request, if: :request?, unless: :importing?
+ after_create :create_notification_setting, unless: [:pending?, :importing?]
+ after_create :post_create_hook, unless: [:pending?, :importing?]
+ after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
after_destroy :post_decline_request, if: :request?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7b8858b24d6..36bc98bdb1e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -4,6 +4,7 @@ class MergeRequest < ActiveRecord::Base
include Referable
include Sortable
include Taskable
+ include Importable
belongs_to :target_project, foreign_key: :target_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
serialize :merge_params, Hash
- after_create :create_merge_request_diff
+ after_create :create_merge_request_diff, unless: :importing
after_update :update_merge_request_diff
delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
@@ -95,12 +96,12 @@ class MergeRequest < ActiveRecord::Base
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 :target_project, presence: true
validates :target_branch, presence: true
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
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
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
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.
#
# This method uses a UNION as it usually operates on the result of
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 7d5103748f5..aca377cc600 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,5 +1,6 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
+ include Importable
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
@@ -22,7 +23,7 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
- after_create :reload_content
+ after_create :reload_content, unless: :importing?
def reload_content
reload_commits
diff --git a/app/models/note.rb b/app/models/note.rb
index 4b6748053ff..8d164647550 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -4,6 +4,7 @@ class Note < ActiveRecord::Base
include Participable
include Mentionable
include Awardable
+ include Importable
default_value_for :system, false
@@ -28,11 +29,11 @@ class Note < ActiveRecord::Base
validates :attachment, file_size: { maximum: :max_attachment_size }
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 :author, presence: true
- validate unless: :for_commit? do |note|
+ validate unless: [:for_commit?, :importing?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 0ce87968e46..d41fc7073c6 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -1,5 +1,5 @@
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]
@@ -15,6 +15,24 @@ class NotificationSetting < ActiveRecord::Base
scope :for_groups, -> { where(source_type: 'Namespace') }
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)
setting = find_or_initialize_by(source: source)
@@ -24,4 +42,21 @@ class NotificationSetting < ActiveRecord::Base
setting
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
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
new file mode 100644
index 00000000000..c4b095e0c04
--- /dev/null
+++ b/app/models/personal_access_token.rb
@@ -0,0 +1,20 @@
+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
diff --git a/app/models/project.rb b/app/models/project.rb
index 8eef22356e2..ca3bc04e2dd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -367,6 +367,11 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC')
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
def team
@@ -470,7 +475,7 @@ class Project < ActiveRecord::Base
end
def import?
- external_import? || forked?
+ external_import? || forked? || gitlab_project_import?
end
def no_import?
@@ -501,6 +506,10 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new(import_url).masked_url
end
+ def gitlab_project_import?
+ import_type == 'gitlab_project'
+ end
+
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit
@@ -1096,4 +1105,27 @@ class Project < ActiveRecord::Base
ensure
@errors = original_errors
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
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 65d1bad511d..bbd7682d8e7 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -598,6 +598,21 @@ class Repository
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
commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
@@ -995,4 +1010,8 @@ class Repository
def file_on_head(regex)
tree(:head).blobs.find { |file| file.name =~ regex }
end
+
+ def tags_sorted_by_committed_date
+ tags.sort_by { |tag| commit(tag.target).committed_date }
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8d0427da5ab..2e458329cb9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -51,6 +51,7 @@ class User < ActiveRecord::Base
# Profile
has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy
+ has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
@@ -267,6 +268,11 @@ class User < ActiveRecord::Base
find_by!('lower(username) = ?', username.downcase)
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)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
@@ -821,6 +827,23 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true)
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
def projects_union(min_access_level = nil)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f804ac171c4..19832a19b2b 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -29,9 +29,10 @@ class NotificationService
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the issue's labels
+ # * users with custom level checked with "new issue"
#
def new_issue(issue, current_user)
- new_resource_email(issue, issue.project, 'new_issue_email')
+ new_resource_email(issue, issue.project, :new_issue_email)
end
# When we close an issue we should send an email to:
@@ -39,18 +40,20 @@ class NotificationService
# * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
+ # * users with custom level checked with "close issue"
#
def close_issue(issue, current_user)
- close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
+ close_resource_email(issue, issue.project, current_user, :closed_issue_email)
end
# When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
+ # * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user)
- reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
+ reassign_resource_email(issue, issue.project, current_user, :reassigned_issue_email)
end
# When we add labels to an issue we should send an email to:
@@ -58,7 +61,7 @@ class NotificationService
# * watchers of the issue's labels
#
def relabeled_issue(issue, added_labels, current_user)
- relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
+ relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
end
# When create a merge request we should send an email to:
@@ -66,18 +69,20 @@ class NotificationService
# * mr assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
+ # * users with custom level checked with "new merge request"
#
def new_merge_request(merge_request, current_user)
- new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
+ new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
end
# When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
+ # * users with custom level checked with "reassign merge request"
#
def reassigned_merge_request(merge_request, current_user)
- reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
+ reassign_resource_email(merge_request, merge_request.target_project, current_user, :reassigned_merge_request_email)
end
# When we add labels to a merge request we should send an email to:
@@ -85,15 +90,15 @@ class NotificationService
# * watchers of the mr's labels
#
def relabeled_merge_request(merge_request, added_labels, current_user)
- relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
+ relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
end
def close_mr(merge_request, current_user)
- close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
+ close_resource_email(merge_request, merge_request.target_project, current_user, :closed_merge_request_email)
end
def reopen_issue(issue, current_user)
- reopen_resource_email(issue, issue.project, current_user, 'issue_status_changed_email', 'reopened')
+ reopen_resource_email(issue, issue.project, current_user, :issue_status_changed_email, 'reopened')
end
def merge_mr(merge_request, current_user)
@@ -101,7 +106,7 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- 'merged_merge_request_email'
+ :merged_merge_request_email
)
end
@@ -110,7 +115,7 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- 'merge_request_status_email',
+ :merge_request_status_email,
'reopened'
)
end
@@ -153,6 +158,9 @@ class NotificationService
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, note.project, :new_note)
+
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
recipients = recipients + mentioned_users
@@ -222,7 +230,7 @@ class NotificationService
end
def accept_group_invite(group_member)
- mailer.member_invite_accepted_email(group_member.id).deliver_later
+ mailer.member_invite_accepted_email(group_member.real_source_type, group_member.id).deliver_later
end
def decline_group_invite(group_member)
@@ -266,14 +274,41 @@ class NotificationService
end
end
+ def project_exported(project, current_user)
+ mailer.project_was_exported_email(current_user, project).deliver_later
+ end
+
+ def project_not_exported(project, current_user, errors)
+ mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
+ end
+
protected
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications(recipients, project, action)
+ user_ids = []
+
+ # Users with a notification setting on group or project
+ user_ids += notification_settings_for(project, :custom, action)
+ user_ids += notification_settings_for(project.group, :custom, action)
+
+ # Users with global level custom
+ users_with_project_level_global = notification_settings_for(project, :global)
+ users_with_group_level_global = notification_settings_for(project.group, :global)
+
+ global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
+ user_ids += users_with_global_level_custom(global_users_ids, action)
+
+ recipients.concat(User.find(user_ids))
+ end
+
# Get project users with WATCH notification level
def project_watchers(project)
- project_members = project_member_notification(project)
+ project_members = notification_settings_for(project)
+
+ users_with_project_level_global = notification_settings_for(project, :global)
+ users_with_group_level_global = notification_settings_for(project.group, :global)
- users_with_project_level_global = project_member_notification(project, :global)
- users_with_group_level_global = group_member_notification(project, :global)
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
@@ -282,33 +317,39 @@ class NotificationService
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
- def project_member_notification(project, notification_level=nil)
+ def notification_settings_for(resource, notification_level = nil, action = nil)
+ return [] unless resource
+
if notification_level
- project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
+ settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+ settings = settings.select { |setting| setting.events[action] } if action.present?
+ settings.map(&:user_id)
else
- project.notification_settings.pluck(:user_id)
+ resource.notification_settings.pluck(:user_id)
end
end
- def group_member_notification(project, notification_level)
- if project.group
- project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
- else
- []
- end
+ def users_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
end
- def users_with_global_level_watch(ids)
+ def users_with_global_level_custom(ids, action)
+ settings = settings_with_global_level_of(:custom, ids)
+ settings = settings.select { |setting| setting.events[action] }
+ settings.map(&:user_id)
+ end
+
+ def settings_with_global_level_of(level, ids)
NotificationSetting.where(
user_id: ids,
source_type: nil,
- level: NotificationSetting.levels[:watch]
- ).pluck(:user_id)
+ level: NotificationSetting.levels[level]
+ )
end
# Build a list of users based on project notifcation settings
def select_project_member_setting(project, global_setting, users_global_level_watch)
- users = project_member_notification(project, :watch)
+ users = notification_settings_for(project, :watch)
# If project setting is global, add to watch list if global setting is watch
global_setting.each do |user_id|
@@ -322,7 +363,7 @@ class NotificationService
# Build a list of users based on group notification settings
def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
- uids = group_member_notification(project, :watch)
+ uids = notification_settings_for(project, :watch)
# Group setting is watch, add to users list if user is not project member
users = []
@@ -343,7 +384,7 @@ class NotificationService
end
def add_project_watchers(recipients, project)
- recipients.concat(project_watchers(project)).compact.uniq
+ recipients.concat(project_watchers(project)).compact
end
# Remove users with disabled notifications from array
@@ -428,7 +469,7 @@ class NotificationService
end
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author, action: :new)
+ recipients = build_recipients(target, project, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -436,7 +477,8 @@ class NotificationService
end
def close_resource_email(target, project, current_user, method)
- recipients = build_recipients(target, project, current_user)
+ action = method == :merged_merge_request_email ? "merge" : "close"
+ recipients = build_recipients(target, project, current_user, action: action)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
@@ -447,7 +489,7 @@ class NotificationService
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, action: :reassign, previous_assignee: previous_assignee)
+ recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
recipients.each do |recipient|
mailer.send(
@@ -470,7 +512,7 @@ class NotificationService
end
def reopen_resource_email(target, project, current_user, method, status)
- recipients = build_recipients(target, project, current_user)
+ recipients = build_recipients(target, project, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
@@ -478,14 +520,20 @@ class NotificationService
end
def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
+ custom_action = build_custom_key(action, target)
+
recipients = target.participants(current_user)
recipients = add_project_watchers(recipients, project)
+
+ recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
+ recipients = recipients.uniq
+
# Re-assign is considered as a mention of the new assignee so we add the
# new assignee to the list of recipients after we rejected users with
# the "on mention" notification level
- if action == :reassign
+ if [:reassign_merge_request, :reassign_issue].include?(custom_action)
recipients << previous_assignee if previous_assignee
recipients << target.assignee
end
@@ -493,7 +541,7 @@ class NotificationService
recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target)
- if action == :new
+ if [:new_issue, :new_merge_request].include?(custom_action)
recipients = add_labels_subscribers(recipients, target)
end
@@ -523,4 +571,10 @@ class NotificationService
end
end
end
+
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def build_custom_key(action, object)
+ "#{action}_#{object.class.name.underscore}".to_sym
+ end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 61cac5419ad..55956be2844 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -80,16 +80,18 @@ module Projects
def after_create_actions
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)
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]
end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
new file mode 100644
index 00000000000..80c7193efcb
--- /dev/null
+++ b/app/services/projects/import_export/export_service.rb
@@ -0,0 +1,57 @@
+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
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index c4838d31f2f..9159ec08959 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -9,26 +9,31 @@ module Projects
'fogbugz',
'gitlab',
'github',
- 'google_code'
+ 'google_code',
+ 'gitlab_project'
]
def execute
- if unknown_url?
- # In this case, we only want to import issues, not a repository.
- create_repository
- else
- import_repository
- end
+ add_repository_to_project unless project.gitlab_project_import?
import_data
success
- rescue Error => e
+ rescue => e
error(e.message)
end
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
unless project.create_repository
raise Error, 'The repository could not be created.'
@@ -46,7 +51,7 @@ module Projects
def import_data
return unless has_importer?
- project.repository.before_import
+ project.repository.before_import unless project.gitlab_project_import?
unless importer.execute
raise Error, 'The remote data could not be imported.'
@@ -58,6 +63,8 @@ module Projects
end
def importer
+ return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
+
class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
class_name.constantize.new(project)
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index e1f9ea64dc4..540bf54b920 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -1,6 +1,6 @@
# TodoService class
#
-# Used for creating todos after certain user actions
+# Used for creating/updating todos after certain user actions
#
# Ex.
# TodoService.new.new_issue(issue, current_user)
@@ -137,6 +137,15 @@ class TodoService
def mark_pending_todos_as_done(target, user)
attributes = attributes_for_target(target)
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
# When user marks an issue as todo
@@ -151,6 +160,7 @@ class TodoService
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
Todo.create(attributes.merge(user_id: user.id))
+ user.update_todos_count_cache
end
end
@@ -161,11 +171,16 @@ class TodoService
def update_issuable(issuable, author)
# 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)
end
+ def toggling_tasks?(issuable)
+ issuable.previous_changes.include?('description') &&
+ issuable.tasks? && issuable.updated_tasks.any?
+ end
+
def handle_note(note, author)
# Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet?
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index 7b3f88c24df..b74da64f82e 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -20,3 +20,7 @@
= link_to admin_builds_path, title: 'Builds' do
%span
Builds
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 2dad64b8d0f..5eff77aff2d 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,67 +1,73 @@
-%p.lead.prepend-top-default
- %span
- 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}
+- @no_container = true
+= render "admin/dashboard/head"
-.bs-callout.clearfix
- .pull-left
- %p
- You can reset runners registration token by pressing a button below.
- %p
- = button_to reset_runners_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: 'Are you sure you want to reset registration token?' } do
- = icon('refresh')
- Reset runners registration token
+%div{ class: (container_class) }
+
+ %p.prepend-top-default
+ %span
+ 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.
+ %br
+ Registration token is
+ %code{ id: 'runners-token' } #{current_application_settings.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
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ You can reset runners registration token by pressing a button below.
+ %p
+ = button_to reset_runners_token_admin_application_settings_path,
+ 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
- %span Each runner can be in one of the following states:
- %ul
- %li
- %span.label.label-success shared
- \- run builds from all unassigned projects
- %li
- %span.label.label-info specific
- \- run builds from assigned projects
- %li
- %span.label.label-danger paused
- \- runner will not receive any new builds
+ %div
+ %span Each runner can be in one of the following states:
+ %ul
+ %li
+ %span.label.label-success shared
+ \- run builds from all unassigned projects
+ %li
+ %span.label.label-info specific
+ \- run builds from assigned projects
+ %li
+ %span.label.label-danger paused
+ \- runner will not receive any new builds
-.append-bottom-20.clearfix
- .pull-left
- = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
- .form-group
- = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
- = submit_tag 'Search', class: 'btn'
+ .append-bottom-20.clearfix
+ .pull-left
+ = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
+ = submit_tag 'Search', class: 'btn'
- .pull-right.light
- Runners with last contact less than a minute ago: #{@active_runners_cnt}
+ .pull-right.light
+ Runners with last contact less than a minute ago: #{@active_runners_cnt}
-%br
+ %br
-.table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Projects
- %th Builds
- %th Tags
- %th Last contact
- %th
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Projects
+ %th Builds
+ %th Tags
+ %th Last contact
+ %th
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
-= paginate @runners
+ - @runners.each do |runner|
+ = render "admin/runners/runner", runner: runner
+ = paginate @runners
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 6c4a9d68d1f..7486b1423e2 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -6,7 +6,7 @@
%p
%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
Select projects you want to import.
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
new file mode 100644
index 00000000000..44e2653ca4a
--- /dev/null
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -0,0 +1,25 @@
+- 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'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 54aa34bee0b..f02ac949699 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,6 +1,6 @@
%ul.nav-links.scrolling-tabs
.fade-left
- = nav_link(controller: %w(dashboard admin projects users groups builds), html_options: {class: 'home'}) do
+ = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
@@ -12,10 +12,6 @@
= 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
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index d4b1f477f3f..bb6f14a6225 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -13,6 +13,10 @@
= link_to applications_profile_path, title: 'Applications' do
%span
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
= link_to profile_emails_path, title: 'Emails' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index a851cae4b56..39ea4920ccc 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -5,18 +5,19 @@
= icon('cog')
= icon('caret-down')
%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)
- can_edit = can?(current_user, :admin_project, @project)
= render 'layouts/nav/project_settings', access: access, can_edit: can_edit
- - if can_edit || access
+ - if can_edit || is_project_member
%li.divider
- if can_edit
%li
= link_to edit_project_path(@project) do
Edit Project
- - if access
+ - if is_project_member
%li
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
new file mode 100644
index 00000000000..b28fea35ad5
--- /dev/null
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -0,0 +1,8 @@
+%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.
diff --git a/app/views/notify/project_was_exported_email.text.erb b/app/views/notify/project_was_exported_email.text.erb
new file mode 100644
index 00000000000..42c4d176876
--- /dev/null
+++ b/app/views/notify/project_was_exported_email.text.erb
@@ -0,0 +1,6 @@
+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.
diff --git a/app/views/notify/project_was_not_exported_email.html.haml b/app/views/notify/project_was_not_exported_email.html.haml
new file mode 100644
index 00000000000..c9e9ade2cf1
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.html.haml
@@ -0,0 +1,9 @@
+%p
+ Project #{@project.name} couldn't be exported.
+%p
+ The errors we encountered were:
+
+ %ul
+ - @errors.each do |error|
+ %li
+ error
diff --git a/app/views/notify/project_was_not_exported_email.text.erb b/app/views/notify/project_was_not_exported_email.text.erb
new file mode 100644
index 00000000000..a07f6edacf7
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.text.erb
@@ -0,0 +1,6 @@
+Project <%= @project.name %> couldn't be exported.
+
+The errors we encountered were:
+
+- @errors.each do |error|
+<%= error %> \ No newline at end of file
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 3d2a245ecbd..8efe486e01b 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -62,10 +62,14 @@
.provider-btn-image
= provider_image_tag(provider)
- if auth_active?(provider)
- = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
- Disconnect
+ - if provider.to_s == 'saml'
+ %a.provider-btn
+ Active
+ - else
+ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
+ Disconnect
- 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
%hr
- if current_user.can_change_username?
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index f0cf82afe83..537bba21f4a 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -9,5 +9,4 @@
= link_to group.name, group_path(group)
.pull-right
- = form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f|
- = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index e0fad555c09..5b2a69b8891 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -9,5 +9,4 @@
= link_to_project(project)
.pull-right
- = form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f|
- = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index f2659ac14b5..5afd83a522e 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -24,12 +24,15 @@
.form-group
= f.label :notification_email, class: "label-light"
= 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
- = f.submit 'Update settings', class: "btn btn-create"
+ = label_tag :global_notification_level, "Global notification level", class: "label-light"
+ %br
+ .clearfix
+ .form-group.pull-left
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
+
+ .clearfix
+
%hr
%h5
Groups (#{@group_notifications.count})
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
new file mode 100644
index 00000000000..1b45548bd02
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -0,0 +1,105 @@
+- 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);
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 2b19ee93eea..86ea08dd229 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -21,7 +21,7 @@
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- .project-repo-buttons
+ .project-repo-buttons.project-action-buttons
.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
@@ -29,13 +29,13 @@
.project-clone-holder
= render "shared/clone_panel"
- .project-repo-buttons.project-right-buttons
+ .project-repo-buttons.btn-group.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
- .btn-group
- = render "projects/buttons/download"
- = render 'projects/buttons/dropdown'
- = render 'projects/buttons/notifications'
+
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
+ = render 'shared/notifications/button', notification_setting: @notification_setting
:javascript
new Star();
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 87c732626a6..4bd85061240 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -20,15 +20,15 @@
protected
.controls.hidden-xs
- 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
- 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
- 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")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
deleted file mode 100644
index a7a97181096..00000000000
--- a/app/views/projects/buttons/_notifications.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- 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)
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index b117517c0dd..3ad866bb2f1 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -6,10 +6,10 @@
.pull-right.commit-action-buttons
- 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')
= @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
.dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 8449fe1e4e0..27a94fe02dc 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -120,6 +120,42 @@
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save"
%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
.row.prepend-top-default
.col-lg-3
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index c0d1ce0d120..4d8ee562e6a 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -7,7 +7,7 @@
Forking in progress.
- else
Import in progress.
- - unless @project.forked?
+ - if @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index b151393abab..c2f4457b60b 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- 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 '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 '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-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 0dbd159298e..b3bea900d42 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -8,7 +8,7 @@
%p
%strong Step 1.
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
- if @merge_request.for_fork?
:preserve
@@ -25,7 +25,7 @@
%p
%strong Step 3.
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
- if @merge_request.for_fork?
:preserve
@@ -38,7 +38,7 @@
%p
%strong Step 4.
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
:preserve
git push origin #{h @merge_request.target_branch}
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 7e8b8f83467..3c1c6060504 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -84,7 +84,12 @@
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
%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
= render "shared/import_form", f: f
@@ -115,6 +120,33 @@
e.preventDefault();
var import_modal = $(this).next(".modal").show();
});
+
$('.modal-header .close').bind('click', function() {
$(".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.');
+ }
+ })
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index c87a3fadf72..8620f492282 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -6,6 +6,6 @@
= render 'projects/notes/hints'
.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' }
Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 67ed38a7b22..03b3f6935d1 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -14,7 +14,7 @@
.error-alert
.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)
%a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
Discard draft
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 844e1055810..2c11c0e5b21 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -15,7 +15,7 @@
= render 'projects/tags/download', ref: tag.name, project: @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")
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 2779084fe38..4ca1f58ac5c 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -11,12 +11,23 @@
.nav-controls
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
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
- unless @tags.empty?
%ul.content-list
- - @tags.each do |tag|
- = render 'tag', tag: @repository.find_tag(tag)
+ = render partial: 'tag', collection: @tags
= paginate @tags, theme: 'gitlab'
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 380ab465bf4..094d6636c66 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -12,7 +12,7 @@
- if params[:author_id].present?
= 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",
- 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
- if params[:assignee_id].present?
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 210c9b9aab5..adfab1af53e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,4 +1,4 @@
-- todo = has_todo(issuable)
+- todo = issuable_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
@@ -9,12 +9,12 @@
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon
- 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
- - if todo.nil?
- Add Todo
- - else
+ - if todo
Mark Done
+ - else
+ Add Todo
= 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|
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index ed0a6ebcf84..480e8ba6c85 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,12 +1,14 @@
- 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
- - if member.request?
- = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: remove_member_message(member) },
+- unless group_member
+ - if member
+ - if member.request?
+ = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+ 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'
-- else
- = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
- method: :post,
- class: 'btn access-request-button hidden-xs'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 0191814849a..a884e78e6e7 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,5 +1,4 @@
-- 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)
+- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
- show_controls = local_assigns.fetch(:show_controls, true)
- user = member.user
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 385c6596606..975c74f4ea6 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -10,6 +10,13 @@
open and
%strong= milestone.issues_visible_to_user(current_user).closed.size
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
%strong== #{milestone.percent_complete(current_user)}%
complete
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
new file mode 100644
index 00000000000..ff1cf966a9b
--- /dev/null
+++ b/app/views/shared/notifications/_button.html.haml
@@ -0,0 +1,25 @@
+- 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
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
new file mode 100644
index 00000000000..b704981e3db
--- /dev/null
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -0,0 +1,31 @@
+.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), aria: { labelledby: "custom-notifications-title" } }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
+ %span{ aria: { hidden: "true" } } ×
+ %h4#custom-notifications-title.modal-title
+ Custom notification events
+
+ .modal-body
+ .container-fluid
+ = form_for notification_setting, html: { class: "custom-notifications-form" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ .row
+ .col-lg-4
+ %h4.prepend-top-0
+ Notification events
+ %p
+ Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
+ = succeed "." do
+ %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails
+ .col-lg-8
+ - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
+ - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
+ .form-group
+ .checkbox{ class: ("prepend-top-0" if index == 0) }
+ %label{ for: field_id }
+ = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
+ %strong
+ = event.to_s.humanize
+ = icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
new file mode 100644
index 00000000000..d3258ee64cb
--- /dev/null
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -0,0 +1,13 @@
+- left_align = local_assigns[:left_align]
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+ - NotificationSetting.levels.each_key do |level|
+ - next if level == "custom"
+ - next if level == "global" && notification_setting.source.nil?
+
+ = notification_list_item(level, notification_setting)
+
+ %li.divider
+ %li
+ %a.update-notification{ href: "#", role: "button", class: ("is-active" if notification_setting.custom?), data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } }
+ %strong.dropdown-menu-inner-title Custom
+ %span.dropdown-menu-inner-content= notification_description("custom")
diff --git a/app/workers/gitlab_remove_project_export_worker.rb b/app/workers/gitlab_remove_project_export_worker.rb
new file mode 100644
index 00000000000..1d91897d520
--- /dev/null
+++ b/app/workers/gitlab_remove_project_export_worker.rb
@@ -0,0 +1,9 @@
+class GitlabRemoveProjectExportWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform
+ Project.remove_gitlab_exports!
+ end
+end
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
new file mode 100644
index 00000000000..39f6037e077
--- /dev/null
+++ b/app/workers/project_export_worker.rb
@@ -0,0 +1,12 @@
+class ProjectExportWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :gitlab_shell, retry: true
+
+ def perform(current_user_id, project_id)
+ current_user = User.find(current_user_id)
+ project = Project.find(project_id)
+
+ ::Projects::ImportExport::ExportService.new(project, current_user).execute
+ end
+end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index f2d12ba5a7d..98ddf5d0688 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -15,7 +15,7 @@ module RepositoryCheck
private
def check(project)
- if !git_fsck(project.repository)
+ if has_pushes?(project) && !git_fsck(project.repository)
false
elsif project.wiki_enabled?
# Historically some projects never had their wiki repos initialized;
@@ -44,5 +44,9 @@ module RepositoryCheck
false
end
end
+
+ def has_pushes?(project)
+ Project.with_push.exists?(project.id)
+ end
end
end