diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-07-26 14:54:45 +0200 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-07-26 14:54:45 +0200 |
commit | 79a7f7b6e59fa1225c440547796331caedabeaab (patch) | |
tree | f002dd7d26c38f358e626e269543628e06c4cb4b /app | |
parent | 7d6538f2e2a6f1b0808a77e347a9083295b17c8c (diff) | |
parent | 5f35901a01ff822ba1e637251d9726a41e73ed17 (diff) | |
download | gitlab-ce-79a7f7b6e59fa1225c440547796331caedabeaab.tar.gz |
Merge branch 'master' into backstage/gb/migrate-stages-statuses
* master: (110 commits)
Add missing colon
Fix project wiki web_url spec
Resolve "Memory usage notice doesn't link anywhere"
Docs new topic "user/index"
Implement GRPC call to RepositoryService
Pending delete projects should not show in deploy keys
Remove outdated ~Frontend label in CONTRIBUTING.md
Fixes 500 error caused by pending delete projects in admin dashboard
Add lower path index to redirect_routes
Remove project_key from the Jira configuration
Update CHANGELOG.md for 9.4.1
Enable gitaly_post_upload_pack by default
Add `api` prefix as a top level route in the spec.
Move relative_path to the element that is being clicked
Bumps Gitlab Omniauth LDAP version
Add directives to Vue component ordering
synchronize ukrainian translation in zanata again
v3 API is unsupported after 9.5, but may not be removed
Fix vertical alignment in firefox and safari for pipeline mini graph
Adds link_to_gfm method instrumentation
...
Conflicts:
db/schema.rb
Diffstat (limited to 'app')
75 files changed, 582 insertions, 182 deletions
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index ba9d9a3e1f7..54257531284 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ import './lib/utils/common_utils'; +import { placeholderImage } from './lazy_loader'; const gfmRules = { // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert @@ -56,6 +57,11 @@ const gfmRules = { return text; }, }, + ImageLazyLoadFilter: { + 'img'(el, text) { + return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + }, + }, VideoLinkFilter: { '.video-container'(el) { const videoEl = el.querySelector('video'); @@ -163,7 +169,9 @@ const gfmRules = { return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); }, 'img'(el) { - return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + const imageSrc = el.src; + const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || ''); + return `![${el.getAttribute('alt')}](${imageUrl})`; }, 'a.anchor'(el, text) { // Don't render a Markdown link for the anchor link inside a heading diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js new file mode 100644 index 00000000000..f739db751a6 --- /dev/null +++ b/app/assets/javascripts/how_to_merge.js @@ -0,0 +1,12 @@ +document.addEventListener('DOMContentLoaded', () => { + const modal = $('#modal_merge_info').modal({ + modal: true, + show: false, + }); + $('.how_to_merge_link').bind('click', () => { + modal.show(); + }); + $('.modal-header .close').bind('click', () => { + modal.hide(); + }); +}); diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js new file mode 100644 index 00000000000..3d64b121fa7 --- /dev/null +++ b/app/assets/javascripts/lazy_loader.js @@ -0,0 +1,76 @@ +/* eslint-disable one-export, one-var, one-var-declaration-per-line */ + +import _ from 'underscore'; + +export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; +const SCROLL_THRESHOLD = 300; + +export default class LazyLoader { + constructor(options = {}) { + this.lazyImages = []; + this.observerNode = options.observerNode || '#content-body'; + + const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300); + const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300); + + window.addEventListener('scroll', throttledScrollCheck); + window.addEventListener('resize', debouncedElementsInView); + + const scrollContainer = options.scrollContainer || window; + scrollContainer.addEventListener('load', () => this.loadCheck()); + } + searchLazyImages() { + this.lazyImages = [].slice.call(document.querySelectorAll('.lazy')); + this.checkElementsInView(); + } + startContentObserver() { + const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); + + if (contentNode) { + const observer = new MutationObserver(() => this.searchLazyImages()); + + observer.observe(contentNode, { + childList: true, + subtree: true, + }); + } + } + loadCheck() { + this.searchLazyImages(); + this.startContentObserver(); + } + scrollCheck() { + requestAnimationFrame(() => this.checkElementsInView()); + } + checkElementsInView() { + const scrollTop = pageYOffset; + const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; + let imgBoundRect, imgTop, imgBound; + + // Loading Images which are in the current viewport or close to them + this.lazyImages = this.lazyImages.filter((selectedImage) => { + if (selectedImage.getAttribute('data-src')) { + imgBoundRect = selectedImage.getBoundingClientRect(); + + imgTop = scrollTop + imgBoundRect.top; + imgBound = imgTop + imgBoundRect.height; + + if (scrollTop < imgBound && visHeight > imgTop) { + LazyLoader.loadImage(selectedImage); + return false; + } + + return true; + } + return false; + }); + } + static loadImage(img) { + if (img.getAttribute('data-src')) { + img.setAttribute('src', img.getAttribute('data-src')); + img.removeAttribute('data-src'); + img.classList.remove('lazy'); + img.classList.add('js-lazy-loaded'); + } + } +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 26c67fb721c..e96d51de838 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -109,6 +109,7 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; +import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; import './member_expiration_date'; @@ -166,6 +167,11 @@ window.addEventListener('load', function onLoad() { gl.utils.handleLocationHash(); }, false); +gl.lazyLoader = new LazyLoader({ + scrollContainer: window, + observerNode: '#content-body' +}); + $(function () { var $body = $('body'); var $document = $(document); @@ -284,13 +290,7 @@ $(function () { return $container.remove(); // Commit show suppressed diff }); - $('.navbar-toggle').on('click', function () { - $('.header-content .title, .header-content .navbar-sub-nav').toggle(); - $('.header-content .header-logo').toggle(); - $('.header-content .navbar-collapse').toggle(); - $('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle(); - return $('.navbar-toggle').toggleClass('active'); - }); + $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded')); // Show/hide comments on diff $body.on('click', '.js-toggle-diff-comments', function (e) { var $this = $(this); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index c4e379a4a0b..8be7314ded8 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -175,7 +175,7 @@ import Cookies from 'js-cookie'; getConflictsCountText() { const count = this.getConflictsCount(); - const text = count ? 'conflicts' : 'conflict'; + const text = count > 1 ? 'conflicts' : 'conflict'; return `${count} ${text}`; }, diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 6d38124f1c1..3a06b477d7c 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,6 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* global Flash */ +import { __, s__ } from './locale'; + export default class Star { constructor() { $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { @@ -11,10 +13,10 @@ export default class Star { toggleStar = function(isStarred) { $this.parent().find('.star-count').text(data.star_count); if (isStarred) { - $starSpan.removeClass('starred').text('Star'); + $starSpan.removeClass('starred').text(s__('StarProject|Star')); $starIcon.removeClass('fa-star').addClass('fa-star-o'); } else { - $starSpan.addClass('starred').text('Unstar'); + $starSpan.addClass('starred').text(__('Unstar')); $starIcon.removeClass('fa-star-o').addClass('fa-star'); } }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e8e22ad93a5..744a1cd24fa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -108,7 +108,8 @@ export default { </div> <mr-widget-memory-usage v-if="deployment.metrics_url" - :metricsUrl="deployment.metrics_url" + :metrics-url="deployment.metrics_url" + :metrics-monitoring-url="deployment.metrics_monitoring_url" /> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 76cb71b6c12..534e2a88eff 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -7,7 +7,14 @@ import MRWidgetService from '../services/mr_widget_service'; export default { name: 'MemoryUsage', props: { - metricsUrl: { type: String, required: true }, + metricsUrl: { + type: String, + required: true, + }, + metricsMonitoringUrl: { + type: String, + required: true, + }, }, data() { return { @@ -124,7 +131,7 @@ export default { <p v-if="shouldShowMemoryGraph" class="usage-info js-usage-info"> - Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB + <a :href="metricsMonitoringUrl">Memory</a> usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB </p> <p v-if="shouldShowLoadFailure" diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 06f7af33f94..0dfa7a31d31 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -35,6 +35,8 @@ width: 40px; height: 40px; padding: 0; + background: $avatar-background; + overflow: hidden; &.avatar-inline { float: none; diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 759401a7806..0ac095f7d8f 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -93,7 +93,7 @@ .is-selected .pika-day, .pika-day:hover, - .is-today .pika-day:hover { + .is-today .pika-day { background: $gl-primary; color: $white-light; box-shadow: none; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index c7c2684d548..8ad082f7a65 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -163,8 +163,18 @@ td.blame-commit { padding: 5px 10px; min-width: 400px; + max-width: 400px; background: $gray-light; border-left: 3px solid; + + .commit-row-title { + display: flex; + } + + .item-title { + flex: 1; + margin-right: 0.5em; + } } @for $i from 0 through 5 { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 20fb10c28d4..605f4284bb5 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -132,6 +132,22 @@ header { } } + &.navbar-gitlab-new { + .fa-times { + display: none; + } + + .menu-expanded { + .fa-ellipsis-v { + display: none; + } + + .fa-times { + display: block; + } + } + } + .global-dropdown { position: absolute; left: -10px; @@ -171,6 +187,19 @@ header { min-height: $header-height; padding-left: 30px; + &.menu-expanded { + @media (max-width: $screen-xs-max) { + .header-logo, + .title-container { + display: none; + } + + .navbar-collapse { + display: block; + } + } + } + .dropdown-menu { margin-top: -5px; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 99eea97377c..35b4d77a5ab 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -182,6 +182,12 @@ } } + &.nav-controls-new-nav { + > .dropdown { + margin-right: 0; + } + } + > .btn-grouped { float: none; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 8a58c1ed567..befd8133be0 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -11,8 +11,17 @@ } img { - max-width: 100%; + /*max-width: 100%;*/ margin: 0 0 8px; + min-width: 200px; + min-height: 100px; + background-color: $gray-lightest; + } + + img.js-lazy-loaded { + min-width: none; + min-height: none; + background-color: none; } p a:not(.no-attachment-icon) img { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7016208f624..cf0a1ad57d0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -379,7 +379,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); * Avatar */ $avatar_radius: 50%; -$avatar-border: $border-color; +$avatar-border: $gray-normal; +$avatar-border-hover: $gray-darker; +$avatar-background: $gray-lightest; $gl-avatar-size: 40px; /* diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 9f3e278ebfc..360ffda8d71 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -21,6 +21,11 @@ header.navbar-gitlab-new { padding-right: 0; color: currentColor; + img { + height: 28px; + margin-right: 10px; + } + > a { display: flex; align-items: center; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index df858cffe09..6039cda96d8 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -431,7 +431,10 @@ margin: 5px; } -.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { +.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar, +.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { + position: absolute; + &.right-sidebar { top: 0; bottom: 0; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 9637d26e56d..d3862df20d3 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -597,7 +597,7 @@ } // Dropdown button in mini pipeline graph -.mini-pipeline-graph-dropdown-toggle { +button.mini-pipeline-graph-dropdown-toggle { border-radius: 100px; background-color: $white-light; border-width: 1px; @@ -608,6 +608,7 @@ padding: 0; transition: all 0.2s linear; position: relative; + vertical-align: middle; > .fa.fa-caret-down { position: absolute; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c1bc4c0d675..4c0f7556894 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -76,11 +76,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.require(:application_setting).permit( - application_setting_params_ce + application_setting_params_attributes ) end - def application_setting_params_ce + def application_setting_params_attributes [ :admin_notification_email, :after_sign_out_path, diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 8360ce08bdc..05e749c00c0 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,6 +1,6 @@ class Admin::DashboardController < Admin::ApplicationController def index - @projects = Project.with_route.limit(10) + @projects = Project.without_deleted.with_route.limit(10) @users = User.limit(10) @groups = Group.with_route.limit(10) end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 984d5398708..0b6cd71e651 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -3,18 +3,9 @@ class Admin::ProjectsController < Admin::ApplicationController before_action :group, only: [:show, :transfer] def index - params[:sort] ||= 'latest_activity_desc' - @projects = Project.with_statistics - @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? - @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].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[:archived].present? - @projects = @projects.personal(current_user) if params[:personal].present? - @projects = @projects.search(params[:name]) if params[:name].present? - @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]) + finder = Admin::ProjectsFinder.new(params: params, current_user: current_user) + @projects = finder.execute + @sort = finder.sort respond_to do |format| format.html diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 95de3a44641..221e01b415a 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -22,6 +22,7 @@ class Projects::ApplicationController < ApplicationController def project return @project if @project + return nil unless params[:project_id] || params[:id] path = File.join(params[:namespace_id], params[:project_id] || params[:id]) auth_proc = ->(project) { !project.pending_delete? } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 0ac9da2ff0f..e2ccabb22db 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -8,7 +8,6 @@ class Projects::IssuesController < Projects::ApplicationController prepend_before_action :authenticate_user!, only: [:new] - before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :check_issues_available! before_action :issue, except: [:index, :new, :create, :bulk_update] @@ -243,19 +242,19 @@ class Projects::IssuesController < Projects::ApplicationController end def authorize_update_issue! - return render_404 unless can?(current_user, :update_issue, @issue) + render_404 unless can?(current_user, :update_issue, @issue) end def authorize_admin_issues! - return render_404 unless can?(current_user, :admin_issue, @project) + render_404 unless can?(current_user, :admin_issue, @project) end def authorize_create_merge_request! - return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) + render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) end def check_issues_available! - return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? + return render_404 unless @project.feature_available?(:issues, current_user) end def redirect_to_external_issue_tracker diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 70c41da4de5..d361e661d0e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -223,12 +223,18 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo if can?(current_user, :read_environment, environment) && environment.has_metrics? metrics_project_environment_deployment_path(environment.project, environment, deployment) end + + metrics_monitoring_url = + if can?(current_user, :read_environment, environment) + environment_metrics_path(environment) + end { id: environment.id, name: environment.name, url: project_environment_path(project, environment), metrics_url: metrics_url, + metrics_monitoring_url: metrics_monitoring_url, stop_url: stop_url, external_url: environment.external_url, external_url_formatted: environment.formatted_external_url, diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c769693255c..2d7cbd4614e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -296,10 +296,10 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project) - .permit(project_params_ce) + .permit(project_params_attributes) end - def project_params_ce + def project_params_attributes [ :avatar, :build_allow_git_fetch, diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 0e8a57f8e03..69513f4dadc 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -5,6 +5,14 @@ class SessionsController < Devise::SessionsController skip_before_action :check_two_factor_requirement, only: [:destroy] + # Explicitly call protect from forgery before anything else. Otherwise the + # CSFR-token might be cleared before authentication is done. This was the case + # when LDAP was enabled and the `OmniauthCallbacksController` is loaded + # + # *Note:* `prepend: true` is the default for rails4, but this will be changed + # to `prepend: false` in rails5. + protect_from_forgery prepend: true, with: :exception + prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb new file mode 100644 index 00000000000..a5ba791a513 --- /dev/null +++ b/app/finders/admin/projects_finder.rb @@ -0,0 +1,33 @@ +class Admin::ProjectsFinder + attr_reader :sort, :namespace_id, :visibility_level, :with_push, + :abandoned, :last_repository_check_failed, :archived, + :personal, :name, :page, :current_user + + def initialize(params:, current_user:) + @current_user = current_user + @sort = params.fetch(:sort) { 'latest_activity_desc' } + @namespace_id = params[:namespace_id] + @visibility_level = params[:visibility_level] + @with_push = params[:with_push] + @abandoned = params[:abandoned] + @last_repository_check_failed = params[:last_repository_check_failed] + @archived = params[:archived] + @personal = params[:personal] + @name = params[:name] + @page = params[:page] + end + + def execute + items = Project.with_statistics + items = items.in_namespace(namespace_id) if namespace_id.present? + items = items.where(visibility_level: visibility_level) if visibility_level.present? + items = items.with_push if with_push.present? + items = items.abandoned if abandoned.present? + items = items.where(last_repository_check_failed: true) if last_repository_check_failed.present? + items = items.non_archived unless archived.present? + items = items.personal(current_user) if personal.present? + items = items.search(name) if name.present? + items = items.sort(sort) + items.includes(:namespace).order("namespaces.path, projects.name ASC").page(page) + end +end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index bbe7f3c8fb4..0e068d4b51c 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -11,17 +11,12 @@ module AvatarsHelper def user_avatar_without_link(options = {}) avatar_size = options[:size] || 16 user_name = options[:user].try(:name) || options[:user_name] - css_class = options[:css_class] || '' avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) data_attributes = { container: 'body' } - if options[:lazy] - data_attributes[:src] = avatar_url - end - image_tag( - options[:lazy] ? '' : avatar_url, - class: "avatar has-tooltip s#{avatar_size} #{css_class}", + avatar_url, + class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]), alt: "#{user_name}'s avatar", title: user_name, data: data_attributes diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index fdbca789d21..5f11fe62030 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -61,8 +61,8 @@ module EmailsHelper else image_tag( image_url('mailers/gitlab_header_logo.gif'), - size: "55x50", - alt: "GitLab" + size: '55x50', + alt: 'GitLab' ) end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 42b6cfdf02f..7e1ccb23e9e 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -17,10 +17,10 @@ module IssuesHelper return '' if project.nil? url = - if options[:only_path] - project.issues_tracker.issue_path(issue_iid) + if options[:internal] + url_for_internal_issue(issue_iid, project, options) else - project.issues_tracker.issue_url(issue_iid) + url_for_tracker_issue(issue_iid, project, options) end # Ensure we return a valid URL to prevent possible XSS. @@ -29,6 +29,24 @@ module IssuesHelper '' end + def url_for_tracker_issue(issue_iid, project, options) + if options[:only_path] + project.issues_tracker.issue_path(issue_iid) + else + project.issues_tracker.issue_url(issue_iid) + end + end + + def url_for_internal_issue(issue_iid, project = @project, options = {}) + helpers = Gitlab::Routing.url_helpers + + if options[:only_path] + helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue_iid) + else + helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue_iid) + end + end + def bulk_update_milestone_options milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) @@ -158,4 +176,6 @@ module IssuesHelper # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue + module_function :url_for_internal_issue + module_function :url_for_tracker_issue end diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb new file mode 100644 index 00000000000..2c5619ac41b --- /dev/null +++ b/app/helpers/lazy_image_tag_helper.rb @@ -0,0 +1,24 @@ +module LazyImageTagHelper + def placeholder_image + "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + end + + # Override the default ActionView `image_tag` helper to support lazy-loading + def image_tag(source, options = {}) + options = options.symbolize_keys + + unless options.delete(:lazy) == false + options[:data] ||= {} + options[:data][:src] = path_to_image(source) + options[:class] ||= "" + options[:class] << " lazy" + + source = placeholder_image + end + + super(source, options) + end + + # Required for Banzai::Filter::ImageLazyLoadFilter + module_function :placeholder_image +end diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 209bd56b78a..08fd97cd048 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -18,7 +18,8 @@ module SystemNoteHelper 'milestone' => 'icon_clock_o', 'discussion' => 'icon_comment_o', 'moved' => 'icon_arrow_circle_o_right', - 'outdated' => 'icon_edit' + 'outdated' => 'icon_edit', + 'duplicate' => 'icon_clone' }.freeze def icon_for_system_note(note) diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index 456598b4c28..3b175251446 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -2,7 +2,7 @@ module VersionCheckHelper def version_status_badge if Rails.env.production? && current_application_settings.version_check_enabled image_url = VersionCheck.new.url - image_tag image_url, class: 'js-version-status-badge' + image_tag image_url, class: 'js-version-status-badge', lazy: false end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index b646b32fc64..e5b615a7cc0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,7 +21,7 @@ module Ci has_many :merge_requests, foreign_key: "head_pipeline_id" has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build' - has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index e4ae1b35f66..085eeeae157 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -40,10 +40,6 @@ module Ci update_attribute(:active, false) end - def runnable_by_owner? - Ability.allowed?(owner, :create_pipeline, project) - end - def set_next_run_at self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 95152dcd68c..48547a938fc 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -11,7 +11,7 @@ module CacheMarkdownField extend ActiveSupport::Concern # Increment this number every time the renderer changes its output - CACHE_VERSION = 1 + CACHE_VERSION = 2 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index fc6b840f7a8..ef95d6b0f98 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -17,7 +17,13 @@ module ProtectedRef class_methods do def protected_ref_access_levels(*types) types.each do |type| - has_many :"#{type}_access_levels", dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + # We need to set `inverse_of` to make sure the `belongs_to`-object is set + # when creating children using `accepts_nested_attributes_for`. + # + # If we don't `protected_branch` or `protected_tag` would be empty and + # `project` cannot be delegated to it, which in turn would cause validations + # to fail. + has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." } @@ -25,8 +31,8 @@ module ProtectedRef end end - def protected_ref_accessible_to?(ref, user, action:) - access_levels_for_ref(ref, action: action).any? do |access_level| + def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil) + access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| access_level.check_access(user) end end @@ -37,8 +43,9 @@ module ProtectedRef end end - def access_levels_for_ref(ref, action:) - self.matching(ref).map(&:"#{action}_access_levels").flatten + def access_levels_for_ref(ref, action:, protected_refs: nil) + self.matching(ref, protected_refs: protected_refs) + .map(&:"#{action}_access_levels").flatten end def matching(ref_name, protected_refs: nil) diff --git a/app/models/group.rb b/app/models/group.rb index dfa4e8adedd..bd5735ed82e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -167,10 +167,14 @@ class Group < Namespace end def has_owner?(user) + return false unless user + members_with_parents.owners.where(user_id: user).any? end def has_master?(user) + return false unless user + members_with_parents.masters.where(user_id: user).any? end @@ -212,7 +216,7 @@ class Group < Namespace end def members_with_parents - GroupMember.non_request.where(source_id: ancestors.pluck(:id).push(id)) + GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil) end def users_with_parents diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index e4e7999d0f2..a910099b4c1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -596,7 +596,7 @@ class MergeRequest < ActiveRecord::Base # running `ReferenceExtractor` on each of them separately. # This optimization does not apply to issues from external sources. def cache_merge_request_closes_issues!(current_user) - return if project.has_external_issue_tracker? + return unless project.issues_enabled? transaction do self.merge_requests_closing_issues.delete_all diff --git a/app/models/project.rb b/app/models/project.rb index 0b357d5d003..d827bfaa806 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -734,9 +734,11 @@ class Project < ActiveRecord::Base end def get_issue(issue_id, current_user) - if default_issues_tracker? - IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) - else + issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled? + + if issue + issue + elsif external_issue_tracker ExternalIssue.new(issue_id, self) end end @@ -758,7 +760,7 @@ class Project < ActiveRecord::Base end def external_issue_reference_pattern - external_issue_tracker.class.reference_pattern + external_issue_tracker.class.reference_pattern(only_long: issues_enabled?) end def default_issues_tracker? diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 6d6a3ae3647..31984c5d7ed 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -8,8 +8,12 @@ class IssueTrackerService < Service # This pattern does not support cross-project references # The other code assumes that this pattern is a superset of all # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN - def self.reference_pattern - @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} + def self.reference_pattern(only_long: false) + if only_long + %r{(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)} + else + %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} + end end def default? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 5498a2e17b2..37f2c96a22f 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,10 +3,8 @@ class JiraService < IssueTrackerService validates :url, url: true, presence: true, if: :activated? validates :api_url, url: true, allow_blank: true - validates :project_key, presence: true, if: :activated? - prop_accessor :username, :password, :url, :api_url, :project_key, - :jira_issue_transition_id, :title, :description + prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description before_update :reset_password @@ -18,7 +16,7 @@ class JiraService < IssueTrackerService end # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 - def self.reference_pattern + def self.reference_pattern(only_long: true) @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end @@ -54,10 +52,6 @@ class JiraService < IssueTrackerService @client ||= JIRA::Client.new(options) end - def jira_project - @jira_project ||= jira_request { client.Project.find(project_key) } - end - def help "You need to configure JIRA before enabling this service. For more details read the @@ -88,18 +82,12 @@ class JiraService < IssueTrackerService [ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true }, { type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' }, - { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true }, { type: 'text', name: 'username', placeholder: '', required: true }, { type: 'password', name: 'password', placeholder: '', required: true }, - { type: 'text', name: 'jira_issue_transition_id', placeholder: '' } + { type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID', placeholder: '' } ] end - # URLs to redirect from Gitlab issues pages to jira issue tracker - def project_url - "#{url}/issues/?jql=project=#{project_key}" - end - def issues_url "#{url}/browse/:id" end @@ -184,7 +172,7 @@ class JiraService < IssueTrackerService def test_settings return unless client_url.present? # Test settings by getting the project - jira_request { jira_project.present? } + jira_request { client.ServerInfo.all.attrs } end private diff --git a/app/models/repository.rb b/app/models/repository.rb index 8663cf5e602..d27eeff9fb4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -471,8 +471,17 @@ class Repository end cache_method :root_ref + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/314 def exists? - refs_directory_exists? + return false unless path_with_namespace + + Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| + if enabled + raw_repository.exists? + else + refs_directory_exists? + end + end end cache_method :exists? @@ -1095,8 +1104,6 @@ class Repository end def refs_directory_exists? - return false unless path_with_namespace - File.exist?(File.join(path_to_repo, 'refs')) end diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 414c95f7705..0b33e45473b 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -1,7 +1,8 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference - title time_tracking branch milestone discussion task moved opened closed merged + title time_tracking branch milestone discussion task moved + opened closed merged duplicate outdated ].freeze diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 386822d3ff6..984e5482288 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,17 +1,15 @@ module Ci class BuildPolicy < CommitStatusPolicy - condition(:protected_action) do - next false unless @subject.action? - + condition(:protected_ref) do access = ::Gitlab::UserAccess.new(@user, project: @subject.project) if @subject.tag? !access.can_create_tag?(@subject.ref) else - !access.can_merge_to_branch?(@subject.ref) + !access.can_update_branch?(@subject.ref) end end - rule { protected_action }.prevent :update_build + rule { protected_ref }.prevent :update_build end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index a2dde95dbc8..4e689a9efd5 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -1,5 +1,17 @@ module Ci class PipelinePolicy < BasePolicy delegate { @subject.project } + + condition(:protected_ref) do + access = ::Gitlab::UserAccess.new(@user, project: @subject.project) + + if @subject.tag? + !access.can_create_tag?(@subject.ref) + else + !access.can_update_branch?(@subject.ref) + end + end + + rule { protected_ref }.prevent :update_pipeline end end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 55eefa76d3f..1c91425f589 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -44,7 +44,7 @@ class GlobalPolicy < BasePolicy prevent :log_in end - rule { ~restricted_public_level }.policy do + rule { admin | ~restricted_public_level }.policy do enable :read_users_list end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 323131c0f7e..0133091db57 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -10,7 +10,8 @@ class ProjectPolicy < BasePolicy desc "User is a project owner" condition :owner do - @user && project.owner == @user || (project.group && project.group.has_owner?(@user)) + (project.owner.present? && project.owner == @user) || + project.group&.has_owner?(@user) end desc "Project has public builds enabled" @@ -287,9 +288,6 @@ class ProjectPolicy < BasePolicy prevent :create_issue prevent :update_issue prevent :admin_issue - end - - rule { issues_disabled & default_issues_tracker }.policy do prevent :read_issue end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 20f9938f038..743a08acefe 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -16,7 +16,8 @@ class BuildDetailsEntity < JobEntity end expose :path do |build| - project_merge_request_path(project, build.merge_request) + project_merge_request_path(build.merge_request.project, + build.merge_request) end end diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb index 068013c8829..c75431a79ae 100644 --- a/app/serializers/deploy_key_entity.rb +++ b/app/serializers/deploy_key_entity.rb @@ -9,7 +9,7 @@ class DeployKeyEntity < Grape::Entity expose :created_at expose :updated_at expose :projects, using: ProjectEntity do |deploy_key| - deploy_key.projects.select { |project| options[:user].can?(:read_project, project) } + deploy_key.projects.without_deleted.select { |project| options[:user].can?(:read_project, project) } end expose :can_edit diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 273386776fa..21e2ef153de 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -15,12 +15,40 @@ module Ci pipeline_schedule: schedule ) + result = validate(current_user || trigger_request.trigger.owner, + ignore_skip_ci: ignore_skip_ci, + save_on_errors: save_on_errors) + + return result if result + + Ci::Pipeline.transaction do + update_merge_requests_head_pipeline if pipeline.save + + Ci::CreatePipelineStagesService + .new(project, current_user) + .execute(pipeline) + end + + cancel_pending_pipelines if project.auto_cancel_pending_pipelines? + + pipeline_created_counter.increment(source: source) + + pipeline.tap(&:process!) + end + + private + + def validate(triggering_user, ignore_skip_ci:, save_on_errors:) unless project.builds_enabled? return error('Pipeline is disabled') end - unless trigger_request || can?(current_user, :create_pipeline, project) - return error('Insufficient permissions to create a new pipeline') + unless allowed_to_trigger_pipeline?(triggering_user) + if can?(triggering_user, :create_pipeline, project) + return error("Insufficient permissions for protected ref '#{ref}'") + else + return error('Insufficient permissions to create a new pipeline') + end end unless branch? || tag? @@ -46,24 +74,29 @@ module Ci unless pipeline.has_stage_seeds? return error('No stages / jobs for this pipeline.') end + end - Ci::Pipeline.transaction do - update_merge_requests_head_pipeline if pipeline.save - - Ci::CreatePipelineStagesService - .new(project, current_user) - .execute(pipeline) + def allowed_to_trigger_pipeline?(triggering_user) + if triggering_user + allowed_to_create?(triggering_user) + else # legacy triggers don't have a corresponding user + !project.protected_for?(ref) end + end - cancel_pending_pipelines if project.auto_cancel_pending_pipelines? - - pipeline_created_counter.increment(source: source) + def allowed_to_create?(triggering_user) + access = Gitlab::UserAccess.new(triggering_user, project: project) - pipeline.tap(&:process!) + can?(triggering_user, :create_pipeline, project) && + if branch? + access.can_update_branch?(ref) + elsif tag? + access.can_create_tag?(ref) + else + true # Allow it for now and we'll reject when we check ref existence + end end - private - def update_merge_requests_head_pipeline return unless pipeline.latest? @@ -113,15 +146,21 @@ module Ci end def branch? - project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref) + return @is_branch if defined?(@is_branch) + + @is_branch = + project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref) end def tag? - project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref) + return @is_tag if defined?(@is_tag) + + @is_tag = + project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref) end def ref - Gitlab::Git.ref_name(origin_ref) + @ref ||= Gitlab::Git.ref_name(origin_ref) end def valid_sha? diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index cf3d4aee2bc..a43d0e4593c 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,12 +1,14 @@ module Ci - class CreateTriggerRequestService - def execute(project, trigger, ref, variables = nil) + module CreateTriggerRequestService + Result = Struct.new(:trigger_request, :pipeline) + + def self.execute(project, trigger, ref, variables = nil) trigger_request = trigger.trigger_requests.create(variables: variables) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref) .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) - trigger_request if pipeline.persisted? + Result.new(trigger_request, pipeline) end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 9078b1f0983..ea497729115 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -58,6 +58,7 @@ class IssuableBaseService < BaseService params.delete(:assignee_ids) params.delete(:assignee_id) params.delete(:due_date) + params.delete(:canonical_issue_id) end filter_assignee(issuable) diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 34199eb5d13..4c198fc96ea 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -7,6 +7,14 @@ module Issues issue_data end + def reopen_service + Issues::ReopenService + end + + def close_service + Issues::CloseService + end + private def create_assignee_note(issue, old_assignees) diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index ddef5281498..74459c3342c 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -16,13 +16,13 @@ module Issues # The code calling this method is responsible for ensuring that a user is # allowed to close the given issue. def close_issue(issue, commit: nil, notifications: true, system_note: true) - if project.jira_tracker? && project.jira_service.active + if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue) project.jira_service.close_issue(commit, issue) todo_service.close_issue(issue, current_user) return issue end - if project.default_issues_tracker? && issue.close + if project.issues_enabled? && issue.close event_service.close_issue(issue, current_user) create_note(issue, commit) if system_note notification_service.close_issue(issue, current_user) if notifications diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb new file mode 100644 index 00000000000..5c0854e664d --- /dev/null +++ b/app/services/issues/duplicate_service.rb @@ -0,0 +1,24 @@ +module Issues + class DuplicateService < Issues::BaseService + def execute(duplicate_issue, canonical_issue) + return if canonical_issue == duplicate_issue + return unless can?(current_user, :update_issue, duplicate_issue) + return unless can?(current_user, :create_note, canonical_issue) + + create_issue_duplicate_note(duplicate_issue, canonical_issue) + create_issue_canonical_note(canonical_issue, duplicate_issue) + + close_service.new(project, current_user, {}).execute(duplicate_issue) + end + + private + + def create_issue_duplicate_note(duplicate_issue, canonical_issue) + SystemNoteService.mark_duplicate_issue(duplicate_issue, duplicate_issue.project, current_user, canonical_issue) + end + + def create_issue_canonical_note(canonical_issue, duplicate_issue) + SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue) + end + end +end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index cd9f9a4a16e..8d918ccc635 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -5,6 +5,7 @@ module Issues def execute(issue) handle_move_between_iids(issue) filter_spam_check_params + change_issue_duplicate(issue) update(issue) end @@ -53,14 +54,6 @@ module Issues end end - def reopen_service - Issues::ReopenService - end - - def close_service - Issues::CloseService - end - def handle_move_between_iids(issue) return unless params[:move_between_iids] @@ -72,6 +65,15 @@ module Issues issue.move_between(issue_before, issue_after) end + def change_issue_duplicate(issue) + canonical_issue_id = params.delete(:canonical_issue_id) + canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id) + + if canonical_issue + Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue) + end + end + private def get_issue_if_allowed(project, iid) diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 30ca95eef7a..d81035e4eba 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -5,7 +5,7 @@ module Projects return error('New visibility level not allowed!') end - if project.has_container_registry_tags? + if renaming_project_with_container_registry_tags? return error('Cannot rename project because it contains container registry tags!') end @@ -44,6 +44,13 @@ module Projects true end + def renaming_project_with_container_registry_tags? + new_path = params[:path] + + new_path && new_path != project.path && + project.has_container_registry_tags? + end + def changing_default_branch? new_branch = params[:default_branch] diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 6f82159e6c7..5dc1b91d2c0 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -471,6 +471,24 @@ module QuickActions end end + desc 'Mark this issue as a duplicate of another issue' + explanation do |duplicate_reference| + "Marks this issue as a duplicate of #{duplicate_reference}." + end + params '#issue' + condition do + issuable.is_a?(Issue) && + issuable.persisted? && + current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + end + command :duplicate do |duplicate_param| + canonical_issue = extract_references(duplicate_param, :issue).first + + if canonical_issue.present? + @updates[:canonical_issue_id] = canonical_issue.id + end + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index da0f21d449a..2dbee9c246e 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -552,6 +552,44 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) end + # Called when a Noteable has been marked as a duplicate of another Issue + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # canonical_issue - Issue that this is a duplicate of + # + # Example Note text: + # + # "marked this issue as a duplicate of #1234" + # + # "marked this issue as a duplicate of other_project#5678" + # + # Returns the created Note object + def mark_duplicate_issue(noteable, project, author, canonical_issue) + body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" + create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) + end + + # Called when a Noteable has been marked as the canonical Issue of a duplicate + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # duplicate_issue - Issue that was a duplicate of this + # + # Example Note text: + # + # "marked #1234 as a duplicate of this issue" + # + # "marked other_project#5678 as a duplicate of this issue" + # + # Returns the created Note object + def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue) + body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" + create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 128b5dc01ab..8e94e68bc11 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -150,7 +150,7 @@ .well-segment.well-centered = link_to admin_groups_path do %h3.text-center - Groups + Groups: = number_with_delimiter(Group.count) %hr = link_to 'New group', new_admin_group_path, class: "btn btn-new" diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml index 76367bd9734..60940dba475 100644 --- a/app/views/layouts/header/_new.html.haml +++ b/app/views/layouts/header/_new.html.haml @@ -81,6 +81,6 @@ %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %span.sr-only Toggle navigation = icon('ellipsis-v', class: 'js-navbar-toggle-right') - = icon('times', class: 'js-navbar-toggle-left', style: 'display: none;') + = icon('times', class: 'js-navbar-toggle-left') = render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index 21f175291fa..00395b222e4 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -75,10 +75,10 @@ Registry - if project_nav_tab? :issues - = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do + = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do %span - - if @project.default_issues_tracker? + - if @project.issues_enabled? %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) Issues @@ -113,7 +113,7 @@ Milestones - if project_nav_tab? :merge_requests - = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index fb90bb4b472..924cd2e9681 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -23,16 +23,16 @@ Registry - if project_nav_tab? :issues - = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do + = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do %span Issues - - if @project.default_issues_tracker? + - if @project.issues_enabled? %span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id))) - if project_nav_tab? :merge_requests - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] - - controllers.push(:merge_requests, :labels, :milestones) unless @project.default_issues_tracker? + - controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled? = nav_link(controller: controllers) do = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index f11afe8fc22..c7359d873d9 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -21,8 +21,8 @@ .commit = author_avatar(commit, size: 36) .commit-row-title - %strong - = link_to_gfm truncate(commit.title, length: 35), project_commit_path(@project, commit.id), class: "cdark" + %span.item-title.str-truncated-100 + = link_to_gfm commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title .pull-right = link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha" diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml index 640d59b3174..1650aa8197f 100644 --- a/app/views/projects/blob/viewers/_image.html.haml +++ b/app/views/projects/blob/viewers/_image.html.haml @@ -1,2 +1,2 @@ .file-content.image_file - %img{ src: blob_raw_url, alt: viewer.blob.name } + %img{ 'data-src': blob_raw_url, alt: viewer.blob.name } diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index e248676be0d..c82ae35a685 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -2,7 +2,7 @@ = link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do - if current_user.starred?(@project) = icon('star') - %span.starred= _('Unstar') + %span.starred= _('Unstar') - else = icon('star-o') %span= s_('StarProject|Star') diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml index 33d3dcbeafa..05877ceed3d 100644 --- a/app/views/projects/diffs/viewers/_image.html.haml +++ b/app/views/projects/diffs/viewers/_image.html.haml @@ -8,7 +8,7 @@ .image %span.wrap .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') } - %img{ src: blob_raw_path, alt: diff_file.file_path } + %img{ 'data-src': blob_raw_path, alt: diff_file.file_path } %p.image-info= number_to_human_size(blob.size) - else .image @@ -16,7 +16,7 @@ %span.wrap .frame.deleted %a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) } - %img{ src: old_blob_raw_path, alt: diff_file.old_path } + %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path } %p.image-info.hide %span.meta-filesize= number_to_human_size(old_blob.size) | @@ -28,7 +28,7 @@ %span.wrap .frame.added %a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) } - %img{ src: blob_raw_path, alt: diff_file.new_path } + %img{ 'data-src': blob_raw_path, alt: diff_file.new_path } %p.image-info.hide %span.meta-filesize= number_to_human_size(blob.size) | @@ -41,10 +41,10 @@ .swipe.view.hide .swipe-frame .frame.deleted - %img{ src: old_blob_raw_path, alt: diff_file.old_path } + %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path } .swipe-wrap .frame.added - %img{ src: blob_raw_path, alt: diff_file.new_path } + %img{ 'data-src': blob_raw_path, alt: diff_file.new_path } %span.swipe-bar %span.top-handle %span.bottom-handle @@ -52,9 +52,9 @@ .onion-skin.view.hide .onion-skin-frame .frame.deleted - %img{ src: old_blob_raw_path, alt: diff_file.old_path } + %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path } .frame.added - %img{ src: blob_raw_path, alt: diff_file.new_path } + %img{ 'data-src': blob_raw_path, alt: diff_file.new_path } .controls .transparent .drag-track diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 766cb272bec..917ec7fdbda 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -1,3 +1,6 @@ +- content_for :page_specific_javascripts do + = webpack_bundle_tag('how_to_merge') + #modal_merge_info.modal .modal-dialog .modal-content @@ -50,14 +53,3 @@ = succeed '.' do You can also checkout merge requests locally by = link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer' - -:javascript - $(function(){ - var modal = $('#modal_merge_info').modal({modal: true, show:false}); - $('.how_to_merge_link').bind("click", function(){ - modal.show(); - }); - $('.modal-header .close').bind("click", function(){ - modal.hide(); - }) - }) diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index bfeb746ee83..c020e7db380 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -4,7 +4,7 @@ - new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project - page_title "Merge Requests" -- unless @project.default_issues_tracker? +- unless @project.issues_enabled? = content_for :sub_nav do = render "projects/merge_requests/head" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index a89387bc8f1..e0b29b0c2e1 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,7 +1,7 @@ - @no_container = true - page_title 'Milestones' -- if show_new_nav? +- if show_new_nav? && can?(current_user, :admin_milestone, @project) - content_for :breadcrumbs_extra do = link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone' @@ -11,10 +11,10 @@ .top-area = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) - .nav-controls + .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: 'btn btn-new', title: 'New milestone' do + = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do New milestone .milestones diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml index 4211ec6351d..e7355ae2eea 100644 --- a/app/views/shared/_mr_head.html.haml +++ b/app/views/shared/_mr_head.html.haml @@ -1,4 +1,4 @@ -- if @project.default_issues_tracker? +- if @project.issues_enabled? = render "projects/issues/head" - else = render "projects/merge_requests/head" diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index c1acee1a211..5f3cdaefd54 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,6 +1,6 @@ - if @projects.any? .project-item-select-holder - = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }, with_feature_enabled: local_assigns[:with_feature_enabled] - %a.btn.btn-new.new-project-item-select-button{ data: { relative_path: local_assigns[:path] } } + = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] + %a.btn.btn-new.new-project-item-select-button = local_assigns[:label] = icon('caret-down') diff --git a/app/views/shared/icons/_icon_clone.svg b/app/views/shared/icons/_icon_clone.svg new file mode 100644 index 00000000000..ccc897aa98f --- /dev/null +++ b/app/views/shared/icons/_icon_clone.svg @@ -0,0 +1,3 @@ +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 14 14"> +<path d="M13 12.75v-8.5q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h8.5q0.102 0 0.176-0.074t0.074-0.176zM14 4.25v8.5q0 0.516-0.367 0.883t-0.883 0.367h-8.5q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883zM11 1.25v1.25h-1v-1.25q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h1.25v1h-1.25q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883z"></path> +</svg> diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index 7b485b3363c..d7087f20dfc 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -6,15 +6,12 @@ class PipelineScheduleWorker Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) .preload(:owner, :project).find_each do |schedule| begin - unless schedule.runnable_by_owner? - schedule.deactivate! - next - end - - Ci::CreatePipelineService.new(schedule.project, - schedule.owner, - ref: schedule.ref) + pipeline = Ci::CreatePipelineService.new(schedule.project, + schedule.owner, + ref: schedule.ref) .execute(:schedule, save_on_errors: false, schedule: schedule) + + schedule.deactivate! unless pipeline.persisted? rescue => e Rails.logger.error "#{schedule.id}: Failed to create a scheduled pipeline: #{e.message}" ensure |