diff options
Diffstat (limited to 'app')
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 |