diff options
Diffstat (limited to 'app')
89 files changed, 981 insertions, 431 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 4718bcf7a1e..61e3f811e73 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -2,6 +2,8 @@ window.GitLab ?= {} GitLab.GfmAutoComplete = + dataLoading: false + dataSource: '' # Emoji @@ -17,17 +19,41 @@ GitLab.GfmAutoComplete = template: '<li><small>${id}</small> ${title}</li>' # Add GFM auto-completion to all input fields, that accept GFM input. - setup: -> - input = $('.js-gfm-input') + setup: (wrap) -> + @input = $('.js-gfm-input') + + # destroy previous instances + @destroyAtWho() + + # set up instances + @setupAtWho() + + if @dataSource + if !@dataLoading + @dataLoading = true + # We should wait until initializations are done + # and only trigger the last .setup since + # The previous .dataSource belongs to the previous issuable + # and the last one will have the **proper** .dataSource property + # TODO: Make this a singleton and turn off events when moving to another page + setTimeout( => + fetch = @fetchData(@dataSource) + fetch.done (data) => + @dataLoading = false + @loadData(data) + , 1000) + + + setupAtWho: -> # Emoji - input.atwho + @input.atwho at: ':' displayTpl: @Emoji.template insertTpl: ':${name}:' # Team Members - input.atwho + @input.atwho at: '@' displayTpl: @Members.template insertTpl: '${atwho-at}${username}' @@ -42,7 +68,7 @@ GitLab.GfmAutoComplete = title: sanitize(title) search: sanitize("#{m.username} #{m.name}") - input.atwho + @input.atwho at: '#' alias: 'issues' searchKey: 'search' @@ -55,7 +81,7 @@ GitLab.GfmAutoComplete = title: sanitize(i.title) search: "#{i.iid} #{i.title}" - input.atwho + @input.atwho at: '!' alias: 'mergerequests' searchKey: 'search' @@ -68,13 +94,18 @@ GitLab.GfmAutoComplete = title: sanitize(m.title) search: "#{m.iid} #{m.title}" - if @dataSource - $.getJSON(@dataSource).done (data) -> - # load members - input.atwho 'load', '@', data.members - # load issues - input.atwho 'load', 'issues', data.issues - # load merge requests - input.atwho 'load', 'mergerequests', data.mergerequests - # load emojis - input.atwho 'load', ':', data.emojis + destroyAtWho: -> + @input.atwho('destroy') + + fetchData: (dataSource) -> + $.getJSON(dataSource) + + loadData: (data) -> + # load members + @input.atwho 'load', '@', data.members + # load issues + @input.atwho 'load', 'issues', data.issues + # load merge requests + @input.atwho 'load', 'mergerequests', data.mergerequests + # load emojis + @input.atwho 'load', ':', data.emojis diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee index be8d225e73b..b0edc895649 100644 --- a/app/assets/javascripts/importer_status.js.coffee +++ b/app/assets/javascripts/importer_status.js.coffee @@ -4,18 +4,33 @@ class @ImporterStatus this.setAutoUpdate() initStatusPage: -> - $(".js-add-to-import").click (event) => - new_namespace = null - tr = $(event.currentTarget).closest("tr") - id = tr.attr("id").replace("repo_", "") - if tr.find(".import-target input").length > 0 - new_namespace = tr.find(".import-target input").prop("value") - tr.find(".import-target").empty().append(new_namespace + "/" + tr.find(".import-target").data("project_name")) - $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' - - $(".js-import-all").click (event) => - $(".js-add-to-import").each -> - $(this).click() + $('.js-add-to-import') + .off 'click' + .on 'click', (e) => + new_namespace = null + $btn = $(e.currentTarget) + $tr = $btn.closest('tr') + id = $tr.attr('id').replace('repo_', '') + if $tr.find('.import-target input').length > 0 + new_namespace = $tr.find('.import-target input').prop('value') + $tr.find('.import-target').empty().append("#{new_namespace} / #{$tr.find('.import-target').data('project_name')}") + + $btn + .disable() + .addClass 'is-loading' + + $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' + + $('.js-import-all') + .off 'click' + .on 'click', (e) -> + $btn = $(@) + $btn + .disable() + .addClass 'is-loading' + + $('.js-add-to-import').each -> + $(this).trigger('click') setAutoUpdate: -> setInterval (=> diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 1ab6e5114bc..e8613cab72b 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -183,9 +183,10 @@ class @MergeRequestTabs else $diffLine = $('td', $diffLine) - $diffLine.addClass 'hll' - diffLineTop = $diffLine.offset().top - navBarHeight = $('.navbar-gitlab').outerHeight() + if $diffLine.length + $diffLine.addClass 'hll' + diffLineTop = $diffLine.offset().top + navBarHeight = $('.navbar-gitlab').outerHeight() loadBuilds: (source) -> return if @buildsLoaded diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index fa91baa07c0..82e210fed7d 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -75,6 +75,9 @@ class @Notes # when issue status changes, we need to refresh data $(document).on "issuable:change", @refresh + # when a key is clicked on the notes + $(document).on "keydown", ".js-note-text", @keydownNoteText + cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" @@ -92,10 +95,19 @@ class @Notes $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" $(document).off "click", ".js-note-discard" + $(document).off "keydown", ".js-note-text" $('.note .js-task-list-container').taskList('disable') $(document).off 'tasklist:changed', '.note .js-task-list-container' + keydownNoteText: (e) -> + $this = $(this) + if $this.val() is '' and e.which is 38 #aka the up key + myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last") + if myLastNote.length + myLastNoteEditBtn = myLastNote.find('.js-note-edit') + myLastNoteEditBtn.trigger('click', [true, myLastNote]) + initRefresh: -> clearInterval(Notes.interval) Notes.interval = setInterval => @@ -343,7 +355,7 @@ class @Notes Adds a hidden div with the original content of the note to fill the edit note form with if the user cancels ### - showEditForm: (e) -> + showEditForm: (e, scrollTo, myLastNote) -> e.preventDefault() note = $(this).closest(".note") note.addClass "is-editting" @@ -354,9 +366,27 @@ class @Notes # Show the attachment delete link note.find(".js-note-attachment-delete").show() - new GLForm form + done = ($noteText) -> + # Neat little trick to put the cursor at the end + noteTextVal = $noteText.val() + $noteText.val('').val(noteTextVal); - form.find(".js-note-text").focus() + new GLForm form + if scrollTo? and myLastNote? + # scroll to the bottom + # so the open of the last element doesn't make a jump + $('html, body').scrollTop($(document).height()); + $('html, body').animate({ + scrollTop: myLastNote.offset().top - 150 + }, 500, -> + $noteText = form.find(".js-note-text") + $noteText.focus() + done($noteText) + ); + else + $noteText = form.find('.js-note-text') + $noteText.focus() + done($noteText) ### Called in response to clicking the edit note link diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index e4b7a3172ec..1a430f3aa47 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -2,7 +2,7 @@ class @Subscription constructor: (container) -> $container = $(container) @url = $container.attr('data-url') - @subscribe_button = $container.find('.subscribe-button') + @subscribe_button = $container.find('.js-subscribe-button') @subscription_status = $container.find('.subscription-status') @subscribe_button.unbind('click').click(@toggleSubscription) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index eee9b6e690e..a7e934936e9 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -12,6 +12,7 @@ class @UsersSelect showNullUser = $dropdown.data('null-user') showAnyUser = $dropdown.data('any-user') firstUser = $dropdown.data('first-user') + @authorId = $dropdown.data('author-id') selectedId = $dropdown.data('selected') defaultLabel = $dropdown.data('default-label') issueURL = $dropdown.data('issueUpdate') @@ -207,6 +208,7 @@ class @UsersSelect @projectId = $(select).data('project-id') @groupId = $(select).data('group-id') @showCurrentUser = $(select).data('current-user') + @authorId = $(select).data('author-id') showNullUser = $(select).data('null-user') showAnyUser = $(select).data('any-user') showEmailUser = $(select).data('email-user') @@ -312,6 +314,7 @@ class @UsersSelect project_id: @projectId group_id: @groupId current_user: @showCurrentUser + author_id: @authorId dataType: "json" ).done (users) -> callback(users) diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 7f7b7c806e7..8bfc0d583c5 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -5,7 +5,7 @@ */ .status-box { - + /* Extra small devices (phones, less than 768px) */ /* No media query since this is the default in Bootstrap */ padding: 5px 11px; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 66180f38a4f..7eb451c124e 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -70,13 +70,6 @@ display: none; } - .issue-details { - .creator, - .page-title .btn-close { - display: none; - } - } - %ul.notes .note-role, .note-actions { display: none; } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index b91f2f6f898..f0ec250de2b 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -39,8 +39,7 @@ .diff-file { border: 1px solid $border-color; border-bottom: none; - margin-left: 0; - margin-right: 0; + margin: 0; } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 6453c91d955..c8c6bbde084 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -75,6 +75,11 @@ li.commit { } } + .item-title { + display: inline-block; + max-width: 70%; + } + .commit-row-description { font-size: 14px; border-left: 1px solid #eee; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5917f089720..2c2ac903f29 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,5 +1,5 @@ .detail-page-header { - padding: 11px 0; + padding: $gl-padding-top 0; border-bottom: 1px solid $border-color; color: #5c5d5e; font-size: 16px; @@ -16,11 +16,6 @@ .issue_created_ago, .author_link { white-space: nowrap; } - - .issue-meta { - display: inline-block; - line-height: 20px; - } } .detail-page-description { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d0855f66911..183f22a1b24 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -67,6 +67,24 @@ line-height: $code_line_height; font-size: $code_font_size; + &.noteable_line { + position: relative; + + &.old { + &:before { + content: '-'; + position: absolute; + } + } + + &.new { + &:before { + content: '+'; + position: absolute; + } + } + } + span { white-space: pre; } @@ -391,3 +409,23 @@ margin-bottom: 0; } } + +.file-holder { + .diff-line-num:not(.js-unfold-bottom) { + a { + &:before { + content: attr(data-linenumber); + } + } + } +} + +.discussion { + .diff-content { + .diff-line-num { + &:before { + content: attr(data-linenumber); + } + } + } +} diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss index 6a99cd9cb94..84cc35239f9 100644 --- a/app/assets/stylesheets/pages/import.scss +++ b/app/assets/stylesheets/pages/import.scss @@ -16,3 +16,24 @@ i.icon-gitorious-big { width: 18px; height: 18px; } + +.import-jobs-from-col, +.import-jobs-to-col { + width: 40%; +} + +.import-jobs-status-col { + width: 20%; +} + +.btn-import { + .loading-icon { + display: none; + } + + &.is-loading { + .loading-icon { + display: inline-block; + } + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d9218e15095..5bf44c1cdb6 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -173,12 +173,6 @@ } } - .subscribe-button { - span { - margin-top: 0; - } - } - &.right-sidebar-collapsed { /* Extra small devices (phones, less than 768px) */ display: none; @@ -279,10 +273,6 @@ } } -.btn-default.gutter-toggle { - margin-top: 4px; -} - .detail-page-description { small { color: $gray-darkest; @@ -328,3 +318,50 @@ padding-top: 7px; } } + +.issuable-status-box { + float: none; + display: inline-block; + margin-top: 0; + + @media (max-width: $screen-xs-max) { + position: absolute; + top: 0; + left: 0; + } +} + +.issuable-header { + position: relative; + padding-left: 45px; + padding-right: 45px; + line-height: 35px; + + @media (min-width: $screen-sm-min) { + float: left; + padding-left: 0; + padding-right: 0; + } +} + +.issuable-actions { + padding-top: 10px; + + @media (min-width: $screen-sm-min) { + float: right; + padding-top: 0; + } +} + +.issuable-gutter-toggle { + @media (max-width: $screen-sm-max) { + position: absolute; + top: 0; + right: 0; + } +} + +.issuable-meta { + display: inline-block; + line-height: 18px; +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6a1d28590c2..fc9db97132d 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -86,41 +86,9 @@ form.edit-issue { @media (max-width: $screen-xs-max) { .issue-btn-group { width: 100%; - margin-top: 5px; - - .btn-group { - width: 100%; - - ul { - width: 100%; - text-align: center; - } - } .btn { width: 100%; - - &:first-child:not(:last-child) { - - } - - &:not(:first-child):not(:last-child) { - margin-top: 10px; - } - - &:last-child:not(:first-child) { - margin-top: 10px; - } - } - } - - .issue { - &:hover .issue-actions { - display: none !important; - } - - .issue-updated-at { - display: none; } } } @@ -133,11 +101,3 @@ form.edit-issue { color: $gl-text-color; margin-left: 52px; } - -.editor-details { - display: block; - - @media (min-width: $screen-sm-min) { - display: inline-block; - } -} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index da20fa28802..e179bdf0048 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -117,7 +117,7 @@ padding: 6px; color: $gl-text-color; - &.subscribe-button { + &.label-subscribe-button { padding-left: 0; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e421a31549a..a0fbf7d67c5 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -198,6 +198,12 @@ ul.notes { color: $notes-light-color; } +.discussion-headline-light { + a { + color: $gl-link-color; + } +} + /** * Actions for Discussions/Notes */ @@ -209,6 +215,17 @@ ul.notes { color: $notes-action-color; } +.discussion-actions { + @media (max-width: $screen-sm-max) { + float: none; + margin-left: 0; + + .note-action-button { + margin-left: 0; + } + } +} + .note-action-button, .discussion-action-button { display: inline-block; @@ -276,8 +293,7 @@ ul.notes { .diff-file tr.line_holder { @mixin show-add-diff-note { - filter: alpha(opacity=100); - opacity: 1.0; + display: inline-block; } .add-diff-note { @@ -291,13 +307,8 @@ ul.notes { position: absolute; z-index: 10; width: 32px; - - transition: all 0.2s ease; - // "hide" it by default - opacity: 0.0; - filter: alpha(opacity=0); - + display: none; &:hover { background: $gl-info; color: #fff; diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 1be0551ad3b..a30b6492572 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -1,17 +1,37 @@ -/* Generic print styles */ -header, nav, nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse {display: none!important;} -.profiler-results {display: none;} - -/* Styles targeted specifically at printing files */ -.tree-ref-holder, .tree-holder .breadcrumb, .blob-commit-info {display: none;} -.file-title {display: none;} -.file-holder {border: none;} - .wiki h1, .wiki h2, .wiki h3, .wiki h4, .wiki h5, .wiki h6 {margin-top: 17px; } .wiki h1 {font-size: 30px;} .wiki h2 {font-size: 22px;} .wiki h3 {font-size: 18px; font-weight: bold; } -.sidebar-wrapper { display: none; } -.nav { display: none; } -.btn { display: none; } +header, +nav, +nav.main-nav, +nav.navbar-collapse, +nav.navbar-collapse.collapse, +.profiler-results, +.tree-ref-holder, +.tree-holder .breadcrumb, +.blob-commit-info, +.file-title, +.file-holder, +.sidebar-wrapper, +.nav, +.btn, +ul.notes-form, +.merge-request-ci-status .ci-status-link:after, +.issuable-gutter-toggle, +.gutter-toggle, +.issuable-details .content-block-small, +.edit-link, +.note-action-button { + display: none!important; +} + +.page-gutter { + padding-top: 0; + padding-left: 0; +} + +.right-sidebar { + top: 0; +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index f010436bd36..b4a28b8dd3f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -19,6 +19,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to admin_runners_path end + def clear_repository_check_states + RepositoryCheck::ClearWorker.perform_async + + redirect_to( + admin_application_settings_path, + notice: 'Started asynchronous removal of all repository check states.' + ) + end + private def set_application_setting @@ -82,6 +91,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_enabled, :akismet_api_key, :email_author_in_body, + :repository_checks_enabled, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index c6b3105544a..87986fdf8b1 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,5 +1,5 @@ class Admin::ProjectsController < Admin::ApplicationController - before_action :project, only: [:show, :transfer] + before_action :project, only: [:show, :transfer, :repository_check] before_action :group, only: [:show, :transfer] def index @@ -8,6 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? + @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present? @projects = @projects.non_archived unless params[:with_archived].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @@ -30,6 +31,15 @@ class Admin::ProjectsController < Admin::ApplicationController redirect_to admin_namespace_project_path(@project.namespace, @project) end + def repository_check + RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id) + + redirect_to( + admin_namespace_project_path(@project.namespace, @project), + notice: 'Repository check was triggered.' + ) + end + protected def project diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97d53acde94..1c53b0b21a3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,7 @@ require 'fogbugz' class ApplicationController < ActionController::Base include Gitlab::CurrentSettings + include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper @@ -13,7 +14,7 @@ class ApplicationController < ActionController::Base before_action :check_password_expiration before_action :check_2fa_requirement before_action :ldap_security_check - before_action :sentry_user_context + before_action :sentry_context before_action :default_headers before_action :add_gon_variables before_action :configure_permitted_parameters, if: :devise_controller? @@ -40,13 +41,15 @@ class ApplicationController < ActionController::Base protected - def sentry_user_context - if Rails.env.production? && current_application_settings.sentry_enabled && current_user - Raven.user_context( - id: current_user.id, - email: current_user.email, - username: current_user.username, - ) + def sentry_context + if Rails.env.production? && current_application_settings.sentry_enabled + if current_user + Raven.user_context( + id: current_user.id, + email: current_user.email, + username: current_user.username, + ) + end Raven.tags_context(program: sentry_program_context) end @@ -158,20 +161,6 @@ class ApplicationController < ActionController::Base end end - def add_gon_variables - gon.api_version = API::API.version - gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s - gon.default_issues_tracker = Project.new.default_issue_tracker.to_param - gon.max_file_size = current_application_settings.max_attachment_size - gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - - if current_user - gon.current_user_id = current_user.id - gon.api_token = current_user.private_token - end - end - def validate_user_service_ticket! return unless signed_in? && session[:service_tickets] diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 81ba58ce49c..eb0abc80ab4 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -12,8 +12,15 @@ class AutocompleteController < ApplicationController if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user] && current_user - @users = [*@users, current_user].uniq + @users = [*@users, current_user] end + + if params[:author_id].present? + author = User.find_by_id(params[:author_id]) + @users = [author, *@users] if author + end + + @users.uniq! end render json: @users, only: [:name, :username, :id], methods: [:avatar_url] diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index c1adc999567..ee4fcc4e360 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) + @projects = @projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) if params[:filter_projects].blank? diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 55050615473..9b5c43b17e2 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -51,6 +51,7 @@ class HelpController < ApplicationController end def ui + @user = User.new(id: 0, name: 'John Doe', username: '@johndoe') end private diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index d1e4ac10f6c..c6bdd0602c1 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,9 +1,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings + include Gitlab::GonHelper include PageLayoutHelper before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! + before_action :add_gon_variables layout 'profile' diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index b88c080352b..a12549d6bcb 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -10,6 +10,11 @@ class Profiles::KeysController < Profiles::ApplicationController @key = current_user.keys.find(params[:id]) end + # Back-compat: We need to support this URL since git-annex webapp points to it + def new + redirect_to profile_keys_path + end + def create @key = current_user.keys.new(key_params) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c26cfeccf1d..38214f04793 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -60,8 +60,8 @@ class Projects::IssuesController < Projects::ApplicationController end def show - @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.nonawards.with_associations.fresh + @note = @project.notes.new(noteable: @issue) + @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue respond_to do |format| diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8e7956da48f..2ae180c8a12 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,7 @@ class UsersController < ApplicationController skip_before_action :authenticate_user! - before_action :set_user + before_action :user + before_action :authorize_read_user!, only: [:show] def show respond_to do |format| @@ -75,22 +76,26 @@ class UsersController < ApplicationController private - def set_user - @user = User.find_by_username!(params[:username]) + def authorize_read_user! + render_404 unless can?(current_user, :read_user, user) + end + + def user + @user ||= User.find_by_username!(params[:username]) end def contributed_projects - ContributedProjectsFinder.new(@user).execute(current_user) + ContributedProjectsFinder.new(user).execute(current_user) end def contributions_calendar @contributions_calendar ||= Gitlab::ContributionsCalendar. - new(contributed_projects, @user) + new(contributed_projects, user) end def load_events # Get user activity feed for projects common for both users - @events = @user.recent_events. + @events = user.recent_events. merge(projects_for_current_user). references(:project). with_associations. @@ -99,16 +104,16 @@ class UsersController < ApplicationController def load_projects @projects = - PersonalProjectsFinder.new(@user).execute(current_user) + PersonalProjectsFinder.new(user).execute(current_user) .page(params[:page]) end def load_contributed_projects - @contributed_projects = contributed_projects.joined(@user) + @contributed_projects = contributed_projects.joined(user) end def load_groups - @groups = JoinedGroupsFinder.new(@user).execute(current_user) + @groups = JoinedGroupsFinder.new(user).execute(current_user) end def projects_for_current_user diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index ff32e834499..6a3ec83b8c0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -40,10 +40,11 @@ module DiffHelper (unfold) ? 'unfold js-unfold' : '' end - def diff_line_content(line) + def diff_line_content(line, line_type = nil) if line.blank? " ".html_safe else + line[0] = ' ' if %w[new old].include?(line_type) line end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index b14b8218d02..49b6b79ce35 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -55,6 +55,15 @@ module IssuablesHelper h(milestone_title.presence || default_label) end + def issuable_meta(issuable, project, text) + output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" + output << " opened #{time_ago_with_tooltip(issuable.created_at)} by".html_safe + output << content_tag(:strong) do + author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs") + author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") + end + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 05386d790ca..4fc6de59a8b 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -6,12 +6,13 @@ module SelectsHelper value = opts[:selected] || '' placeholder = opts[:placeholder] || 'Search for a user' - null_user = opts[:null_user] || false - any_user = opts[:any_user] || false - email_user = opts[:email_user] || false - first_user = opts[:first_user] && current_user ? current_user.username : false - current_user = opts[:current_user] || false - project = opts[:project] || @project + null_user = opts[:null_user] || false + any_user = opts[:any_user] || false + email_user = opts[:email_user] || false + first_user = opts[:first_user] && current_user ? current_user.username : false + current_user = opts[:current_user] || false + author_id = opts[:author_id] || '' + project = opts[:project] || @project html = { class: css_class, @@ -21,7 +22,8 @@ module SelectsHelper any_user: any_user, email_user: email_user, first_user: first_user, - current_user: current_user + current_user: current_user, + author_id: author_id } } diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb new file mode 100644 index 00000000000..2bff5b63cc4 --- /dev/null +++ b/app/mailers/repository_check_mailer.rb @@ -0,0 +1,14 @@ +class RepositoryCheckMailer < BaseMailer + def notify(failed_count) + if failed_count == 1 + @message = "One project failed its last repository check" + else + @message = "#{failed_count} projects failed their last repository check" + end + + mail( + to: User.admins.pluck(:email), + subject: @message + ) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index c0bf6def7c5..6103a2947e2 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -18,6 +18,7 @@ class Ability when Namespace then namespace_abilities(user, subject) when GroupMember then group_member_abilities(user, subject) when ProjectMember then project_member_abilities(user, subject) + when User then user_abilities else [] end.concat(global_abilities(user)) end @@ -35,6 +36,8 @@ class Ability anonymous_project_abilities(subject) when subject.is_a?(Group) || subject.respond_to?(:group) anonymous_group_abilities(subject) + when subject.is_a?(User) + anonymous_user_abilities else [] end @@ -81,17 +84,17 @@ class Ability end def anonymous_group_abilities(subject) + rules = [] + group = if subject.is_a?(Group) subject else subject.group end - if group && group.public? - [:read_group] - else - [] - end + rules << :read_group if group.public? + + rules end def anonymous_personal_snippet_abilities(snippet) @@ -110,9 +113,14 @@ class Ability end end + def anonymous_user_abilities + [:read_user] unless restricted_public_level? + end + def global_abilities(user) rules = [] rules << :create_group if user.can_create_group + rules << :read_users_list rules end @@ -163,7 +171,7 @@ class Ability @public_project_rules ||= project_guest_rules + [ :download_code, :fork_project, - :read_commit_status, + :read_commit_status ] end @@ -284,7 +292,6 @@ class Ability def group_abilities(user, group) rules = [] - rules << :read_group if can_read_group?(user, group) # Only group masters and group owners can create new projects @@ -456,6 +463,10 @@ class Ability rules end + def user_abilities + [:read_user] + end + def abilities @abilities ||= begin abilities = Six.new @@ -470,6 +481,10 @@ class Ability private + def restricted_public_level? + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end + def named_abilities(name) [ :"read_#{name}", diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 052cd874733..36f88154232 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base require_two_factor_authentication: false, two_factor_grace_period: 48, recaptcha_enabled: false, - akismet_enabled: false + akismet_enabled: false, + repository_checks_enabled: true, ) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7d33838044b..85ef0523b31 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -365,11 +365,23 @@ module Ci self.update(erased_by: user, erased_at: Time.now) end - private - def yaml_variables + global_yaml_variables + job_yaml_variables + end + + def global_yaml_variables + if commit.config_processor + commit.config_processor.global_variables.map do |key, value| + { key: key, value: value, public: true } + end + else + [] + end + end + + def job_yaml_variables if commit.config_processor - commit.config_processor.variables.map do |key, value| + commit.config_processor.job_variables(name).map do |key, value| { key: key, value: value, public: true } end else diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index b8585d4e577..b7894c99846 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -37,4 +37,10 @@ class ExternalIssue def to_reference(_from_project = nil) id end + + def reference_link_text(from_project = nil) + return "##{id}" if /^\d+$/.match(id) + + id + end end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb new file mode 100644 index 00000000000..c78c7f4aa0e --- /dev/null +++ b/app/models/oauth_access_token.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer +# application_id :integer +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string +# + +class OauthAccessToken < ActiveRecord::Base + belongs_to :resource_owner, class_name: 'User' + belongs_to :application, class_name: 'Doorkeeper::Application' +end diff --git a/app/models/project.rb b/app/models/project.rb index fadc8bb2c9e..8f20922e3c5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -409,6 +409,35 @@ class Project < ActiveRecord::Base self.import_data.destroy if self.import_data end + def import_url=(value) + import_url = Gitlab::ImportUrl.new(value) + create_or_update_import_data(credentials: import_url.credentials) + super(import_url.sanitized_url) + end + + def import_url + if import_data && super + import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials) + import_url.full_url + else + super + end + end + + def create_or_update_import_data(data: nil, credentials: nil) + project_import_data = import_data || build_import_data + if data + project_import_data.data ||= {} + project_import_data.data = project_import_data.data.merge(data) + end + if credentials + project_import_data.credentials ||= {} + project_import_data.credentials = project_import_data.credentials.merge(credentials) + end + + project_import_data.save + end + def import? external_import? || forked? end @@ -865,7 +894,9 @@ class Project < ActiveRecord::Base def change_head(branch) repository.before_change_head - gitlab_shell.update_repository_head(self.path_with_namespace, branch) + repository.rugged.references.create('HEAD', + "refs/heads/#{branch}", + force: true) reload_default_branch end diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index cd3319f077e..79efb403058 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -12,8 +12,20 @@ require 'file_size_validator' class ProjectImportData < ActiveRecord::Base belongs_to :project - + attr_encrypted :credentials, + key: Gitlab::Application.secrets.db_key_base, + marshal: true, + encode: true, + mode: :per_attribute_iv_and_salt + serialize :data, JSON validates :project, presence: true + + before_validation :symbolize_credentials + + def symbolize_credentials + # bang doesn't work here - attr_encrypted makes it not to work + self.credentials = self.credentials.deep_symbolize_keys unless self.credentials.blank? + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 0b2289cfa39..589756f8531 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -12,11 +12,13 @@ class Repository attr_accessor :path_with_namespace, :project def self.clean_old_archives - repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path + Gitlab::Metrics.measure(:clean_old_archives) do + repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path - return unless File.directory?(repository_downloads_path) + return unless File.directory?(repository_downloads_path) - Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) + Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) + end end def initialize(path_with_namespace, project) @@ -169,7 +171,12 @@ class Repository def rm_tag(tag_name) before_remove_tag - gitlab_shell.rm_tag(path_with_namespace, tag_name) + begin + rugged.tags.delete(tag_name) + true + rescue Rugged::ReferenceError + false + end end def branch_names @@ -797,7 +804,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 0004a399f47..02c4eee3d02 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -1,28 +1,37 @@ module Projects class ParticipantsService < BaseService - def execute(note_type, note_id) - participating = - if note_type && note_id - participants_in(note_type, note_id) - else - [] - end + def execute(noteable_type, noteable_id) + @noteable_type = noteable_type + @noteable_id = noteable_id project_members = sorted(project.team.members) - participants = all_members + groups + project_members + participating + participants = target_owner + participants_in_target + all_members + groups + project_members participants.uniq end - def participants_in(type, id) - target = - case type + def target + @target ||= + case @noteable_type when "Issue" - project.issues.find_by_iid(id) + project.issues.find_by_iid(@noteable_id) when "MergeRequest" - project.merge_requests.find_by_iid(id) + project.merge_requests.find_by_iid(@noteable_id) when "Commit" - project.commit(id) + project.commit(@noteable_id) + else + nil end - + end + + def target_owner + return [] unless target && target.author.present? + + [{ + name: target.author.name, + username: target.author.username + }] + end + + def participants_in_target return [] unless target users = target.participants(current_user) @@ -30,13 +39,13 @@ module Projects end def sorted(users) - users.uniq.to_a.compact.sort_by(&:username).map do |user| + users.uniq.to_a.compact.sort_by(&:username).map do |user| { username: user.username, name: user.name } end end def groups - current_user.authorized_groups.sort_by(&:path).map do |group| + current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count { username: group.path, name: group.name, count: count } end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 2e734654466..79a27f4af7e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,8 +34,9 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end - # Apply new namespace id + # Apply new namespace id and visibility level project.namespace = new_namespace + project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? project.save! # Notifications @@ -56,7 +57,7 @@ module Projects Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) project.old_path_with_namespace = old_path - + SystemHooksService.new.execute_hooks_for(project, :transfer) true end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a8cca1a81cb..aadd2c54f20 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -26,7 +26,9 @@ .btn-group{ data: data_attrs } - restricted_level_checkboxes('restricted-visibility-help').each do |level| = level - %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets + %span.help-block#restricted-visibility-help + Selected levels cannot be used by non-admin users for projects or snippets. + If the public level is restricted, user profiles are only visible to logged in users. .form-group = f.label :import_sources, class: 'control-label col-sm-2' .col-sm-10 @@ -271,5 +273,24 @@ .col-sm-10 = f.text_field :sentry_dsn, class: 'form-control' + %fieldset + %legend Repository Checks + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :repository_checks_enabled do + = f.check_box :repository_checks_enabled + Enable Repository Checks + .help-block + GitLab will periodically run + %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck' + in all project and wiki repositories to look for silent disk corruption issues. + .form-group + .col-sm-offset-2.col-sm-10 + = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .help-block + If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. + + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index af9fdeb0734..4b475a4d8fa 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,6 +1,7 @@ - page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, - Gitlab::ProductionLogger, Gitlab::SidekiqLogger] + Gitlab::ProductionLogger, Gitlab::SidekiqLogger, + Gitlab::RepositoryCheckLogger] %ul.nav-links.log-tabs - loggers.each do |klass| %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index d39c0f44031..aa07afa0d62 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -3,7 +3,7 @@ .row.prepend-top-default %aside.col-md-3 - .admin-filter + .panel.admin-filter = form_tag admin_namespaces_projects_path, method: :get, class: '' do .form-group = label_tag :name, 'Name:' @@ -38,7 +38,13 @@ %span.descr = visibility_level_icon(level) = label - %hr + %fieldset + %strong Problems + .checkbox + = label_tag :last_repository_check_failed do + = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] + %span Last repository check failed + = hidden_field_tag :sort, params[:sort] = button_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index c638c32a654..73986d21bcf 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -5,6 +5,16 @@ %i.fa.fa-pencil-square-o Edit %hr +- if @project.last_repository_check_failed? + .row + .col-md-12 + .panel + .panel-heading.alert.alert-danger + Last repository check + = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)" + failed. See + = link_to 'repocheck.log', admin_logs_path + for error messages. .row .col-md-6 .panel.panel-default @@ -95,6 +105,32 @@ .col-sm-offset-2.col-sm-10 = f.submit 'Transfer', class: 'btn btn-primary' + .panel.panel-default.repository-check + .panel-heading + Repository check + .panel-body + = form_for @project, url: repository_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f| + .form-group + - if @project.last_repository_check_at.nil? + This repository has never been checked. + - else + This repository was last checked + = @project.last_repository_check_at.to_s(:medium) + '.' + The check + - if @project.last_repository_check_failed? + = succeed '.' do + %strong.cred failed + See + = link_to 'repocheck.log', admin_logs_path + for error messages. + - else + passed. + + = link_to icon('question-circle'), help_page_path('administration', 'repository_checks') + + .form-group + = f.submit 'Trigger repository check', class: 'btn btn-primary' + .col-md-6 - if @group .panel.panel-default diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index cb93ff2465e..e5607dacd0d 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -6,18 +6,17 @@ .login-heading %h3 Create an account .login-body - - user = params[:user].present? ? params[:user] : {} = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| .devise-errors = devise_error_messages! %div - = f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true + = f.text_field :name, class: "form-control top", placeholder: "Name", required: true %div - = f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true + = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true %div - = f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true + = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true + = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true %div - if current_application_settings.recaptcha_enabled = recaptcha_tags diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 55f4a6f287d..0aff79749ef 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -68,7 +68,7 @@ %td= app.name %td= token.created_at %td= token.scopes - %td= render 'delete_form', application: app + %td= render 'doorkeeper/authorized_applications/delete_form', application: app - @authorized_anonymous_tokens.each do |token| %tr %td diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index d084559abc3..f12df5c8ffe 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -345,11 +345,11 @@ %ul %li %a.dropdown-menu-user-link.is-active{href: "#"} - = link_to_member_avatar(current_user, size: 30) + = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name - = current_user.name + = @user.name .dropdown-menu-user-username - = current_user.to_reference + = @user.to_reference .example %div @@ -372,11 +372,11 @@ %ul %li %a.dropdown-menu-user-link.is-active{href: "#"} - = link_to_member_avatar(current_user, size: 30) + = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name - = current_user.name + = @user.name .dropdown-menu-user-username - = current_user.to_reference + = @user.to_reference .dropdown-page-two .dropdown-title %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index d8af0295b2d..dfebf7768d9 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -20,10 +20,10 @@ job.attr("id", "project_#{@project.id}") target_field = job.find(".import-target") target_field.empty() - target_field.append('<strong>#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}</strong>') + target_field.append('#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started") - else :plain job = $("tr#repo_#{@repo_id}") - job.find(".import-actions").html("<i class='fa fa-exclamation-circle'> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}</i>") + job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index aec2e836c9f..6e993e58f0d 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -10,13 +10,19 @@ %hr %p - if @incompatible_repos.any? - = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all compatible projects + = icon("spinner spin", class: "loading-icon") - else - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") - -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From Bitbucket @@ -28,7 +34,7 @@ %td = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -47,7 +53,9 @@ %td.import-target = "#{repo["owner"]}/#{repo["slug"]}" %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") - @incompatible_repos.each do |repo| %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} %td diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index 6ee16c8be4b..d3d3c595c17 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -13,10 +13,15 @@ how FogBugz email addresses and usernames are imported into GitLab. %hr %p - = button_tag 'Import all projects', class: 'btn btn-success js-import-all' + = button_tag class: 'btn btn-import btn-success js-import-all' do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From FogBugz @@ -28,7 +33,7 @@ %td = project.import_source %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -47,7 +52,9 @@ %td.import-target = "#{current_user.username}/#{repo.name}" %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}"); diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 1416ee5bd5a..9639da4cb58 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -8,10 +8,15 @@ Select projects you want to import. %hr %p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From GitHub @@ -23,7 +28,7 @@ %td = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -42,7 +47,9 @@ %td.import-target = repo.full_name %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}"); diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 911a55eb85d..e3a356b5379 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -8,10 +8,15 @@ Select projects you want to import. %hr %p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From GitLab.com @@ -23,7 +28,7 @@ %td = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -42,7 +47,9 @@ %td.import-target = repo["path_with_namespace"] %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}"); diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 6b0fa1edf8c..267eee4f262 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -8,10 +8,15 @@ Select projects you want to import. %hr %p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From Gitorious.org @@ -23,7 +28,7 @@ %td = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -42,7 +47,9 @@ %td.import-target = repo.full_name %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") :javascript new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}"); diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 175ef6921cd..5ada6b174eb 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -14,12 +14,19 @@ %hr %p - if @incompatible_repos.any? - = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all compatible projects + = icon("spinner spin", class: "loading-icon") - else - = button_tag 'Import all projects', class: "btn btn-success js-import-all" + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") -.table-holder +.table-responsive %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col %thead %tr %th From Google Code @@ -31,7 +38,7 @@ %td = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span @@ -50,7 +57,9 @@ %td.import-target = "#{current_user.username}/#{repo.name}" %td.import-actions.job-status - = button_tag "Import", class: "btn js-add-to-import" + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") - @incompatible_repos.each do |repo| %tr{id: "repo_#{repo.id}"} %td diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index 9ae6964aaac..b6074373e2b 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -52,6 +52,9 @@ %li phpunit --coverage-text --colors=never (PHP) - %code ^\s*Lines:\s*\d+.\d+\% + %li + gcovr (C/C++) - + %code ^TOTAL.*\s+(\d+\%)$ .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 7a78d61a611..8de44a6c914 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,6 +1,6 @@ .md-area .md-header - %ul.nav-links + %ul.nav-links.clearfix %li.active %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } Write diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index abcfca4cd11..38e62c81fed 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -1,20 +1,20 @@ - if @lines.present? - if @form.unfold? && @form.since != 1 && !@form.bottom? %tr.line_holder{ id: @form.since } - = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.since, line_new: @form.since, bottom: false, new_file: false} + = render "projects/diffs/match_line", { line: @match_line, + line_old: @form.since, line_new: @form.since, bottom: false, new_file: false } - @lines.each_with_index do |line, index| - line_new = index + @form.since - line_old = line_new - @form.offset %tr.line_holder - %td.old_line.diff-line-num{data: {linenumber: line_old}} + %td.old_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_old), "#" - %td.new_line.diff-line-num + %td.new_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_new) , "#" %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } - = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.to, line_new: @form.to, bottom: true, new_file: false} + = render "projects/diffs/match_line", { line: @match_line, + line_old: @form.to, line_new: @form.to, bottom: true, new_file: false } diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 7da89231243..34f27f1e793 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -11,7 +11,7 @@ = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } .commit-row-title - %span.item-title.str-truncated + %span.item-title = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - if commit.description? %a.text-expander.js-toggle-button ... diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 9464c8dc996..107097ad963 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,26 +1,26 @@ - type = line.type -%tr.line_holder{id: line_code, class: type} +%tr.line_holder{ id: line_code, class: type } - case type - when 'match' - = render "projects/diffs/match_line", {line: line.text, - line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file} + = render "projects/diffs/match_line", { line: line.text, + line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file } - when 'nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{class: type} - - link_text = raw(type == "new" ? " " : line.old_pos) + %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "new" ? " ".html_safe : line.old_pos - if defined?(plain) && plain = link_text - else - = link_to link_text, "##{line_code}", id: line_code + = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) - %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} - - link_text = raw(type == "old" ? " " : line.new_pos) + %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "old" ? " ".html_safe : line.new_pos - if defined?(plain) && plain = link_text - else - = link_to link_text, "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text) + = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } + %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 6d872cd0b21..76a4f41193c 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -210,6 +210,7 @@ %li Be careful. Changing the project's namespace can have unintended side effects. %li You can only transfer the project to namespaces you manage. %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. .form-actions = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - else diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 4aa92d0b39e..7a8009f6da4 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -27,7 +27,7 @@ = icon('thumbs-down') = downvotes - - note_count = issue.notes.user.count + - note_count = issue.notes.user.nonawards.count - if note_count > 0 %li = link_to issue_path(issue) + "#notes" do diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5fe5ddc0819..bde80bbb54b 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,82 +1,79 @@ - page_title "#{@issue.title} (##{@issue.iid})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes +- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project)) -= render "header_title" +.clearfix.detail-page-header + .issuable-header + .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } + = icon('check', class: "hidden-sm hidden-md hidden-lg") + %span.hidden-xs + Closed + .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) } + = icon('circle-o', class: "hidden-sm hidden-md hidden-lg") + %span.hidden-xs Open -.issue - .detail-page-header.issuable-header - .pull-left - .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} - %span.hidden-xs - Closed - %span.hidden-sm.hidden-md.hidden-lg - = icon('check') - .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} - %span.hidden-xs - Open - %span.hidden-sm.hidden-md.hidden-lg - = icon('circle-o') - - %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') - .issue-meta + .issuable-meta = confidential_icon(@issue) - %strong.identifier - Issue ##{@issue.iid} - %span.creator - opened - .editor-details - .editor-details - = time_ago_with_tooltip(@issue.created_at) - by - %strong - = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs") - %strong - = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", - by_username: true, avatar: false) + = issuable_meta(@issue, @project, "Issue") - .pull-right.issue-btn-group - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do - = icon('plus') - New issue - - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-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}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do - = icon('pencil-square-o') - Edit + - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue) + .issuable-actions + .clearfix.issue-btn-group.dropdown + %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } } + %span.caret + Options + .dropdown-menu.dropdown-menu-align-right.hidden-lg + %ul + - if can?(current_user, :create_issue, @project) + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' + - if can?(current_user, :update_issue, @issue) + %li + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + %li + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + %li + = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) + - if can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do + = icon('plus') + New issue + - if can?(current_user, :update_issue, @issue) + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-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}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do + = icon('pencil-square-o') + Edit - .issue-details.issuable-details - .detail-page-description.content-block - %h2.title - = markdown escape_once(@issue.title), pipeline: :single_line - %div - - if @issue.description.present? - .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} - .wiki - = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"]) - %textarea.hidden.js-task-list-field - = @issue.description - = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') +.issue-details.issuable-details + .detail-page-description.content-block + %h2.title + = markdown escape_once(@issue.title), pipeline: :single_line + - if @issue.description.present? + .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } + .wiki + = preserve do + = markdown(@issue.description, cache_key: [@issue, "description"]) + %textarea.hidden.js-task-list-field + = @issue.description + = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') - #merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)} - // This element is filled in using JavaScript. + #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } } + // This element is filled in using JavaScript. - #related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)} - // This element is filled in using JavaScript. + #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } } + // This element is filled in using JavaScript. - .content-block.content-block-small - = render 'new_branch' - = render 'votes/votes_block', votable: @issue + .content-block.content-block-small + = render 'new_branch' + = render 'votes/votes_block', votable: @issue - .row - %section.col-md-12 - .issuable-discussion - = render 'projects/issues/discussion' + %section.issuable-discussion + = render 'projects/issues/discussion' = render 'shared/issuable/sidebar', issuable: @issue diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 986d8c220db..e69de29bb2d 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -1,3 +0,0 @@ -$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}"; -$('aside.right-sidebar').effect('highlight'); -new IssuableContext(); diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 097a65969a6..8bf544b8371 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -14,7 +14,7 @@ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .subscription-status{data: {status: label_subscription_status(label)}} - %a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}} + %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } } %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 391193eed6c..e740fe8c84d 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -35,7 +35,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.mr_and_commit_notes.user.nonawards.count - if note_count > 0 %li = link_to merge_request_path(merge_request) + "#notes" do diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 7d7c487e970..b08524574e4 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -16,11 +16,9 @@ = dropdown_title("Select source project") = dropdown_filter("Search projects") = dropdown_content do - - is_active = f.object.source_project_id == @merge_request.source_project.id - %ul - %li - %a{ href: "#", class: "#{("is-active" if is_active)}", data: { id: @merge_request.source_project.id } } - = @merge_request.source_project_path + = render 'projects/merge_requests/dropdowns/project', + projects: [@merge_request.source_project], + selected: f.object.source_project_id .merge-request-select.dropdown = f.hidden_field :source_branch = dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } @@ -28,11 +26,9 @@ = dropdown_title("Select source branch") = dropdown_filter("Search branches") = dropdown_content do - %ul - - @merge_request.source_branches.each do |branch| - %li - %a{ href: "#", class: "#{("is-active" if f.object.source_branch == branch)}", data: { id: branch } } - = branch + = render 'projects/merge_requests/dropdowns/branch', + branches: @merge_request.source_branches, + selected: f.object.source_branch .panel-footer = icon('spinner spin', class: 'js-source-loading') %ul.list-unstyled.mr_source_commit @@ -50,11 +46,9 @@ = dropdown_title("Select target project") = dropdown_filter("Search projects") = dropdown_content do - %ul - - projects.each do |project| - %li - %a{ href: "#", class: "#{("is-active" if f.object.target_project_id == project.id)}", data: { id: project.id } } - = project.path_with_namespace + = render 'projects/merge_requests/dropdowns/project', + projects: projects, + selected: f.object.target_project_id .merge-request-select.dropdown = f.hidden_field :target_branch = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" } @@ -62,11 +56,9 @@ = dropdown_title("Select target branch") = dropdown_filter("Search branches") = dropdown_content do - %ul - - @merge_request.target_branches.each do |branch| - %li - %a{ href: "#", class: "#{("is-active" if f.object.target_branch == branch)}", data: { id: branch } } - = branch + = render 'projects/merge_requests/dropdowns/branch', + branches: @merge_request.target_branches, + selected: f.object.target_branch .panel-footer = icon('spinner spin', class: "js-target-loading") %ul.list-unstyled.mr_target_commit diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 1dd8f721f7e..285ad26316c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,8 +1,7 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - -= render "header_title" +- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) - if params[:view] == 'parallel' - fluid_layout true @@ -32,8 +31,7 @@ %span Request to merge %span.label-branch= source_branch_with_namespace(@merge_request) %span into - = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do - = @merge_request.target_branch + = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" - if @merge_request.open? && @merge_request.diverged_from_target_branch? %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) @@ -51,7 +49,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count + %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits diff --git a/app/views/projects/merge_requests/dropdowns/_branch.html.haml b/app/views/projects/merge_requests/dropdowns/_branch.html.haml new file mode 100644 index 00000000000..ba8d9a5835c --- /dev/null +++ b/app/views/projects/merge_requests/dropdowns/_branch.html.haml @@ -0,0 +1,5 @@ +%ul + - branches.each do |branch| + %li + %a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } } + = branch diff --git a/app/views/projects/merge_requests/dropdowns/_project.html.haml b/app/views/projects/merge_requests/dropdowns/_project.html.haml new file mode 100644 index 00000000000..25d5dc92f8a --- /dev/null +++ b/app/views/projects/merge_requests/dropdowns/_project.html.haml @@ -0,0 +1,5 @@ +%ul + - projects.each do |project| + %li + %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } } + = project.path_with_namespace diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index ab4b1f14be5..0a99e8c9591 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,35 +1,32 @@ -.detail-page-header - .status-box{ class: status_box_class(@merge_request) } - %span.hidden-xs - = @merge_request.state_human_name - %span.hidden-sm.hidden-md.hidden-lg - = icon(@merge_request.state_icon_name) - %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" } - = icon('angle-double-left') - .issue-meta - %strong.identifier - %span.hidden-sm.hidden-md.hidden-lg - MR +.clearfix.detail-page-header + .issuable-header + .issuable-status-box.status-box{ class: status_box_class(@merge_request) } + = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg") %span.hidden-xs - Merge Request - !#{@merge_request.iid} - %span.creator - opened - .editor-details - = time_ago_with_tooltip(@merge_request.created_at) - by - %strong - = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs") - %strong - = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", - by_username: true, avatar: false) + = @merge_request.state_human_name - .issue-btn-group.pull-right - - if can?(current_user, :update_merge_request, @merge_request) - - if @merge_request.open? - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do + %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } + = icon('angle-double-left') + + .issuable-meta + = issuable_meta(@merge_request, @project, "Merge Request") + + - if can?(current_user, :update_merge_request, @merge_request) + .issuable-actions + .clearfix.issue-btn-group.dropdown + %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } } + %span.caret + Options + .dropdown-menu.dropdown-menu-align-right.hidden-lg + %ul + %li{ class: issue_button_visibility(@merge_request, true) } + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' + %li{ class: issue_button_visibility(@merge_request, false) } + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' + %li + = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request' + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do = icon('pencil-square-o') Edit - - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request' diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index 9cce5660e1c..e69de29bb2d 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -1,3 +0,0 @@ -$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}"; -$('aside.right-sidebar').effect('highlight'); -new IssuableContext(); diff --git a/app/views/projects/merge_requests/update_branches.html.haml b/app/views/projects/merge_requests/update_branches.html.haml index 1b93188a10c..64482973a89 100644 --- a/app/views/projects/merge_requests/update_branches.html.haml +++ b/app/views/projects/merge_requests/update_branches.html.haml @@ -1,5 +1,3 @@ -%ul - - @target_branches.each do |branch| - %li - %a{ href: "#", class: "#{("is-active" if "a" == branch)}", data: { id: branch } } - = branch += render 'projects/merge_requests/dropdowns/branch', +branches: @target_branches, +selected: nil diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index b8068835b3a..572b00a38c7 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -1,5 +1,5 @@ - note = discussion_notes.first -.timeline-entry +%li.note.note-discussion.timeline-entry .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 03a44ca99c0..6e9ecdf7649 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,4 +1,5 @@ -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] } +- note_editable = note_editable?(note) +%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} @@ -15,16 +16,16 @@ - if access %span.note-role = access - - if note_editable?(note) + - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = icon('trash-o') - .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} + .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text = preserve do = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - - if note_editable?(note) + - if note_editable = render 'projects/notes/edit_form', note: note = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index cc42aab5c52..1c39ce897a3 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,6 +1,6 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" -%ul.notes.timeline +%ul.notes.notes-form.timeline %li.timeline-entry - if can? current_user, :create_note, @project .timeline-icon.hidden-xs.hidden-sm diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index cd8a5f0bd02..0ea8862a684 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -6,15 +6,11 @@ = "#{note.author.to_reference} started a discussion" = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do on the diff + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") .discussion-actions = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - .last-update.hide.js-toggle-content - - last_note = discussion_notes.last - last updated by - = link_to_member(@project, last_note.author, avatar: false) - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 46f2ba4bbcf..2a2ead58eeb 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -8,21 +8,18 @@ = "#{note.author.to_reference} started a discussion on #{commit_description}" - if commit = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") .discussion-actions = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - .last-update.hide.js-toggle-content - - last_note = discussion_notes.last - last updated by - = link_to_member(@project, last_note.author, avatar: false) - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content - if note.for_diff_line? = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - else .panel.panel-default .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } - = render discussion_notes + %ul.notes.timeline + = render discussion_notes .discussion-reply-holder = link_to_reply_diff(discussion_notes.first) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 820e31ccd61..d46aab000c3 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -20,11 +20,9 @@ %td.new_line.diff-line-num= "..." %td.line_content.match= line.text - else - %td.old_line.diff-line-num - = raw(type == "new" ? " " : line.old_pos) - %td.new_line.diff-line-num - = raw(type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text) + %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } + %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml index f8e000b424f..45141bcd1df 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -5,14 +5,10 @@ .inline.discussion-headline-light = "#{note.author.to_reference} started a discussion" on the outdated diff + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") .discussion-actions = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-down Show/hide discussion - .last-update.hide.js-toggle-content - - last_note = discussion_notes.last - last updated by - = link_to_member(@project, last_note.author, avatar: false) - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml new file mode 100644 index 00000000000..df16f503570 --- /dev/null +++ b/app/views/repository_check_mailer/notify.html.haml @@ -0,0 +1,5 @@ +%p + #{@message}. + +%p + = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml new file mode 100644 index 00000000000..02f3f80288a --- /dev/null +++ b/app/views/repository_check_mailer/notify.text.haml @@ -0,0 +1,3 @@ +#{@message}. +\ +View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index aed2622a6da..bae15b7f844 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -4,7 +4,7 @@ = f.label :title, class: 'control-label' .col-sm-10 = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', - class: 'form-control pad js-gfm-input', required: true + class: 'form-control pad', required: true - if issuable.is_a?(MergeRequest) %p.help-block diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 94affa4b59a..08bfd93f4e6 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -49,7 +49,7 @@ .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) .block.milestone .sidebar-collapsed-icon @@ -128,7 +128,7 @@ .title.hide-collapsed Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'} + %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' .subscription-status.hide-collapsed{data: {status: subscribtion_status}} .unsubscribed{class: ( 'hidden' if subscribed )} @@ -152,4 +152,4 @@ new LabelsSelect(); new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); new Subscription('.subscription') - new Sidebar();
\ No newline at end of file + new Sidebar(); diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb new file mode 100644 index 00000000000..667fff031dd --- /dev/null +++ b/app/workers/admin_email_worker.rb @@ -0,0 +1,12 @@ +class AdminEmailWorker + include Sidekiq::Worker + + sidekiq_options retry: false # this job auto-repeats via sidekiq-cron + + def perform + repository_check_failed_count = Project.where(last_repository_check_failed: true).count + return if repository_check_failed_count.zero? + + RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now + end +end diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb new file mode 100644 index 00000000000..44b3145d50f --- /dev/null +++ b/app/workers/repository_check/batch_worker.rb @@ -0,0 +1,63 @@ +module RepositoryCheck + class BatchWorker + include Sidekiq::Worker + + RUN_TIME = 3600 + + sidekiq_options retry: false + + def perform + start = Time.now + + # This loop will break after a little more than one hour ('a little + # more' because `git fsck` may take a few minutes), or if it runs out of + # projects to check. By default sidekiq-cron will start a new + # RepositoryCheckWorker each hour so that as long as there are repositories to + # check, only one (or two) will be checked at a time. + project_ids.each do |project_id| + break if Time.now - start >= RUN_TIME + break unless current_settings.repository_checks_enabled + + next unless try_obtain_lease(project_id) + + SingleRepositoryWorker.new.perform(project_id) + end + end + + private + + # Project.find_each does not support WHERE clauses and + # Project.find_in_batches does not support ordering. So we just build an + # array of ID's. This is OK because we do it only once an hour, because + # getting ID's from Postgres is not terribly slow, and because no user + # has to sit and wait for this query to finish. + def project_ids + limit = 10_000 + never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). + pluck(:id) + old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago). + reorder('last_repository_check_at ASC').limit(limit).pluck(:id) + never_checked_projects + old_check_projects + end + + def try_obtain_lease(id) + # Use a 24-hour timeout because on servers/projects where 'git fsck' is + # super slow we definitely do not want to run it twice in parallel. + Gitlab::ExclusiveLease.new( + "project_repository_check:#{id}", + timeout: 24.hours + ).try_obtain + end + + def current_settings + # No caching of the settings! If we cache them and an admin disables + # this feature, an active RepositoryCheckWorker would keep going for up + # to 1 hour after the feature was disabled. + if Rails.env.test? + Gitlab::CurrentSettings.fake_application_settings + else + ApplicationSetting.current + end + end + end +end diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb new file mode 100644 index 00000000000..b7202ddff34 --- /dev/null +++ b/app/workers/repository_check/clear_worker.rb @@ -0,0 +1,17 @@ +module RepositoryCheck + class ClearWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform + # Do small batched updates because these updates will be slow and locking + Project.select(:id).find_in_batches(batch_size: 100) do |batch| + Project.where(id: batch.map(&:id)).update_all( + last_repository_check_failed: nil, + last_repository_check_at: nil, + ) + end + end + end +end diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb new file mode 100644 index 00000000000..e54ae86d06c --- /dev/null +++ b/app/workers/repository_check/single_repository_worker.rb @@ -0,0 +1,36 @@ +module RepositoryCheck + class SingleRepositoryWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform(project_id) + project = Project.find(project_id) + project.update_columns( + last_repository_check_failed: !check(project), + last_repository_check_at: Time.now, + ) + end + + private + + def check(project) + # Use 'map do', not 'all? do', to prevent short-circuiting + [project.repository, project.wiki.repository].map do |repository| + git_fsck(repository.path_to_repo) + end.all? + end + + def git_fsck(path) + cmd = %W(nice git --git-dir=#{path} fsck) + output, status = Gitlab::Popen.popen(cmd) + + if status.zero? + true + else + Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}") + false + end + end + end +end |